Skip to content

Commit b340af5

Browse files
committed
Improve error handling for failed socket operations.
1 parent 7502d88 commit b340af5

File tree

6 files changed

+82
-59
lines changed

6 files changed

+82
-59
lines changed

libs/internal/include/launchdarkly/network/curl_requester.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
#include "http_requester.hpp"
66
#include "asio_requester.hpp"
7-
#include <launchdarkly/network/curl_multi_manager.hpp>
7+
#include <launchdarkly/network/detail/curl_multi_manager.hpp>
88
#include <launchdarkly/config/shared/built/http_properties.hpp>
99
#include <functional>
1010

libs/internal/src/network/curl_requester.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ void CurlRequester::PerformRequestWithMulti(std::shared_ptr<CurlMultiManager> mu
234234
return;
235235
}
236236

237+
// Handle manager error
238+
if (result.type == CurlMultiManager::Result::Type::ManagerError) {
239+
ctx->callback(HttpResult("Internal manager error"));
240+
return;
241+
}
242+
237243
// Check for errors
238244
if (result.curl_code != CURLE_OK) {
239245
std::string error_message = kErrorCurlPrefix;

libs/networking/include/launchdarkly/network/curl_multi_manager.hpp renamed to libs/networking/include/launchdarkly/network/detail/curl_multi_manager.hpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ using SocketHandle = boost::asio::ip::tcp::socket;
3535
class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
3636
public:
3737
/**
38-
* Result of a CURL operation - either CURLcode or read timeout.
38+
* Result of a CURL operation - CURLcode, read timeout, or manager error.
3939
*/
4040
struct Result {
4141
enum class Type {
4242
CurlCode,
43-
ReadTimeout
43+
ReadTimeout,
44+
ManagerError // Internal error in CurlMultiManager (e.g., failed to add handle)
4445
};
4546

4647
Type type;
@@ -53,6 +54,10 @@ class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
5354
static Result FromReadTimeout() {
5455
return Result{Type::ReadTimeout, CURLE_OK};
5556
}
57+
58+
static Result FromManagerError() {
59+
return Result{Type::ManagerError, CURLE_OK};
60+
}
5661
};
5762

5863
/**
@@ -123,7 +128,7 @@ class CurlMultiManager : public std::enable_shared_from_this<CurlMultiManager> {
123128
std::shared_ptr<std::function<void()>> write_handler;
124129
};
125130

126-
void start_socket_monitor(SocketInfo* socket_info, int action);
131+
bool start_socket_monitor(SocketInfo* socket_info, int action);
127132

128133
// Reset read timeout timer for a handle
129134
void reset_read_timeout(CURL* easy);

libs/networking/src/curl_multi_manager.cpp

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
#ifdef LD_CURL_NETWORKING
22

3-
#include "launchdarkly/network/curl_multi_manager.hpp"
3+
#include "../include/launchdarkly/network/detail/curl_multi_manager.hpp"
44

55
#include <boost/asio/post.hpp>
66

7-
#include <iostream>
8-
97
namespace launchdarkly::network {
108
std::shared_ptr<CurlMultiManager> CurlMultiManager::create(
119
boost::asio::any_io_executor executor) {
@@ -58,8 +56,10 @@ void CurlMultiManager::add_handle(const std::shared_ptr<CURL>& easy,
5856
curl_slist_free_all(headers);
5957
}
6058

61-
std::cerr << "Failed to add handle to multi: "
62-
<< curl_multi_strerror(rc) << std::endl;
59+
// Failed to add handle - invoke callback with manager error
60+
boost::asio::post(executor_, [callback = std::move(callback), easy]() {
61+
callback(easy, Result::FromManagerError());
62+
});
6363
return;
6464
}
6565

@@ -111,7 +111,10 @@ int CurlMultiManager::socket_callback(CURL* easy,
111111
// Add or update socket in managed container
112112
auto [it, inserted] = manager->sockets_.try_emplace(
113113
s, SocketInfo{s, nullptr, 0});
114-
manager->start_socket_monitor(&it->second, what);
114+
if (!manager->start_socket_monitor(&it->second, what)) {
115+
// Failed to start monitoring - return error to CURL
116+
return -1;
117+
}
115118
}
116119

117120
return 0;
@@ -153,15 +156,11 @@ int CurlMultiManager::timer_callback(CURLM* multi,
153156
void CurlMultiManager::handle_socket_action(curl_socket_t s,
154157
const int event_bitmask) {
155158
int running_handles = 0;
156-
const CURLMcode rc = curl_multi_socket_action(multi_handle_.get(), s,
157-
event_bitmask,
158-
&running_handles);
159-
160-
if (rc != CURLM_OK) {
161-
std::cerr << "curl_multi_socket_action failed: "
162-
<< curl_multi_strerror(rc) << std::endl;
163-
}
159+
curl_multi_socket_action(multi_handle_.get(), s,
160+
event_bitmask,
161+
&running_handles);
164162

163+
// Check for completed transfers - any errors will be reported via CURLMsg
165164
check_multi_info();
166165

167166
if (running_handles != still_running_) {
@@ -233,7 +232,7 @@ void CurlMultiManager::check_multi_info() {
233232
}
234233
}
235234

236-
void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
235+
bool CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
237236
const int action) {
238237
if (!socket_info->handle) {
239238
// Create tcp::socket and assign the native socket handle
@@ -243,14 +242,15 @@ void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
243242
// Assign the CURL socket to the ASIO socket
244243
// tcp::socket::assign works with native socket handles on both platforms
245244
boost::system::error_code ec;
246-
socket_info->handle->assign(boost::asio::ip::tcp::v4(),
245+
// clang-tidy things that this has a return value, but instead the return
246+
// is being handled by output parameter.
247+
socket_info->handle->assign(boost::asio::ip::tcp::v4(), // NOLINT(*-unused-return-value)
247248
socket_info->sockfd, ec);
248249

249250
if (ec) {
250-
std::cerr << "Failed to assign socket: " << ec.message() <<
251-
std::endl;
251+
// Failed to assign socket - CURL will handle the error
252252
socket_info->handle.reset();
253-
return;
253+
return false;
254254
}
255255
}
256256

@@ -356,6 +356,8 @@ void CurlMultiManager::start_socket_monitor(SocketInfo* socket_info,
356356
(*socket_info->write_handler)(); // Initial call
357357
}
358358
}
359+
360+
return true;
359361
}
360362

361363
void CurlMultiManager::reset_read_timeout(CURL* easy) {

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ void CurlClient::do_run() {
8181
return;
8282
}
8383

84-
auto ctx = request_context_;
84+
const auto ctx = request_context_;
8585
auto weak_self = weak_from_this();
8686
std::weak_ptr<RequestContext> weak_ctx = ctx;
8787
ctx->set_callbacks(Callbacks([weak_self, weak_ctx](const std::string& message) {
@@ -152,7 +152,7 @@ void CurlClient::async_backoff(std::string const& reason) {
152152
auto weak_self = weak_from_this();
153153
backoff_timer_.expires_after(backoff_.delay());
154154
backoff_timer_.async_wait([weak_self](const boost::system::error_code& ec) {
155-
if (auto self = weak_self.lock()) {
155+
if (const auto self = weak_self.lock()) {
156156
self->on_backoff(ec);
157157
}
158158
});
@@ -234,7 +234,7 @@ bool CurlClient::SetupCurlOptions(CURL* curl,
234234

235235
// Add Last-Event-ID if we have one from previous connection
236236
if (context.last_event_id && !context.last_event_id->empty()) {
237-
std::string last_event_header = "Last-Event-ID: " + *context.last_event_id;
237+
const std::string last_event_header = "Last-Event-ID: " + *context.last_event_id;
238238
headers = curl_slist_append(headers, last_event_header.c_str());
239239
}
240240

@@ -406,8 +406,8 @@ size_t CurlClient::HeaderCallback(const char* buffer,
406406
}
407407

408408
void CurlClient::PerformRequestWithMulti(
409-
std::shared_ptr<CurlMultiManager> multi_manager,
410-
std::shared_ptr<RequestContext> context) {
409+
const std::shared_ptr<CurlMultiManager>& multi_manager,
410+
const std::shared_ptr<RequestContext>& context) {
411411

412412
if (context->is_shutting_down()) {
413413
return;
@@ -416,7 +416,7 @@ void CurlClient::PerformRequestWithMulti(
416416
// Initialize parser for new connection (last_event_id is tracked separately)
417417
context->init_parser();
418418

419-
std::shared_ptr<CURL>curl (curl_easy_init(), curl_easy_cleanup);
419+
const std::shared_ptr<CURL>curl (curl_easy_init(), curl_easy_cleanup);
420420
if (!curl) {
421421
if (context->is_shutting_down()) {
422422
return;
@@ -456,6 +456,14 @@ void CurlClient::PerformRequestWithMulti(
456456
return;
457457
}
458458

459+
// Check if this was a manager error (e.g., failed to add handle)
460+
if (result.type == CurlMultiManager::Result::Type::ManagerError) {
461+
if (!context->is_shutting_down()) {
462+
context->backoff("internal manager error");
463+
}
464+
return;
465+
}
466+
459467
// Handle CURLcode result
460468
CURLcode res = result.curl_code;
461469

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

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#ifdef LD_CURL_NETWORKING
44

55
#include <launchdarkly/sse/client.hpp>
6-
#include <launchdarkly/network/curl_multi_manager.hpp>
6+
#include <launchdarkly/network/detail/curl_multi_manager.hpp>
77
#include "backoff.hpp"
88
#include "parser.hpp"
99

@@ -24,23 +24,25 @@ namespace launchdarkly::sse {
2424
namespace http = beast::http;
2525
namespace net = boost::asio;
2626

27-
using launchdarkly::network::CurlMultiManager;
27+
using network::CurlMultiManager;
2828

2929
/**
30-
* The lifecycle of the CurlClient is managed in an RAII manner. This introduces
31-
* some complexity with interaction with CURL, which requires a thread to be
32-
* driven. The desutruction of the CurlClient will signal that the CURL thread
33-
* should stop. Though depending on the operation it may linger for some
34-
* time.
35-
*
36-
* The CurlClient itself is not accessible from the CURL thread and instead
37-
* all their shared state is in a RequestContext. The lifetime of this context
38-
* can exceed that of the CurlClient while CURL shuts down. This approach
39-
* prevents the lifetime of the CurlClient being attached to that of the
40-
* curl thread.
30+
* The CurlClient uses the CURL multi-socket interface to allow for
31+
* single-threaded usage of CURL. We drive this usage using boost::asio
32+
* and every thing is executed in the IO context provided during client
33+
* construction. Calling into the API of the client could be done from threads
34+
* other than the IO context thread, so some thread-safety is required to
35+
* manage those interactions. For example the CurlClient destructor will
36+
* be ran on whatever thread last retains a reference to the client.
4137
*/
4238
class CurlClient final : public Client,
4339
public std::enable_shared_from_this<CurlClient> {
40+
/**
41+
* Structure containing callbacks between the CURL interactions and the
42+
* IO executor. Callbacks are set while a connection is being established,
43+
* instead of at construction time, to allow the use of weak_from_self.
44+
* The weak_from_self method cannot be used during the constructor.
45+
*/
4446
struct Callbacks {
4547
std::function<void(std::string)> do_backoff;
4648
std::function<void(Event)> on_receive;
@@ -63,22 +65,29 @@ class CurlClient final : public Client,
6365
}
6466
};
6567

68+
/**
69+
* The request context represents the state required by the executing CURL
70+
* request. Not directly including the shared data in the CurlClient allows
71+
* for easy seperation of its lifetime from that of the CURL client. This
72+
* facilitates destruction of the CurlClient being used to stop in-progress
73+
* requests.
74+
*
75+
* Also the CURL client can be destructed and pending tasks will still
76+
* have a valid RequestContext and will detect the shutdown.
77+
*/
6678
class RequestContext {
67-
// Only items used by both the curl thread and the executor/main
68-
// thread need to be mutex protected.
6979
std::mutex mutex_;
7080
std::atomic<bool> shutting_down_;
7181
std::atomic<curl_socket_t> curl_socket_;
7282
// End mutex protected items.
7383
std::optional<Callbacks> callbacks_;
7484

7585
public:
76-
// SSE parser using common parser from parser.hpp
7786
using ParserBody = detail::EventBody<std::function<void(Event)>>;
78-
std::unique_ptr<typename ParserBody::value_type> parser_body;
79-
std::unique_ptr<typename ParserBody::reader> parser_reader;
87+
std::unique_ptr<ParserBody::value_type> parser_body;
88+
std::unique_ptr<ParserBody::reader> parser_reader;
8089

81-
// Track last event ID for reconnection (separate from parser state)
90+
// Track last event ID for reconnection.
8291
std::optional<std::string> last_event_id;
8392

8493
// Progress tracking for read timeout
@@ -161,13 +170,6 @@ class CurlClient final : public Client,
161170
void shutdown() {
162171
std::lock_guard lock(mutex_);
163172
shutting_down_ = true;
164-
if (curl_socket_ != CURL_SOCKET_BAD) {
165-
#ifdef _WIN32
166-
closesocket(curl_socket_);
167-
#else
168-
close(curl_socket_);
169-
#endif
170-
}
171173
}
172174

173175

@@ -193,8 +195,8 @@ class CurlClient final : public Client,
193195
}
194196

195197
void init_parser() {
196-
parser_body = std::make_unique<typename ParserBody::value_type>();
197-
parser_reader = std::make_unique<typename ParserBody::reader>(*parser_body);
198+
parser_body = std::make_unique<ParserBody::value_type>();
199+
parser_reader = std::make_unique<ParserBody::reader>(*parser_body);
198200
parser_reader->init();
199201
}
200202
};
@@ -227,8 +229,8 @@ class CurlClient final : public Client,
227229
void async_backoff(std::string const& reason);
228230
void on_backoff(boost::system::error_code const& ec);
229231
static void PerformRequestWithMulti(
230-
std::shared_ptr<CurlMultiManager> multi_manager,
231-
std::shared_ptr<RequestContext> context);
232+
const std::shared_ptr<CurlMultiManager>& multi_manager,
233+
const std::shared_ptr<RequestContext>& context);
232234

233235
static size_t WriteCallback(const char* data,
234236
size_t size,

0 commit comments

Comments
 (0)