Skip to content

Commit a1dce6e

Browse files
committed
[CHORE] Say what the code does, and let the cases outlive what talks to them
A pass over the things this project's reviews ask for every time. Comments describing how the code got here rather than what it is. Three of them: the stale queue check explaining what an earlier version of itself did, the construction result explaining what dispatching from there used to cause, and a test comment citing the ThreadSanitizer run that made it what it is. All three now say what the code does, and the rest is in the commit history where it belongs. A comment that had drifted off its class. The one describing what MultiHandleOutcomeHandler counts had come to rest above CountingLogHandler, which counts something else entirely. It is back where it belongs, and the peer comment that pointed at "the case below" names the case instead, since the case is nine hundred lines below. The ordering that makes a queued request safe was not written down anywhere. ScheduleAddSession inserts the id before Session::SendRequest asks for a background thread, and the retirement check holds background_thread_m_ while it looks for work, so a thread on its way out either sees the id and stays or has already cleared background_thread_ and the spawn that follows makes a new one. Queue first, spawn second, and now it says so. Two cases had no assertion at all, leaving LeakSanitizer as the only thing that could fail them, which also meant an injection that stopped working read as a pass. Both now check that the handle really was queued before asking whether it was released. The count they use is taken under session_ids_m_, which every producer of that queue takes, unlike the retry queue which has no lock and is not a test's to read. And three cases declared their handler after the client. The client dispatches terminal events from its destructor, and members go in reverse, so the handler was already gone by the time it might be called. Nothing reached it, but that was luck rather than design. All three declare it first now. All 40 cases pass, and clean under AddressSanitizer with detect_leaks=1. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent f591784 commit a1dce6e

3 files changed

Lines changed: 39 additions & 21 deletions

File tree

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,10 +1054,9 @@ bool HttpClient::hasActionableWork()
10541054
std::lock_guard<std::mutex> session_lock_guard{sessions_m_};
10551055
std::lock_guard<std::recursive_mutex> session_id_lock_guard{session_ids_m_};
10561056

