Connection pool for keep alive (#500)

* keep-alive. reuse same connection based on host
This commit is contained in:
Victor Vazquez 2020-08-21 22:41:45 +00:00 committed by GitHub
parent 755085f25d
commit 70eeec5984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 239 additions and 106 deletions

View File

@ -10,6 +10,11 @@ option(BUILD_CURL_TRANSPORT "Build internal http transport implementation with C
option(BUILD_TESTING "Build test cases" OFF)
option(BUILD_DOCUMENTATION "Create HTML based API documentation (requires Doxygen)" OFF)
if(BUILD_TESTING)
# define a symbol that enables some test hooks in code
add_compile_definitions(TESTING_BUILD)
endif()
# VCPKG Integration
if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"

View File

@ -9,6 +9,9 @@
#include "http/policy.hpp"
#include <curl/curl.h>
#include <list>
#include <map>
#include <mutex>
#include <type_traits>
#include <vector>
@ -17,8 +20,12 @@ namespace Azure { namespace Core { namespace Http {
namespace Details {
// libcurl CURL_MAX_WRITE_SIZE is 64k. Using same value for default uploading chunk size.
// This can be customizable in the HttpRequest
constexpr int64_t c_UploadDefaultChunkSize = 1024 * 64;
constexpr auto c_LibcurlReaderSize = 1024;
constexpr static int64_t c_DefaultUploadChunkSize = 1024 * 64;
constexpr static auto c_DefaultLibcurlReaderSize = 1024;
// Run time error template
constexpr static const char* c_DefaultFailedToGetNewConnectionTemplate
= "Fail to get a new connection for: ";
constexpr static int c_DefaultMaxOpenNewConnectionIntentsAllowed = 10;
} // namespace Details
/**
@ -34,6 +41,35 @@ namespace Azure { namespace Core { namespace Http {
* transporter to be re usuable in multiple pipelines while every call to network is unique.
*/
class CurlSession : public BodyStream {
// connection handle. It will be taken from a pool
class CurlConnection {
private:
CURL* m_handle;
std::string m_host;
public:
CurlConnection(std::string const& host) : m_handle(curl_easy_init()), m_host(host) {}
~CurlConnection() { curl_easy_cleanup(this->m_handle); }
CURL* GetHandle() { return this->m_handle; }
std::string GetHost() const { return this->m_host; }
};
// TODO: Mutex for this code to access connectionPoolIndex
/*
* Keeps an unique key for each host and creates a connection pool for each key.
* This way getting a connection for an specific host can be done in O(1) instead of looping a
* single connection list to find the first connection for the required host.
*
* There might be multiple connections for each host.
*/
static std::map<std::string, std::list<std::unique_ptr<CurlConnection>>> s_connectionPoolIndex;
static std::unique_ptr<CurlConnection> GetCurlConnection(Request& request);
static void MoveConnectionBackToPool(std::unique_ptr<CurlSession::CurlConnection> connection);
static std::mutex s_connectionPoolMutex;
private:
/**
* @brief Enum used by ResponseBufferParser to control the parsing internal state while building
@ -174,11 +210,7 @@ namespace Azure { namespace Core { namespace Http {
}
};
/**
* @brief libcurl handle to be used in the session.
*
*/
CURL* m_pCurl;
std::unique_ptr<CurlConnection> m_connection;
/**
* @brief libcurl socket abstraction used when working with streams.
@ -257,7 +289,7 @@ namespace Azure { namespace Core { namespace Http {
* provide their own buffer to copy from socket when reading the HTTP body using streams.
*
*/
uint8_t m_readBuffer[Details::c_LibcurlReaderSize]; // to work with libcurl custom read.
uint8_t m_readBuffer[Details::c_DefaultLibcurlReaderSize]; // to work with libcurl custom read.
/**
* @brief convenient function that indicates when the HTTP Request will need to upload a payload
@ -268,23 +300,6 @@ namespace Azure { namespace Core { namespace Http {
*/
bool isUploadRequest();
/**
* @brief Set up libcurl handle with a value for CURLOPT_URL.
*
* @return returns the libcurl result after setting up.
*/
CURLcode SetUrl();
/**
* @brief Set up libcurl handle with a value for CURLOPT_CONNECT_ONLY.
*
* @remark This configuration is required to enabled the custom upload/download from libcurl
* easy interface.
*
* @return returns the libcurl result after setting up.
*/
CURLcode SetConnectOnly();
/**
* @brief Set up libcurl handle to behave as an specific HTTP Method.
*
@ -367,6 +382,15 @@ namespace Azure { namespace Core { namespace Http {
int64_t ReadSocketToBuffer(uint8_t* buffer, int64_t bufferSize);
public:
#ifdef TESTING_BUILD
// Makes possible to know the number of current connections in the connection pool
static int64_t s_ConnectionsOnPool(std::string const& host)
{
auto& pool = s_connectionPoolIndex[host];
return pool.size();
};
#endif
/**
* @brief Construct a new Curl Session object. Init internal libcurl handler.
*
@ -374,18 +398,27 @@ namespace Azure { namespace Core { namespace Http {
*/
CurlSession(Request& request) : m_request(request)
{
this->m_pCurl = curl_easy_init();
this->m_connection = GetCurlConnection(this->m_request);
this->m_bodyStartInBuffer = -1;
this->m_innerBufferSize = Details::c_LibcurlReaderSize;
this->m_innerBufferSize = Details::c_DefaultLibcurlReaderSize;
this->m_rawResponseEOF = false;
this->m_isChunkedResponseType = false;
this->m_uploadedBytes = 0;
}
~CurlSession() override { curl_easy_cleanup(this->m_pCurl); }
~CurlSession() override
{
// mark connection as reusable only if entire response was read
// If not, connection can't be reused because next Read will start from what it is currently
// in the wire. We leave the connection blocked until Server closes the connection
if (this->m_rawResponseEOF)
{
MoveConnectionBackToPool(std::move(this->m_connection));
}
}
/**
* @brief Function will use the HTTP request received in constutor to perform a network call
* @brief Function will use the HTTP request received in constructor to perform a network call
* based on the HTTP request configuration.
*
* @param context TBD

View File

@ -12,8 +12,24 @@ std::unique_ptr<RawResponse> CurlTransport::Send(Context const& context, Request
{
// Create CurlSession to perform request
auto session = std::make_unique<CurlSession>(request);
CURLcode performing;
auto performing = session->Perform(context);
// Try to send the request. If we get CURLE_UNSUPPORTED_PROTOCOL back it means the connection is
// either closed or the socket is not usable any more. In that case, let the session be destroyed
// and create a new session to get another connection from connection pool.
// Prevent from trying forever by using c_DefaultMaxOpenNewConnectionIntentsAllowed.
for (auto getConnectionOpenIntent = 0;
getConnectionOpenIntent < Details::c_DefaultMaxOpenNewConnectionIntentsAllowed;
getConnectionOpenIntent++)
{
performing = session->Perform(context);
if (performing != CURLE_UNSUPPORTED_PROTOCOL)
{
break;
}
// Let session be destroyed and create a new one to get a new connection
session = std::make_unique<CurlSession>(request);
}
if (performing != CURLE_OK)
{
@ -41,15 +57,16 @@ CURLcode CurlSession::Perform(Context const& context)
{
AZURE_UNREFERENCED_PARAMETER(context);
// Working with Body Buffer. let Libcurl use the classic callback to read/write
auto result = SetUrl();
// Get the socket that libcurl is using from handle. Will use this to wait while reading/writing
// into wire
auto result = curl_easy_getinfo(
this->m_connection->GetHandle(), CURLINFO_ACTIVESOCKET, &this->m_curlSocket);
if (result != CURLE_OK)
{
return result;
}
// Make sure host is set
// TODO-> use isEqualNoCase here once it is merged
// LibCurl settings after connection is open (headers)
{
auto headers = this->m_request.GetHeaders();
auto hostHeader = headers.find("Host");
@ -65,38 +82,15 @@ CURLcode CurlSession::Perform(Context const& context)
}
}
result = SetConnectOnly();
if (result != CURLE_OK)
{
return result;
}
// curl_easy_setopt(this->m_pCurl, CURLOPT_VERBOSE, 1L);
// Set timeout to 24h. Libcurl will fail uploading on windows if timeout is:
// timeout >= 25 days. Fails as soon as trying to upload any data
// 25 days < timeout > 1 days. Fail on huge uploads ( > 1GB)
curl_easy_setopt(this->m_pCurl, CURLOPT_TIMEOUT, 60L * 60L * 24L);
// use expect:100 for PUT requests. Server will decide if it can take our request
if (this->m_request.GetMethod() == HttpMethod::Put)
{
this->m_request.AddHeader("expect", "100-continue");
}
// establish connection only (won't send or receive anything yet)
result = curl_easy_perform(this->m_pCurl);
if (result != CURLE_OK)
{
return result;
}
// Record socket to be used
result = curl_easy_getinfo(this->m_pCurl, CURLINFO_ACTIVESOCKET, &this->m_curlSocket);
if (result != CURLE_OK)
{
return result;
}
// Send request
// Send request. If the connection assigned to this curlSession is closed or the socket is
// somehow lost, libcurl will return CURLE_UNSUPPORTED_PROTOCOL
// (https://curl.haxx.se/libcurl/c/curl_easy_send.html). Return the error back.
result = HttpRawSend(context);
if (result != CURLE_OK)
{
@ -201,16 +195,6 @@ bool CurlSession::isUploadRequest()
|| this->m_request.GetMethod() == HttpMethod::Post;
}
CURLcode CurlSession::SetUrl()
{
return curl_easy_setopt(this->m_pCurl, CURLOPT_URL, this->m_request.GetEncodedUrl().data());
}
CURLcode CurlSession::SetConnectOnly()
{
return curl_easy_setopt(this->m_pCurl, CURLOPT_CONNECT_ONLY, 1L);
}
// Send buffer thru the wire
CURLcode CurlSession::SendBuffer(uint8_t const* buffer, size_t bufferSize)
{
@ -220,7 +204,7 @@ CURLcode CurlSession::SendBuffer(uint8_t const* buffer, size_t bufferSize)
{
size_t sentBytesPerRequest = 0;
sendResult = curl_easy_send(
this->m_pCurl,
this->m_connection->GetHandle(),
buffer + sentBytesTotal,
bufferSize - sentBytesTotal,
&sentBytesPerRequest);
@ -259,7 +243,7 @@ CURLcode CurlSession::UploadBody(Context const& context)
if (uploadChunkSize <= 0)
{
// use default size
uploadChunkSize = Details::c_UploadDefaultChunkSize;
uploadChunkSize = Details::c_DefaultUploadChunkSize;
}
auto unique_buffer = std::make_unique<uint8_t[]>(static_cast<size_t>(uploadChunkSize));
@ -294,13 +278,6 @@ CURLcode CurlSession::HttpRawSend(Context const& context)
return sendResult;
}
auto streamBody = this->m_request.GetBodyStream();
if (streamBody->Length() == 0)
{
// Finish request with no body (Head or PUT (expect:100;)
uint8_t const endOfRequest = 0;
return SendBuffer(&endOfRequest, 1); // need one more byte to end request
}
return this->UploadBody(context);
}
@ -333,7 +310,7 @@ void CurlSession::ParseChunkSize()
if (index + 1 == this->m_innerBufferSize)
{ // on last index. Whatever we read is the BodyStart here
this->m_innerBufferSize
= ReadSocketToBuffer(this->m_readBuffer, Details::c_LibcurlReaderSize);
= ReadSocketToBuffer(this->m_readBuffer, Details::c_DefaultLibcurlReaderSize);
this->m_bodyStartInBuffer = 0;
}
else
@ -348,7 +325,7 @@ void CurlSession::ParseChunkSize()
if (keepPolling)
{ // Read all internal buffer and \n was not found, pull from wire
this->m_innerBufferSize
= ReadSocketToBuffer(this->m_readBuffer, Details::c_LibcurlReaderSize);
= ReadSocketToBuffer(this->m_readBuffer, Details::c_DefaultLibcurlReaderSize);
this->m_bodyStartInBuffer = 0;
}
}
@ -366,7 +343,7 @@ void CurlSession::ReadStatusLineAndHeadersFromRawResponse()
{
// Try to fill internal buffer from socket.
// If response is smaller than buffer, we will get back the size of the response
bufferSize = ReadSocketToBuffer(this->m_readBuffer, Details::c_LibcurlReaderSize);
bufferSize = ReadSocketToBuffer(this->m_readBuffer, Details::c_DefaultLibcurlReaderSize);
// returns the number of bytes parsed up to the body Start
auto bytesParsed = parser.Parse(this->m_readBuffer, static_cast<size_t>(bufferSize));
@ -422,7 +399,7 @@ void CurlSession::ReadStatusLineAndHeadersFromRawResponse()
if (this->m_bodyStartInBuffer == -1)
{ // if nothing on inner buffer, pull from wire
this->m_innerBufferSize
= ReadSocketToBuffer(this->m_readBuffer, Details::c_LibcurlReaderSize);
= ReadSocketToBuffer(this->m_readBuffer, Details::c_DefaultLibcurlReaderSize);
this->m_bodyStartInBuffer = 0;
}
@ -462,7 +439,7 @@ int64_t CurlSession::Read(Azure::Core::Context const& context, uint8_t* buffer,
else
{ // end of buffer, pull data from wire
this->m_innerBufferSize
= ReadSocketToBuffer(this->m_readBuffer, Details::c_LibcurlReaderSize);
= ReadSocketToBuffer(this->m_readBuffer, Details::c_DefaultLibcurlReaderSize);
this->m_bodyStartInBuffer = 1; // jump first char (could be \r or \n)
}
}
@ -515,7 +492,8 @@ int64_t CurlSession::Read(Azure::Core::Context const& context, uint8_t* buffer,
// Also if we have already read all contentLength
if (this->m_sessionTotalRead == this->m_contentLength || this->m_rawResponseEOF)
{
// Read everything already
// make sure EOF for response is set to true
this->m_rawResponseEOF = true;
return 0;
}
@ -538,7 +516,8 @@ int64_t CurlSession::ReadSocketToBuffer(uint8_t* buffer, int64_t bufferSize)
size_t readBytes = 0;
for (CURLcode readResult = CURLE_AGAIN; readResult == CURLE_AGAIN;)
{
readResult = curl_easy_recv(this->m_pCurl, buffer, static_cast<size_t>(bufferSize), &readBytes);
readResult = curl_easy_recv(
this->m_connection->GetHandle(), buffer, static_cast<size_t>(bufferSize), &readBytes);
switch (readResult)
{
@ -804,6 +783,88 @@ int64_t CurlSession::ResponseBufferParser::BuildHeader(
// Return the index of the next char to read after delimiter
// No need to advance one more char ('\n') (since we might be at the end of the array)
// Parsing Headers will make sure to move one possition
// Parsing Headers will make sure to move one position
return indexOfEndOfStatusLine + 1 - buffer;
}
std::mutex CurlSession::s_connectionPoolMutex;
std::map<std::string, std::list<std::unique_ptr<CurlSession::CurlConnection>>>
CurlSession::s_connectionPoolIndex;
std::unique_ptr<CurlSession::CurlConnection> CurlSession::GetCurlConnection(Request& request)
{
std::string const& host = request.GetHost();
// Double-check locking. Check if there is any available connection before locking mutex
auto& hostPoolFirstCheck = s_connectionPoolIndex[host];
if (hostPoolFirstCheck.size() > 0)
{
// Critical section. Needs to own s_connectionPoolMutex before executing
// Lock mutex to access connection pool. mutex is unlock as soon as lock is out of scope
std::lock_guard<std::mutex> lock(s_connectionPoolMutex);
// get a ref to the pool from the map of pools
auto& hostPool = s_connectionPoolIndex[host];
if (hostPool.size() > 0)
{
// get ref to first connection
auto fistConnectionIterator = hostPool.begin();
// move the connection ref to temp ref
auto connection = std::move(*fistConnectionIterator);
// Remove the connection ref from list
hostPool.erase(fistConnectionIterator);
// return connection ref
return connection;
}
}
// Creating a new connection is thread safe. No need to lock mutex here.
// No available connection for the pool for the required host. Create one
auto newConnection = std::make_unique<CurlConnection>(host);
// Libcurl setup before open connection (url, connet_only, timeout)
auto result
= curl_easy_setopt(newConnection->GetHandle(), CURLOPT_URL, request.GetEncodedUrl().data());
if (result != CURLE_OK)
{
throw std::runtime_error(
Details::c_DefaultFailedToGetNewConnectionTemplate + host + ". Could not set URL.");
}
result = curl_easy_setopt(newConnection->GetHandle(), CURLOPT_CONNECT_ONLY, 1L);
if (result != CURLE_OK)
{
throw std::runtime_error(
Details::c_DefaultFailedToGetNewConnectionTemplate + host
+ ". Could not set connect only ON.");
}
// curl_easy_setopt(newConnection->GetHandle(), CURLOPT_VERBOSE, 1L);
// Set timeout to 24h. Libcurl will fail uploading on windows if timeout is:
// timeout >= 25 days. Fails as soon as trying to upload any data
// 25 days < timeout > 1 days. Fail on huge uploads ( > 1GB)
result = curl_easy_setopt(newConnection->GetHandle(), CURLOPT_TIMEOUT, 60L * 60L * 24L);
if (result != CURLE_OK)
{
throw std::runtime_error(
Details::c_DefaultFailedToGetNewConnectionTemplate + host + ". Could not set timeout.");
}
result = curl_easy_perform(newConnection->GetHandle());
if (result != CURLE_OK)
{
throw std::runtime_error(
Details::c_DefaultFailedToGetNewConnectionTemplate + host + ". Could not open connection.");
}
return newConnection;
}
// Move the connection back to the connection pool. Push it to the front so it becomes the first
// connection to be picked next time some one ask for a connection to the pool (LIFO)
void CurlSession::MoveConnectionBackToPool(std::unique_ptr<CurlSession::CurlConnection> connection)
{
// Lock mutex to access connection pool. mutex is unlock as soon as lock is out of scope
std::lock_guard<std::mutex> lock(s_connectionPoolMutex);
auto& hostPool = s_connectionPoolIndex[connection->GetHost()];
hostPool.push_front(std::move(connection));
}

View File

@ -81,15 +81,16 @@ void doFileRequest(Context const& context, HttpPipeline& pipeline)
cout << "Creating a Put From File request to" << endl << "Host: " << host << endl;
// Open a file that contains: {{"key":"value"}, {"key2":"value2"}, {"key3":"value3"}}
int fd = open("/home/vagrant/workspace/a", O_RDONLY);
int fd = open("/home/vivazqu/workspace/a", O_RDONLY);
// Create Stream from file starting with offset 18 to 100
auto requestBodyStream = FileBodyStream(fd, 18, 100);
// Limit stream to read up to 17 postions ( {"key2","value2"} )
// Limit stream to read up to 17 positions ( {"key2","value2"} )
auto limitedStream = LimitBodyStream(&requestBodyStream, 17);
// Send request
auto request = Http::Request(Http::HttpMethod::Put, host, &limitedStream);
auto request = Http::Request(Http::HttpMethod::Put, host, &limitedStream, true);
request.AddHeader("Content-Length", std::to_string(limitedStream.Length()));
request.AddHeader("File", "fileeeeeeeeeee");
auto response = pipeline.Send(context, request);
// File can be closed at this point
@ -138,10 +139,10 @@ void doGetRequest(Context const& context, HttpPipeline& pipeline)
cout << "Creating a GET request to" << endl << "Host: " << host << endl;
auto requestBodyStream = std::make_unique<MemoryBodyStream>(buffer.data(), buffer.size());
auto request = Http::Request(Http::HttpMethod::Get, host, requestBodyStream.get());
request.AddHeader("one", "header");
request.AddHeader("other", "header2");
request.AddHeader("header", "value");
auto request = Http::Request(Http::HttpMethod::Get, host, requestBodyStream.get(), true);
request.AddHeader("one", "GetHeader");
request.AddHeader("other", "GetHeader2");
request.AddHeader("header", "GetValue");
request.AddHeader("Host", "httpbin.org");
cout << endl << "GET:";
@ -163,10 +164,10 @@ void doPutRequest(Context const& context, HttpPipeline& pipeline)
buffer[BufferSize - 1] = '}'; // set buffer to look like a Json `{"x":"xxx...xxx"}`
auto requestBodyStream = std::make_unique<MemoryBodyStream>(buffer.data(), buffer.size());
auto request = Http::Request(Http::HttpMethod::Put, host, requestBodyStream.get());
request.AddHeader("one", "header");
request.AddHeader("other", "header2");
request.AddHeader("header", "value");
auto request = Http::Request(Http::HttpMethod::Put, host, requestBodyStream.get(), true);
request.AddHeader("PUT", "header");
request.AddHeader("PUT2", "header2");
request.AddHeader("PUT3", "value");
request.AddHeader("Host", "httpbin.org");
request.AddHeader("Content-Length", std::to_string(BufferSize));
@ -214,8 +215,7 @@ void doPatchRequest(Context const& context, HttpPipeline& pipeline)
string host("https://httpbin.org/patch");
cout << "Creating an PATCH request to" << endl << "Host: " << host << endl;
auto request = Http::Request(Http::HttpMethod::Patch, host);
request.AddHeader("Host", "httpbin.org");
auto request = Http::Request(Http::HttpMethod::Patch, host, true);
cout << endl << "PATCH:";
printRespose(pipeline.Send(context, request));
@ -226,8 +226,8 @@ void doDeleteRequest(Context const& context, HttpPipeline& pipeline)
string host("https://httpbin.org/delete");
cout << "Creating an DELETE request to" << endl << "Host: " << host << endl;
auto request = Http::Request(Http::HttpMethod::Delete, host);
request.AddHeader("Host", "httpbin.org");
auto request = Http::Request(Http::HttpMethod::Delete, host, true);
// request.AddHeader("deleteeeee", "httpbin.org");
cout << endl << "DELETE:";
printRespose(pipeline.Send(context, request));
@ -238,8 +238,8 @@ void doHeadRequest(Context const& context, HttpPipeline& pipeline)
string host("https://httpbin.org/get");
cout << "Creating an HEAD request to" << endl << "Host: " << host << endl;
auto request = Http::Request(Http::HttpMethod::Head, host);
request.AddHeader("Host", "httpbin.org");
auto request = Http::Request(Http::HttpMethod::Head, host, true);
request.AddHeader("HEAD", "httpbin.org");
cout << endl << "HEAD:";
printRespose(pipeline.Send(context, request));

View File

@ -5,6 +5,7 @@
#include <context.hpp>
#include <response.hpp>
#include <string>
#include <thread>
namespace Azure { namespace Core { namespace Test {
@ -42,6 +43,39 @@ namespace Azure { namespace Core { namespace Test {
CheckBodyFromBuffer(*response, expectedResponseBodySize + 6 + 13);
}
// multiThread test requires `s_ConnectionsOnPool` hook which is only available when building
// TESTING_BUILD. This test cases are only built when that case is true.`
TEST_F(TransportAdapter, getMultiThread)
{
std::string host("http://httpbin.org/get");
auto threadRoutine = [host]() {
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, host);
auto response = pipeline.Send(context, request);
checkResponseCode(response->GetStatusCode());
auto expectedResponseBodySize = std::stoull(response->GetHeaders().at("content-length"));
CheckBodyFromBuffer(*response, expectedResponseBodySize);
};
std::thread t1(threadRoutine);
std::thread t2(threadRoutine);
t1.join();
t2.join();
auto connectionsNow = Http::CurlSession::s_ConnectionsOnPool("httpbin.org");
// 2 connections must be available at this point
EXPECT_EQ(connectionsNow, 2);
std::thread t3(threadRoutine);
std::thread t4(threadRoutine);
std::thread t5(threadRoutine);
t3.join();
t4.join();
t5.join();
connectionsNow = Http::CurlSession::s_ConnectionsOnPool("httpbin.org");
// Two connections re-used plus one connection created
EXPECT_EQ(connectionsNow, 3);
}
TEST_F(TransportAdapter, get204)
{
std::string host("http://mt3.google.com/generate_204");
@ -60,7 +94,7 @@ namespace Azure { namespace Core { namespace Test {
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, host);
// loop sending request
for (auto i = 0; i < 20; i++)
for (auto i = 0; i < 500; i++)
{
auto response = pipeline.Send(context, request);
auto expectedResponseBodySize = std::stoull(response->GetHeaders().at("content-length"));
@ -179,7 +213,7 @@ namespace Azure { namespace Core { namespace Test {
auto request = Azure::Core::Http::Request(Azure::Core::Http::HttpMethod::Get, host, true);
// loop sending request
for (auto i = 0; i < 20; i++)
for (auto i = 0; i < 50; i++)
{
auto response = pipeline.Send(context, request);
auto expectedResponseBodySize = std::stoull(response->GetHeaders().at("content-length"));