Skip to content

fix(replication): raise default REPLICATION_BLOBTIMEOUT from 120000 to 900000ms - #520

Merged
kriszyp merged 1 commit into
mainfrom
fix/blob-timeout-default-900s
Jul 6, 2026
Merged

kriszyp merged 1 commit into
mainfrom
fix/blob-timeout-default-900s

Conversation

@ldt1996

@ldt1996 ldt1996 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Why

Under a v4→v5 rolling upgrade with concurrent write load (3 k6 VUs, 30k×50KB preload + workload), the 120s default REPLICATION_BLOBTIMEOUT produced 4,472 permanently-orphaned blob files — records committed on all nodes with refs to blob paths that no node held on disk. The mechanism: blob transfer stalls 120s → receive watchdog kills the subscription → audit cursor advances past the "unavailable" blob (harper-pro#388 divergence marker) → record permanently references a blob that will never land.

Evidence

Same cluster, same k6, same roll, only the timeout changed (from 120000 to 900000 via env override):

Metric 120s default 900s override
Truly-orphan blobs (missing on all 4 nodes) 4,472 0
Master unique missing paths 8,574 13
Blob source stream idle events 9,213 262
Records advanced-past-blob (#388 marker) 813 8

The remaining 13 "partial" paths are mid-flight batches (adjacent fileIds), not permanent loss.

Fix

Change the default at three sites in replication/replicationConnection.ts:

  • Line 309: PAUSE_STALL_THRESHOLD_MS inheritance base
  • Line 1532: blobTimeout main assignment
  • Line 1657: floor guard when the env var is set to 0/negative

HARPER_BLOB_SEND_CHUNK_TIMEOUT_MS falls through to blobTimeout when unset, so raising the receive default cascades to the send default automatically. Operators can still override to a lower value via env var.

Not addressed here

Making blob receive resumable across a source restart (so the timeout matters less) is a bigger change and stays a future PR.

@ldt1996
ldt1996 requested a review from a team as a code owner July 3, 2026 13:09

@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 increases the default replication blob timeout from 2 minutes (120,000 ms) to 15 minutes (900,000 ms) to prevent subscription termination and permanent divergence during rolling upgrades and concurrent writes. The review feedback highlights a potential CPU pegging issue if the timeout is configured to 0 or a negative value, and suggests guarding the definition of blobTimeout. Additionally, the reviewer points out that tying the blobsTimer cleanup interval to the increased timeout could delay memory reclamation and lead to out-of-memory (OOM) issues, recommending that the sweep interval be decoupled from the timeout.

// upgrade + concurrent writes had blob transfers routinely stalling past the timeout, the
// receive watchdog then killed the subscription, and the audit cursor advanced past the
// missing blob. 900000 lets in-flight transfers complete across a peer restart window.
const blobTimeout = env.get(CONFIG_PARAMS.REPLICATION_BLOBTIMEOUT) ?? 900000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If REPLICATION_BLOBTIMEOUT is configured to 0 or a negative value, blobTimeout will be 0 or negative. This will cause setInterval(..., blobTimeout) for blobsTimer to run in a tight loop (as Node.js treats 0 or negative delays as 1 ms), pegging the CPU at 100%. Additionally, a negative blobTimeout would cause all blob streams to instantly time out.

Guarding blobTimeout at its definition ensures it is always positive and safe to use throughout the file.

Suggested change
const blobTimeout = env.get(CONFIG_PARAMS.REPLICATION_BLOBTIMEOUT) ?? 900000;
const rawBlobTimeout = env.get(CONFIG_PARAMS.REPLICATION_BLOBTIMEOUT) ?? 900000;
const blobTimeout = rawBlobTimeout > 0 ? rawBlobTimeout : 900000;

Comment on lines +1655 to +1657
// blobTimeout (REPLICATION_BLOBTIMEOUT) defaults to 900000 and is shared with blobsTimer; guard
// against a misconfigured 0/negative that would otherwise forceReconnect in a tight loop.
intervalMs: blobTimeout > 0 ? blobTimeout : 120000,
intervalMs: blobTimeout > 0 ? blobTimeout : 900000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

With blobTimeout now guaranteed to be strictly positive (by guarding it at its definition), we can simplify the copyProgressWatchdog interval configuration and remove the redundant inline guard.

Suggested change
// blobTimeout (REPLICATION_BLOBTIMEOUT) defaults to 900000 and is shared with blobsTimer; guard
// against a misconfigured 0/negative that would otherwise forceReconnect in a tight loop.
intervalMs: blobTimeout > 0 ? blobTimeout : 120000,
intervalMs: blobTimeout > 0 ? blobTimeout : 900000,
// blobTimeout (REPLICATION_BLOBTIMEOUT) defaults to 900000 and is shared with blobsTimer.
intervalMs: blobTimeout,

Comment on lines +1532 to +1535
// Default 15min: a 120s cap dropped ~4,500 blobs to permanent divergence when a rolling
// upgrade + concurrent writes had blob transfers routinely stalling past the timeout, the
// receive watchdog then killed the subscription, and the audit cursor advanced past the
// missing blob. 900000 lets in-flight transfers complete across a peer restart window.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Increasing the default blobTimeout to 15 minutes (900,000 ms) has an unintended side-effect on the blobsTimer cleanup interval (defined at line 4438), which is set to run at blobTimeout interval.

With a 15-minute interval, any stalled blob stream in blobsInFlight can remain in memory for up to 30 minutes before being swept. Under high concurrent write load with stalling transfers, this slow reclamation can lead to significant memory accumulation (holding PassThrough streams and buffers) and potentially cause out-of-memory (OOM) issues.

Consider decoupling the blobsTimer sweep interval from blobTimeout (e.g., running the timer every 30 seconds or 1 minute) while keeping the expiration check against blobTimeout.

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@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.

@kriszyp
kriszyp merged commit b9af431 into main Jul 6, 2026
39 checks passed
@kriszyp
kriszyp deleted the fix/blob-timeout-default-900s branch July 6, 2026 15:49
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