test(quic): cover the peer being told that a ClientQUIC closed
ShouldTellPeerOfCloseQUIC: twenty clients connect to a listener whose idle timeout is 20 s and are destroyed in turn; for each, the server side's RecieveDatagramSync, which throws once the peer has closed the connection, must return within 2 s. Before the fix the first one never did, since ~ClientQUIC closed its handle before msquic had sent the CONNECTION_CLOSE frame; the loop stops at the first miss, as every later connection would wait out its 2 s too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
795f06178d
commit
793b518718
2 changed files with 122 additions and 0 deletions
121
tests/ShouldTellPeerOfCloseQUIC/main.cpp
Normal file
121
tests/ShouldTellPeerOfCloseQUIC/main.cpp
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// ~ClientQUIC tells the peer that the connection is over. It used to ask
|
||||
// msquic to shut the connection down and then close the handle at once.
|
||||
// msquic sends the CONNECTION_CLOSE frame from a flush it queues while it
|
||||
// processes that shutdown, and the close the destructor queued meanwhile
|
||||
// usually reached the worker first: it completed the shutdown silently, the
|
||||
// flush found the connection finished and sent nothing, and the peer learned
|
||||
// of the close only from its idle timeout, two minutes by default. A server
|
||||
// whose clients each live for one request kept every one of them that long.
|
||||
//
|
||||
// Twenty clients connect and are destroyed in turn. For each, the server
|
||||
// side's RecieveDatagramSync, which throws once the peer has closed the
|
||||
// connection, must return within 2 s; the listener's idle timeout is 20 s, so
|
||||
// a close the peer was never told about shows as a timeout here.
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
// The server's side of one connection: when it saw the peer close.
|
||||
struct Seen {
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
std::optional<std::chrono::steady_clock::time_point> closedAt;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
ThreadPool::Start();
|
||||
std::thread([] {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(45));
|
||||
std::println("FAIL timed out");
|
||||
std::fflush(stdout);
|
||||
std::_Exit(1);
|
||||
}).detach();
|
||||
|
||||
const std::string alpn = "crafter/test-close-notify";
|
||||
QUICServerCredentials serverCreds;
|
||||
serverCreds.selfSigned = true;
|
||||
serverCreds.settings.idleTimeoutMs = 20'000;
|
||||
QUICClientCredentials clientCreds;
|
||||
clientCreds.insecureNoServerValidation = true;
|
||||
|
||||
std::mutex mutex;
|
||||
std::deque<std::shared_ptr<Seen>> waiting;
|
||||
|
||||
try {
|
||||
// Each accepted connection gets a thread parked in RecieveDatagramSync
|
||||
// until the connection closes. The ClientQUIC is left alone: what is
|
||||
// under test is the peer hearing of the close, not its owner.
|
||||
ListenerQUIC listener("127.0.0.1", 0, alpn, serverCreds, [&](ClientQUIC* peer) {
|
||||
std::shared_ptr<Seen> seen;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
seen = waiting.front();
|
||||
waiting.pop_front();
|
||||
std::thread([peer, seen] {
|
||||
try {
|
||||
while (true) (void)peer->RecieveDatagramSync();
|
||||
} catch (const QUICClosedException&) {}
|
||||
{
|
||||
std::lock_guard lock(seen->mutex);
|
||||
seen->closedAt = std::chrono::steady_clock::now();
|
||||
}
|
||||
seen->changed.notify_all();
|
||||
}).detach();
|
||||
}
|
||||
});
|
||||
listener.ListenAsyncSync();
|
||||
|
||||
int told = 0;
|
||||
std::chrono::milliseconds slowest{ 0 };
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
auto seen = std::make_shared<Seen>();
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
waiting.push_back(seen);
|
||||
}
|
||||
std::chrono::steady_clock::time_point destroyed;
|
||||
{
|
||||
ClientQUIC client("127.0.0.1", listener.Port(), alpn, clientCreds);
|
||||
destroyed = std::chrono::steady_clock::now();
|
||||
// ~ClientQUIC runs here.
|
||||
}
|
||||
std::unique_lock lock(seen->mutex);
|
||||
if (!seen->changed.wait_for(lock, std::chrono::seconds(2), [&] { return seen->closedAt.has_value(); })) {
|
||||
// Every later connection would wait out its 2 s as well.
|
||||
std::println(" connection {}: the server was not told within 2 s", i);
|
||||
break;
|
||||
}
|
||||
slowest = std::max(slowest, std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
*seen->closedAt - destroyed));
|
||||
++told;
|
||||
}
|
||||
Check(told == 20, std::format("the server heard each of 20 clients close within 2 s ({} did; slowest {} ms)",
|
||||
told, slowest.count()));
|
||||
} catch (const std::exception& e) {
|
||||
Check(false, std::format("threw: {}", e.what()));
|
||||
}
|
||||
|
||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||
// The accepted ClientQUICs 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