Skip to content

fix: refuse worker pins on a closing connection so closeConn cannot free with live pins - #328

Merged
cryo2010 merged 1 commit into
mainfrom
fix/stress-sync-totalpins
Sep 25, 2026
Merged

cryo2010 merged 1 commit into
mainfrom
fix/stress-sync-totalpins

Conversation

@cryo2010

Copy link
Copy Markdown
Owner

What

Fixes a crash in the HTTP/2 sync event loop where closeConn could free a connection that still had live worker pins, tripping doAssert c.totalPins == 0 and killing the event loop at first traffic.

Root cause

closeConn fires its teardown callbacks (onBody last=true, ws onClose, and the h2 per-stream onRespDrain via h2NotifyClosed) between the pinned-defer guard and the final doAssert c.totalPins == 0. On the HTTP/2 sync path, a streamed sendFile download whose next-chunk read was parked in onRespDrain (backlog sitting at the read-ahead budget, so totalPins was momentarily 0) would, when the connection was torn down, have that parked callback fire dispatchNextRead and acquire a fresh pkFileChunk pin on the very connection being freed. That tripped the assertion.

h1 never hit this because it does not multiplex, so it never parks a per-stream read across a teardown. Only h2 stream multiplexing exposes it.

Fix

  • Set c.closeRequested before firing the teardown callbacks in closeConn.
  • Make acquireDispatchPin (and the h1 ws.blocking connection-pin path) refuse to pin a connection that is already closing. A refused dispatch runs its existing dead-connection cleanup (reclaim the pooled buffer, no-op the send / abort), so the totalPins invariant holds by construction rather than being papered over by relaxing the assertion.

Changed: src/vortex/eventloop.nim, src/vortex/request.nim (24 insertions, 3 deletions).

How it was found / validated

Surfaced by the vortex-stress Docker soak matrix: the h2/sync cell crashed instantly on every workload with eventloop.nim(381,3) c.totalPins == 0 connection freed with live worker pins [AssertionDefect]. After the fix, a focused pinned soak (VORTEX_PROTO=h2 VORTEX_SERVER=sync VORTEX_SECONDS=120 nimble stress) reached == stress smoke: all workloads passed == across all five workloads with all chaos sidecars passing and no recurrence. The fix also held clean for ~53 min under the full parallel matrix before an unrelated host-contention/OOM event (not a vortex defect) ended that run.

…ree with live pins

closeConn fires teardown callbacks (onBody last=true, ws onClose, and the h2
per-stream onRespDrain via h2NotifyClosed) between the pinned-defer guard and the
final `doAssert c.totalPins == 0`. On the HTTP/2 sync path a streamed sendFile
download whose next chunk read was parked in onRespDrain (backlog at the
read-ahead budget, so totalPins was momentarily 0) would, when the connection was
torn down, have that parked callback fire dispatchNextRead and acquire a fresh
pkFileChunk pin on the very connection being freed. That tripped the assertion and
crashed the event loop at first traffic. h1 never hit it because it does not
multiplex, so it never parks a per-stream read across a teardown.

Set c.closeRequested before firing the teardown callbacks and make
acquireDispatchPin (and the h1 ws.blocking connection-pin path) refuse to pin a
connection that is already closing. A refused dispatch runs its existing
dead-connection cleanup (reclaim the pooled buffer, no-op the send / abort), so
the totalPins invariant holds by construction instead of being papered over.
@cryo2010
cryo2010 merged commit 77db02e into main Sep 25, 2026
35 checks passed
@cryo2010
cryo2010 deleted the fix/stress-sync-totalpins branch September 25, 2026 03:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant