From 9f4f482c627eff3494efdc0ee63094efd609e576 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 28 Oct 2026 08:23:14 -0400 Subject: [PATCH 1/2] Wait for every held message before closing in the resume-order test TransportTest.HeldBackConnectionsResumeInTheOrderTheyWereHeld raced the network thread in two places: - The first connection's close is raised outside the inbound ring's room, so it could reach core while later connections were still parked. The test then closed them and their messages never arrived. It now also waits until every expected message has been delivered. - wait_taken() shows that the kernel bytes were read, not that the push was refused. The last later message could therefore be pushed once core freed room, ahead of the parked ones. A probe connection that core closes after that message is taken now ends only after the network thread has pushed it. Under loaded TSan runs on macOS the test failed 26 times in 900 before this change and 0 times in 1800 after it. --- tests/test_transport.cpp | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/test_transport.cpp b/tests/test_transport.cpp index 15b27af..2142406 100644 --- a/tests/test_transport.cpp +++ b/tests/test_transport.cpp @@ -974,6 +974,18 @@ TEST_F(TransportTest, HeldBackConnectionsResumeInTheOrderTheyWereHeld) { open_sends.push_back(std::make_unique(*send[n])); this->run_until([&] { return this->accepted.size() == n + 2; }); } + // A probe that sends nothing and reads until the server has closed it + Gate probe_ended; + const Peer probe(this->io, [&] { + const OpenOnExit open_ended(probe_ended); + asio::io_context peer_io; + PeerSocket ws(peer_io); + connect_peer(ws, port); + beast::flat_buffer buffer; + error_code ec; + ws.read(buffer, ec); + }); + this->run_until([&] { return this->accepted.size() == LATER + 2; }); std::vector order; std::vector> handlers; for (size_t c = 0; c < this->accepted.size(); ++c) { @@ -996,24 +1008,34 @@ TEST_F(TransportTest, HeldBackConnectionsResumeInTheOrderTheyWereHeld) { written[n]->wait(); wait_taken(server_fd, later_fds[n]); } + // The network thread ends the probe's closing handshake only after it pushed the last later + // message it took, so that message was refused before core drains the ring + this->accepted[LATER + 1].close(close_code::NORMAL); + probe_ended.wait(); + std::vector expected(held, "first"); + for (size_t n = 0; n < LATER; ++n) { + expected.push_back("later " + std::to_string(n)); + } first_close.open(); - this->run_until([&] { return first.done() && !handlers[0]->closes.empty(); }); + // The first's close is not held for room in the ring, so it can reach core while later + // connections still wait to push: those are closed only once every message arrived + this->run_until([&] { + return first.done() && !handlers[0]->closes.empty() && order.size() == expected.size(); + }); for (size_t n = 0; n < LATER; ++n) { this->accepted[n + 1].close(close_code::NORMAL); } this->run_until([&] { - return std::all_of(later.begin(), later.end(), + return probe.done() && + std::all_of(later.begin(), later.end(), [](const std::unique_ptr& peer) { return peer->done(); }); }); ASSERT_EQ(first.error(), ""); + ASSERT_EQ(probe.error(), ""); for (const std::unique_ptr& peer : later) { ASSERT_EQ(peer->error(), ""); } - std::vector expected(held, "first"); - for (size_t n = 0; n < LATER; ++n) { - expected.push_back("later " + std::to_string(n)); - } EXPECT_EQ(order, expected); } From e685385a1c61db3f129b1fcf77e4721a794b5827 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 28 Oct 2026 08:33:25 -0400 Subject: [PATCH 2/2] Declare the resume-order test's probe peer non-const The peer's completion handler assigns its members after construction, which a const object does not allow. --- tests/test_transport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transport.cpp b/tests/test_transport.cpp index 2142406..cd04177 100644 --- a/tests/test_transport.cpp +++ b/tests/test_transport.cpp @@ -976,7 +976,7 @@ TEST_F(TransportTest, HeldBackConnectionsResumeInTheOrderTheyWereHeld) { } // A probe that sends nothing and reads until the server has closed it Gate probe_ended; - const Peer probe(this->io, [&] { + Peer probe(this->io, [&] { const OpenOnExit open_ended(probe_ended); asio::io_context peer_io; PeerSocket ws(peer_io);