test(quic): SendSync returns for a send the peer answered at once
ShouldReturnFromSendSyncQUIC: 500 rounds of four fresh connections, each sending one message on its first stream to a server that echoes it with a FIN and drops the stream; every SendSync must return and every echo come back. The race it guards needs the sending thread descheduled for a round trip, so it is a stress test, not a deterministic one: against the previous SendSync it failed 6 runs of 7, with one or two of the 2000 sends throwing each time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b1b0633a27
commit
be55036b18
2 changed files with 110 additions and 0 deletions
109
tests/ShouldReturnFromSendSyncQUIC/main.cpp
Normal file
109
tests/ShouldReturnFromSendSyncQUIC/main.cpp
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// QUICStream::SendSync returns once msquic has taken its buffer, and throws
|
||||
// only when that send failed. It used to wait on one flag per stream and,
|
||||
// once woken, throw QUICClosedException whenever the stream had finished
|
||||
// shutting down meanwhile, though its own send had completed. A peer that
|
||||
// answers at once and finishes the stream (here: echo, FIN, and drop the
|
||||
// stream, whose Stop() stops our sending side too) can complete the stream
|
||||
// within a round trip, which on loopback is shorter than a busy machine can
|
||||
// take to wake the sending thread: the sender threw for a send the peer had
|
||||
// already answered.
|
||||
//
|
||||
// The race needs the sending thread to be descheduled at the wrong moment,
|
||||
// so no test hits it every time. The shape that showed it: the first stream
|
||||
// of a fresh connection, four connections at once, one exchange each; a
|
||||
// throwaway run of that shape threw once in 3000 exchanges, the echo of the
|
||||
// failed send arriving intact. Here 2000 such exchanges must all return
|
||||
// from SendSync, and all be echoed.
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
import Crafter.Network;
|
||||
import Crafter.Thread;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
ThreadPool::Start();
|
||||
std::thread([] {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(50));
|
||||
std::println("FAIL timed out");
|
||||
std::fflush(stdout);
|
||||
std::_Exit(1);
|
||||
}).detach();
|
||||
|
||||
const std::string alpn = "crafter/test-send-sync";
|
||||
QUICServerCredentials serverCreds;
|
||||
serverCreds.selfSigned = true;
|
||||
QUICClientCredentials clientCreds;
|
||||
clientCreds.insecureNoServerValidation = true;
|
||||
|
||||
constexpr int rounds = 500;
|
||||
constexpr int together = 4;
|
||||
std::atomic<int> threw = 0;
|
||||
std::atomic<int> echoed = 0;
|
||||
std::atomic<int> other = 0;
|
||||
try {
|
||||
// Answer each stream with what arrived and a FIN, then drop it: its
|
||||
// destructor's Stop() aborts our receive side, which also ends the
|
||||
// client's sending side.
|
||||
ListenerQUIC listener("127.0.0.1", 0, alpn, serverCreds, [](ClientQUIC* peer) {
|
||||
peer->OnStream([](QUICStream stream) {
|
||||
try {
|
||||
std::vector<char> got = stream.RecieveSync();
|
||||
stream.SendSync(got.data(), static_cast<std::uint32_t>(got.size()), true);
|
||||
} catch (...) {}
|
||||
});
|
||||
});
|
||||
listener.ListenAsyncAsync();
|
||||
|
||||
for (int round = 0; round < rounds; ++round) {
|
||||
std::vector<std::thread> clients;
|
||||
for (int i = 0; i < together; ++i) {
|
||||
clients.emplace_back([&] {
|
||||
try {
|
||||
ClientQUIC client("127.0.0.1", listener.Port(), alpn, clientCreds);
|
||||
QUICStream stream = client.OpenStream();
|
||||
try {
|
||||
stream.SendSync("ping", 4, false);
|
||||
} catch (const QUICClosedException&) {
|
||||
++threw;
|
||||
}
|
||||
std::vector<char> back = stream.RecieveUntilCloseSync();
|
||||
if (std::string(back.begin(), back.end()) == "ping") ++echoed;
|
||||
} catch (const std::exception& e) {
|
||||
if (++other == 1) std::println(" {}", e.what());
|
||||
}
|
||||
});
|
||||
}
|
||||
for (std::thread& client : clients) client.join();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
Check(false, std::format("threw: {}", e.what()));
|
||||
}
|
||||
constexpr int exchanges = rounds * together;
|
||||
Check(threw.load() == 0,
|
||||
std::format("SendSync returned for every send msquic took ({} of {} threw QUICClosedException)",
|
||||
threw.load(), exchanges));
|
||||
Check(echoed.load() == exchanges && other.load() == 0,
|
||||
std::format("and every echo came back ({} of {}, {} other errors)", echoed.load(), exchanges, other.load()));
|
||||
|
||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||
// The accepted peers are left to the process: skip msquic's static
|
||||
// teardown, as the other QUIC tests do. _Exit doesn't flush.
|
||||
std::fflush(stdout);
|
||||
std::_Exit(failures == 0 ? 0 : 1);
|
||||
}
|
||||
Loading…
Reference in a new issue