fix(quic): let the streams already stopped finish before ~ClientQUIC closes
~ClientQUIC shut the connection down first and drained its streams after. msquic aborts every stream with the connection, and with them whatever it had not sent yet, so a server that replied with SendAsync(..., finish = true), let the stream go and deleted the connection lost the reply whenever the shutdown, queued right behind the send, reached the worker before the flush that would have sent it. The interface promised the opposite: that the destructor waits for every stream "to finish closing first". Found with Crafter.Debug, whose session answers every request still waiting when it ends and then deletes its connections: the follower it told "shutting down" missed the reply in 4 runs of 30. Streams now count as finishing from QUICStream::Stop to their SHUTDOWN_COMPLETE, in the registry beside the open count, and the destructor waits for that count to reach zero before the shutdown. Only streams that can send are counted, since a stream that cannot has nothing on its way. A stopped stream finishes once the peer has acknowledged its FIN, about a round trip; the wait is bounded at 1 s so a peer that stopped answering, or one that never reads, holds the destructor no longer than that, and a peer closing meanwhile aborts the streams, which ends the wait too. Streams their owner still holds are not waited for: they are aborted as before. Measured with ShouldFinishStoppedStreamsQUIC: before, 0 of 20 replies of 64 KiB arrived; after, 20 of 20, with the slowest delete at 1 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f346237944
commit
9ea07c54f1
3 changed files with 78 additions and 15 deletions
|
|
@ -246,7 +246,8 @@ namespace Crafter {
|
|||
// asynchronous. Called by the destructor, so an explicit call is only
|
||||
// needed to shut a stream down ahead of its wrapper going away.
|
||||
// Natively it also unregisters OnRecieve's callbacks (see there);
|
||||
// sends already made still go out, ahead of the FIN.
|
||||
// sends already made still go out, ahead of the FIN, and
|
||||
// ~ClientQUIC waits (bounded) for them before closing the connection.
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
|
|
@ -268,12 +269,17 @@ namespace Crafter {
|
|||
// via OnStream()).
|
||||
// - Unreliable, unordered datagrams (SendDatagram() / OnDatagram()).
|
||||
//
|
||||
// Lifetime: ~ClientQUIC shuts the connection down and closes it, waiting
|
||||
// (bounded — 5s) for every stream on it to finish closing first, since
|
||||
// QUICStream::Stop only initiates a shutdown that completes asynchronously,
|
||||
// and then (bounded — 1s, about a round trip when the peer answers) for
|
||||
// the shutdown to complete, so the peer is sent the close rather than
|
||||
// left to time the connection out.
|
||||
// Lifetime: ~ClientQUIC first lets the streams already stopped finish
|
||||
// (bounded — 1s, about a round trip when the peer answers): what was
|
||||
// sent on them, the FIN last, reaches the peer, so a server may reply
|
||||
// with SendAsync(..., finish = true), let the stream go and delete the
|
||||
// connection at once. Then it shuts the connection down, which aborts
|
||||
// any stream still open, and closes it, waiting (bounded — 5s) for every
|
||||
// stream on it to finish closing first, since QUICStream::Stop only
|
||||
// initiates a shutdown that completes asynchronously, and then (bounded
|
||||
// — 1s, about a round trip when the peer answers) for the shutdown to
|
||||
// complete, so the peer is sent the close rather than left to time the
|
||||
// connection out.
|
||||
// Streams outliving the connection are therefore safe to hold, though they
|
||||
// are dead once it goes; prefer destroying or moving them out first.
|
||||
//
|
||||
|
|
|
|||
Loading…
Reference in a new issue