Repair dangling blob references when a base copy re-delivers the record; add replication.blobGapReconnectMs - #2177
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to repair copy-apply duplicate records when their referenced blob files are missing or incomplete on disk. It adds the blobFileMissingOrIncomplete function in resources/blob.ts to synchronously verify the integrity of file-backed blobs, and integrates this check into the table write path in resources/Table.ts via recordReferencesMissingBlob. Additionally, new replication configuration parameters (blobGapReconnectMs, copyCursorFlushBytes, and copyCursorFlushIntervalMs) are added to the terms and validator, and comprehensive unit tests are provided for the new blob integrity checks. There are no review comments, so I have no feedback to provide.
|
Reviewed; no blockers found. |
6ab3f42 to
f3db5f6
Compare
|
Design rework since the original draft (per Kris): |
Adds REPLICATION_BLOBGAPRECONNECTMS (replication_blobGapReconnectMs) to CONFIG_PARAMS and the replication config schema. Consumed by harper-pro's blob-gap reconnect watchdog (harper-pro#683/#699) as its own interval: previously hard-wired to replication_blobTimeout (900s, shared with blob stream timeouts), so operators could not shorten gap-heal cycles without also making blob streams time out more aggressively. Default unchanged (falls back to blobTimeout). Also registers the existing replication_copyCursorFlushBytes and replication_copyCursorFlushIntervalMs knobs (harper-pro#480), which harper-pro reads via env.get but were never in CONFIG_PARAMS, so setting them in config had no effect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: GPT-5 Codex <noreply@openai.com>
…record is re-delivered A record whose receive-side blob save failed transiently commits with a reference to a PENDING stub (harper-pro#481 stamps aborted re-streamable saves, assuming the reconnect re-stream overwrites the stub — it never did: the re-stream saved under a fresh fileId, the duplicate record was tie-skipped, the dangling reference persisted forever, and the fresh blob was swept as an orphan: the 'record exists but blob file is missing' field class, harper-pro#699, kohls). New repairBlobFile(blob, source): streams replacement bytes INTO the blob's existing fileId — the record needs no rewrite and no duplicate-handling changes. Only fires when the file classifies missing/incomplete (new blobFileMissingOrIncomplete: absent, PENDING/ERROR header, pre-completion size sentinel, truncated-uncompressed; declines on uncertainty) and no writer holds the file's :blob lock; the size header is deferred during the overwrite so concurrent readers keep retryable not-yet-complete semantics. The replication receiver calls it for identity-tie duplicates (harper-pro side). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: GPT-5 Codex <noreply@openai.com>
f3db5f6 to
612de4a
Compare
|
Drafted fix for the --- a/resources/blob.ts
+++ b/resources/blob.ts
@@ -1603,14 +1603,26 @@ export function repairBlobFile(blob: Blob, source: Readable): Promise<void> | un
const lockKey = storageInfo.fileId + ':blob';
if (!storageInfo.store.tryLock(lockKey)) return undefined;
const stampPendingBestEffort = (reason: string) => {
+ // The settle handlers below run AFTER writeBlobWithStream's completion path released the
+ // :blob lock, so a NEW writer for this fileId (a retried repair, or a fresh save — plausible
+ // here, since repair fires on identity-tie duplicate deliveries that can arrive on several
+ // links) may already own the file. Re-acquire before stamping and hold the lock through the
+ // async write (the same pattern as the idle-timeout retry stamp in writeBlobWithStream);
+ // failing to acquire means someone is actively writing — their outcome supersedes this failed
+ // attempt, so the stamp must be skipped, never raced over their finalized header.
+ if (!storageInfo.store.tryLock(lockKey)) return;
try {
const messageBuffer = Buffer.from(reason);
const header = new Uint8Array(HEADER_SIZE);
new DataView(header.buffer).setBigInt64(0, BigInt(messageBuffer.length) | (BigInt(PENDING_TYPE) << 48n));
writeFile(filePath, Buffer.concat([header, messageBuffer]), (writeError: Error) => {
+ storageInfo.store.unlock(lockKey);
if (writeError) logger.debug?.('Error re-stamping pending marker after failed blob repair', writeError);
});
- } catch {}
+ } catch (stampError) {
+ storageInfo.store.unlock(lockKey);
+ logger.debug?.('Error re-stamping pending marker after failed blob repair', stampError);
+ }
};
try {
writeBlobWithStream(blob as any, source, storageInfo, { deferSizeHeader: true, lockHeld: true });Structural notes: Validation extent, honestly: compiles clean under harper-pro's tsc (which builds |
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
|
Reviewed What's better, specifically:
Two findings, both narrow: 1. 2. Repairs are written uncompressed even when the original was DEFLATE (reviewer note 3), which interacts with finding 1. The note frames the tradeoff as storage growth, and I agree that's fine. The part I'd double-check is the header the repair writes: the size field must hold the uncompressed length with Neither is a blocker in my read. 1 is a documentation/intent question, 2 is a belt-and-braces assertion on an invariant I believe already holds. Scope note, not an objection: the two dead #480 config keys and I have not run core's suite from my checkout (harper-pro submodule; its harness needs a standalone core env), so I'm relying on your 98-passing |
Resolve the core submodule conflict to the latest head of HarperFast/harper#2177. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Core companion to “Bank bulk-copy progress across blob-gap reconnects and repair missing blob files on copy re-delivery”. A transient replication save can leave a committed record pointing at a missing, PENDING, or incomplete blob file. When Pro later receives the exact same record, it can now repair that existing local file ID instead of saving a new orphan that the duplicate-skipped record will never reference.
The repair is fail-atomic: it holds the blob lock, writes and verifies a sibling temporary file, flushes it, and renames it over the target before releasing either lock. A failed repair leaves the referenced file byte-for-byte unchanged. The orphan sweeper respects active repair locks and still reclaims stale repair files.
Core also adds the shared header classifier and the replication configuration keys consumed by Pro:
blobGapReconnectMs,copyCursorFlushBytes, andcopyCursorFlushIntervalMs.For the human reviewer
fdatasyncbefore rename; the tradeoff is possible storage growth for a formerly compressed blob.Verification
npm run build— passing.npm run lint:required— passing.npx prettier --check resources/blob.ts unitTests/resources/blob.test.jsandgit diff --check— passing.npx mocha unitTests/resources/blob.test.js— 98 passing.npx mocha unitTests/validation/configValidator.test.js— 72 passing.npm run test:unit:resources— 1,581 passing, 15 pending, with the same three unrelated baseline failures in random-access defaults and replay-structure representation.Review coverage
Full reviews ran through Claude, Gemini, and the Harper domain adjudicator. They found and drove fixes for atomic failure handling, source/descriptor size agreement, lock cleanup, normal-save state isolation, orphan-sweeper coordination, and sliced-blob rejection. Exact-head delta review at
c3d9c7b8fc8dused Claude and Gemini. Claude graded the remaining core findings as minor scope boundaries and explicitly confirmed thatfdatasyncis imported and that the settledWriteStreamretains neither an FD nor a timer. The automatic score remains 4 because the low-risk documentation delta pruned domain adjudication while Gemini repeated that already-refuted stream-lifetime finding.Human-Review-Need: 4 @ c3d9c7b