//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 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 threw = 0; std::atomic echoed = 0; std::atomic 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 got = stream.RecieveSync(); stream.SendSync(got.data(), static_cast(got.size()), true); } catch (...) {} }); }); listener.ListenAsyncAsync(); for (int round = 0; round < rounds; ++round) { std::vector 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 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); }