diff --git a/sdk/core/azure-core/inc/azure/core/http/curl_transport.hpp b/sdk/core/azure-core/inc/azure/core/http/curl_transport.hpp index d1780a5e12..872acd58b9 100644 --- a/sdk/core/azure-core/inc/azure/core/http/curl_transport.hpp +++ b/sdk/core/azure-core/inc/azure/core/http/curl_transport.hpp @@ -13,9 +13,14 @@ #include "azure/core/nullable.hpp" #include +#include #include #include - +#if defined(_azure_TESTING_BUILD) +namespace Azure { namespace Core { namespace Test { + class CurlTransport_VerifyKeepAliveHeaders_Test; +}}} // namespace Azure::Core::Test +#endif namespace Azure { namespace Core { namespace Http { class CurlNetworkConnection; @@ -26,6 +31,32 @@ namespace Azure { namespace Core { namespace Http { * */ constexpr std::chrono::milliseconds DefaultConnectionTimeout = std::chrono::minutes(5); + + /** + * @brief The configuration options for the keep-alive feature. + * + * @remark The keep-alive feature allows the SDK to reuse the same connection to the service for + * multiple requests. + */ + class KeepAliveOptions { + public: + /** + * @brief The time in seconds that the host will allow an idle connection to remain open + * before it is closed. + * + * @remark A connection is idle if no data is sent or received by a host. A host + * may keep an idle connection open for longer than timeout seconds, but the host should + * attempt to retain a connection for at least timeout seconds. + * + */ + std::chrono::seconds ConnectionTimeout = std::chrono::seconds(0); + + /** + * @brief The maximum number of requests that a host will allow over a single connection. + * + */ + std::size_t MaxRequests = size_t(1); + }; } // namespace _detail /** @@ -151,6 +182,15 @@ namespace Azure { namespace Core { namespace Http { */ bool HttpKeepAlive = true; + /** + * @brief Options specified in the keep-alive request header + * + * @remark The keep-alive feature allows the SDK to reuse the same connection to the service for + * multiple requests. This field is populated if the Keep-Alive header is present in the + * request. + */ + Azure::Nullable KeepAliveOptions; + /** * @brief This option determines whether libcurl verifies the authenticity of the peer's * certificate. @@ -200,6 +240,9 @@ namespace Azure { namespace Core { namespace Http { * @brief Concrete implementation of an HTTP Transport that uses libcurl. */ class CurlTransport : public HttpTransport { +#if defined(_azure_TESTING_BUILD) + friend class Azure::Core::Test::CurlTransport_VerifyKeepAliveHeaders_Test; +#endif private: CurlTransportOptions m_options; @@ -209,6 +252,8 @@ namespace Azure { namespace Core { namespace Http { */ virtual void OnUpgradedConnection(std::unique_ptr&&){}; + void ValidateKeepAliveHeaders(Request& request, std::unique_ptr& response); + public: /** * @brief Construct a new CurlTransport object. diff --git a/sdk/core/azure-core/src/http/curl/curl.cpp b/sdk/core/azure-core/src/http/curl/curl.cpp index 176e612472..77d7bb7dcb 100644 --- a/sdk/core/azure-core/src/http/curl/curl.cpp +++ b/sdk/core/azure-core/src/http/curl/curl.cpp @@ -403,9 +403,48 @@ std::unique_ptr CurlTransport::Send(Request& request, Context const auto response = session->ExtractResponse(); // Move the ownership of the CurlSession (bodyStream) to the response response->SetBodyStream(std::move(session)); + // check the consistency of the keep alive headers between the request and the response + // as suggested by RFC 7230 section 6.3 (https://tools.ietf.org/html/rfc7230#section-6.3) + ValidateKeepAliveHeaders(request, response); + return response; } +// Check if the server supports keep alive and if the headers are consistent between the request and +// the response. If they are not consistent, the keep alive header in the request is removed and the +// calculated options are reset in order to prevent the wrongful caching of the keep alive settings. +void CurlTransport::ValidateKeepAliveHeaders( + Request& request, + std::unique_ptr& response) +{ + // if the server supports keep alive the headers should be present in the response. If they are + // they should be the same as the request headers. + if (response->GetHeaders().find("Connection") != response->GetHeaders().end() + && request.GetHeader("Connection").HasValue() + && response->GetHeaders().find("Keep-Alive") != response->GetHeaders().end() + && request.GetHeader("Keep-Alive").HasValue() + // Case sensitivity only applies to the `Key` in the map. Thus. compare `Value` in a case insensitive manor. + && Azure::Core::_internal::StringExtensions::ToLower( + response->GetHeaders().find("Connection")->second) + == Azure::Core::_internal::StringExtensions::ToLower( + request.GetHeader("Connection").Value()) + // just in case the server sends the keep-alive header in a different case + && Azure::Core::_internal::StringExtensions::ToLower( + response->GetHeaders().find("Keep-Alive")->second) + == Azure::Core::_internal::StringExtensions::ToLower( + request.GetHeader("Keep-Alive").Value())) + { + Log::Write(Logger::Level::Verbose, LogMsgPrefix + "Response has same keep-alive settings"); + } + else + { + // cleanup keep-alive header in the request since they don't match up the response from + // the server. + request.RemoveHeader("Keep-Alive"); + m_options.KeepAliveOptions.Reset(); + } +} + CURLcode CurlSession::Perform(Context const& context) { // Set the session state @@ -2220,10 +2259,18 @@ std::unique_ptr CurlConnectionPool::ExtractOrCreateCurlCo { g_curlConnectionPool.ConnectionPoolIndex.erase(hostPoolIndex); } - - Log::Write(Logger::Level::Verbose, LogMsgPrefix + "Re-using connection from the pool."); - // return connection ref - return connection; + // if the connection is expired do not return it, let the code flow and return a new one. + if (!connection->IsKeepAliveExpired()) + { + connection->IncreaseUsageCount(); + Log::Write(Logger::Level::Verbose, LogMsgPrefix + "Re-using connection from the pool."); + // return connection ref + return connection; + } + else + { + Log::Write(Logger::Level::Verbose, LogMsgPrefix + "Connection expired. Discarding."); + } } } } @@ -2235,6 +2282,58 @@ std::unique_ptr CurlConnectionPool::ExtractOrCreateCurlCo return std::make_unique(request, options, hostDisplayName, connectionKey); } +Azure::Core::Http::_detail::KeepAliveOptions CurlConnection::ParseKeepAliveHeader( + std::string const& keepAlive) +{ + // Parse the Keep-Alive header to determine if the connection should be kept alive. + // The Keep-Alive header is in the format of: + // Keep-Alive: timeout=5, max=1000 + // The timeout is the number of seconds the connection is allowed to be idle before it is closed. + // The max is the maximum number of requests that can be made on the connection before it is + // closed. + // If the header is not present, the connection will be kept alive for the lifetime of the + // application. + Azure::Core::Http::_detail::KeepAliveOptions keepAliveOptions; + std::string const timeoutKey = "timeout="; + std::string const maxKey = "max="; + auto timeoutPos = keepAlive.find(timeoutKey); + auto maxPos = keepAlive.find(maxKey); + try + { + + if (timeoutPos != std::string::npos) + { + auto timeoutEnd = keepAlive.find(',', timeoutPos); + if (timeoutEnd == std::string::npos) + { + timeoutEnd = keepAlive.size(); + } + + keepAliveOptions.ConnectionTimeout = std::chrono::seconds(std::stoi(keepAlive.substr( + timeoutPos + timeoutKey.size(), timeoutEnd - timeoutPos - timeoutKey.size()))); + } + + if (maxPos != std::string::npos) + { + auto maxEnd = keepAlive.find(',', maxPos); + if (maxEnd == std::string::npos) + { + maxEnd = keepAlive.size(); + } + keepAliveOptions.MaxRequests + = std::stoi(keepAlive.substr(maxPos + maxKey.size(), maxEnd - maxPos - maxKey.size())); + } + } + catch (std::invalid_argument const&) + { + Log::Write( + Logger::Level::Error, + "Failed to parse max value / timeout from Keep-Alive header: " + keepAlive); + return Azure::Core::Http::_detail::KeepAliveOptions(); + } + return keepAliveOptions; +} + // 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 CurlConnectionPool::MoveConnectionBackToPool( @@ -2246,9 +2345,9 @@ void CurlConnectionPool::MoveConnectionBackToPool( return; // The server has asked us to not re-use this connection. } - if (connection->IsShutdown()) + if (connection->IsShutdown() || connection->IsKeepAliveExpired()) { - // Can't re-used a shut down connection + // Can't re-use a shutdown connection or an expired connection return; } @@ -2309,8 +2408,29 @@ CurlConnection::CurlConnection( _detail::DefaultFailedToGetNewConnectionTemplate + hostDisplayName + ". " + std::string("curl_easy_init returned Null")); } + CURLcode result; + if (request.GetHeader("connection").HasValue() && request.GetHeader("keep-alive").HasValue()) + { + auto connectionHeader = Azure::Core::_internal::StringExtensions::ToLower( + request.GetHeader("connection").Value()); + auto keepAliveHeader = Azure::Core::_internal::StringExtensions::ToLower( + request.GetHeader("keep-alive").Value()); + + if (connectionHeader == "keep-alive") + { + auto keepAliveValue = ParseKeepAliveHeader(keepAliveHeader); + // if we have issues parsing this header , or the data in the header is invalid no point in + // setting it up + if (keepAliveValue.ConnectionTimeout != std::chrono::seconds(0) + || keepAliveValue.MaxRequests > 0) + { + m_keepAliveOptions = keepAliveValue; + } + } + } + if (options.EnableCurlTracing) { if (!SetLibcurlOption( diff --git a/sdk/core/azure-core/src/http/curl/curl_connection_private.hpp b/sdk/core/azure-core/src/http/curl/curl_connection_private.hpp index 7c360d3fbc..669b4872f9 100644 --- a/sdk/core/azure-core/src/http/curl/curl_connection_private.hpp +++ b/sdk/core/azure-core/src/http/curl/curl_connection_private.hpp @@ -29,7 +29,11 @@ /// From openssl/x509.h. Avoids needing to include openssl headers typedef struct x509_store_ctx_st X509_STORE_CTX; - +#if defined(_azure_TESTING_BUILD) +namespace Azure { namespace Core { namespace Test { + class CurlConnectionTest_ParseKeepAliveHeader_Test; +}}} // namespace Azure::Core::Test +#endif namespace Azure { namespace Core { namespace _detail { /** @@ -105,6 +109,12 @@ namespace Azure { namespace Core { */ virtual bool IsExpired() = 0; + /** + * @brief Checks whether this CURL connection is expired due to keep-alive settings. + * + */ + virtual bool IsKeepAliveExpired() { return false; }; + /** * @brief This function is used when working with streams to pull more data from the wire. * Function will try to keep pulling data from socket until the buffer is all written or until @@ -134,6 +144,19 @@ namespace Azure { namespace Core { * @return `true` is the connection was shut it down; otherwise, `false`. */ bool IsShutdown() const { return m_isShutDown; } + + /** + * @brief Increase the usage count. + * + */ + virtual void IncreaseUsageCount(){}; + + /** + * @brief Get the connection usage count. + * + * @return The usage count + */ + virtual size_t GetUsageCount() const { return 0; }; }; /** @@ -141,15 +164,20 @@ namespace Azure { namespace Core { * */ class CurlConnection final : public CurlNetworkConnection { +#if defined(_azure_TESTING_BUILD) + friend class Azure::Core::Test::CurlConnectionTest_ParseKeepAliveHeader_Test; +#endif private: Azure::Core::_internal::UniqueHandle m_handle; curl_socket_t m_curlSocket; std::chrono::steady_clock::time_point m_lastUseTime; + std::chrono::steady_clock::time_point m_firstUseTime = std::chrono::steady_clock::now(); std::string m_connectionKey; // CRL validation is disabled by default to be consistent with WinHTTP behavior bool m_enableCrlValidation{false}; // Allow the connection to proceed if retrieving the CRL failed. bool m_allowFailedCrlRetrieval{true}; + size_t m_usedCount = size_t(1); static int CurlLoggingCallback( CURL* handle, @@ -161,6 +189,9 @@ namespace Azure { namespace Core { static int CurlSslCtxCallback(CURL* curl, void* sslctx, void* parm); int SslCtxCallback(CURL* curl, void* sslctx); int VerifyCertificateError(int ok, X509_STORE_CTX* storeContext); + Azure::Nullable m_keepAliveOptions; + Azure::Core::Http::_detail::KeepAliveOptions ParseKeepAliveHeader( + std::string const& keepAlive); public: /** @@ -203,7 +234,34 @@ namespace Azure { namespace Core { { auto connectionOnWaitingTimeMs = std::chrono::duration_cast( std::chrono::steady_clock::now() - this->m_lastUseTime); - return connectionOnWaitingTimeMs.count() >= _detail::DefaultConnectionExpiredMilliseconds; + return connectionOnWaitingTimeMs.count() >= _detail::DefaultConnectionExpiredMilliseconds + || IsKeepAliveExpired(); + } + + /** + * @brief Checks whether this CURL connection is expired due to keep-alive settings. + * @return `true` if this connection is considered expired; otherwise, `false`. + */ + bool IsKeepAliveExpired() override + { + // if we have keep alive options and we haven reached the max requests declare expired + if (m_keepAliveOptions.HasValue() && m_keepAliveOptions.Value().MaxRequests > 0 + && m_keepAliveOptions.Value().MaxRequests <= m_usedCount) + { + return true; + } + + // if we have keep alive options and we have a connection timeout and the connection time + // frame has passed declare expired + if (m_keepAliveOptions.HasValue() + && m_keepAliveOptions.Value().ConnectionTimeout > std::chrono::seconds(0) + && m_firstUseTime + m_keepAliveOptions.Value().ConnectionTimeout + < std::chrono::steady_clock::now()) + { + return true; + } + + return false; } /** @@ -231,6 +289,19 @@ namespace Azure { namespace Core { */ CURLcode SendBuffer(uint8_t const* buffer, size_t bufferSize, Context const& context) override; + + /** + * @brief Increase the usage count. + * + */ + void IncreaseUsageCount() override { m_usedCount++; }; + + /** + * @brief Get the connection usage count. + * + * @return The usage count + */ + size_t GetUsageCount() const override { return m_usedCount; } }; } // namespace Http }} // namespace Azure::Core diff --git a/sdk/core/azure-core/test/ut/CMakeLists.txt b/sdk/core/azure-core/test/ut/CMakeLists.txt index 9dbcfec817..7c9885ab29 100644 --- a/sdk/core/azure-core/test/ut/CMakeLists.txt +++ b/sdk/core/azure-core/test/ut/CMakeLists.txt @@ -30,7 +30,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) if(BUILD_TRANSPORT_CURL) SET(CURL_OPTIONS_TESTS curl_options_test.cpp) SET(CURL_SESSION_TESTS curl_session_test_test.cpp curl_session_test.hpp) - SET(CURL_CONNECTION_POOL_TESTS curl_connection_pool_test.cpp) + SET(CURL_CONNECTION_POOL_TESTS curl_connection_pool_test.cpp curl_connection_tests.cpp) endif() if(RUN_LONG_UNIT_TESTS) diff --git a/sdk/core/azure-core/test/ut/curl_connection_pool_test.cpp b/sdk/core/azure-core/test/ut/curl_connection_pool_test.cpp index 61ecc10046..29442c1355 100644 --- a/sdk/core/azure-core/test/ut/curl_connection_pool_test.cpp +++ b/sdk/core/azure-core/test/ut/curl_connection_pool_test.cpp @@ -39,636 +39,767 @@ inline std::string CreateConnectionKey( namespace Azure { namespace Core { namespace Test { -/*********************** Unique Tests for Libcurl ********************************/ + /*********************** Unique Tests for Libcurl ********************************/ #if defined(BUILD_CURL_HTTP_TRANSPORT_ADAPTER) - TEST(CurlConnectionPool, connectionPoolTest) + TEST(CurlConnectionPool, connectionPoolTest) + { { - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); - // Make sure there are nothing in the pool - EXPECT_EQ(CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.size(), 0); - } + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + // Make sure there are nothing in the pool + EXPECT_EQ(CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.size(), 0); + } - // Use the same request for all connections. - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); - std::string const expectedConnectionKey(CreateConnectionKey( - AzureSdkHttpbinServer::Schema(), - AzureSdkHttpbinServer::Host(), - ",0,0,0,0,0,1,1,0,0,0,0")); + // Use the same request for all connections. + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), AzureSdkHttpbinServer::Host(), ",0,0,0,0,0,1,1,0,0,0,0")); - { - // Creating a new connection with default options - Azure::Core::Http::CurlTransportOptions options; - auto connection - = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + { + // Creating a new connection with default options + Azure::Core::Http::CurlTransportOptions options; + auto connection + = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); - EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); - auto session - = std::make_unique(req, std::move(connection), options); - // Simulate connection was used already - session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; - session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; - session->m_httpKeepAlive = true; - } - // Check that after the connection is gone, it is moved back to the pool - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .size(), - 1); - auto connectionFromPool - = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.begin() - ->second.begin() - ->get(); - EXPECT_EQ(connectionFromPool->GetConnectionKey(), expectedConnectionKey); - } + auto session + = std::make_unique(req, std::move(connection), options); + // Simulate connection was used already + session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; + session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; + session->m_httpKeepAlive = true; + } + // Check that after the connection is gone, it is moved back to the pool + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 1); + auto connectionFromPool + = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .begin() + ->second.begin() + ->get(); + EXPECT_EQ(connectionFromPool->GetConnectionKey(), expectedConnectionKey); + } - // Test that asking a connection with same config will re-use the same connection - { - // Creating a new connection with default options - Azure::Core::Http::CurlTransportOptions options; - auto connection - = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + // Test that asking a connection with same config will re-use the same connection + { + // Creating a new connection with default options + Azure::Core::Http::CurlTransportOptions options; + auto connection + = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); - // There was just one connection in the pool, it should be empty now - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .size(), - 0); - // And the connection key for the connection we got is the expected - EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + // There was just one connection in the pool, it should be empty now + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 0); + // And the connection key for the connection we got is the expected + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + + auto session + = std::make_unique(req, std::move(connection), options); + // Simulate connection was used already + session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; + session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; + session->m_httpKeepAlive = true; + } + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // Check that after the connection is gone, it is moved back to the pool + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 1); + auto values = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ConnectionPoolIndex.begin(); + EXPECT_EQ(values->second.size(), 1); + EXPECT_EQ(values->second.begin()->get()->GetConnectionKey(), expectedConnectionKey); + } - auto session - = std::make_unique(req, std::move(connection), options); - // Simulate connection was used already - session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; - session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; - session->m_httpKeepAlive = true; - } - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // Check that after the connection is gone, it is moved back to the pool - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .size(), - 1); - auto values = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.begin(); - EXPECT_EQ(values->second.size(), 1); - EXPECT_EQ(values->second.begin()->get()->GetConnectionKey(), expectedConnectionKey); - } + // Now test that using a different connection config won't re-use the same connection + std::string const secondExpectedKey = AzureSdkHttpbinServer::Schema() + "://" + + AzureSdkHttpbinServer::Host() + ",0,0,0,0,0,1,0,0,0,0,200000"; + { + // Creating a new connection with options + Azure::Core::Http::CurlTransportOptions options; + options.SslVerifyPeer = false; + options.ConnectionTimeout = std::chrono::seconds(200); + auto connection + = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + EXPECT_EQ(connection->GetConnectionKey(), secondExpectedKey); + // One connection still in the pool after getting a new connection and with first expected + // key + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 1); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .begin() + ->second.begin() + ->get() + ->GetConnectionKey(), + expectedConnectionKey); + + auto session + = std::make_unique(req, std::move(connection), options); + // Simulate connection was used already + session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; + session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; + session->m_httpKeepAlive = true; + } - // Now test that using a different connection config won't re-use the same connection - std::string const secondExpectedKey = AzureSdkHttpbinServer::Schema() + "://" - + AzureSdkHttpbinServer::Host() + ",0,0,0,0,0,1,0,0,0,0,200000"; + // Now there should be 2 index wit one connection each + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 2); + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + for (auto& val : + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex) { - // Creating a new connection with options - Azure::Core::Http::CurlTransportOptions options; - options.SslVerifyPeer = false; - options.ConnectionTimeout = std::chrono::seconds(200); - auto connection - = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); - EXPECT_EQ(connection->GetConnectionKey(), secondExpectedKey); - // One connection still in the pool after getting a new connection and with first expected - // key - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .size(), - 1); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .begin() - ->second.begin() - ->get() - ->GetConnectionKey(), - expectedConnectionKey); - - auto session - = std::make_unique(req, std::move(connection), options); - // Simulate connection was used already - session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; - session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; - session->m_httpKeepAlive = true; + EXPECT_EQ(val.second.size(), 1); } + // The connection pool should have the two connections we added earlier. + EXPECT_NE( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .end(), + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .find(expectedConnectionKey)); + EXPECT_NE( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .end(), + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .find(secondExpectedKey)); + } - // Now there should be 2 index wit one connection each + { + Azure::Core::Http::CurlSession::ResponseBufferParser responseParser; + EXPECT_EQ(responseParser.ExtractResponse(), nullptr); + + const uint8_t responseBuf[] = "HTTP/1.1 200 OK\r\n\r\n"; + static_cast(responseParser.Parse(responseBuf, sizeof(responseBuf) - 1)); + EXPECT_NE(responseParser.ExtractResponse(), nullptr); + EXPECT_EQ(responseParser.ExtractResponse(), nullptr); + } + + // Test re-using same custom config + { + // Creating a new connection with default options + Azure::Core::Http::CurlTransportOptions options; + auto connection + = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + // One connection still in the pool after getting a new connection and with first expected + // key EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), - 2); + 1); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .begin() + ->second.begin() + ->get() + ->GetConnectionKey(), + secondExpectedKey); + + auto session + = std::make_unique(req, std::move(connection), options); + // Simulate connection was used already + session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; + session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; + session->m_httpKeepAlive = true; + } + // Now there should be 2 index wit one connection each + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 2); + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + for (auto& val : + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex) { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - for (auto& val : Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex) - { - EXPECT_EQ(val.second.size(), 1); - } - // The connection pool should have the two connections we added earlier. - EXPECT_NE( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .end(), - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .find(expectedConnectionKey)); - EXPECT_NE( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .end(), - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .find(secondExpectedKey)); + EXPECT_EQ(val.second.size(), 1); } + // The connection pool should have the two connections we added earlier. + EXPECT_NE( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .end(), + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .find(expectedConnectionKey)); + EXPECT_NE( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .end(), + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .find(secondExpectedKey)); + } + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // clean the pool + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + } - { - Azure::Core::Http::CurlSession::ResponseBufferParser responseParser; - EXPECT_EQ(responseParser.ExtractResponse(), nullptr); +#ifdef RUN_LONG_UNIT_TESTS + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // clean the pool + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 0); + } - const uint8_t responseBuf[] = "HTTP/1.1 200 OK\r\n\r\n"; - static_cast(responseParser.Parse(responseBuf, sizeof(responseBuf) - 1)); - EXPECT_NE(responseParser.ExtractResponse(), nullptr); - EXPECT_EQ(responseParser.ExtractResponse(), nullptr); + // Test pool clean routine. + std::cout << "Running Connection Pool Cleaner Test. This test can take up to 2 minutes to " + "complete." + << std::endl + << "Add compiler option -DRUN_LONG_UNIT_TESTS=OFF when building if you want to " + "skip this test." + << std::endl; + { + // Make sure the clean pool thread is started by adding 5 connections to the pool + std::vector> connections; + for (int count = 0; count < 5; count++) + { + connections.emplace_back( + CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, {})); } - - // Test re-using same custom config + for (int count = 0; count < 5; count++) { - // Creating a new connection with default options - Azure::Core::Http::CurlTransportOptions options; - auto connection - = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); - EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); - // One connection still in the pool after getting a new connection and with first expected - // key - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .size(), - 1); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .begin() - ->second.begin() - ->get() - ->GetConnectionKey(), - secondExpectedKey); - - auto session - = std::make_unique(req, std::move(connection), options); - // Simulate connection was used already - session->m_lastStatusCode = Azure::Core::Http::HttpStatusCode::Ok; - session->m_sessionState = Azure::Core::Http::CurlSession::SessionState::STREAMING; - session->m_httpKeepAlive = true; + CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connections[count]), true); } - // Now there should be 2 index wit one connection each + } + + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), - 2); - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - for (auto& val : Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex) - { - EXPECT_EQ(val.second.size(), 1); - } - // The connection pool should have the two connections we added earlier. - EXPECT_NE( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .end(), - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .find(expectedConnectionKey)); - EXPECT_NE( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .end(), - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .find(secondExpectedKey)); - } + 1); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ConnectionPoolIndex[expectedConnectionKey] + .size(), + 5); + } + + // Wait for 60 secs (default time to expire a connection) + std::this_thread::sleep_for(60ms); + + { + // Now check the pool until the clean thread until finishes removing the connections or + // fail after 5 minutes (indicates a problem with the clean routine) + + auto timeOut = Context{std::chrono::system_clock::now() + 5min}; + bool poolIsEmpty = false; + while (!poolIsEmpty && !timeOut.IsCancelled()) { + std::this_thread::sleep_for(10ms); + // If test wakes while clean pool is running, it will wait until lock is released by + // the clean pool thread. std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // clean the pool - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + poolIsEmpty = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ConnectionPoolIndex.size() + == 0; } + EXPECT_TRUE(poolIsEmpty); + } + +#endif + // Test max connections in pool. Try to add 2k connections to the pool. + // Using fake connections to avoid opening real HTTP connections :) + // { + // using ::testing::_; + // using ::testing::Return; + // using ::testing::ReturnRef; + + // { + // std::lock_guard lock( + // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // // clean the pool + // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + // } + + // std::string hostKey("key"); + // for (uint64_t count = 0; count < 2000; count++) + // { + // MockCurlNetworkConnection* curlMock = new MockCurlNetworkConnection(); + // EXPECT_CALL(*curlMock, GetConnectionKey()).WillRepeatedly(ReturnRef(hostKey)); + // EXPECT_CALL(*curlMock, UpdateLastUsageTime()).WillRepeatedly(Return()); + // EXPECT_CALL(*curlMock, IsExpired()).WillRepeatedly(Return(false)); + // EXPECT_CALL(*curlMock, ReadFromSocket(_, _, _)).WillRepeatedly(Return(count)); + // EXPECT_CALL(*curlMock, DestructObj()); + + // CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + // std::unique_ptr(curlMock), + // true); + // } + // // No need to take look here because connections are mocked to never be expired. + // EXPECT_EQ( + // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + // .size(), + // 1); + // EXPECT_EQ( + // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + // .ConnectionPoolIndex[hostKey] + // .size(), + // Azure::Core::Http::_detail::MaxConnectionsPerIndex); + // // Test the first and last connection. Each connection should remove the last and + // oldest auto connectionIt = + // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + // .ConnectionPoolIndex[hostKey] + // .begin(); + // EXPECT_EQ( + // connectionIt->get()->ReadFromSocket(nullptr, 0, Context{}), + // 2000 - 1); // starting from zero + // connectionIt = --(Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + // .ConnectionPoolIndex[hostKey] + // .end()); + // EXPECT_EQ( + // connectionIt->get()->ReadFromSocket(nullptr, 0, Context{}), + // 2000 - 1024); + + // // Check the pool will take other host-key + // { + // std::string otherKey("otherHostKey"); + // MockCurlNetworkConnection* curlMock = new MockCurlNetworkConnection(); + // EXPECT_CALL(*curlMock, GetConnectionKey()).WillRepeatedly(ReturnRef(otherKey)); + // EXPECT_CALL(*curlMock, UpdateLastUsageTime()).WillRepeatedly(Return()); + // EXPECT_CALL(*curlMock, IsExpired()).WillRepeatedly(Return(false)); + // EXPECT_CALL(*curlMock, DestructObj()); + + // CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + // std::unique_ptr(curlMock), + // true); + + // EXPECT_EQ( + // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + // .ConnectionPoolIndex.size(), + // 2); + // EXPECT_EQ( + // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + // .ConnectionPoolIndex[otherKey] + // .size(), + // 1); + // // No changes to the full pool + // EXPECT_EQ( + // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + // .ConnectionPoolIndex[hostKey] + // .size(), + // Azure::Core::Http::_detail::MaxConnectionsPerIndex); + // } + // { + // std::lock_guard lock( + // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // // clean the pool + // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + // } + // } + } + + TEST(CurlConnectionPool, uniquePort) + { + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .clear(); + // Make sure there is nothing in the pool + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 0); + } + + { + // Request with no port + std::string const authority(AzureSdkHttpbinServer::Get()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ",0,0,0,0,0,1,1,0,0,0,0")); + + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); -#ifdef RUN_LONG_UNIT_TESTS { std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // clean the pool - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), 0); + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); } + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); + } - // Test pool clean routine. - std::cout << "Running Connection Pool Cleaner Test. This test can take up to 2 minutes to " - "complete." - << std::endl - << "Add compiler option -DRUN_LONG_UNIT_TESTS=OFF when building if you want to " - "skip this test." - << std::endl; - { - // Make sure the clean pool thread is started by adding 5 connections to the pool - std::vector> connections; - for (int count = 0; count < 5; count++) - { - connections.emplace_back( - CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, {})); - } - for (int count = 0; count < 5; count++) - { - CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( - std::move(connections[count]), true); - } - } + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // Test connection was moved to the pool + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 1); + } + + { + // Request with port + std::string const authority(AzureSdkHttpbinServer::GetWithPort()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ":443,0,0,0,0,0,1,1,0,0,0,0")); + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); + + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); { std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // Check connection in pool is not re-used because the port is different EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), 1); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex[expectedConnectionKey] - .size(), - 5); } + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); + } + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // Check 2 connections in the pool + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 2); + } - // Wait for 60 secs (default time to expire a connection) - std::this_thread::sleep_for(60ms); + // Re-use connections + { + // Request with no port + std::string const authority(AzureSdkHttpbinServer::Get()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ",0,0,0,0,0,1,1,0,0,0,0")); - { - // Now check the pool until the clean thread until finishes removing the connections or - // fail after 5 minutes (indicates a problem with the clean routine) - - auto timeOut = Context{std::chrono::system_clock::now() + 5min}; - bool poolIsEmpty = false; - while (!poolIsEmpty && !timeOut.IsCancelled()) - { - std::this_thread::sleep_for(10ms); - // If test wakes while clean pool is running, it will wait until lock is released by - // the clean pool thread. - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - poolIsEmpty = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.size() - == 0; - } - EXPECT_TRUE(poolIsEmpty); - } + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); -#endif - // Test max connections in pool. Try to add 2k connections to the pool. - // Using fake connections to avoid opening real HTTP connections :) - // { - // using ::testing::_; - // using ::testing::Return; - // using ::testing::ReturnRef; - - // { - // std::lock_guard lock( - // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // // clean the pool - // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); - // } - - // std::string hostKey("key"); - // for (uint64_t count = 0; count < 2000; count++) - // { - // MockCurlNetworkConnection* curlMock = new MockCurlNetworkConnection(); - // EXPECT_CALL(*curlMock, GetConnectionKey()).WillRepeatedly(ReturnRef(hostKey)); - // EXPECT_CALL(*curlMock, UpdateLastUsageTime()).WillRepeatedly(Return()); - // EXPECT_CALL(*curlMock, IsExpired()).WillRepeatedly(Return(false)); - // EXPECT_CALL(*curlMock, ReadFromSocket(_, _, _)).WillRepeatedly(Return(count)); - // EXPECT_CALL(*curlMock, DestructObj()); - - // CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( - // std::unique_ptr(curlMock), - // true); - // } - // // No need to take look here because connections are mocked to never be expired. - // EXPECT_EQ( - // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - // .size(), - // 1); - // EXPECT_EQ( - // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - // .ConnectionPoolIndex[hostKey] - // .size(), - // Azure::Core::Http::_detail::MaxConnectionsPerIndex); - // // Test the first and last connection. Each connection should remove the last and - // oldest auto connectionIt = - // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - // .ConnectionPoolIndex[hostKey] - // .begin(); - // EXPECT_EQ( - // connectionIt->get()->ReadFromSocket(nullptr, 0, Context{}), - // 2000 - 1); // starting from zero - // connectionIt = --(Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - // .ConnectionPoolIndex[hostKey] - // .end()); - // EXPECT_EQ( - // connectionIt->get()->ReadFromSocket(nullptr, 0, Context{}), - // 2000 - 1024); - - // // Check the pool will take other host-key - // { - // std::string otherKey("otherHostKey"); - // MockCurlNetworkConnection* curlMock = new MockCurlNetworkConnection(); - // EXPECT_CALL(*curlMock, GetConnectionKey()).WillRepeatedly(ReturnRef(otherKey)); - // EXPECT_CALL(*curlMock, UpdateLastUsageTime()).WillRepeatedly(Return()); - // EXPECT_CALL(*curlMock, IsExpired()).WillRepeatedly(Return(false)); - // EXPECT_CALL(*curlMock, DestructObj()); - - // CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( - // std::unique_ptr(curlMock), - // true); - - // EXPECT_EQ( - // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - // .ConnectionPoolIndex.size(), - // 2); - // EXPECT_EQ( - // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - // .ConnectionPoolIndex[otherKey] - // .size(), - // 1); - // // No changes to the full pool - // EXPECT_EQ( - // Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - // .ConnectionPoolIndex[hostKey] - // .size(), - // Azure::Core::Http::_detail::MaxConnectionsPerIndex); - // } - // { - // std::lock_guard lock( - // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // // clean the pool - // CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); - // } - // } - } - - TEST(CurlConnectionPool, uniquePort) - { { std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .clear(); - // Make sure there is nothing in the pool EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), - 0); + 1); } + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); + } - { - // Request with no port - std::string const authority(AzureSdkHttpbinServer::Get()); - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); - std::string const expectedConnectionKey(CreateConnectionKey( - AzureSdkHttpbinServer::Schema(), - AzureSdkHttpbinServer::Host(), - ",0,0,0,0,0,1,1,0,0,0,0")); - - // Creating a new connection with default options - auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ExtractOrCreateCurlConnection(req, {}); - - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.size(), - 0); - EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); - } - // move connection back to the pool - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .MoveConnectionBackToPool(std::move(connection), true); - } + { + // Make sure there is nothing in the pool + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 2); + } + { + // Request with port + std::string const authority(AzureSdkHttpbinServer::GetWithPort()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ":443,0,0,0,0,0,1,1,0,0,0,0")); + + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); { std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // Test connection was moved to the pool + // Check connection in pool is not re-used because the port is different EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), 1); } + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); + } + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 2); + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .clear(); + } + } - { - // Request with port - std::string const authority(AzureSdkHttpbinServer::GetWithPort()); - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); - std::string const expectedConnectionKey(CreateConnectionKey( - AzureSdkHttpbinServer::Schema(), - AzureSdkHttpbinServer::Host(), - ":443,0,0,0,0,0,1,1,0,0,0,0")); - - // Creating a new connection with default options - auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ExtractOrCreateCurlConnection(req, {}); + TEST(CurlConnectionPool, connectionPoolKeepAlive) + { + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .clear(); + // Make sure there is nothing in the pool + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 0); + } + + { + // Request with no port + std::string const authority(AzureSdkHttpbinServer::Get()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + req.SetHeader("Connection", "keep-alive"); + req.SetHeader("Keep-Alive", "timeout=50, max=2"); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ",0,0,0,0,0,1,1,0,0,0,0")); + + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); - EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // Check connection in pool is not re-used because the port is different - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.size(), - 1); - } - // move connection back to the pool - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .MoveConnectionBackToPool(std::move(connection), true); - } { std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // Check 2 connections in the pool EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), - 2); - } - - // Re-use connections - { - // Request with no port - std::string const authority(AzureSdkHttpbinServer::Get()); - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); - std::string const expectedConnectionKey(CreateConnectionKey( - AzureSdkHttpbinServer::Schema(), - AzureSdkHttpbinServer::Host(), - ",0,0,0,0,0,1,1,0,0,0,0")); - - // Creating a new connection with default options - auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ExtractOrCreateCurlConnection(req, {}); - - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.size(), - 1); - } + 0); EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); - // move connection back to the pool - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .MoveConnectionBackToPool(std::move(connection), true); } + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); + } + + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + // Test connection was moved to the pool + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 1); + } + + // Re-use connections + { + // Request with no port + std::string const authority(AzureSdkHttpbinServer::Get()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ",0,0,0,0,0,1,1,0,0,0,0")); + + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); { - // Make sure there is nothing in the pool std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), - 2); + 0); } - { - // Request with port - std::string const authority(AzureSdkHttpbinServer::GetWithPort()); - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); - std::string const expectedConnectionKey(CreateConnectionKey( - AzureSdkHttpbinServer::Schema(), - AzureSdkHttpbinServer::Host(), - ":443,0,0,0,0,0,1,1,0,0,0,0")); - - // Creating a new connection with default options - auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ExtractOrCreateCurlConnection(req, {}); + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); + } + + { + // Make sure there is nothing in the pool since we used the connection twice it is now expired + // thus it should not be moved back into the pool + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 0); + } + { + // Request with no port + std::string const authority(AzureSdkHttpbinServer::Get()); + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(authority)); + std::string const expectedConnectionKey(CreateConnectionKey( + AzureSdkHttpbinServer::Schema(), + AzureSdkHttpbinServer::Host(), + ",0,0,0,0,0,1,1,0,0,0,0")); + + // Creating a new connection with default options + auto connection = Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool + .ExtractOrCreateCurlConnection(req, {}); - EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - // Check connection in pool is not re-used because the port is different - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .ConnectionPoolIndex.size(), - 1); - } - // move connection back to the pool - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool - .MoveConnectionBackToPool(std::move(connection), true); - } { std::lock_guard lock( CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); EXPECT_EQ( Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex .size(), - 2); - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .clear(); + 0); } + EXPECT_EQ(connection->GetConnectionKey(), expectedConnectionKey); + // move connection back to the pool + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.MoveConnectionBackToPool( + std::move(connection), true); } - TEST(CurlConnectionPool, resiliencyOnConnectionClosed) { - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 1); + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .clear(); + } + } - Azure::Core::Http::CurlTransportOptions options; - auto connection - = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); - // Simulate connection lost (like server disconnection). - connection->Shutdown(); + TEST(CurlConnectionPool, resiliencyOnConnectionClosed) + { + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); - { - // Check that CURLE_SEND_ERROR is produced when trying to use the connection. - auto session - = std::make_unique(req, std::move(connection), options); - auto r = session->Perform(Azure::Core::Context{}); - EXPECT_EQ(CURLE_SEND_ERROR, r); - } - } + Azure::Core::Http::CurlTransportOptions options; + auto connection + = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + // Simulate connection lost (like server disconnection). + connection->Shutdown(); - TEST(CurlConnectionPool, forceConnectionClosed) { - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Status(101))); + // Check that CURLE_SEND_ERROR is produced when trying to use the connection. + auto session + = std::make_unique(req, std::move(connection), options); + auto r = session->Perform(Azure::Core::Context{}); + EXPECT_EQ(CURLE_SEND_ERROR, r); + } + } - Azure::Core::Http::CurlTransportOptions options; - auto connection - = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + TEST(CurlConnectionPool, forceConnectionClosed) + { + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Status(101))); - { - // Check we can use the connection to retrieve headers in the response, but the connection - // is still flagged as shutdown. - auto session - = std::make_unique(req, std::move(connection), options); - - auto r = session->Perform(Azure::Core::Context{}); - EXPECT_EQ(CURLE_OK, r); - auto response = session->ExtractResponse(); - EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::SwitchingProtocols); - EXPECT_EQ("close", response->GetHeaders().find("Connection")->second); - } + Azure::Core::Http::CurlTransportOptions options; + auto connection + = CurlConnectionPool::g_curlConnectionPool.ExtractOrCreateCurlConnection(req, options); + + { + // Check we can use the connection to retrieve headers in the response, but the connection + // is still flagged as shutdown. + auto session + = std::make_unique(req, std::move(connection), options); + + auto r = session->Perform(Azure::Core::Context{}); + EXPECT_EQ(CURLE_OK, r); + auto response = session->ExtractResponse(); + EXPECT_EQ(response->GetStatusCode(), Azure::Core::Http::HttpStatusCode::SwitchingProtocols); + EXPECT_EQ("close", response->GetHeaders().find("Connection")->second); } + } - TEST(CurlConnectionPool, connectionClose) + TEST(CurlConnectionPool, connectionClose) + { + /// When getting the header connection: close from an HTTP response, the connection should not + /// be moved back to the pool. { - /// When getting the header connection: close from an HTTP response, the connection should not - /// be moved back to the pool. - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); - // Make sure there are nothing in the pool - EXPECT_EQ(CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.size(), 0); - } + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.clear(); + // Make sure there are nothing in the pool + EXPECT_EQ(CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex.size(), 0); + } - // Use the same request for all connections. - Azure::Core::Http::Request req( - Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Headers())); - // Server will return this header back - req.SetHeader("connection", "close"); + // Use the same request for all connections. + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Headers())); + // Server will return this header back + req.SetHeader("connection", "close"); - { - // Create a pipeline to send the request and dispose after it. - Azure::Core::Http::_internal::HttpPipeline pipeline({}, "test", "test", {}, {}); - auto response = pipeline.Send(req, Azure::Core::Context{}); - EXPECT_PRED2( - [](Azure::Core::Http::HttpStatusCode a, Azure::Core::Http::HttpStatusCode b) { - return a == b; - }, - response->GetStatusCode(), - Azure::Core::Http::HttpStatusCode::Ok); - } + { + // Create a pipeline to send the request and dispose after it. + Azure::Core::Http::_internal::HttpPipeline pipeline({}, "test", "test", {}, {}); + auto response = pipeline.Send(req, Azure::Core::Context{}); + EXPECT_PRED2( + [](Azure::Core::Http::HttpStatusCode a, Azure::Core::Http::HttpStatusCode b) { + return a == b; + }, + response->GetStatusCode(), + Azure::Core::Http::HttpStatusCode::Ok); + } - // Check that after the connection is gone, it is moved back to the pool - { - std::lock_guard lock( - CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); - EXPECT_EQ( - Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex - .size(), - 0); - } + // Check that after the connection is gone, it is moved back to the pool + { + std::lock_guard lock( + CurlConnectionPool::g_curlConnectionPool.ConnectionPoolMutex); + EXPECT_EQ( + Azure::Core::Http::_detail::CurlConnectionPool::g_curlConnectionPool.ConnectionPoolIndex + .size(), + 0); } + } #endif }}} // namespace Azure::Core::Test diff --git a/sdk/core/azure-core/test/ut/curl_connection_tests.cpp b/sdk/core/azure-core/test/ut/curl_connection_tests.cpp new file mode 100644 index 0000000000..a40c43969d --- /dev/null +++ b/sdk/core/azure-core/test/ut/curl_connection_tests.cpp @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file + * @brief The base class for testing a curl session. + * + * @remark The curl connection mock is defined here. + * + */ +#include "transport_adapter_base_test.hpp" + +#include +#include +#include + +#if defined(BUILD_CURL_HTTP_TRANSPORT_ADAPTER) +#include "azure/core/http/curl_transport.hpp" + +#include + +#include +#include +#include + +#include +#include + +namespace Azure { namespace Core { namespace Test { + + class CurlConnectionTest : public ::testing::Test { + }; + + TEST(CurlConnectionTest, ParseKeepAliveHeader) + { + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); + + Azure::Core::Http::CurlConnection curlConnection( + req, Azure::Core::Http::CurlTransportOptions(), "hostName", "propKey"); + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max=10"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("max=10, timeout=5"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5,max=10"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("max=10,timeout=5"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("max=10"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader(""); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max=10, extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max=10, extra=1,"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max=10, extra=1, "); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(5)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader(" max=10, extra=1,"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(10)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader(", , extra=1, "); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=, extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout= , extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("max=, extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("max= , extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=, max=10, extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max=, extra=1,"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout= , max= , extra=1, "); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout= , max=10, extra=1"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max= , extra=1,"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout= , max= , extra=1, "); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5 max= , extra=1,"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout= , max= 10 extra=1, "); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=x, max=10"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + { + auto parsedHeader = curlConnection.ParseKeepAliveHeader("timeout=5, max=n"); + EXPECT_EQ(parsedHeader.MaxRequests, size_t(1)); + EXPECT_EQ(parsedHeader.ConnectionTimeout, std::chrono::seconds(0)); + } + } + + TEST(CurlConnectionTest, IsExpiredNot) + { + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); + req.SetHeader("Connection", "keep-alive"); + req.SetHeader("Keep-Alive", "timeout=120, max=2"); + Azure::Core::Http::CurlConnection curlConnection( + req, Azure::Core::Http::CurlTransportOptions(), "hostName", "propKey"); + curlConnection.UpdateLastUsageTime(); + EXPECT_TRUE(!curlConnection.IsExpired()); + } + + TEST(CurlConnectionTest, IsExpiredMaxUsage) + { + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); + req.SetHeader("Connection", "keep-alive"); + req.SetHeader("Keep-Alive", "timeout=120, max=2"); + Azure::Core::Http::CurlConnection curlConnection( + req, Azure::Core::Http::CurlTransportOptions(), "hostName", "propKey"); + curlConnection.IncreaseUsageCount(); + curlConnection.IncreaseUsageCount(); // usage == max + curlConnection.UpdateLastUsageTime(); + EXPECT_TRUE(curlConnection.IsExpired()); + curlConnection.IncreaseUsageCount(); + curlConnection.IncreaseUsageCount(); // usage > max + EXPECT_TRUE(curlConnection.IsExpired()); + } + + TEST(CurlConnectionTest, IsExpiredTimeout) + { + Azure::Core::Http::Request req( + Azure::Core::Http::HttpMethod::Get, Azure::Core::Url(AzureSdkHttpbinServer::Get())); + req.SetHeader("Connection", "keep-alive"); + req.SetHeader("Keep-Alive", "timeout=1, max=2"); + Azure::Core::Http::CurlConnection curlConnection( + req, Azure::Core::Http::CurlTransportOptions(), "hostName", "propKey"); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + curlConnection.UpdateLastUsageTime(); + EXPECT_TRUE(curlConnection.IsExpired()); + } +}}} // namespace Azure::Core::Test + +#endif diff --git a/sdk/core/azure-core/test/ut/curl_options_test.cpp b/sdk/core/azure-core/test/ut/curl_options_test.cpp index 72f10fb73a..e6487094fd 100644 --- a/sdk/core/azure-core/test/ut/curl_options_test.cpp +++ b/sdk/core/azure-core/test/ut/curl_options_test.cpp @@ -365,4 +365,28 @@ namespace Azure { namespace Core { namespace Test { 0); } + TEST(CurlTransport, VerifyKeepAliveHeaders) + { + Azure::Core::Http::CurlTransport transport; + Azure::Core::Url url(AzureSdkHttpbinServer::Get()); + Azure::Core::Http::Request request(Azure::Core::Http::HttpMethod::Get, url); + request.SetHeader("Connection", "keep-alive"); + request.SetHeader("Keep-Alive", "timeout=5, max=1000"); + std::unique_ptr response + = std::make_unique( + 1, 0, Azure::Core::Http::HttpStatusCode(200), "OK"); + response->SetHeader("Connection", "keep-alive"); + response->SetHeader("Keep-Alive", "timeout=5, max=1000"); + + transport.ValidateKeepAliveHeaders(request, response); + + EXPECT_EQ(request.GetHeader("Connection").Value(), "keep-alive"); + EXPECT_EQ(request.GetHeader("Keep-Alive").Value(), "timeout=5, max=1000"); + + response->SetHeader("Connection", "close"); + transport.ValidateKeepAliveHeaders(request, response); + + EXPECT_EQ(request.GetHeader("Connection").Value(), "keep-alive"); + EXPECT_EQ(request.GetHeader("Keep-Alive").HasValue(), false); + } }}} // namespace Azure::Core::Test