From 70eeec598496de78ba5f6a5ccd7bdb4b4724fd87 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 21 Aug 2020 22:41:45 +0000 Subject: [PATCH] Connection pool for keep alive (#500) * keep-alive. reuse same connection based on host --- CMakeLists.txt | 5 + sdk/core/azure-core/inc/http/curl/curl.hpp | 91 ++++++--- sdk/core/azure-core/src/http/curl/curl.cpp | 177 ++++++++++++------ .../e2e/azure_core_with_curl_bodyBuffer.cpp | 34 ++-- .../azure-core/test/ut/transport_adapter.cpp | 38 +++- 5 files changed, 239 insertions(+), 106 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c3210cbc7..abe825f56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" diff --git a/sdk/core/azure-core/inc/http/curl/curl.hpp b/sdk/core/azure-core/inc/http/curl/curl.hpp index 504185499..dc0cbae03 100644 --- a/sdk/core/azure-core/inc/http/curl/curl.hpp +++ b/sdk/core/azure-core/inc/http/curl/curl.hpp @@ -9,6 +9,9 @@ #include "http/policy.hpp" #include +#include +#include +#include #include #include @@ -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>> s_connectionPoolIndex; + static std::unique_ptr GetCurlConnection(Request& request); + static void MoveConnectionBackToPool(std::unique_ptr 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 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 diff --git a/sdk/core/azure-core/src/http/curl/curl.cpp b/sdk/core/azure-core/src/http/curl/curl.cpp index 82692cacc..c15a1f84d 100644 --- a/sdk/core/azure-core/src/http/curl/curl.cpp +++ b/sdk/core/azure-core/src/http/curl/curl.cpp @@ -12,8 +12,24 @@ std::unique_ptr CurlTransport::Send(Context const& context, Request { // Create CurlSession to perform request auto session = std::make_unique(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(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(static_cast(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(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(bufferSize), &readBytes); + readResult = curl_easy_recv( + this->m_connection->GetHandle(), buffer, static_cast(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>> + CurlSession::s_connectionPoolIndex; + +std::unique_ptr 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 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(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 connection) +{ + // Lock mutex to access connection pool. mutex is unlock as soon as lock is out of scope + std::lock_guard lock(s_connectionPoolMutex); + auto& hostPool = s_connectionPoolIndex[connection->GetHost()]; + hostPool.push_front(std::move(connection)); +} diff --git a/sdk/core/azure-core/test/e2e/azure_core_with_curl_bodyBuffer.cpp b/sdk/core/azure-core/test/e2e/azure_core_with_curl_bodyBuffer.cpp index e2bc8ce90..8cb3cb8b8 100644 --- a/sdk/core/azure-core/test/e2e/azure_core_with_curl_bodyBuffer.cpp +++ b/sdk/core/azure-core/test/e2e/azure_core_with_curl_bodyBuffer.cpp @@ -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(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(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)); diff --git a/sdk/core/azure-core/test/ut/transport_adapter.cpp b/sdk/core/azure-core/test/ut/transport_adapter.cpp index 704015821..bdaa90949 100644 --- a/sdk/core/azure-core/test/ut/transport_adapter.cpp +++ b/sdk/core/azure-core/test/ut/transport_adapter.cpp @@ -5,6 +5,7 @@ #include #include #include +#include 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"));