Skip to content

fix(blob): tolerate source-unavailable blobs in pre-commit so a missing blob can't wedge replication - #1353

Merged
kriszyp merged 3 commits into
mainfrom
kris/blob-commit-tolerate-source-missing
Jun 18, 2026
Merged

fix(blob): tolerate source-unavailable blobs in pre-commit so a missing blob can't wedge replication#1353
kriszyp merged 3 commits into
mainfrom
kris/blob-commit-tolerate-source-missing

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

startPreCommitBlobsForRecord().complete() (resources/blob.ts) now tolerates a blob whose save rejected because the replication source can no longer provide it — flagged sourceBlobUnavailable (new isSourceBlobUnavailable helper). The record commits with a diverged blob reference; local/transient save faults still reject and abort the write.

Purpose

On a replicated expiration cache table with a Blob! field, a base copy (e.g. after an upgrade) sources blobs the origin has since TTL-evicted. complete() awaited the raw saveBlob().saving, whose ENOENT rejection propagated out of the pre-commit phase and was swallowed as error in subscription handler (Table.ts), aborting that record's commit. With a contiguous run of orphaned blobs, every apply re-threw, the base copy never advanced, and the connection wedged at ~100% backpressure permanently.

Confirmed live on preprod.jjl: the receiver was both advancing the resume cursor past the blob (harper-pro#405, the durability-watermark side) and re-throwing the same sourceBlobUnavailable error in the apply path. This fixes the apply/commit side that #405 didn't cover.

Where to look

  • resources/blob.tsisSourceBlobUnavailable (the classification gate) and the .catch in complete(). The tolerance fires only on the sourceBlobUnavailable flag, which only the replication receiver sets (markSourceBlobUnavailable, harper-pro#403) — so local writes (which never set it) are unaffected and the no-silent-loss guarantee holds.
  • Tradeoff worth your judgement: a replicated record can now commit with a known-missing blob (diverged, backfilled via harper-pro#388). For a TTL cache table this is correct (the entry re-fetches/expires); it's the same hold-vs-advance policy fix: Add schemas files to npm package #405 established, applied to the commit path.

Reviews / open items for the reviewer

  • Codex: clean — "No actionable correctness issues were found in the changed code. The targeted blob tests and TypeScript build pass."
  • Gemini: ⚠️ only one cross-model review on this PR — the agy CLI ran as an autonomous agent and timed out on environment setup without producing a review, and the gemini CLI isn't available in this environment. A careful human read of the no-silent-loss reasoning (tolerate only sourceBlobUnavailable, never a local/transient fault) is the main ask.
  • Scope: this is the urgent stall fix. The orphan-creation root cause (expiration-scan unlink-vs-commit ordering) is a separate, durability-sensitive change — entangled with the deferred-delete / RecordEncoder path — and is tracked as a focused follow-up rather than bundled here.

Tests

unitTests/resources/blob.test.js: the isSourceBlobUnavailable classification gate, and complete() tolerates-marked / rejects-unmarked (faithfully, via a destroyed receive stream). Full resources suite green locally (828 passing, 0 failing).

🤖 Generated by Claude (Opus 4.8).

…ng blob can't wedge replication

A replicated record whose blob is gone at the source (evicted/expired at the origin;
the replication receiver flags the save rejection `sourceBlobUnavailable`, harper-pro#403)
was aborting the record's apply: `startPreCommitBlobsForRecord().complete()` awaits the raw
`saveBlob().saving`, whose rejection propagates out of the commit's pre-commit phase and is
caught/swallowed as `error in subscription handler` (Table.ts). The record never commits, so
on an expiration cache table full of TTL-evicted blobs every orphaned record re-threw, the
base-copy never advanced, and backpressure pinned at ~100% — a permanent replication stall
(observed on preprod.jjl).

`complete()` now tolerates a `sourceBlobUnavailable` rejection (`isSourceBlobUnavailable`):
the record commits with a diverged blob reference, left for proactive backfill (harper-pro#388),
instead of aborting the apply. Local/transient save faults stay unmarked and still reject, so
the write aborts and a reconnect retries it — no silent loss. This is the apply/commit-side
complement to the receiver's resume-cursor advance (harper-pro#403/#405), which handles the
durability-watermark side of the same blob.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kriszyp
kriszyp requested a review from cb1kenobi June 17, 2026 23:07

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

Copy link
Copy Markdown
Contributor

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 ensures that source-unavailable blobs do not abort replication commits, preventing replication copy streams from getting wedged. It introduces the 'isSourceBlobUnavailable' helper to identify unrecoverable source-missing errors and updates 'startPreCommitBlobsForRecord' to tolerate them while still failing on local or transient faults. Corresponding unit tests and design documentation have been added. The feedback suggests using 'await createBlob(...)' in the new tests to maintain style consistency with the rest of the test suite.

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.

Comment thread unitTests/resources/blob.test.js Outdated
Comment thread unitTests/resources/blob.test.js Outdated
@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

…ni review)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review June 18, 2026 00:16
@kriszyp
kriszyp merged commit 0913320 into main Jun 18, 2026
45 of 47 checks passed
@kriszyp
kriszyp deleted the kris/blob-commit-tolerate-source-missing branch June 18, 2026 18:44
kriszyp added a commit that referenced this pull request Jun 19, 2026
The harper-bench host serves harper + harper-pro via a repo-scoped just-in-time
supervisor (one job at a time), not an org-level runner — org/enterprise runners
aren't routed jobs for these repos. Also merges current main to pick up the blob
fixes (#1353, #1364/#1369) the stale branch was missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kriszyp added a commit that referenced this pull request Jun 19, 2026
… (ST-5), perf-nightly workflow (#1379)

Adds three single-node storage benchmarks + the perf-benchmarks-nightly workflow (repo-scoped JIT bench-runner). CI red is pre-existing on main (blob.test.js:586 since #1353), unrelated to these benchmark-only files.
kriszyp added a commit that referenced this pull request Jun 19, 2026
…-saving blobs (#1353/#1376)

The blob unit test asserting complete() "tolerates a source-missing blob
but still rejects a transient fault" was RED on main. The assertion was a
stale leftover from #1353, not a code regression.

#1353 made complete() await EVERY pre-commit blob, so any save fault aborted
the commit. #1376 then split pre-commit blobs into blobsNeedingSaving
(saveInRecord / saveBeforeCommit — still awaited) and blobsToTrackOnly
(already-saving, replication-received — deliberately NOT awaited, to avoid a
WS deadlock when a back-pressured copy stream can't supply more chunks:
harper-pro#414). Track-only durability moved off commit-abort onto the
replication resume cursor (hasBlobGap pins lastDurableSequenceId so the blob
is re-streamed on reconnect).

The old test drove the transient-fault blob through the track-only path,
which complete() no longer awaits, so the expected rejection never fired.

This is a test-only contract correction (resources/blob.ts is unchanged):
- the "transient fault must still abort" assertion now targets a
  needs-saving (saveBeforeCommit) blob, where abort is still the contract;
- a new case asserts a transient fault on a track-only blob does NOT abort;
- the source-unavailable→tolerated case is kept (now on the awaited
  needs-saving path, so it genuinely exercises the isSourceBlobUnavailable
  tolerance in complete());
- a comment records the #1353#1376 history so it isn't "fixed" back.

Not a v5.1.5 data-loss regression and no 5.1.6 code patch is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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