Skip to content

Commit 4784881

Browse files
committed
[BUG] Settle a refused session before reporting it
doAddSessions logged the refusal, then read GetOperation(), then finished it. The log goes through the global handler, which is replaceable application code and is not inside a callback scope for that operation, so a handler answering with Session::FinishSession() reached HttpOperation::Finish(). That is not InsideCallbackFor(this), so it waits on a promise only the FinishUnscheduled below it fulfils, on the same thread. Reading the operation after the log was the other half. A handler calling SendRequest() from there swaps the operation, and the loop then finishes the new one and leaves the refused one unsettled. FinishUnscheduled dispatches its event before it cleans up, and a handler calling FinishSession() from that event is inside a callback for the operation, so Finish() returns instead of waiting. Reporting after it is what puts the log handler inside the same protection. ARejectedSessionIsSettledBeforeItIsReported installs a global log handler that records whether the terminal event has arrived by the time the refusal is logged, and restores the previous handler and level through a scope guard. It passes with this change and fails at the ordering assertion with the report moved back above the settlement. The first version of that assertion asked only whether the handler had seen any event. That is true in both orders, because SendAsync dispatches Created during setup, and the case passed with the defect put back. It asks for CreateFailed now. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 828b322 commit 4784881

2 files changed

Lines changed: 112 additions & 1 deletion

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,6 @@ bool HttpClient::doAddSessions()
784784
for (auto &rejected : rejected_by_multi)
785785
{
786786
const char *reason = curl_multi_strerror(rejected.second);
787-
OTEL_INTERNAL_LOG_ERROR("[HTTP Client Curl] curl_multi_add_handle failed: " << reason);
788787

789788
// Told the same way as a session this client never registered, because it is the same thing
790789
// from the handler's side: nothing is going to run this request.
@@ -793,6 +792,11 @@ bool HttpClient::doAddSessions()
793792
{
794793
operation->FinishUnscheduled(reason);
795794
}
795+
796+
// Settled first. A log handler is replaceable application code, and one that calls
797+
// FinishSession() from here would otherwise wait on a promise only the line above fulfils,
798+
// on this thread.
799+
OTEL_INTERNAL_LOG_ERROR("[HTTP Client Curl] curl_multi_add_handle failed: " << reason);
796800
}
797801

798802
// Finishing a rejected session queues its removal, and the loop's idle check has already run

‎ext/test/http/curl_http_test.cc‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# include <numeric>
1414
#endif // ENABLE_OTLP_COMPRESSION_PREVIEW
1515

16+
#include <algorithm>
1617
#include <atomic>
1718
#include <chrono>
1819
#include <condition_variable>
@@ -34,12 +35,14 @@
3435
#include "opentelemetry/ext/http/server/http_server.h"
3536
#include "opentelemetry/nostd/function_ref.h"
3637
#include "opentelemetry/nostd/string_view.h"
38+
#include "opentelemetry/sdk/common/global_log_handler.h"
3739
#include "opentelemetry/version.h"
3840

3941
constexpr int HTTP_PORT{19000};
4042

4143
namespace curl = opentelemetry::ext::http::client::curl;
4244
namespace http_client = opentelemetry::ext::http::client;
45+
namespace sdk_common = opentelemetry::sdk::common;
4346
namespace nostd = opentelemetry::nostd;
4447

4548
OPENTELEMETRY_BEGIN_NAMESPACE
@@ -783,6 +786,110 @@ TEST_F(BasicCurlHttpTests, AnUnscheduledSessionTellsAHandlerNothingElseHolds)
783786
http_client::curl::HttpClientTestPeer::RemoveSessions(client);
784787
}
785788

