fix(wind-core): reap idle half-closed relays#50
Merged
Conversation
`copy_io` delegates to `copy_bidirectional`, which only returns once BOTH directions reach EOF. When the outbound peer half-closes — e.g. an origin server FINs after its response while the downstream TUIC client leaves its upload half of the QUIC bi-stream open — the inbound→outbound direction never EOFs, so the outbound TCP socket sits in CLOSE_WAIT and the relay task hangs for the entire lifetime of the long-lived QUIC connection. Proxied TCP connections then only get cleaned up when the connection itself tears down (client disconnect or server kill). #49 reaps connection children on connection teardown only, not per stream. Arm an idle reaper once the outbound peer closes (detected when `copy_bidirectional` shuts down the inbound writer): after RELAY_HALF_CLOSE_TIMEOUT (30s) with no traffic the half-open relay is torn down, dropping both streams and closing the lingering sockets. Any byte moved resets the window, so a slow-but-live transfer is never cut off; a fully-open idle tunnel (keep-alive / long-poll) is never reaped because the timer only arms after the outbound side has closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap `RelayMeters::activity` and the reaper match per the repo's nightly rustfmt config (`wrap_comments`, `imports_granularity`, …). No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Merging this PR will not alter performance
Comparing Footnotes
|
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
Proxied TCP connections through
tuic-serverare never terminated until the server is killed (Ctrl+C). They accumulate inCLOSE_WAIT.All TCP relays funnel through
copy_io, which delegates totokio::io::copy_bidirectional_with_sizes— that only returns once both directions reach EOF. When the outbound peer half-closes (e.g. an origin server FINs after its response while the downstream TUIC client leaves its upload half of the QUIC bi-stream open), the inbound→outbound direction never EOFs. So:CLOSE_WAIT, andThese half-open relays accumulate and are only reaped on full connection teardown. #49 wired
spawn_connection_childso children are cancelled on connection teardown, but nothing reaps an individual stream that half-closed while the connection stays up.Both QUIC backends (quinn/quiche) propagate EOF correctly, so this is purely
copy_bidirectional's "wait for both" semantics, not a missing-EOF bug.Fix
Keep
copy_bidirectional's correct half-close data handling (it still shuts down the opposite writer and drains in-flight bytes), but arm an idle reaper once the outbound peer closes:Trackedwrapper counts per-direction throughput and observes the half-close (copy_bidirectionalshuts down the inbound writer only after the outbound reader hit EOF).RELAY_HALF_CLOSE_TIMEOUT(30s) with no bytes moved, the half-open relay is torn down, dropping both streams and closing the lingering sockets.The asymmetry (arm on "outbound closed, inbound lingering"; stay disarmed on "inbound closed first, wait for a slow outbound response") is correct at all five
copy_iocall sites — the convention isa = inbound,b = outboundeverywhere.The public
copy_iosignature is unchanged; the timeout is injected via a privatecopy_io_with_timeoutso tests don't wait the production grace period.Tests
Three new unit tests in
wind-core::io:clean_full_close_completes— normal exchange returns correct byte counts, reaper does not fire.half_open_idle_relay_is_reaped— the regression: outbound closes, inbound lingers silent;copy_ioreturns instead of hanging.fully_open_idle_relay_is_not_reaped— an idle but fully-open tunnel is left running.wind-core(129) +wind-base+wind-tuic(18) suites pass; clippy and fmt clean.🤖 Generated with Claude Code