526 lines
25 KiB
C++
526 lines
25 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// QUICStream::OnRecieve delivers a stream's bytes and its end by callback,
|
||
|
|
// holding no thread while nothing arrives. Until now every way to wait for
|
||
|
|
// bytes held one: RecieveSync and RecieveUntilCloseSync block their caller
|
||
|
|
// with no bound, and RecieveAsync and RecieveUntilCloseAsync block a worker
|
||
|
|
// of the pool the host shares, which for a stream a client keeps open (a log
|
||
|
|
// follower) is for good. A server embedded in an app cannot afford either.
|
||
|
|
//
|
||
|
|
// - Bytes that arrived before OnRecieve are delivered by it, before it
|
||
|
|
// returns, and the end with them: clean for a FIN.
|
||
|
|
// - Registered while bytes keep arriving, with a callback slow enough to
|
||
|
|
// let the registering thread and msquic's worker overlap, and an echo
|
||
|
|
// sent from inside onData: no two callbacks of the stream ever run at
|
||
|
|
// once, and every byte arrives in order.
|
||
|
|
// - With every pool worker parked, a stream is echoed both ways, callbacks
|
||
|
|
// and SendAsync only: nothing needed the pool.
|
||
|
|
// - The end fires once: not clean for the peer's Abort() and for a lost
|
||
|
|
// connection.
|
||
|
|
// - Abort() from inside onData enforces a byte cap: no onData after it,
|
||
|
|
// onEnd(false) once, and the peer sees both directions reset.
|
||
|
|
// - The wrapper destroyed inside its own onEnd, or moved while bytes flow,
|
||
|
|
// breaks nothing; destroyed from another thread mid-flow, no callback
|
||
|
|
// runs once its destructor has returned.
|
||
|
|
// - A second OnRecieve throws; a stream that cannot receive, or no stream,
|
||
|
|
// gets onEnd(false) at once.
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool WaitFor(std::function<bool()> done, std::chrono::milliseconds limit) {
|
||
|
|
const auto deadline = std::chrono::steady_clock::now() + limit;
|
||
|
|
while (!done()) {
|
||
|
|
if (std::chrono::steady_clock::now() >= deadline) return false;
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
char Pattern(std::uint64_t offset) { return static_cast<char>((offset * 131 + 7) & 0xff); }
|
||
|
|
|
||
|
|
bool FollowsPattern(const std::string& bytes, std::size_t expected) {
|
||
|
|
if (bytes.size() != expected) return false;
|
||
|
|
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
||
|
|
if (bytes[i] != Pattern(i)) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const std::string alpn = "crafter/test-on-recieve";
|
||
|
|
|
||
|
|
QUICClientCredentials ClientCredentials() {
|
||
|
|
QUICClientCredentials creds;
|
||
|
|
creds.insecureNoServerValidation = true;
|
||
|
|
return creds;
|
||
|
|
}
|
||
|
|
|
||
|
|
// What one stream's callbacks saw. Shared with them, and leaked, so a
|
||
|
|
// callback that comes late on a failure path finds it alive.
|
||
|
|
struct Seen {
|
||
|
|
std::mutex mutex;
|
||
|
|
std::string bytes;
|
||
|
|
int ends = 0;
|
||
|
|
bool clean = false;
|
||
|
|
int dataAfterEnd = 0;
|
||
|
|
std::atomic<int> inside = 0; // callbacks of this stream running now
|
||
|
|
std::atomic<int> overlaps = 0; // times two ran at once
|
||
|
|
|
||
|
|
void Enter() { if (inside.fetch_add(1) != 0) ++overlaps; }
|
||
|
|
void Leave() { --inside; }
|
||
|
|
|
||
|
|
std::function<void(std::span<const char>)> OnData() {
|
||
|
|
return [this](std::span<const char> data) {
|
||
|
|
Enter();
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
if (ends != 0) ++dataAfterEnd;
|
||
|
|
bytes.append(data.begin(), data.end());
|
||
|
|
}
|
||
|
|
Leave();
|
||
|
|
};
|
||
|
|
}
|
||
|
|
std::function<void(bool)> OnEnd() {
|
||
|
|
return [this](bool wasClean) {
|
||
|
|
Enter();
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
++ends;
|
||
|
|
clean = wasClean;
|
||
|
|
}
|
||
|
|
Leave();
|
||
|
|
};
|
||
|
|
}
|
||
|
|
int Ends() {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
return ends;
|
||
|
|
}
|
||
|
|
std::string Bytes() {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
return bytes;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Every pool worker parked until Release(), which returns once each has
|
||
|
|
// left, so nothing reads this after its owner is gone.
|
||
|
|
struct Parking {
|
||
|
|
std::atomic<bool> release = false;
|
||
|
|
std::atomic<unsigned> parked = 0;
|
||
|
|
std::atomic<unsigned> left = 0;
|
||
|
|
unsigned tasks = 0;
|
||
|
|
|
||
|
|
bool Park() {
|
||
|
|
const unsigned workers = ThreadPool::activeThreads.load();
|
||
|
|
tasks = workers * 2;
|
||
|
|
for (unsigned i = 0; i < tasks; ++i) {
|
||
|
|
ThreadPool::Enqueue([this] {
|
||
|
|
++parked;
|
||
|
|
while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||
|
|
++left;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return WaitFor([&] { return parked.load() >= workers; }, std::chrono::seconds(10));
|
||
|
|
}
|
||
|
|
void Release() {
|
||
|
|
release = true;
|
||
|
|
WaitFor([&] { return left.load() == tasks; }, std::chrono::seconds(10));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// A listener whose every accepted stream goes to `onStream`, as ListenerQUIC
|
||
|
|
// delivers it: on a pool task. Wrappers the handler keeps go in `kept`.
|
||
|
|
struct Server {
|
||
|
|
std::mutex mutex;
|
||
|
|
std::vector<std::shared_ptr<QUICStream>> kept;
|
||
|
|
std::unique_ptr<ListenerQUIC> listener;
|
||
|
|
|
||
|
|
explicit Server(std::function<void(Server&, std::shared_ptr<QUICStream>)> onStream) {
|
||
|
|
QUICServerCredentials creds;
|
||
|
|
creds.selfSigned = true;
|
||
|
|
listener = std::make_unique<ListenerQUIC>("127.0.0.1", 0, alpn, creds, [this, onStream](ClientQUIC* peer) {
|
||
|
|
peer->OnStream([this, onStream](QUICStream stream) {
|
||
|
|
onStream(*this, std::make_shared<QUICStream>(std::move(stream)));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
listener->ListenAsyncAsync();
|
||
|
|
}
|
||
|
|
void Keep(std::shared_ptr<QUICStream> stream) {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
kept.push_back(std::move(stream));
|
||
|
|
}
|
||
|
|
std::uint16_t Port() const { return listener->Port(); }
|
||
|
|
};
|
||
|
|
|
||
|
|
} // 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();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// ── What arrived first is delivered by OnRecieve ─────────────────────
|
||
|
|
{
|
||
|
|
Seen& seen = *new Seen;
|
||
|
|
std::atomic<bool> registering = false;
|
||
|
|
std::atomic<int> deliveredInside = 0;
|
||
|
|
Server& server = *new Server([&](Server& self, std::shared_ptr<QUICStream> stream) {
|
||
|
|
// Long enough for "hello" and its FIN to be waiting.
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
||
|
|
registering = true;
|
||
|
|
auto onData = seen.OnData();
|
||
|
|
auto onEnd = seen.OnEnd();
|
||
|
|
stream->OnRecieve(
|
||
|
|
[&, onData](std::span<const char> data) { if (registering) ++deliveredInside; onData(data); },
|
||
|
|
[&, onEnd](bool clean) { if (registering) ++deliveredInside; onEnd(clean); });
|
||
|
|
registering = false;
|
||
|
|
self.Keep(stream);
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
stream.SendSync("hello", 5, true);
|
||
|
|
Check(WaitFor([&] { return seen.Ends() == 1; }, std::chrono::seconds(10)) && seen.Bytes() == "hello" && seen.clean,
|
||
|
|
std::format("bytes and a FIN that came before OnRecieve are delivered by it: '{}', clean", seen.Bytes()));
|
||
|
|
Check(deliveredInside.load() == 2, "on the registering thread, before OnRecieve returned");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Registered mid-flow: one callback at a time, every byte in order ──
|
||
|
|
{
|
||
|
|
Seen& seen = *new Seen;
|
||
|
|
auto slow = std::make_shared<std::atomic<int>>(50);
|
||
|
|
Server& server = *new Server([&seen, slow](Server& self, std::shared_ptr<QUICStream> stream) {
|
||
|
|
// Bytes keep coming meanwhile, so the backlog this thread
|
||
|
|
// delivers and the worker's new arrivals overlap in time.
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||
|
|
auto onData = seen.OnData();
|
||
|
|
std::weak_ptr<QUICStream> weak = stream;
|
||
|
|
stream->OnRecieve(
|
||
|
|
[&seen, slow, onData, weak](std::span<const char> data) {
|
||
|
|
if (slow->fetch_sub(1) > 0) std::this_thread::sleep_for(std::chrono::microseconds(200));
|
||
|
|
onData(data);
|
||
|
|
// Echoed from inside the callback; its onSent is one
|
||
|
|
// of the stream's callbacks too.
|
||
|
|
if (auto s = weak.lock()) {
|
||
|
|
s->SendAsync(data.data(), static_cast<std::uint32_t>(data.size()), false, [&seen](bool) {
|
||
|
|
seen.Enter();
|
||
|
|
seen.Leave();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[weak, onEnd = seen.OnEnd()](bool clean) {
|
||
|
|
onEnd(clean);
|
||
|
|
if (auto s = weak.lock()) s->SendAsync(nullptr, 0, true, nullptr);
|
||
|
|
});
|
||
|
|
self.Keep(stream);
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
Seen& echo = *new Seen;
|
||
|
|
stream.OnRecieve(echo.OnData(), echo.OnEnd());
|
||
|
|
constexpr std::size_t total = 2000 * 512;
|
||
|
|
std::vector<char> chunk(512);
|
||
|
|
for (std::size_t sent = 0; sent < total; sent += chunk.size()) {
|
||
|
|
for (std::size_t i = 0; i < chunk.size(); ++i) chunk[i] = Pattern(sent + i);
|
||
|
|
stream.SendAsync(chunk.data(), static_cast<std::uint32_t>(chunk.size()), sent + chunk.size() == total, nullptr);
|
||
|
|
}
|
||
|
|
const bool ended = WaitFor([&] { return seen.Ends() == 1 && echo.Ends() == 1; }, std::chrono::seconds(20));
|
||
|
|
Check(ended && seen.clean && FollowsPattern(seen.Bytes(), total) && seen.dataAfterEnd == 0,
|
||
|
|
std::format("registered mid-flow, every byte arrived in order, then a clean end ({} of {})", seen.Bytes().size(), total));
|
||
|
|
Check(seen.overlaps.load() == 0, "and no two of the stream's callbacks, onSent included, ever ran at once");
|
||
|
|
Check(echo.clean && FollowsPattern(echo.Bytes(), total), "the echo sent from inside onData came back whole");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── No pool ──────────────────────────────────────────────────────────
|
||
|
|
{
|
||
|
|
Seen& seen = *new Seen;
|
||
|
|
std::atomic<bool> registered = false;
|
||
|
|
Server& server = *new Server([&](Server& self, std::shared_ptr<QUICStream> stream) {
|
||
|
|
std::weak_ptr<QUICStream> weak = stream;
|
||
|
|
stream->OnRecieve(
|
||
|
|
[&, weak](std::span<const char> data) {
|
||
|
|
seen.OnData()(data);
|
||
|
|
if (auto s = weak.lock()) s->SendAsync(data.data(), static_cast<std::uint32_t>(data.size()), false, nullptr);
|
||
|
|
},
|
||
|
|
[weak](bool) { if (auto s = weak.lock()) s->SendAsync(nullptr, 0, true, nullptr); });
|
||
|
|
self.Keep(stream);
|
||
|
|
registered = true;
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
Seen& echo = *new Seen;
|
||
|
|
stream.OnRecieve(echo.OnData(), echo.OnEnd());
|
||
|
|
const char first = Pattern(0);
|
||
|
|
stream.SendAsync(&first, 1, false, nullptr);
|
||
|
|
// The server's OnStream is a pool task: let it run first.
|
||
|
|
WaitFor([&] { return registered.load(); }, std::chrono::seconds(10));
|
||
|
|
|
||
|
|
Parking& parking = *new Parking;
|
||
|
|
const bool allParked = parking.Park();
|
||
|
|
constexpr std::size_t total = 256 * 1024;
|
||
|
|
std::vector<char> rest(total - 1);
|
||
|
|
for (std::size_t i = 0; i < rest.size(); ++i) rest[i] = Pattern(i + 1);
|
||
|
|
stream.SendAsync(rest.data(), static_cast<std::uint32_t>(rest.size()), true, nullptr);
|
||
|
|
const bool ended = WaitFor([&] { return echo.Ends() == 1; }, std::chrono::seconds(10));
|
||
|
|
Check(allParked && ended && echo.clean && FollowsPattern(echo.Bytes(), total),
|
||
|
|
std::format("with all {} pool workers parked, 256 KiB went out and came back by callback ({} bytes)",
|
||
|
|
parking.parked.load(), echo.Bytes().size()));
|
||
|
|
parking.Release();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Unclean ends ─────────────────────────────────────────────────────
|
||
|
|
{
|
||
|
|
std::mutex mutex;
|
||
|
|
std::vector<Seen*> seens;
|
||
|
|
Server& server = *new Server([&](Server& self, std::shared_ptr<QUICStream> stream) {
|
||
|
|
Seen& seen = *new Seen;
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
seens.push_back(&seen);
|
||
|
|
}
|
||
|
|
stream->OnRecieve(seen.OnData(), seen.OnEnd());
|
||
|
|
self.Keep(stream);
|
||
|
|
});
|
||
|
|
auto latest = [&]() -> Seen* {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
return seens.empty() ? nullptr : seens.back();
|
||
|
|
};
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
{
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
stream.SendSync("x", 1, false);
|
||
|
|
WaitFor([&] { return latest() != nullptr && latest()->Bytes() == "x"; }, std::chrono::seconds(10));
|
||
|
|
stream.Abort(7);
|
||
|
|
Seen* seen = latest();
|
||
|
|
Check(seen && WaitFor([&] { return seen->Ends() == 1; }, std::chrono::seconds(10)) && !seen->clean,
|
||
|
|
"the peer's Abort() ends the stream, not clean");
|
||
|
|
}
|
||
|
|
{
|
||
|
|
ClientQUIC lost("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = lost.OpenStream();
|
||
|
|
Seen* before = latest();
|
||
|
|
stream.SendSync("y", 1, false);
|
||
|
|
WaitFor([&] { return latest() != before && latest()->Bytes() == "y"; }, std::chrono::seconds(10));
|
||
|
|
Seen* seen = latest();
|
||
|
|
lost.Stop(); // the connection goes; the stream gets no FIN
|
||
|
|
Check(seen != before && WaitFor([&] { return seen->Ends() == 1; }, std::chrono::seconds(10)) && !seen->clean,
|
||
|
|
"a lost connection ends it, not clean");
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||
|
|
Check(seen->Ends() == 1, "each end exactly once");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── A byte cap, by Abort() from inside onData ────────────────────────
|
||
|
|
{
|
||
|
|
Seen& seen = *new Seen;
|
||
|
|
std::atomic<int> afterAbort = 0;
|
||
|
|
std::atomic<bool> aborted = false;
|
||
|
|
constexpr std::size_t cap = 64 * 1024;
|
||
|
|
Server& server = *new Server([&](Server& self, std::shared_ptr<QUICStream> stream) {
|
||
|
|
std::weak_ptr<QUICStream> weak = stream;
|
||
|
|
stream->OnRecieve(
|
||
|
|
[&, weak](std::span<const char> data) {
|
||
|
|
if (aborted) ++afterAbort;
|
||
|
|
seen.OnData()(data);
|
||
|
|
if (seen.Bytes().size() > cap && !aborted) {
|
||
|
|
aborted = true;
|
||
|
|
if (auto s = weak.lock()) s->Abort(0x42);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
seen.OnEnd());
|
||
|
|
self.Keep(stream);
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
Seen& back = *new Seen;
|
||
|
|
stream.OnRecieve(back.OnData(), back.OnEnd());
|
||
|
|
constexpr int sends = 16;
|
||
|
|
std::atomic<int> reported = 0;
|
||
|
|
std::atomic<int> refused = 0;
|
||
|
|
const std::vector<char> chunk(64 * 1024, 'z');
|
||
|
|
for (int i = 0; i < sends; ++i) {
|
||
|
|
stream.SendAsync(chunk.data(), static_cast<std::uint32_t>(chunk.size()), false, [&](bool sent) {
|
||
|
|
if (!sent) ++refused;
|
||
|
|
++reported;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const bool ended = WaitFor([&] { return seen.Ends() == 1; }, std::chrono::seconds(10));
|
||
|
|
Check(ended && !seen.clean && afterAbort.load() == 0 && seen.Bytes().size() > cap
|
||
|
|
&& seen.Bytes().size() < sends * chunk.size(),
|
||
|
|
std::format("Abort() inside onData stopped the stream at {} KiB: no onData after it, onEnd(false)",
|
||
|
|
seen.Bytes().size() / 1024));
|
||
|
|
// Its sends may all report true: msquic takes them into its send
|
||
|
|
// buffer before the reset arrives, and true means just that. A
|
||
|
|
// send made once the reset has arrived is the one refused.
|
||
|
|
const bool backEnded = WaitFor([&] { return back.Ends() == 1 && reported.load() == sends; }, std::chrono::seconds(10));
|
||
|
|
bool probeRefused = false;
|
||
|
|
for (int attempt = 0; attempt < 100 && !probeRefused; ++attempt) {
|
||
|
|
auto result = std::make_shared<std::atomic<int>>(-1);
|
||
|
|
stream.SendAsync("p", 1, false, [result](bool sent) { *result = sent ? 1 : 0; });
|
||
|
|
WaitFor([&] { return result->load() != -1; }, std::chrono::seconds(5));
|
||
|
|
probeRefused = result->load() == 0;
|
||
|
|
if (!probeRefused) std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||
|
|
}
|
||
|
|
Check(backEnded && !back.clean && probeRefused,
|
||
|
|
std::format("the peer saw both directions reset: its receive side ended not clean, and its sends are refused ({} of the first {} already were)",
|
||
|
|
refused.load(), sends));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── The wrapper destroyed or moved under its callbacks ───────────────
|
||
|
|
{
|
||
|
|
// Destroyed inside its own onEnd: the callback holds the last
|
||
|
|
// reference.
|
||
|
|
std::atomic<int> destroyedInside = 0;
|
||
|
|
std::atomic<int> streams = 0;
|
||
|
|
Server& server = *new Server([&](Server&, std::shared_ptr<QUICStream> stream) {
|
||
|
|
++streams;
|
||
|
|
auto holder = std::make_shared<std::shared_ptr<QUICStream>>(stream);
|
||
|
|
stream->OnRecieve([](std::span<const char>) {}, [holder, &destroyedInside](bool) {
|
||
|
|
holder->reset();
|
||
|
|
++destroyedInside;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
for (int i = 0; i < 3; ++i) {
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
stream.SendSync("bye", 3, true);
|
||
|
|
}
|
||
|
|
Check(WaitFor([&] { return destroyedInside.load() == 3; }, std::chrono::seconds(10)),
|
||
|
|
"a wrapper destroyed inside its own onEnd breaks nothing, three times over");
|
||
|
|
}
|
||
|
|
{
|
||
|
|
// Moved while bytes flow: the callbacks never touch the wrapper.
|
||
|
|
Seen& seen = *new Seen;
|
||
|
|
std::atomic<bool> registered = false;
|
||
|
|
std::shared_ptr<QUICStream> first;
|
||
|
|
Server& server = *new Server([&](Server&, std::shared_ptr<QUICStream> stream) {
|
||
|
|
stream->OnRecieve(seen.OnData(), seen.OnEnd());
|
||
|
|
first = stream;
|
||
|
|
registered = true;
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
const char lead = Pattern(0);
|
||
|
|
stream.SendSync(&lead, 1, false);
|
||
|
|
WaitFor([&] { return registered.load(); }, std::chrono::seconds(10));
|
||
|
|
constexpr std::size_t total = 512 * 1024;
|
||
|
|
std::thread sender([&] {
|
||
|
|
std::vector<char> chunk(1024);
|
||
|
|
for (std::size_t sent = 1; sent < total; sent += chunk.size()) {
|
||
|
|
const std::size_t size = std::min(chunk.size(), total - sent);
|
||
|
|
for (std::size_t i = 0; i < size; ++i) chunk[i] = Pattern(sent + i);
|
||
|
|
stream.SendSync(chunk.data(), static_cast<std::uint32_t>(size), sent + size == total);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||
|
|
auto second = std::make_shared<QUICStream>(std::move(*first));
|
||
|
|
first.reset();
|
||
|
|
sender.join();
|
||
|
|
Check(WaitFor([&] { return seen.Ends() == 1; }, std::chrono::seconds(10)) && seen.clean
|
||
|
|
&& FollowsPattern(seen.Bytes(), total),
|
||
|
|
"a wrapper moved while bytes flow keeps receiving, every byte in order");
|
||
|
|
second.reset();
|
||
|
|
}
|
||
|
|
{
|
||
|
|
// Destroyed from another thread mid-flow: no callback after.
|
||
|
|
// What the callbacks touch is leaked, in case they do run late.
|
||
|
|
struct Watch {
|
||
|
|
Seen seen;
|
||
|
|
std::atomic<bool> destroyed = false;
|
||
|
|
std::atomic<int> afterDestroyed = 0;
|
||
|
|
std::shared_ptr<QUICStream> held;
|
||
|
|
std::atomic<bool> registered = false;
|
||
|
|
};
|
||
|
|
Watch& watch = *new Watch;
|
||
|
|
Seen& seen = watch.seen;
|
||
|
|
std::atomic<bool>& destroyed = watch.destroyed;
|
||
|
|
std::atomic<int>& afterDestroyed = watch.afterDestroyed;
|
||
|
|
std::shared_ptr<QUICStream>& held = watch.held;
|
||
|
|
std::atomic<bool>& registered = watch.registered;
|
||
|
|
Server& server = *new Server([&watch](Server&, std::shared_ptr<QUICStream> stream) {
|
||
|
|
stream->OnRecieve(
|
||
|
|
[&watch](std::span<const char>) {
|
||
|
|
watch.seen.Enter();
|
||
|
|
// Slow, so destroying the wrapper often catches one.
|
||
|
|
std::this_thread::sleep_for(std::chrono::microseconds(500));
|
||
|
|
if (watch.destroyed) ++watch.afterDestroyed;
|
||
|
|
watch.seen.Leave();
|
||
|
|
},
|
||
|
|
[&watch](bool) { if (watch.destroyed) ++watch.afterDestroyed; });
|
||
|
|
watch.held = stream;
|
||
|
|
watch.registered = true;
|
||
|
|
});
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
stream.SendSync("go", 2, false);
|
||
|
|
WaitFor([&] { return registered.load(); }, std::chrono::seconds(10));
|
||
|
|
std::atomic<bool> stop = false;
|
||
|
|
std::thread sender([&] {
|
||
|
|
const std::vector<char> chunk(256, 'q');
|
||
|
|
while (!stop) {
|
||
|
|
stream.SendAsync(chunk.data(), static_cast<std::uint32_t>(chunk.size()), false, nullptr);
|
||
|
|
std::this_thread::sleep_for(std::chrono::microseconds(200));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||
|
|
held.reset(); // ~QUICStream on this thread, callbacks running on the worker
|
||
|
|
destroyed = true;
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||
|
|
stop = true;
|
||
|
|
sender.join();
|
||
|
|
Check(afterDestroyed.load() == 0 && seen.overlaps.load() == 0,
|
||
|
|
"once a wrapper destroyed on another thread has gone, no callback of it runs again");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Misuse and edge cases ────────────────────────────────────────────
|
||
|
|
{
|
||
|
|
Server& server = *new Server([](Server& self, std::shared_ptr<QUICStream> stream) { self.Keep(stream); });
|
||
|
|
ClientQUIC client("127.0.0.1", server.Port(), alpn, ClientCredentials());
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
stream.OnRecieve([](std::span<const char>) {}, [](bool) {});
|
||
|
|
bool threw = false;
|
||
|
|
try {
|
||
|
|
stream.OnRecieve([](std::span<const char>) {}, [](bool) {});
|
||
|
|
} catch (const QUICException&) {
|
||
|
|
threw = true;
|
||
|
|
}
|
||
|
|
Check(threw, "a second OnRecieve on one stream throws QUICException");
|
||
|
|
|
||
|
|
QUICStream sendOnly = client.OpenStream(/*unidirectional=*/true);
|
||
|
|
std::atomic<int> sendOnlyEnds = 0;
|
||
|
|
std::atomic<bool> sendOnlyClean = true;
|
||
|
|
sendOnly.OnRecieve([](std::span<const char>) {}, [&](bool clean) { ++sendOnlyEnds; sendOnlyClean = clean; });
|
||
|
|
Check(sendOnlyEnds.load() == 1 && !sendOnlyClean.load(), "a stream that cannot receive gets onEnd(false) at once");
|
||
|
|
|
||
|
|
QUICStream none;
|
||
|
|
std::atomic<int> noneEnds = 0;
|
||
|
|
none.OnRecieve([](std::span<const char>) {}, [&](bool clean) { if (!clean) ++noneEnds; });
|
||
|
|
Check(noneEnds.load() == 1, "so does no stream at all");
|
||
|
|
}
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
Check(false, std::format("threw: {}", e.what()));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||
|
|
// As the other QUIC tests: skip the static teardown, which waits for the
|
||
|
|
// accepted peers' idle timeout. _Exit doesn't flush.
|
||
|
|
std::fflush(stdout);
|
||
|
|
std::_Exit(failures == 0 ? 0 : 1);
|
||
|
|
}
|