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
|
|
@ -207,6 +207,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("ShouldReportListenerPortQUIC").Dependencies({ &cfg });
|
cfg.AddTest("ShouldReportListenerPortQUIC").Dependencies({ &cfg });
|
||||||
cfg.AddTest("ShouldShareSelfSignedCertificateQUIC").Dependencies({ &cfg });
|
cfg.AddTest("ShouldShareSelfSignedCertificateQUIC").Dependencies({ &cfg });
|
||||||
cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });
|
cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });
|
||||||
|
cfg.AddTest("ShouldTellPeerOfCloseQUIC").Dependencies({ &cfg });
|
||||||
} else {
|
} else {
|
||||||
// Only a --no-quic build can show that msquic stayed out.
|
// Only a --no-quic build can show that msquic stayed out.
|
||||||
cfg.AddTest("ShouldBuildWithoutQUIC").Dependencies({ &cfg });
|
cfg.AddTest("ShouldBuildWithoutQUIC").Dependencies({ &cfg });
|
||||||
|
|
|
||||||
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