Skip to content

Commit b3d9965

Browse files
committed
[BUG] Publish the completion callback before the first event
Cleanup() takes is_cleaned_ at its start and swaps async_data_->callback about forty lines later, so the is_cleaned_ recheck in SendAsync only says cleanup has begun, not that it has finished reading the callback. A handler cancelling from the Connecting event wakes the IO thread, which can reach the swap before SendAsync stores the callback. Cleanup then finds an empty one, the completion never runs, and the lambda that clears is_session_active_ never runs with it, so the session stays active for good. The two accesses to the std::function also race. The callback is published with the flags and the session route now, before the event. The future stays behind it, so a handler calling FinishSession() from that event still returns rather than waiting on a transfer that has not been scheduled. Found by @lalitb in review. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 73f5f82 commit b3d9965

2 files changed

Lines changed: 16 additions & 12 deletions

File tree

‎ext/src/http/client/curl/http_operation_curl.cc‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,20 +1452,24 @@ CURLcode HttpOperation::SendAsync(Session *session, std::function<void(HttpOpera
14521452
}
14531453
curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PRIVATE, session);
14541454

1455-
// A handler can cancel from this event, so the flags and the route it needs are published
1456-
// before it runs. They used to be written afterwards, which threw such a cancel away.
1455+
// A handler can cancel from this event, so everything Cleanup() needs has to be published
1456+
// before it runs. The callback most of all: Cleanup() takes is_cleaned_ at its start and
1457+
// swaps the callback much later, so a cleanup racing an assignment placed after the event
1458+
// would swap an empty one, never run the completion, and leave the session marked active
1459+
// for good.
14571460
is_finished_.store(false, std::memory_order_release);
14581461
is_aborted_.store(false, std::memory_order_release);
14591462
is_cleaned_.store(false, std::memory_order_release);
1460-
async_data_->session = session;
1463+
async_data_->session = session;
1464+
async_data_->callback = std::move(callback);
14611465

14621466
DispatchEvent(opentelemetry::ext::http::client::SessionState::Connecting);
14631467

1464-
// The future stays unpublished until after the event, so a handler that calls FinishSession()
1465-
// from it returns instead of waiting on a transfer that has not been scheduled yet.
1468+
// The future is the one thing that stays unpublished until after the event, so a handler that
1469+
// calls FinishSession() from it returns instead of waiting on a transfer that has not been
1470+
// scheduled yet.
14661471
async_data_->result_promise = std::promise<CURLcode>();
14671472
async_data_->result_future = async_data_->result_promise.get_future();
1468-
async_data_->callback = std::move(callback);
14691473
async_data_->is_promise_running.store(true, std::memory_order_release);
14701474

14711475
// Cancelling from the event hands the session to the IO thread, which may already have torn

‎ext/test/http/curl_http_test.cc‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -593,10 +593,10 @@ TEST_F(BasicCurlHttpTests, RetryJitterIsNotSharedAcrossThreads)
593593
drawing_second.join();
594594
}
595595

596-
// A handler is allowed to cancel from the events SendRequest dispatches. Those used to be
597-
// dispatched before the flags and the cancel route were published, so the cancel was thrown
598-
// away and the request was neither sent nor completed, leaving FinishSession() waiting on a
599-
// promise nobody could fulfil. See #4390.
596+
// A handler is allowed to cancel from the events SendRequest dispatches, so the flags and the
597+
// cancel route have to be published before the dispatch. Publish them after it and the cancel
598+
// is thrown away, the request is neither sent nor completed, and FinishSession() waits on a
599+
// promise nobody can fulfil. See #4390.
600600
TEST_F(BasicCurlHttpTests, CancelFromCreatedCompletes)
601601
{
602602
auto session_manager = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
@@ -640,8 +640,8 @@ TEST_F(BasicCurlHttpTests, CancelFromConnectingCompletes)
640640
}
641641

642642
// CreateSession hands back an unregistered session when the URL does not parse, and
643-
// CURLOPT_URL is not checked when it is set, so the operation used to reach the same dead end
644-
// without any handler being involved. See #4393.
643+
// CURLOPT_URL is not checked when it is set, so nothing on this path stops the operation
644+
// reaching the same dead end with no handler involved at all. See #4393.
645645
TEST_F(BasicCurlHttpTests, InvalidUrlCompletes)
646646
{
647647
auto session_manager = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();

0 commit comments

Comments
 (0)