Skip to content

fix(replication): re-request in-flight blobs on connection close instead of waiting out the idle timeout - #527

Merged
kriszyp merged 3 commits into
mainfrom
kris/blob-receive-restart-recovery
Jul 8, 2026
Merged

kriszyp merged 3 commits into
mainfrom
kris/blob-receive-restart-recovery

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

On a replication WS close, abort each still-receiving blob immediately instead of leaving it in blobsInFlight for core's source-idle watchdog to reap up to blobTimeout (REPLICATION_BLOBTIMEOUT, now 900s) later. The abort is classified transient by receiveBlobs, so it clamps the resume cursor and the reconnect re-streams the blob promptly.

Why

Live Fabric finding (harper-pro 5.1.14): a deploy-payload blob was diverged on one node as a week-old 62-byte PENDING stub. Root cause, confirmed by a deterministic correlation (every Blob source stream idle for 120000ms receive failure preceded ~120s earlier by a Restarting http_workers on the peer):

  • deploy_component triggers Restarting http_workers; the rolling restart tears down the worker mid-stream while it is sending a blob to a peer.
  • The receiver sees the source go silent and only aborts after the full idle timeout, stamping a PENDING stub — so the blob stays diverged for the whole timeout window before the reconnect can re-request it. On deploy-heavy hosts the transfer keeps getting caught.

Raising the timeout (#520) widens the window but doesn't stop the interruption or resume the killed transfer. This makes the receiver react to the close immediately instead of waiting it out.

What to look at

  • receiveBlobs's .catch classification interplay: the abort error is marked replicationConnectionClosed so it takes a quiet transient path — clamp + re-request, but no error log and no cluster_status.blobReplicationFailures bump (this is routine on every deploy, not a divergence).
  • writableEnded preservation: a completed-but-unconnected stream (chunks that arrived ahead of their record) is intentionally not aborted, so an in-flight handler can still attach and save it.

Scope / follow-up

Receiver-side only (harper-pro). A complementary sender-side hardening — graceful bounded drain of in-flight blob sends before the worker restart — is a planned follow-up (coordinated core + harper-pro). The DeployLifecycle listener-leak warning seen alongside this is separately addressed by harper#1465.

Reviewed cross-model (Codex + Gemini); their findings (preserve completed streams; avoid log/metric spam on routine closes) are already folded into the diff.

🤖 Generated with Claude Code (model: Claude Opus 4.8)

…ead of waiting out the idle timeout

A component deploy triggers `Restarting http_workers`, which tears down the worker
mid-stream while it is sending a replication blob. The receiver was leaving the
half-written blob receive streams in `blobsInFlight` for core's source-idle watchdog
to reap up to `blobTimeout` (REPLICATION_BLOBTIMEOUT, now 900s) later, only then
stamping a PENDING stub — so a diverged deploy-payload blob lingered for the whole
timeout before the reconnect could re-request it (seen live as a week-old PENDING stub).

On `ws` close, abort each still-receiving blob immediately (new `abortInFlightBlobsOnClose`):
the plain Error is classified transient by `receiveBlobs`, which clamps the resume cursor
so the reconnect re-streams the blob promptly. Completed-but-unconnected streams
(`writableEnded` — chunks that arrived ahead of their record) are preserved so an in-flight
handler can still attach and save them. The abort error is marked
`replicationConnectionClosed` so `receiveBlobs` treats it as a routine, self-healing
interruption — clamp + re-request — without logging an error or bumping the
cluster_status divergence metric on every deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to immediately abort in-flight blob receives when a replication connection closes, avoiding long timeouts and ensuring prompt re-requests upon reconnection. It includes the helper functions abortInFlightBlobsOnClose and isReplicationConnectionClosedError, integrates them into the WebSocket teardown and error handling paths, and adds comprehensive unit tests. The review feedback suggests improving the robustness of the error message when remoteNodeName is undefined, and adding connectionId to the debug log for better traceability.

Comment thread replication/replicationConnection.ts
Comment thread replication/replicationConnection.ts Outdated
@claude

This comment has been minimized.

- fall back to 'unknown' in the abort error when remoteNodeName is unset (pre-handshake close)
- include connectionId in the connection-close debug log for correlation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review July 6, 2026 17:52
@kriszyp
kriszyp requested a review from a team as a code owner July 6, 2026 17:52
@kriszyp
kriszyp removed request for a team and kylebernhardy July 6, 2026 17:56
@kriszyp kriszyp added the patch label Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Patch cherry-pick: merged

Cherry-picked onto v5.1.

Comment thread replication/replicationConnection.ts Outdated
…ableEnded blob streams on close

A completed-but-unconnected blob stream (writableEnded, chunks outran its record) is
intentionally preserved in blobsInFlight on connection close so an in-flight handler can
still attach and save it. But its core-level registerBlobReceiveInFlight marker was never
released if the record never arrived: receiveBlobs's .finally (the normal release site)
never runs for a stream it never touched, and blobsTimer is already cleared. That pinned
isBlobReceiveInFlight true for the process lifetime, permanently 503-ing reads of that blob.

abortInFlightBlobsOnClose now invokes onAbort for the preserved stream too (without
destroying it or removing it from the map), trading a brief window where an in-flight
handler on the closing connection could still attach after the marker is released for
closing a leak that otherwise never heals. unregisterBlobReceiveInFlight is idempotent, so
a later legitimate release for the same stream is a safe no-op.

Addresses cb1kenobi's review comment on PR #527.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jul 8, 2026
…ableEnded blob streams on close

A completed-but-unconnected blob stream (writableEnded, chunks outran its record) is
intentionally preserved in blobsInFlight on connection close so an in-flight handler can
still attach and save it. But its core-level registerBlobReceiveInFlight marker was never
released if the record never arrived: receiveBlobs's .finally (the normal release site)
never runs for a stream it never touched, and blobsTimer is already cleared. That pinned
isBlobReceiveInFlight true for the process lifetime, permanently 503-ing reads of that blob.

abortInFlightBlobsOnClose now invokes onAbort for the preserved stream too (without
destroying it or removing it from the map), trading a brief window where an in-flight
handler on the closing connection could still attach after the marker is released for
closing a leak that otherwise never heals. unregisterBlobReceiveInFlight is idempotent, so
a later legitimate release for the same stream is a safe no-op.

Addresses cb1kenobi's review comment on PR #527.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kriszyp
kriszyp merged commit d7134c2 into main Jul 8, 2026
47 of 49 checks passed
@kriszyp
kriszyp deleted the kris/blob-receive-restart-recovery branch July 8, 2026 22:38
github-actions Bot pushed a commit that referenced this pull request Jul 8, 2026
…overy

fix(replication): re-request in-flight blobs on connection close instead of waiting out the idle timeout
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants