Skip to content

Commit 92b8bb5

Browse files
committed
[BUG] Publish the curl session state before the event that reports it
DispatchEvent stored session_state_ after the handler had returned. A handler that cancels from the first event of a client that is already polling lets the background thread finish the operation and dispatch a state of its own, so both threads wrote that member, and the store left until after the handler returned overwrote the cancel with a state the operation had left. Found by @lalitb in review. The store moves ahead of the handler and the member becomes atomic. SessionState is a uint8_t enum and std::atomic of it is one byte with alignment one, so the layout of an installed type does not move. The new case warms the client with a completed request first, because the background thread is only spawned after SendAsync returns and without one there is nothing for the event to overlap. It is the only case in this file that reaches the is_cleaned_ recheck at the end of SendAsync. Its counters are relaxed deliberately, since an acquire or a release on them gives the two threads an ordering the code under test does not have and the racing store stops being reported. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 8a01384 commit 92b8bb5

4 files changed

Lines changed: 92 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Increment the:
4040
* [BUG] Finish a curl operation that never gets scheduled, instead of leaving
4141
FinishSession blocked forever
4242
([#4395](https://github.com/open-telemetry/opentelemetry-cpp/pull/4395))
43+
* [BUG] Store the curl session state before the event that reports it, so a
44+
cancel dispatched by the IO thread is not overwritten or raced
45+
([#4395](https://github.com/open-telemetry/opentelemetry-cpp/pull/4395))
4346
* [CODE HEALTH] Enable clang-tidy `modernize-deprecated-headers` and replace
4447
deprecated C headers (`stdint.h`, `stddef.h`, `stdlib.h`, `string.h`,
4548
`stdio.h`, `ctype.h`, `limits.h`, `assert.h`) with their C++ equivalents

‎ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# include <future>
1414
#endif
1515

16+
#include <atomic>
1617
#include <cstdint>
1718
#include <functional>
1819
#include <map>
@@ -246,7 +247,7 @@ class HttpOperation
246247
*/
247248
opentelemetry::ext::http::client::SessionState GetSessionState() const noexcept
248249
{
249-
return session_state_;
250+
return session_state_.load(std::memory_order_acquire);
250251
}
251252

252253
/**
@@ -338,7 +339,9 @@ class HttpOperation
338339
const Headers &request_headers_;
339340
const opentelemetry::ext::http::client::Body &request_body_;
340341
size_t request_nwrite_{0};
341-
opentelemetry::ext::http::client::SessionState session_state_{
342+
// Written by whichever thread dispatches an event. A handler that cancels from one event
343+
// lets the background thread dispatch another, so the two dispatches can overlap.
344+
std::atomic<opentelemetry::ext::http::client::SessionState> session_state_{
342345
opentelemetry::ext::http::client::SessionState::Created};
343346

344347
const opentelemetry::ext::http::client::Compression &compression_;

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,12 +407,15 @@ int HttpOperation::OnProgressCallback(void *clientp,
407407
void HttpOperation::DispatchEvent(opentelemetry::ext::http::client::SessionState type,
408408
const std::string &reason)
409409
{
410+
// Published before the handler runs. A handler can cancel from here, which lets the background
411+
// thread finish the operation and publish a state of its own, and a store left until after the
412+
// handler returned would overwrite that one.
413+
session_state_.store(type, std::memory_order_release);
414+
410415
if (event_handle_ != nullptr)
411416
{
412417
event_handle_->OnEvent(type, reason);
413418
}
414-
415-
session_state_ = type;
416419
}
417420

418421
HttpOperation::HttpOperation(opentelemetry::ext::http::client::Method method,

‎ext/test/http/curl_http_test.cc‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,85 @@ TEST_F(BasicCurlHttpTests, InvalidUrlCompletes)
698698
EXPECT_GE(handler->terminal_count_.load(std::memory_order_acquire), 1);
699699
}
700700

701+
// The counters say whether the IO thread reached this handler while the caller was still
702+
// inside an event of its own. They are relaxed on purpose. An acquire or a release on them
703+
// would give the two threads an ordering the code under test does not have, and a state store
704+
// racing another would stop being reported.
705+
class OverlappingCancelHandler : public TerminalCountingHandler
706+
{
707+
public:
708+
void OnEvent(http_client::SessionState state, nostd::string_view reason) noexcept override
709+
{
710+
const int depth = inside_events_.fetch_add(1, std::memory_order_relaxed) + 1;
711+
int highest = max_concurrent_events_.load(std::memory_order_relaxed);
712+
while (depth > highest &&
713+
!max_concurrent_events_.compare_exchange_weak(highest, depth, std::memory_order_relaxed,
714+
std::memory_order_relaxed))
715+
{
716+
}
717+
718+
TerminalCountingHandler::OnEvent(state, reason);
719+
720+
if (state == cancel_at_)
721+
{
722+
// Cancelling wakes the IO thread, which finishes the operation and dispatches a Cancelled
723+
// of its own. Stay in this event until that one arrives so the two really do overlap, and
724+
// give up rather than hang if it never does.
725+
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
726+
while (inside_events_.load(std::memory_order_relaxed) < 2 &&
727+
std::chrono::steady_clock::now() < deadline)
728+
{
729+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
730+
}
731+
}
732+
733+
inside_events_.fetch_sub(1, std::memory_order_relaxed);
734+
}
735+
736+
std::atomic<int> inside_events_{0};
737+
std::atomic<int> max_concurrent_events_{0};
738+
};
739+
740+
// A client spawns its IO thread only after a request has been scheduled, so cancelling from
741+
// the first event of the first request has nothing to overlap. On a client that is already
742+
// polling, the IO thread finishes the operation while the handler is still inside that event,
743+
// and the caller reaches the end of SendAsync with the operation already cleaned up.
744+
TEST_F(BasicCurlHttpTests, CancelFromConnectingWhilePollingCompletes)
745+
{
746+
received_requests_.clear();
747+
auto session_manager = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
748+
ASSERT_TRUE(session_manager != nullptr);
749+
750+
{
751+
auto warm = session_manager->CreateSession("http://127.0.0.1:19000");
752+
auto warm_request = warm->CreateRequest();
753+
warm_request->SetUri("get/");
754+
auto warm_handler = std::make_shared<TerminalCountingHandler>();
755+
warm->SendRequest(warm_handler);
756+
ASSERT_TRUE(waitForRequests(30, 1));
757+
warm->FinishSession();
758+
ASSERT_TRUE(warm_handler->got_response_.load(std::memory_order_acquire));
759+
}
760+
761+
// Nothing listens on 19937, so this operation cannot answer on its own.
762+
auto session = session_manager->CreateSession("http://127.0.0.1:19937");
763+
auto request = session->CreateRequest();
764+
request->SetUri("get/");
765+
766+
auto handler = std::make_shared<OverlappingCancelHandler>();
767+
handler->cancel_target_ = session.get();
768+
handler->cancel_at_ = http_client::SessionState::Connecting;
769+
770+
session->SendRequest(handler);
771+
session->FinishSession();
772+
session_manager->FinishAllSessions();
773+
774+
EXPECT_EQ(2, handler->max_concurrent_events_.load(std::memory_order_acquire));
775+
EXPECT_FALSE(handler->got_response_.load(std::memory_order_acquire));
776+
EXPECT_EQ(1, handler->cancelled_from_callback_.load(std::memory_order_acquire));
777+
EXPECT_FALSE(session->IsSessionActive());
778+
}
779+
701780
TEST_F(BasicCurlHttpTests, SendGetRequestSync)
702781
{
703782
received_requests_.clear();

0 commit comments

Comments
 (0)