1057-
// An id whose session has gone is what doAddSessions would drop on its next pass, so it is
1058-
// dropped here too rather than counted. The difference matters: the first version of this
1059-
// check took every queue at its size, and an entry nothing could be done about held the
1060-
// thread open against the join in the destructor.
1057+
// An id whose session has gone is what doAddSessions drops on its next pass, so it is
1058+
// dropped here too rather than counted. Counting an entry that can never drain would keep
1059+
// this thread alive against the join in the destructor.
10611060
for (auto id = pending_to_add_session_ids_.begin(); id != pending_to_add_session_ids_.end();)
10621061
{
10631062
const auto session = sessions_.find(*id);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,8 @@ HttpOperation::HttpOperation(opentelemetry::ext::http::client::Method method,
455455
last_curl_result_ = CURLE_FAILED_INIT;
456456
// Refuses Send() and SendAsync(), which would otherwise drive a null handle into libcurl.
457457
construction_result_ = CURLE_FAILED_INIT;
458-
// Terminal already, and reported by the caller that sees SendAsync refuse, the same way the
459-
// header list failure below is. Dispatching here as well told the handler twice.
458+
// Terminal already, and reported once by the caller that sees SendAsync refuse, the same
459+
// way the header list failure below is. Nothing is dispatched from here.
460460
session_state_ = opentelemetry::ext::http::client::SessionState::CreateFailed;
461461
return;
462462
}

‎ext/test/http/curl_http_test.cc‎

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,22 @@ namespace client
5454
namespace curl
5555
{
5656
// resetMultiHandle only runs when curl_multi_perform fails, which a test cannot provoke, so
57-
// the case below reaches it directly. See #4389.
57+
// ResetMultiHandleWithASessionDoesNotDeadlock reaches it directly. See #4389.
5858
class HttpClientTestPeer
5959
{
6060
public:
6161
static void ResetMultiHandle(HttpClient &client) { client.resetMultiHandle(); }
6262

6363
static bool RemoveSessions(HttpClient &client) { return client.doRemoveSessions(); }
6464

65+
// Guarded by session_ids_m_, which is what every producer of this queue takes, so a case may
66+
// read it whatever else is running.
67+
static std::size_t PendingRemovalCount(HttpClient &client)
68+
{
69+
std::lock_guard<std::recursive_mutex> lock_guard{client.session_ids_m_};
70+
return client.pending_to_remove_session_handles_.size();
71+
}
72+
6573
static CURLM *ExchangeMultiHandle(HttpClient &client, CURLM *replacement)
6674
{
6775
CURLM *previous = client.multi_handle_;
@@ -412,9 +420,6 @@ struct FailCurlCallocEverywhere
412420
FailCurlCallocEverywhere &operator=(FailCurlCallocEverywhere &&) = delete;
413421
};
414422

415-
// Counts terminal outcomes without caring which one, since a client whose multi handle could
416-
// not be created may still recover and answer, and the case below is about the caller being
417-
// told either way rather than about which answer it gets.
418423
// Counts the internal log lines that say a particular thing, so a case can hold that a run of
419424
// failures is reported a bounded number of times rather than once per pass of the IO loop. Only
420425
// the ones it asked for: anything else the binary writes while this is installed would move the
@@ -471,6 +476,9 @@ class ScopedLogHandler
471476
const nostd::shared_ptr<opentelemetry::sdk::common::internal_log::LogHandler> previous_;
472477
};
473478

479+
// Counts terminal outcomes without caring which one, since a client whose multi handle could
480+
// not be created may still recover and answer, and what the cases using this ask is whether the
481+
// caller was told either way rather than which answer it got.
474482
class MultiHandleOutcomeHandler : public http_client::EventHandler
475483
{
476484
public:
@@ -1851,13 +1859,12 @@ TEST_F(BasicCurlHttpTests, ACancelledRetryDoesNotHoldTheClientOpen)
18511859

18521860
session->SendRequest(handler);
18531861

1854-
// Watched from outside the client. The retry queue has no lock, because only the
1855-
// background thread is supposed to touch it, and a peer that read it from here would make
1856-
// that untrue: ThreadSanitizer reports exactly that, twice, against a version of this case
1857-
// that did. So it waits on what the server saw, which is behind a mutex, and then for long
1858-
// enough that the answer has been read and the session put back with its wait on it. A
1859-
// second in, that wait still has seven to run, so anything outstanding here is outstanding
1860-
// because it is queued.
1862+
// Watched from outside the client, because the retry queue has no lock: only the
1863+
// background thread is meant to touch it, and reading it from here would make that untrue.
1864+
// So this waits on what the server saw, which is behind a mutex, and then long enough for
1865+
// the answer to have been read and the session put back with its wait on it. A second in,
1866+
// that wait still has seven to run, so anything outstanding here is outstanding because it
1867+
// is queued.
18611868
ASSERT_TRUE(waitForRequests(30, 1)) << "the request never reached the server";
18621869
std::this_thread::sleep_for(std::chrono::seconds(1));
18631870
ASSERT_EQ(0, handler->terminal_.load(std::memory_order_acquire))
@@ -1889,6 +1896,9 @@ TEST_F(BasicCurlHttpTests, AQueuedHandleIsReleasedWithTheClientThatQueuedIt)
18891896
resource.headers_chunk = curl_slist_append(nullptr, "X-Test: 1");
18901897
ASSERT_TRUE(resource.headers_chunk != nullptr);
18911898
client->ScheduleRemoveSession(4404, std::move(resource));
1899+
ASSERT_EQ(static_cast<std::size_t>(1),
1900+
http_client::curl::HttpClientTestPeer::PendingRemovalCount(*client))
1901+
<< "the handle was never queued, so nothing was tested";
18921902

18931903
// Nothing was sent, so there is no IO thread and nobody else is coming for that queue. The
18941904
// record is two raw pointers and the container that holds it frees neither, so what is still
@@ -1911,6 +1921,9 @@ TEST_F(BasicCurlHttpTests, AHandleQueuedWithoutAMultiHandleIsReleased)
19111921

19121922
CURLM *previous = http_client::curl::HttpClientTestPeer::ExchangeMultiHandle(*concrete, nullptr);
19131923
concrete->ScheduleRemoveSession(4404, std::move(resource));
1924+
ASSERT_EQ(static_cast<std::size_t>(1),
1925+
http_client::curl::HttpClientTestPeer::PendingRemovalCount(*concrete))
1926+
<< "the handle was never queued, so nothing was tested";
19141927

19151928
// What resetMultiHandle does when curl_multi_init has just failed on it. The easy handle and
19161929
// its header list are released rather than held until a multi handle comes back, and neither
@@ -1929,6 +1942,9 @@ TEST_F(BasicCurlHttpTests, AQueuedRequestSurvivesAMissingMultiHandle)
19291942
// Built without a handle, for the reason given above: a case that took one away would be
19301943
// using it from two threads at once, and could not then say whether what it saw was the
19311944
// client's behaviour or its own.
1945+
// Before the client, so that it outlives it. The client dispatches terminal events from its
1946+
// destructor, and a handler declared after it would be gone by then.
1947+
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
19321948
FailCurlCallocEverywhere failing;
19331949
auto client = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
19341950
ASSERT_TRUE(client != nullptr);
@@ -1948,7 +1964,6 @@ TEST_F(BasicCurlHttpTests, AQueuedRequestSurvivesAMissingMultiHandle)
19481964
auto session = client->CreateSession("http://127.0.0.1:19000");
19491965
auto request = session->CreateRequest();
19501966
request->SetUri("get/");
1951-
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
19521967

19531968
// Back to zero after the constructor's own attempt and before there is an IO thread to make
19541969
// one, and this thread is exempt from here on, so what is counted below was refused to that
@@ -1991,13 +2006,15 @@ TEST_F(BasicCurlHttpTests, AFailedEasyHandleIsReportedOnce)
19912006
ASSERT_TRUE(g_curl_hooks_installed);
19922007
received_requests_.clear();
19932008

1994-
auto client = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
2009+
// Before the client, so that it outlives it. The client dispatches terminal events from its
2010+
// destructor, and a handler declared after it would be gone by then.
2011+
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
2012+
auto client = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
19952013
ASSERT_TRUE(client != nullptr);
19962014

19972015
auto session = client->CreateSession("http://127.0.0.1:19000");
19982016
auto request = session->CreateRequest();
19992017
request->SetUri("get/");
2000-
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
20012018

20022019
{
20032020
// Armed here so it reaches the curl_easy_init inside SendRequest and nothing before it.
@@ -2039,6 +2056,9 @@ TEST_F(BasicCurlHttpTests, APersistentMultiHandleFailureDoesNotSpin)
20392056
// would leave any result this case reported open to being an artefact of the injection.
20402057
// What fails is curl_multi_init, on whichever thread calls it, and after the constructor
20412058
// that is the IO thread every time.
2059+
// Before the client, so that it outlives it. The client dispatches terminal events from its
2060+
// destructor, and a handler declared after it would be gone by then.
2061+
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
20422062
FailCurlCallocEverywhere failing;
20432063
auto client = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
20442064
ASSERT_TRUE(client != nullptr);
@@ -2052,7 +2072,6 @@ TEST_F(BasicCurlHttpTests, APersistentMultiHandleFailureDoesNotSpin)
20522072
auto session = client->CreateSession("http://127.0.0.1:19000");
20532073
auto request = session->CreateRequest();
20542074
request->SetUri("get/");
2055-
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
20562075

20572076
// Back to zero after the constructor's own attempt and before there is an IO thread to
20582077
// make one, and this thread is exempt from here on, so what is counted below was refused

0 commit comments

Comments
 (0)