-
Notifications
You must be signed in to change notification settings - Fork 1k
Add connection pool option (V3) #1219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
ac27258
Add connection pool support for HTTP requests
cleaton 5841ce0
Use default destructor
cleaton 7091614
Fix tidy warnings
cleaton 5e85a6a
PR comments update
cleaton 60bdb17
remove unused import
cleaton 707acaa
Merge remote-tracking branch 'upstream/master' into connection-pool-f…
cleaton 5210495
add inline docs
cleaton d433da8
simplify test - reduce server pressure for windows
cleaton d10cbfe
suppress false positive cppcheck error
cleaton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| #include "cpr/connection_pool.h" | ||
| #include <curl/curl.h> | ||
| #include <memory> | ||
| #include <mutex> | ||
|
|
||
| namespace cpr { | ||
| ConnectionPool::ConnectionPool() { | ||
| CURLSH* curl_share = curl_share_init(); | ||
| this->connection_mutex_ = std::make_shared<std::mutex>(); | ||
|
|
||
| auto lock_f = +[](CURL* /*handle*/, curl_lock_data /*data*/, curl_lock_access /*access*/, void* userptr) { | ||
| std::mutex* lock = static_cast<std::mutex*>(userptr); | ||
| lock->lock(); // cppcheck-suppress localMutex // False positive: mutex is used as callback for libcurl, not local scope | ||
| }; | ||
|
|
||
| auto unlock_f = +[](CURL* /*handle*/, curl_lock_data /*data*/, void* userptr) { | ||
| std::mutex* lock = static_cast<std::mutex*>(userptr); | ||
| lock->unlock(); | ||
| }; | ||
|
|
||
| curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); | ||
| curl_share_setopt(curl_share, CURLSHOPT_USERDATA, this->connection_mutex_.get()); | ||
| curl_share_setopt(curl_share, CURLSHOPT_LOCKFUNC, lock_f); | ||
| curl_share_setopt(curl_share, CURLSHOPT_UNLOCKFUNC, unlock_f); | ||
|
|
||
| this->curl_sh_ = std::shared_ptr<CURLSH>(curl_share, | ||
| [](CURLSH* ptr) { | ||
| // Make sure to reset callbacks before cleanup to avoid deadlocks | ||
| curl_share_setopt(ptr, CURLSHOPT_LOCKFUNC, nullptr); | ||
| curl_share_setopt(ptr, CURLSHOPT_UNLOCKFUNC, nullptr); | ||
| curl_share_cleanup(ptr); | ||
| }); | ||
| } | ||
|
|
||
| void ConnectionPool::SetupHandler(CURL* easy_handler) const { | ||
| curl_easy_setopt(easy_handler, CURLOPT_SHARE, this->curl_sh_.get()); | ||
| } | ||
|
|
||
| } // namespace cpr |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| #ifndef CPR_CONNECTION_POOL_H | ||
| #define CPR_CONNECTION_POOL_H | ||
|
|
||
| #include <curl/curl.h> | ||
| #include <memory> | ||
| #include <mutex> | ||
|
|
||
| namespace cpr { | ||
| /** | ||
| * cpr connection pool implementation for sharing connections between HTTP requests. | ||
| * | ||
| * The ConnectionPool enables connection reuse across multiple HTTP requests to the same host, | ||
| * which can significantly improve performance by avoiding the overhead of establishing new | ||
| * connections for each request. It uses libcurl's CURLSH (share) interface to manage | ||
| * connection sharing in a thread-safe manner. | ||
| * | ||
| * Example: | ||
| * ```cpp | ||
| * // Create a connection pool | ||
| * cpr::ConnectionPool pool; | ||
| * | ||
| * // Use the pool with requests to reuse connections | ||
| * cpr::Response r1 = cpr::Get(cpr::Url{"http://example.com/api/data"}, pool); | ||
| * cpr::Response r2 = cpr::Get(cpr::Url{"http://example.com/api/more"}, pool); | ||
| * | ||
| * // Or with async requests | ||
| * auto future1 = cpr::GetAsync(cpr::Url{"http://example.com/api/data"}, pool); | ||
| * auto future2 = cpr::GetAsync(cpr::Url{"http://example.com/api/more"}, pool); | ||
| * ``` | ||
| **/ | ||
| class ConnectionPool { | ||
| public: | ||
| /** | ||
| * Creates a new connection pool with shared connection state. | ||
| * Initializes the underlying CURLSH handle and sets up thread-safe locking mechanisms. | ||
| **/ | ||
| ConnectionPool(); | ||
|
|
||
| /** | ||
| * Copy constructor - creates a new connection pool sharing the same connection state. | ||
| * Multiple ConnectionPool instances can share the same underlying connection pool. | ||
| **/ | ||
| ConnectionPool(const ConnectionPool&) = default; | ||
|
|
||
| /** | ||
| * Copy assignment operator is deleted to prevent accidental copying. | ||
| * Use the copy constructor if you need to share the connection pool. | ||
| **/ | ||
| ConnectionPool& operator=(const ConnectionPool&) = delete; | ||
|
|
||
| /** | ||
| * Configures a CURL easy handle to use this connection pool. | ||
| * This method sets up the easy handle to participate in connection sharing | ||
| * managed by this pool. | ||
| * | ||
| * @param easy_handler The CURL easy handle to configure for connection sharing. | ||
| **/ | ||
| void SetupHandler(CURL* easy_handler) const; | ||
|
|
||
| private: | ||
| /** | ||
| * Thread-safe mutex used for synchronizing access to shared connections. | ||
| * This mutex is passed to libcurl's locking callbacks to ensure thread safety | ||
| * when multiple threads access the same connection pool. It's declared first | ||
| * to ensure it's destroyed last, after the CURLSH handle that references it. | ||
| **/ | ||
| std::shared_ptr<std::mutex> connection_mutex_; | ||
|
|
||
| /** | ||
| * Shared CURL handle (CURLSH) that manages the actual connection sharing. | ||
| * This handle maintains the pool of reusable connections and is configured | ||
| * with appropriate locking callbacks for thread safety. The shared_ptr uses | ||
| * a custom deleter that safely resets the lock/unlock callbacks before | ||
| * calling curl_share_cleanup() to prevent use-after-free issues during destruction. | ||
| * Declared last to ensure it's destroyed first, before the mutex it references. | ||
| **/ | ||
| std::shared_ptr<CURLSH> curl_sh_; | ||
| }; | ||
| } // namespace cpr | ||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| #include <gtest/gtest.h> | ||
|
|
||
| #include <string> | ||
| #include <vector> | ||
| #include <thread> | ||
| #include <chrono> | ||
|
|
||
| #include <cpr/cpr.h> | ||
|
|
||
| #include "httpServer.hpp" | ||
|
|
||
| using namespace cpr; | ||
|
|
||
| static HttpServer* server = new HttpServer(); | ||
| const size_t NUM_REQUESTS = 10; | ||
|
|
||
| TEST(MultipleGetTests, PoolBasicMultipleGetTest) { | ||
| Url url{server->GetBaseUrl() + "/hello.html"}; | ||
| ConnectionPool pool; | ||
| server->ResetConnectionCount(); | ||
|
|
||
| // Without shared connection pool - make 10 sequential requests | ||
| for (size_t i = 0; i < NUM_REQUESTS; ++i) { | ||
| Response response = cpr::Get(url); | ||
| EXPECT_EQ(std::string{"Hello world!"}, response.text); | ||
| EXPECT_EQ(url, response.url); | ||
| EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); | ||
| EXPECT_EQ(200, response.status_code); | ||
| } | ||
| EXPECT_EQ(server->GetConnectionCount(), NUM_REQUESTS); | ||
|
|
||
| // With shared connection pool - make 10 sequential requests | ||
| server->ResetConnectionCount(); | ||
| for (size_t i = 0; i < NUM_REQUESTS; ++i) { | ||
| Response response = cpr::Get(url, pool); | ||
| EXPECT_EQ(std::string{"Hello world!"}, response.text); | ||
| EXPECT_EQ(url, response.url); | ||
| EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); | ||
| EXPECT_EQ(200, response.status_code); | ||
| } | ||
| EXPECT_LT(server->GetConnectionCount(), NUM_REQUESTS); | ||
| } | ||
|
|
||
| TEST(MultipleGetTests, PoolAsyncGetMultipleTest) { | ||
| Url url{server->GetBaseUrl() + "/hello.html"}; | ||
| ConnectionPool pool; | ||
| std::vector<AsyncResponse> responses; | ||
| server->ResetConnectionCount(); | ||
|
|
||
| const size_t NUM_BATCHES = 2; | ||
| const size_t BATCH_SIZE = NUM_REQUESTS / 2; // 5 requests per batch | ||
|
|
||
| // Without shared connection pool - two batches with 10ms sleep | ||
| responses.reserve(NUM_REQUESTS); | ||
|
|
||
| for (size_t batch = 0; batch < NUM_BATCHES; ++batch) { | ||
| for (size_t i = 0; i < BATCH_SIZE; ++i) { | ||
| responses.emplace_back(cpr::GetAsync(url)); | ||
| } | ||
|
|
||
| // Sleep between batches but not after the last batch | ||
| if (batch != NUM_BATCHES - 1) { | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(5)); | ||
| } | ||
| } | ||
|
|
||
| // Wait for all responses | ||
| for (AsyncResponse& future : responses) { | ||
| Response response = future.get(); | ||
| EXPECT_EQ(std::string{"Hello world!"}, response.text); | ||
| EXPECT_EQ(url, response.url); | ||
| EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); | ||
| EXPECT_EQ(200, response.status_code); | ||
| } | ||
| EXPECT_EQ(server->GetConnectionCount(), NUM_REQUESTS); | ||
|
|
||
| // With shared connection pool - same two-batch approach | ||
| server->ResetConnectionCount(); | ||
| responses.clear(); | ||
| responses.reserve(NUM_REQUESTS); | ||
|
|
||
| for (size_t batch = 0; batch < NUM_BATCHES; ++batch) { | ||
| for (size_t i = 0; i < BATCH_SIZE; ++i) { | ||
| responses.emplace_back(cpr::GetAsync(url, pool)); | ||
| } | ||
|
|
||
| // Sleep between batches but not after the last batch | ||
| if (batch != NUM_BATCHES - 1) { | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(5)); | ||
| } | ||
| } | ||
|
|
||
| // Wait for all responses | ||
| for (AsyncResponse& future : responses) { | ||
| Response response = future.get(); | ||
| EXPECT_EQ(std::string{"Hello world!"}, response.text); | ||
| EXPECT_EQ(url, response.url); | ||
| EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); | ||
| EXPECT_EQ(200, response.status_code); | ||
| } | ||
|
|
||
| // With connection pooling, should use fewer connections than requests | ||
| EXPECT_LT(server->GetConnectionCount(), NUM_REQUESTS); | ||
| } | ||
|
|
||
| int main(int argc, char** argv) { | ||
| ::testing::InitGoogleTest(&argc, argv); | ||
| ::testing::AddGlobalTestEnvironment(server); | ||
| return RUN_ALL_TESTS(); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.