789+
// Restores the global log handler and level whatever the case does, since both are process wide
790+
// and gtest runs one process per case only under CTest.
791+
class ScopedLogHandler
792+
{
793+
public:
794+
explicit ScopedLogHandler(nostd::shared_ptr<sdk_common::internal_log::LogHandler> handler)
795+
: previous_handler_(sdk_common::internal_log::GlobalLogHandler::GetLogHandler()),
796+
previous_level_(sdk_common::internal_log::GlobalLogHandler::GetLogLevel())
797+
{
798+
sdk_common::internal_log::GlobalLogHandler::SetLogHandler(handler);
799+
sdk_common::internal_log::GlobalLogHandler::SetLogLevel(
800+
sdk_common::internal_log::LogLevel::Debug);
801+
}
802+
803+
~ScopedLogHandler()
804+
{
805+
sdk_common::internal_log::GlobalLogHandler::SetLogHandler(previous_handler_);
806+
sdk_common::internal_log::GlobalLogHandler::SetLogLevel(previous_level_);
807+
}
808+
809+
ScopedLogHandler(const ScopedLogHandler &) = delete;
810+
ScopedLogHandler &operator=(const ScopedLogHandler &) = delete;
811+
812+
private:
813+
nostd::shared_ptr<sdk_common::internal_log::LogHandler> previous_handler_;
814+
sdk_common::internal_log::LogLevel previous_level_;
815+
};
816+
817+
// Runs whatever the case gives it, on the thread that wrote the log line.
818+
class ObservingLogHandler : public sdk_common::internal_log::LogHandler
819+
{
820+
public:
821+
explicit ObservingLogHandler(std::function<void()> observe) : observe_(std::move(observe)) {}
822+
823+
void Handle(sdk_common::internal_log::LogLevel /* level */,
824+
const char * /* file */,
825+
int /* line */,
826+
const char * /* msg */,
827+
const sdk_common::AttributeMap & /* attributes */) noexcept override
828+
{
829+
observe_();
830+
}
831+
832+
private:
833+
std::function<void()> observe_;
834+
};
835+
836+
// The refusal is reported through the global log handler, which is application code that can call
837+
// back into this client. Reporting before the operation is settled leaves a handler that answers
838+
// with FinishSession() waiting on a promise only this thread can fulfil.
839+
TEST_F(BasicCurlHttpTests, ARejectedSessionIsSettledBeforeItIsReported)
840+
{
841+
curl::HttpClient client;
842+
843+
auto session = client.CreateSession("http://127.0.0.1:19000");
844+
ASSERT_TRUE(session != nullptr);
845+
auto curl_session = std::static_pointer_cast<curl::Session>(session);
846+
847+
auto handler = std::make_shared<RecordingHandler>();
848+
http_client::HttpSslOptions no_ssl;
849+
http_client::Body body;
850+
http_client::Headers headers;
851+
http_client::RetryPolicy no_retry{};
852+
http_client::Compression compression = http_client::Compression::kNone;
853+
854+
curl_session->GetOperation().reset(new curl::HttpOperation(
855+
http_client::Method::Get, "http://127.0.0.1:19000/get/", no_ssl, handler.get(), headers, body,
856+
compression, false, curl::kDefaultHttpConnTimeout, false, false, no_retry));
857+
858+
ASSERT_EQ(CURLE_OK, curl_session->GetOperation()->SendAsync(
859+
curl_session.get(), [](curl::HttpOperation & /* operation */) {}));
860+
861+
std::atomic<int> log_calls{0};
862+
std::atomic<int> settled_when_logged{0};
863+
auto observer = nostd::shared_ptr<sdk_common::internal_log::LogHandler>(
864+
new ObservingLogHandler([&handler, &log_calls, &settled_when_logged]() {
865+
log_calls.fetch_add(1, std::memory_order_release);
866+
867+
// The terminal event specifically. Created arrives during setup, so "the handler has
868+
// seen something" is true in either order.
869+
const std::vector<http_client::SessionState> states = handler->States();
870+
if (std::find(states.begin(), states.end(), http_client::SessionState::CreateFailed) !=
871+
states.end())
872+
{
873+
settled_when_logged.fetch_add(1, std::memory_order_release);
874+
}
875+
}));
876+
877+
{
878+
ScopedLogHandler scoped{observer};
879+
http_client::curl::HttpClientTestPeer::RefuseAdds(client);
880+
http_client::curl::HttpClientTestPeer::AddSessions(client);
881+
http_client::curl::HttpClientTestPeer::AllowAdds(client);
882+
}
883+
884+
ASSERT_EQ(1, log_calls.load(std::memory_order_acquire))
885+
<< "the refusal was not reported once, so this case is not observing what it says it is";
886+
EXPECT_EQ(1, settled_when_logged.load(std::memory_order_acquire))
887+
<< "the refusal was reported while the operation was still unsettled, so a log handler "
888+
<< "calling FinishSession() from here would wait on a promise only this thread fulfils";
889+
890+
http_client::curl::HttpClientTestPeer::RemoveSessions(client);
891+
}
892+
786893
// The third way an operation ends up with nothing to run it. SendAsync does the whole async
787894
// setup and Session::SendRequest is what spawns the worker, so the add runs here on one thread
788895
// against a multi handle that refuses it, which is what a multi handle that failed to initialize

0 commit comments

Comments
 (0)