fix: complete the cancellation chain across all binaries and crates#22
Merged
Conversation
The quiche inbound created its own root CancellationToken and its accept
loop never checked it, so the binary's ctrl-c handler had nothing to
cancel: the listen task and every live connection kept running until
main's 10s drain timeout force-aborted them ('Server did not drain
within 10s of Ctrl-C').
- wind-tuic (quiche): add TuicheInboundBuilder::cancel_token; the accept
loop now selects on the token and per-connection tokens are children
of it, so one cancel() stops accepting and closes every connection.
- tuic-server: wire ctx.cancel into the quiche builder, mirroring the
quinn backend.
- wind-quic (quiche): the accept-forwarding task exits once all
QuicheAcceptors are dropped, releasing the listener socket/router.
- wind-tuic (quinn): close the endpoint and wait_idle() after the listen
loop exits so CONNECTION_CLOSE frames flush before the runtime drops.
Verified on Windows: pre-fix the quiche backend hung the full 10s after
Ctrl-C; post-fix both backends exit gracefully within a second.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the quiche ctrl-c fix: a full audit of the cancellation
mechanism found several more components that ignored the cancel token,
leaked detached tasks past shutdown, or risked blocking a runtime worker.
- tuic-client: had no shutdown path at all (ctrl-c hard-killed the
process; the server only noticed via idle timeout). Add
run_with_cancel(), wire ctrl-c -> cancel -> 10s drain in main (same
structure as tuic-server), select the token in the SOCKS5 accept loop
and TCP/UDP forwarders, and track per-session tasks in TaskTrackers so
shutdown waits for them.
- wind-tuic (quinn outbound): on cancellation the heartbeat poll now
closes the QUIC connection ("client shutdown") so the server reaps it
immediately instead of waiting out its idle timeout.
- wind-tuic (quiche inbound): track serve_connection tasks and wait for
them after the accept loop exits so CONNECTION_CLOSE frames flush
before the caller drops the runtime.
- wind-tuic (quinn inbound): spawn connection handlers into the shared
ctx.tasks TaskTracker (previously created but unused) so context
owners can drain them.
- wind-socks: per-connection tasks get a child token and a TaskTracker;
the detached UDP association task is cancel-guarded; listen waits for
in-flight sessions on shutdown.
- wind-naive: stop join()ing the bridge I/O thread from async — a
parked blocking read would pin a tokio worker indefinitely. Drop the
handle instead (same strategy as the UoT relay).
- tuic-server: the CertResolver file watcher now takes a cancel token
instead of looping forever.
Verified end-to-end on Windows: client/server pair relays traffic, then
ctrl-c on the client exits in <1s with the server logging the connection
close immediately; ctrl-c on the server exits in <1s on both the quinn
and quiche backends. 164 unit tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Ctrl-C did not terminate
tuic-serverwhen running the quiche backend (backend.mode = "quiche"): the quiche inbound created its own standaloneCancellationTokenand its accept loop never checked any token, so the listen task and every live connection kept running untilmain's 10s drain timeout force-aborted them.A follow-up audit of the cancellation mechanism across the whole workspace found several more components that ignored the cancel token, leaked detached tasks past shutdown, or risked pinning a runtime worker.
Changes
tuic-server (quiche backend)
TuicheInboundBuilder::cancel_token(): the accept loop nowselect!s the token; per-connection tokens are children of it, so onecancel()stops accepting and closes every live connection.create_quiche_inboundwiresctx.cancel.child_token()into the builder, mirroring the quinn backend.TaskTrackerand awaited after the accept loop exits, so CONNECTION_CLOSE frames flush before the runtime drops.bind_serverexits once allQuicheAcceptors are dropped, releasing the listener socket/router.tuic-server (quinn backend)
endpoint.close()+wait_idle()so queued close frames flush; previously peers only learned about the shutdown via idle timeout.ctx.tasksTaskTracker(previously created but unused).CertResolverfile watcher takes a cancel token instead of looping forever.tuic-client
run_with_cancel(), ctrl-c → cancel → 10s drain inmain(same structure as tuic-server), token-aware SOCKS5 accept loop and TCP/UDP forwarders, per-session tasks tracked inTaskTrackers.wind / wind-socks
listenwaits for in-flight sessions on shutdown.wind-naive
join()ing the bridge I/O thread from async — a parked blockingnaive.read()would pin a tokio worker indefinitely. Drop the handle instead (same strategy as the UoT relay path).Verification
Tested on Windows by sending real
CTRL_C_EVENTs (viaAttachConsole+GenerateConsoleCtrlEvent):Connection closedimmediatelycargo build/cargo clippyclean with thequichefeature; 164 unit tests pass across the modified crates.🤖 Generated with Claude Code