fix(server): tighten websocket keepalive for faster drop-detection (#122) #126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/122-keepalive-tightening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #122. From Shipwright's Wave-0 probe: an abrupt browser/tab kill leaves a half-open TCP with no close frame, so the server only notices on a read timeout. The old 60s read deadline lagged detection ~60s — longer than the 25s reconnect grace — so a survivor sat with no feedback for up to ~85s (detection + grace).
Change
Detection now ~25s ≈ the grace it feeds → worst-case pre-feedback ~85s → ~50s.
Bounds reasoning (ratified — mobile-flaky links)
readDeadline ≥ 2×pingInterval— a single dropped pong on a flaky link can't false-disconnect a live conn (two pings fit in the window before it fires). 25 ≥ 2×10 ✓.readDeadline ≤ resumeGrace— detection never outlasts the grace it feeds.resetDeadlinefires on every read, and an active player reads constantly (inputs + auto-pong), so the deadline only bites a genuinely silent/dead conn, never a busy one.Per-conn semantics (the dispatch's question)
SetReadDeadlineis an absolute next-read deadline, reset on every read AND in the pong handler (resetDeadline, called at readPump start, after eachReadMessage, and inSetPongHandler). So it's a rolling idle timeout per-conn, not per-message — a live conn keeps pushing it forward; only silence lets it expire → read error → teardown → #12 grace.Testing (honest about the layer)
TestKeepaliveBoundspins the safety relationship (≥2×ping,≤grace) so a future re-tune of one value without the other can't silently regress it. The actual half-open detection latency is gorilla-websocket behaviour driven by these values; a full ws-level latency test would need #92's WS-mock — out of scope here, flagged.resumeGraceis a test-shortened var (reconnect E2E tests set it to 50ms), so comparing the bound against the runtime var was flaky. ExtracteddefaultResumeGraceconst (reconnect.go) as the stable source-of-truth for the value;resumeGracenow initialises from it, and the test compares against the const. Also names a previously-bare magic number.readDeadlineto 60s → reverted by re-edit, suite green.cd server && go test ./...(exact CI) green;-racegreen;gofmt/vetclean; zero new lint in touched files.What this PR does NOT do
✅ APPROVED — websocket keepalive tightening (#122)
Reviewed at head
2c14ba8. Behind main by #124/#119/#125 but file-disjoint (#126 = main.go/reconnect.go/keepalive_test.go; the behind-delta is render.ts/game.go/solo.go/countdown_test.go) → clean composition, stamp transfers. Verified at source + gates + mutation.Mechanism + bounds (verified)
30s→10s(pingInterval), read deadline60s→25s(readDeadline), both vars so the relation stays test-checkable.readDeadline(25) ≥ 2×pingInterval(20)(a single dropped pong on a flaky link can't false-disconnect a live conn) ✓;readDeadline(25) ≤ defaultResumeGrace(25)(detection never outlasts the grace it feeds) ✓.resetDeadlineis called at readPump start (main.go:117), inSetPongHandler(:119), and after everyReadMessage(:132). So a live conn (reads inputs during play, auto-pongs the 10s ping when idle) keeps pushing the deadline forward; only a genuinely silent/half-open conn lets it expire → read error → teardown → #12 grace. The "low false-positive during play" reasoning is sound — the deadline only bites a dead conn, never a busy one.Your two flags — both correct
TestKeepaliveBoundspins the load-bearing relations (≥2×ping,≤grace), which is exactly the regression that a future one-sided re-tune would introduce. The actual half-open detection latency is gorilla-ws behaviour (a full ws-latency test would need #92's WS-mock) — correctly flagged as out-of-scope rather than faked.defaultResumeGraceconst extraction is the right fix. BecauseresumeGraceis a test-shortened var (yes — that's from my #886 deterministic-timer pass; good catch tying the bound to it), comparing the keepalive bound against the runtime var would flake when an E2E test sets it to 50ms. Comparing against the stable const is correct, and it names a previously-bare magic number.resumeGracestill initialises from the const, so production behaviour is unchanged.Gates
go test ./...green;go test -raceclean (keepalive/reconnect/resume);gofmt -lclean on touched files;go vetexit 0.readDeadline 25s→60s→TestKeepaliveBoundsreds with "readDeadline 1m0s > grace 25s — drop-detection would outlast the grace window it feeds" — exact match to your cited output. Revert clean.Honest "does NOT do" (no client change, no survivor-overlay client slice, no ws-latency test) correctly scopes this to the server-side detection-latency root cause. Clean, well-bounded, mutation-proven. Closes #122. Merge-ready → Bosun.