Skip to content

Commit f591784

Browse files
committed
[TEST] Watch the retry from outside the client, not from inside its queue
The two retry cases waited by polling the retry queue through a test peer. That queue has no lock, because only the background thread is supposed to touch it, and a peer reading it from the test thread makes that untrue. ThreadSanitizer says so: two data races, both this read against the background thread's push_back, and both of them mine rather than the client's. So the wait moved outside. It waits on what the server received, which is behind a mutex and a condition variable, and then for long enough that the answer has been read and the session put back with its wait on it. The wait is eight seconds and the settling is one, so a request still outstanding at that point is outstanding because it is queued, and there are seven seconds left for the case to be wrong in. The first attempt at this waited for the server to see a second request, on the grounds that a retry having fired proves the queue works. It does, but it proves it one step too late: at that moment the client is reading the second answer rather than holding a queued session, so cancelling caught a transfer in flight and the mutation check went green. Both cases now fail against a queue check that only drops entries with no operation, three runs out of three, at 5.40 seconds against a bound of four, and pass in about a second. ThreadSanitizer reports nothing on the whole binary now, and all 40 cases pass. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 4b81428 commit f591784

1 file changed

Lines changed: 29 additions & 33 deletions

File tree

‎ext/test/http/curl_http_test.cc‎

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@ class HttpClientTestPeer
6262

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

65-
#ifdef ENABLE_OTLP_RETRY_PREVIEW
66-
static std::size_t RetryQueueSize(const HttpClient &client)
67-
{
68-
return client.pending_to_retry_sessions_.size();
69-
}
70-
#endif // ENABLE_OTLP_RETRY_PREVIEW
71-
7265
static CURLM *ExchangeMultiHandle(HttpClient &client, CURLM *replacement)
7366
{
7467
CURLM *previous = client.multi_handle_;
@@ -1799,30 +1792,30 @@ TEST_F(BasicCurlHttpTests, AClientDestroyedWithAPendingRetryDoesNotWaitForIt)
17991792
// round.
18001793
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
18011794

1802-
const auto started = std::chrono::steady_clock::now();
1795+
std::chrono::steady_clock::time_point started{};
18031796
{
18041797
auto client = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
18051798
ASSERT_TRUE(client != nullptr);
1806-
auto *concrete = static_cast<http_client::curl::HttpClient *>(client.get());
18071799

18081800
auto session = client->CreateSession("http://127.0.0.1:19000");
18091801
auto request = session->CreateRequest();
18101802
request->SetUri("retry/");
18111803
request->SetMethod(http_client::Method::Post);
1812-
1813-
// Long enough that waiting for it would be unmistakable inside the bound below.
18141804
request->SetRetryPolicy(
18151805
{4, std::chrono::duration<float>{8.0f}, std::chrono::duration<float>{16.0f}, 2.0f});
18161806

18171807
session->SendRequest(handler);
18181808

1819-
for (int i = 0;
1820-
i < 300 && 0 == http_client::curl::HttpClientTestPeer::RetryQueueSize(*concrete); ++i)
1821-
{
1822-
std::this_thread::sleep_for(std::chrono::milliseconds(10));
1823-
}
1824-
ASSERT_GE(http_client::curl::HttpClientTestPeer::RetryQueueSize(*concrete), 1u)
1825-
<< "the request was never queued for retry, so nothing was tested";
1809+
ASSERT_TRUE(waitForRequests(30, 1)) << "the request never reached the server";
1810+
std::this_thread::sleep_for(std::chrono::seconds(1));
1811+
ASSERT_EQ(0, handler->terminal_.load(std::memory_order_acquire))
1812+
<< "the request had already finished, so nothing was pending";
1813+
1814+
started = std::chrono::steady_clock::now();
1815+
1816+
// Nothing cancels it. The client simply goes, which is what an exporter shutting down
1817+
// does, and the entry it leaves behind names an operation holding a promise and an easy
1818+
// handle.
18261819
}
18271820
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
18281821
std::chrono::steady_clock::now() - started);
@@ -1840,34 +1833,37 @@ TEST_F(BasicCurlHttpTests, ACancelledRetryDoesNotHoldTheClientOpen)
18401833
{
18411834
received_requests_.clear();
18421835

1843-
const auto started = std::chrono::steady_clock::now();
1836+
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
1837+
std::chrono::steady_clock::time_point started{};
18441838
{
18451839
auto client = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
18461840
ASSERT_TRUE(client != nullptr);
1847-
auto *concrete = static_cast<http_client::curl::HttpClient *>(client.get());
18481841

18491842
auto session = client->CreateSession("http://127.0.0.1:19000");
18501843
auto request = session->CreateRequest();
18511844
request->SetUri("retry/");
18521845
request->SetMethod(http_client::Method::Post);
18531846

1854-
// Long enough that waiting for it would be unmistakable, and long enough that it cannot
1855-
// come round on its own inside the bound below.
1847+
// Long enough that waiting it out would be unmistakable, and long enough that it cannot
1848+
// come round on its own while the case is still setting up.
18561849
request->SetRetryPolicy(
18571850
{4, std::chrono::duration<float>{8.0f}, std::chrono::duration<float>{16.0f}, 2.0f});
18581851

1859-
auto handler = std::make_shared<MultiHandleOutcomeHandler>();
18601852
session->SendRequest(handler);
18611853

1862-
// The server answers this route with something retryable, so the session goes into the
1863-
// retry queue with a time on it that has not come.
1864-
for (int i = 0;
1865-
i < 300 && 0 == http_client::curl::HttpClientTestPeer::RetryQueueSize(*concrete); ++i)
1866-
{
1867-
std::this_thread::sleep_for(std::chrono::milliseconds(10));
1868-
}
1869-
ASSERT_GE(http_client::curl::HttpClientTestPeer::RetryQueueSize(*concrete), 1u)
1870-
<< "the request was never queued for retry, so nothing was tested";
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.
1861+
ASSERT_TRUE(waitForRequests(30, 1)) << "the request never reached the server";
1862+
std::this_thread::sleep_for(std::chrono::seconds(1));
1863+
ASSERT_EQ(0, handler->terminal_.load(std::memory_order_acquire))
1864+
<< "the request had already finished, so nothing was pending";
1865+
1866+
started = std::chrono::steady_clock::now();
18711867

18721868
// Cancelling takes the operation apart and hands its easy handle back, and the entry left
18731869
// behind names an operation that is not going to be retried by anybody.
@@ -1878,7 +1874,7 @@ TEST_F(BasicCurlHttpTests, ACancelledRetryDoesNotHoldTheClientOpen)
18781874
std::chrono::steady_clock::now() - started);
18791875

18801876
EXPECT_LT(elapsed.count(), 4000)
1881-
<< "destroying the client took " << elapsed.count()
1877+
<< "tearing the client down took " << elapsed.count()
18821878
<< " ms, which is it waiting out a retry for an operation that had already finished";
18831879
}
18841880
#endif // ENABLE_OTLP_RETRY_PREVIEW

0 commit comments

Comments
 (0)