fix(replication): per-chunk send timeout to unwedge stalled blob streams - #451
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a per-chunk timeout mechanism when streaming blobs over WebSockets to prevent the send loop from hanging indefinitely if an underlying read stalls. The reviewer noted that manually iterating the async iterator and terminating early on timeout can leak resources (such as file descriptors), and suggested wrapping the iteration in a try-catch block to explicitly call iterator.return() for proper cleanup.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Reviewed; no blockers found. Prior observations resolved: Carry-over observation (not a blocker, already raised): no unit test for the |
|
Nice, surgical fix — racing One blocker before this lands as a fix (vs a dormant capability): it defaults to Live context for why this matters now: a customer cluster preprod 5.1.7 is wedged on exactly this mechanism (tracked in #453). The Suggestion: mirror the existing — Claude (Opus 4.8), from live a customer cluster investigation |
…BTIMEOUT) The per-chunk send timeout added in this PR defaulted to 0 (off), with nothing setting HARPER_BLOB_SEND_CHUNK_TIMEOUT_MS anywhere — so it shipped inert and a stalled blob send could still silently wedge a base copy (observed live on JJill preprod 5.1.7, harper-pro#453: a missing system deployment-payload blob hung sendBlobs, no finishing frame, the receiver stuck at "Receiving"/ver=0 forever, COPY_COMPLETE never reached, replicated deploys blocked). Default it to the existing replication blob timeout (REPLICATION_BLOBTIMEOUT, 120000ms default) so it's on out of the box and shares one operator knob with the receiver-side blob timeout. HARPER_BLOB_SEND_CHUNK_TIMEOUT_MS still overrides it; set it to 0 to disable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pushed an enablement commit ( — Claude (Opus 4.8) |
|
@ldt1996 — a question on whether this is still needed, prompted by the #453 investigation. 🙏 Tracing the sender path on current core,
So for a missing / corrupt / mid-write local blob, the Before I suggest closing this as redundant: was the Akamai v4→v5 stage-cluster case that prompted #450 going through a blob source that bypasses (Separately — your receiver-side harper#1444 looks like it may cover a distinct gap: a Really nice instinct filing these from the soak cluster either way — just want to make sure we're not double-guarding the same path. — Claude (Opus 4.8), from the #453 investigation |
Fixes #450.
Summary
sendBlobsinreplication/replicationConnection.tsiterates the local blob withfor await (const buffer of blob.stream()). When the underlying read stream stalls (production trigger: ENOENT or confidently-corrupt local blob file), the iterator's next.next()never resolves, the for-await waits forever, no finishing BLOB_CHUNK is sent, and the receiver-side apply consumer wedges atlastReceivedStatus:"Receiving"until its own idle watchdog (core/resources/blob.ts, harper#1443/#1444) fires ~120s later.Fix
Rewrite the loop with explicit
iterator.next()calls wrapped inPromise.raceagainst asetTimeoutreject. Gated byHARPER_BLOB_SEND_CHUNK_TIMEOUT_MS(default 0 = off, behavior unchanged for callers that do not set it; replication deploys set 120000ms). On timeout the catch path emits the finishing BLOB_CHUNK witherror: 'Blob send chunk timeout after <ms>ms (fileId=<id>)'anderrorCode/errorStatusderived from the underlying error, so the receiver can advance the resume cursor past a permanently-missing blob via the existingisPermanentSourceBlobErrorCodeclassification.The receive-side idle watchdog (harper#1444) is the rescue when the sender doesn't close the frame; this fix lets the sender close it first, which is the cleaner direction.
Test plan
ts.transpileModulereports no diagnostics on the edited file)analytics/profile.ts,core/dataLayer/...) are unaffectedHARPER_BLOB_SEND_CHUNK_TIMEOUT_MS=2000on a sender, induce ENOENT on a referenced blob file, verify the receiver gets the finishing error frame within ~2s instead of waiting on its own watchdogBlob send chunk timeoutand not the receive-sideBlob source stream idle(sender now closes the frame first)Production observation
Akamai v4→v5 stage cluster (5.1.6-fixes2): nl-ams-1 logged 6077 ENOENT blob-send errors during the post-upgrade catch-up window. The receive-side watchdog (harper#1444) caught 420 of the resulting stalls. With this fix, the sender closes the frame in the configured window rather than depending on the receiver's watchdog.
Companion change
Receive-side idle watchdog: harper#1443 / harper#1444. Both halves are bundled in
harperfast/harper-pro:5.1.6-fixes2.