Skip to content

Commit fd469e0

Browse files
committed
Alternate timeout method.
1 parent 583341e commit fd469e0

4 files changed

Lines changed: 172 additions & 11 deletions

File tree

libs/internal/src/network/curl_requester.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,19 @@ void CurlRequester::PerformRequestWithMulti(std::shared_ptr<CurlMultiManager> mu
225225

226226
// Add handle to multi manager for async processing
227227
// Headers will be freed automatically by CurlMultiManager
228-
multi_manager->add_handle(curl, headers, [ctx](std::shared_ptr<CURL> easy, CURLcode result) {
228+
multi_manager->add_handle(curl, headers, [ctx](std::shared_ptr<CURL> easy, CurlMultiManager::Result result) {
229229
// This callback runs on the executor when the request completes
230230

231+
// Handle read timeout (shouldn't happen for regular requests, but handle it anyway)
232+
if (result.type == CurlMultiManager::Result::Type::ReadTimeout) {
233+
ctx->callback(HttpResult("Request timed out"));
234+
return;
235+
}
236+
231237
// Check for errors
232-
if (result != CURLE_OK) {
238+
if (result.curl_code != CURLE_OK) {
233239
std::string error_message = kErrorCurlPrefix;
234-
error_message += curl_easy_strerror(result);
240+
error_message += curl_easy_strerror(result.curl_code);
235241
ctx->callback(HttpResult(error_message));
236242
return;
237243
}

libs/networking/include/launchdarkly/network/curl_multi_manager.hpp

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <map>
1212
#include <memory>
1313
#include <mutex>
14+
#include <optional>
1415

1516
namespace launchdarkly::network {
1617

@@ -33,11 +34,32 @@ using SocketHandle = boost::asio::ip::tcp::socket;
3334
*/
3435
class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
3536
public:
37+
/**
38+
* Result of a CURL operation - either CURLcode or read timeout.
39+
*/
40+
struct Result {
41+
enum class Type {
42+
CurlCode,
43+
ReadTimeout
44+
};
45+
46+
Type type;
47+
CURLcode curl_code; // Only valid if type == CurlCode
48+
49+
static Result FromCurlCode(CURLcode code) {
50+
return Result{Type::CurlCode, code};
51+
}
52+
53+
static Result FromReadTimeout() {
54+
return Result{Type::ReadTimeout, CURLE_OK};
55+
}
56+
};
57+
3658
/**
3759
* Callback invoked when an easy handle completes (success or error).
38-
* Parameters: CURL* easy handle, CURLcode result
60+
* Parameters: CURL* easy handle, Result
3961
*/
40-
using CompletionCallback = std::function<void(std::shared_ptr<CURL>, CURLcode)>;
62+
using CompletionCallback = std::function<void(std::shared_ptr<CURL>, Result)>;
4163

4264
/**
4365
* Create a CurlMultiManager on the given executor.
@@ -59,10 +81,12 @@ class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
5981
* @param easy The CURL easy handle (must be configured)
6082
* @param headers The curl_slist headers (will be freed automatically)
6183
* @param callback Called when the transfer completes
84+
* @param read_timeout Optional read timeout duration
6285
*/
6386
void add_handle(const std::shared_ptr<CURL>& easy,
6487
curl_slist* headers,
65-
CompletionCallback callback);
88+
CompletionCallback callback,
89+
std::optional<std::chrono::milliseconds> read_timeout = std::nullopt);
6690

6791
private:
6892
explicit CurlMultiManager(boost::asio::any_io_executor executor);
@@ -86,6 +110,9 @@ class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
86110
// Check for completed transfers
87111
void check_multi_info();
88112

113+
// Handle read timeout for a specific handle
114+
void handle_read_timeout(CURL* easy);
115+
89116
// Per-socket data
90117
struct SocketInfo {
91118
curl_socket_t sockfd;
@@ -98,6 +125,15 @@ class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
98125

99126
void start_socket_monitor(SocketInfo* socket_info, int action);
100127

128+
// Reset read timeout timer for a handle
129+
void reset_read_timeout(CURL* easy);
130+
131+
// Per-handle read timeout data
132+
struct HandleTimeoutInfo {
133+
std::optional<std::chrono::milliseconds> timeout_duration;
134+
std::shared_ptr<boost::asio::steady_timer> timer;
135+
};
136+
101137
boost::asio::any_io_executor executor_;
102138
// CURLM* multi_handle_;
103139
std::unique_ptr<CURLM, decltype(&curl_multi_cleanup)> multi_handle_;
@@ -107,6 +143,7 @@ class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
107143
std::map<CURL*, CompletionCallback> callbacks_;
108144
std::map<CURL*, curl_slist*> headers_;
109145
std::map<CURL*, std::shared_ptr<CURL>> handles_;
146+
std::map<CURL*, HandleTimeoutInfo> handle_timeouts_;
110147
std::map<curl_socket_t, SocketInfo> sockets_; // Managed socket info
111148
int still_running_{0};
112149
};

libs/networking/src/curl_multi_manager.cpp

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ CurlMultiManager::~CurlMultiManager() {
4848

4949
void CurlMultiManager::add_handle(const std::shared_ptr<CURL>& easy,
5050
curl_slist* headers,
51-
CompletionCallback callback) {
51+
CompletionCallback callback,
52+
std::optional<std::chrono::milliseconds> read_timeout) {
5253
if (const CURLMcode rc = curl_multi_add_handle(
5354
multi_handle_.get(), easy.get());
5455
rc != CURLM_OK) {
@@ -67,6 +68,24 @@ void CurlMultiManager::add_handle(const std::shared_ptr<CURL>& easy,
6768
callbacks_[easy.get()] = std::move(callback);
6869
headers_[easy.get()] = headers;
6970
handles_[easy.get()] = easy;
71+
72+
// Setup read timeout timer if specified
73+
if (read_timeout) {
74+
auto timer = std::make_shared<boost::asio::steady_timer>(executor_);
75+
handle_timeouts_[easy.get()] = HandleTimeoutInfo{read_timeout, timer};
76+
77+
// Start the timeout timer
78+
timer->expires_after(*read_timeout);
79+
auto weak_self = weak_from_this();
80+
CURL* easy_ptr = easy.get();
81+
timer->async_wait([weak_self, easy_ptr](const boost::system::error_code& ec) {
82+
if (!ec) {
83+
if (auto self = weak_self.lock()) {
84+
self->handle_read_timeout(easy_ptr);
85+
}
86+
}
87+
});
88+
}
7089
}
7190
}
7291

@@ -79,6 +98,9 @@ int CurlMultiManager::socket_callback(CURL* easy,
7998

8099
std::lock_guard lock(manager->mutex_);
81100

101+
// Reset read timeout on any socket activity
102+
manager->reset_read_timeout(easy);
103+
82104
if (what == CURL_POLL_REMOVE) {
83105
// Remove socket from managed container
84106
if (const auto it = manager->sockets_.find(s);
@@ -177,6 +199,13 @@ void CurlMultiManager::check_multi_info() {
177199
headers = header_it->second;
178200
headers_.erase(header_it);
179201
}
202+
203+
// Cancel and remove timeout timer
204+
if (auto timeout_it = handle_timeouts_.find(easy);
205+
timeout_it != handle_timeouts_.end()) {
206+
timeout_it->second.timer->cancel();
207+
handle_timeouts_.erase(timeout_it);
208+
}
180209
}
181210

182211
// Remove from multi handle
@@ -197,7 +226,7 @@ void CurlMultiManager::check_multi_info() {
197226
if (callback) {
198227
boost::asio::post(executor_, [callback = std::move(callback),
199228
result, handle]() {
200-
callback(handle, result);
229+
callback(handle, Result::FromCurlCode(result));
201230
});
202231
}
203232
}
@@ -243,7 +272,7 @@ void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
243272
// Use weak_ptr in capture to avoid circular reference
244273
socket_info->read_handler = std::make_shared<std::function<void
245274
()>>();
246-
std::weak_ptr<std::function<void()>> weak_read_handler = socket_info
275+
std::weak_ptr weak_read_handler = socket_info
247276
->read_handler;
248277
*socket_info->read_handler = [weak_self, sockfd, weak_handle,
249278
weak_read_handler]() {
@@ -328,6 +357,83 @@ void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
328357
}
329358
}
330359
}
360+
361+
void CurlMultiManager::reset_read_timeout(CURL* easy) {
362+
// Must be called with mutex_ locked
363+
auto timeout_it = handle_timeouts_.find(easy);
364+
if (timeout_it != handle_timeouts_.end() && timeout_it->second.timer) {
365+
auto& timeout_info = timeout_it->second;
366+
timeout_info.timer->cancel();
367+
timeout_info.timer->expires_after(*timeout_info.timeout_duration);
368+
369+
auto weak_self = weak_from_this();
370+
CURL* easy_ptr = easy;
371+
timeout_info.timer->async_wait([weak_self, easy_ptr](const boost::system::error_code& ec) {
372+
if (!ec) {
373+
if (auto self = weak_self.lock()) {
374+
self->handle_read_timeout(easy_ptr);
375+
}
376+
}
377+
});
378+
}
379+
}
380+
381+
void CurlMultiManager::handle_read_timeout(CURL* easy) {
382+
CompletionCallback callback;
383+
curl_slist* headers = nullptr;
384+
std::shared_ptr<CURL> handle;
385+
386+
{
387+
std::lock_guard lock(mutex_);
388+
389+
// Check if handle still exists
390+
auto it = callbacks_.find(easy);
391+
if (it == callbacks_.end()) {
392+
return; // Handle already completed
393+
}
394+
395+
// Get and remove callback
396+
callback = std::move(it->second);
397+
callbacks_.erase(it);
398+
399+
// Get and remove headers
400+
if (auto header_it = headers_.find(easy);
401+
header_it != headers_.end()) {
402+
headers = header_it->second;
403+
headers_.erase(header_it);
404+
}
405+
406+
// Get and remove handle
407+
if (auto handle_it = handles_.find(easy);
408+
handle_it != handles_.end()) {
409+
handle = handle_it->second;
410+
handles_.erase(handle_it);
411+
}
412+
413+
// Remove timeout info
414+
if (auto timeout_it = handle_timeouts_.find(easy);
415+
timeout_it != handle_timeouts_.end()) {
416+
timeout_it->second.timer->cancel();
417+
handle_timeouts_.erase(timeout_it);
418+
}
419+
}
420+
421+
// Remove from multi handle
422+
curl_multi_remove_handle(multi_handle_.get(), easy);
423+
424+
// Free headers
425+
if (headers) {
426+
curl_slist_free_all(headers);
427+
}
428+
429+
// Invoke completion callback with read timeout result
430+
if (callback) {
431+
boost::asio::post(executor_, [callback = std::move(callback),
432+
handle]() {
433+
callback(handle, Result::FromReadTimeout());
434+
});
435+
}
436+
}
331437
} // namespace launchdarkly::network
332438

333439
#endif // LD_CURL_NETWORKING

libs/server-sent-events/src/curl_client.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,24 @@ void CurlClient::PerformRequestWithMulti(
441441
// Add handle to multi manager for async processing
442442
// Headers will be freed automatically by CurlMultiManager
443443
std::weak_ptr<RequestContext> weak_context = context;
444-
multi_manager->add_handle(curl, headers, [weak_context](std::shared_ptr<CURL> easy, CURLcode res) {
444+
multi_manager->add_handle(curl, headers, [weak_context](std::shared_ptr<CURL> easy, CurlMultiManager::Result result) {
445445
auto context = weak_context.lock();
446446
if (!context) {
447447
return;
448448
}
449449

450+
// Check if this was a read timeout from the multi manager
451+
if (result.type == CurlMultiManager::Result::Type::ReadTimeout) {
452+
if (!context->is_shutting_down()) {
453+
context->error(errors::ReadTimeout{context->read_timeout});
454+
context->backoff("read timeout - no data received");
455+
}
456+
return;
457+
}
458+
459+
// Handle CURLcode result
460+
CURLcode res = result.curl_code;
461+
450462
// Get response code
451463
long response_code = 0;
452464
curl_easy_getinfo(easy.get(), CURLINFO_RESPONSE_CODE, &response_code);
@@ -524,7 +536,7 @@ void CurlClient::PerformRequestWithMulti(
524536
ss << "HTTP status " << static_cast<int>(status);
525537
context->backoff(ss.str());
526538
}
527-
});
539+
}, context->read_timeout);
528540
}
529541

530542
void CurlClient::async_shutdown(std::function<void()> completion) {

0 commit comments

Comments
 (0)