Repository navigation
daemon: a FIN waits for the data sent before it - #521
Merged
Merged
Conversation
| Seq: sendSeq, | ||
| Ack: finSeq + 1, | ||
| } | ||
| d.tunnels.Send(remoteAddr.Node, finack) |
|
|
||
| // sendFIN puts the FIN on the wire and tracks it for retransmission. | ||
| func (d *Daemon) sendFIN(conn *Connection, fin *protocol.Packet) { | ||
| d.tunnels.Send(fin.Dst.Node, fin) |
A FIN that overtook a lost segment was acted on at once: the receiver closed the stream with the segment missing and answered with a FIN-ACK, and that FIN-ACK made the sender drop the segment from its retransmit queue. Under loss a writer that closed right after its last write lost the end of it: 4 of 30 pub/sub events at 5% loss never reached the broker while the publisher reported success. Every version behaves this way. Receiver: a FIN ahead of undelivered data is remembered, and an immediate ACK with SACK asks for the gap; the FIN is handled (receive side closed, TIME_WAIT, FIN-ACK) once delivery reaches it. A side that has already closed its own receive side still takes the peer's FIN at once. Sender: CloseConnection holds the FIN until its data is acknowledged, at most finLinger (5 s), in a goroutine, so receivers that have not upgraded get the whole write too. FIN placement and the FIN retransmit rules are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connection closed with data unacknowledged holds its FIN until the data is acknowledged. Stopping the daemon in that window dropped the FIN: the waiting goroutine returned on stop, and shutdown sends FINs only to established connections. The peer then kept the connection open until keepalive gave up on it (TestConnectionAfterPeerRestart: 5 of 5 failed at its 60 s budget). The held FIN now lives on the connection, and whichever of the two takes it sends it, once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the peer goes silent Review of the FIN hold found: - The held FIN could be dropped: the goroutine that sends it started before CloseConnection set FIN_WAIT, and a goroutine that found the data already acknowledged read ESTABLISHED and gave up. 3 of 300 runs of the write-then-close test lost the FIN. A held FIN is now dropped only once the connection is in TIME_WAIT or CLOSED. - A FIN held for data the sender never resends (a daemon stopped or restarted with a segment lost, any version) stayed held until keepalive gave up, about two minutes. It is now handled once nothing has come from the peer for 15 s; a sender still there resends at most 10 s apart (RTOMax). - Each closing connection polled every 20 ms for up to 5 s: 2,000 closes at once cost half a core. There is no goroutine now: the ACK path sends the held FIN when the last data is acknowledged, a timer sends it at the linger, and the reaper and shutdown send it if still held. - Shutdown could send a second FIN, at SendSeq, after a held one, and a receiver would hold that one for good. It now sends the held FIN only. The peer FIN's first sequence number is kept. Tests: the early-FIN test reads only what the FIN caused, the end-to-end test's reader no longer races its failure message, and the tail test checks the held FIN's sequence number again. New tests for the ACK-path release, the silent-peer expiry (and data restarting its wait) and a half-done close at shutdown each fail with their fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TeoSlayer
force-pushed
the
fix/fin-in-order
branch
from
October 8, 2026 10:21
bd524a0 to
43e401d
Compare
| // It may still read ESTABLISHED, its close half done; the | ||
| // FIN's sequence number is already reserved, so send that one | ||
| // and no second FIN after it. | ||
| d.tunnels.Send(conn.RemoteAddr.Node, held) |
…old ends with the connection Second review of the FIN hold: - A peer that restarts and dials the same port again (its ephemeral ports start over) matched the old connection while its FIN was held, got the old SYN-ACK back, and had its data acknowledged as duplicates and never delivered. A peer that sent a FIN is past its SYN, so that SYN now ends the old connection, quietly (a FIN-ACK would reach the new one), and opens a new connection. - The hold timer outlived the hold: after an RST, a local close, the reaper or shutdown, it later set TIME_WAIT on the removed connection and sent a FIN-ACK that could close a new connection on the same ports. The hold now ends with the connection (stopFINHold), and the timer acts only on an established connection. - CloseConnection reserves the FIN's sequence number and holds the FIN in one critical section, so shutdown cannot see the reservation without the held FIN and send a second FIN past it. - The silent-peer test has wider margins (1 s hold, data at 250 ms). Co-Authored-By: Claude Opus 5.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
A FIN that overtook a lost segment was acted on at once:
Every version does this. It is the cause of the pub/sub losses in the v1.17.0 lab run: at 5% loss, 4 of 30 events never reached the broker while
pilotctl publishreported success, and the broker loggedunexpected EOF. Any client that closes right after writing is exposed the same way.Fix
Receiver.
finPending/finSeq, the first copy's sequence number) and sends an ACK asking for the gap.acceptFIN: close the receive side, TIME_WAIT, FIN-ACK) once in-order delivery reaches the FIN's sequence number.finHoldIdle), it handles the FIN anyway (expireHeldFIN), and the reader gets EOF without the missing data, as before. A sender still there resends at most 10 s apart (RTOMax), so it always stays inside the bound. Without the bound, a sender that stopped with a segment lost (any version, stopped or restarted) left the stream open until keepalive gave up, about two minutes later.Sender.
CloseConnectionreserves the FIN's sequence number as before.releaseHeldFINsends it in whichever of these comes first:finLinger, a timer);Shutdown. Stopping the daemon sends a FIN that is still held, and no second FIN after it.
CloseConnectionreserves the FIN's sequence number and holds the FIN in one critical section, so shutdown never sees one without the other.A restarted peer. A SYN on the ports of a connection whose peer FIN is held comes from a restarted peer dialling again, whose ephemeral ports start over. It is not a resend: a peer that sent a FIN is past its SYN.
The hold ends with the connection. An RST, a local close, the reaper or shutdown each stop the hold timer, and the timer acts only on an established connection, so it never sends a FIN-ACK for a connection that is gone.
Interop:
Tests
zz_fin_in_order_test.go:TestEarlyFINIsHeldUntilTheDataBeforeItArrives: the reply to the early FIN is a plain ACK asking for the gap, the state stays established, and the reader gets every byte before EOF.TestFINOnAClosingConnectionIsAcceptedAtOnce.TestCloseSendsTheFINAfterTheLingerIfNothingIsAcknowledged.TestHeldFINGoesOutWithTheACKOfTheLastData.TestHeldPeerFINIsHandledOnceThePeerFallsSilent, where arriving data restarts the wait.TestStopSendsAFINThatWasWaitingForDataandTestStopSendsOnlyTheHeldFINOfAHalfDoneClose.TestSYNOnAConnectionWithAHeldFINIsANewConnection.TestHoldEndsWithTheConnection.TestWriteThenCloseSurvivesALostSegment: two daemons, one segment's first send dropped, with the FIN sent at once (old senders) and with the FIN held.Each fails with its fix reverted.
Results:
TestWriteThenCloseSurvivesALostSegmentunder-race -count=300: 0 failures. A review of the first version found 3 in 300, a race that dropped the held FIN, now fixed.go test -race ./pkg/... ./cmd/...: passes../tests: passes.Existing tests adjusted because the FIN now waits for acknowledged data:
TestHandleStreamFINEstablishedClosesAndSendsFINACKsetsExpectedSeqto the FIN's sequence number.TestCloseSendsHeldTailBeforeFINacknowledges the data through the ACK path's release.TestCloseCommitsTheWholeTailDespiteSendErrorschecks the held FIN's sequence number.Not changed
🤖 Generated with Claude Code