Repository navigation
Conversation
restore_backup purged the destination (backups.restore with purgeAllFiles) before anything established that this build could read the backup, so a corrupt backup, a table format_version from a newer binding, or an unsupported transaction-log header destroyed the database it was meant to restore. Restore now stages the engine beside the database, validates every staged transaction-log store, opens and stamps the staged copy with this build, and only then publishes it by two renames. The marker protocol's one-way door moves from the purge to the first rename: any staging failure leaves the destination untouched. A publication interrupted between the renames keeps the displaced database as `.replaced` until a later attempt publishes; a failed second rename moves it back. Both the online operation and the offline CLI share the path. Fixes #2965 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…very Review round 1 on #2965: - a restore into a destination that did not exist (a new target_database) reported nothing destroyed after publishing, so a blob-restore failure cleared its marker; publication now always counts as destruction - a rerun after a crash between the publication renames takes the staging mode from `.replaced` - DESIGN.md states the bound on the open's SST coverage instead of claiming all of it - rename injections re-sync the ESM builtin exports so they also hold under TypeStrip; stale purge wording, a misleading test title and a lenient CLI exit check are corrected Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 on #2965: a live holder the PID check missed now fails fast instead of after a full staging copy; the probe before publication stays. The generated backup README says the staged copy is opened rather than claiming it is verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release cherry-pick
|
Raised by the companion docs review: staging lives beside the database directory, so when that directory is itself a mount point the staged copy lands on the parent filesystem and publication's rename fails only after a full copy. Refuse it up front, as the symlink case already is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 4 on #2965: staging writes a second engine copy, online while every database on that filesystem keeps serving, where the purge it replaced freed the space first. A copy that would not leave headroom (the larger of 256 MiB and a tenth of the copy) is now refused with a 507 before it starts, leaving the database untouched. The symlink refusal under an interrupted earlier restore now says to rerun offline before the next start, since repointing the path moves the restore metadata away from the marker guarding the half-restored directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 5 on #2965: a rerun after a failure past publication kept both `.replaced` and the published candidate, so the space check asked for a third copy and could refuse the only way out of a marked database. Under a preexisting marker with `.replaced` present the database path holds only a disposable candidate, which publication already removed; it is now removed in preparation. DESIGN.md names the space check's limits: in-place restores need two copies even offline, and concurrent restores on one filesystem are not reserved against each other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 6 on #2965: a rerun reads an existing `.replaced` as proof that the database path holds only a publication's candidate, but a finished restore could leave `.replaced` behind when its removal failed, so a later interrupted restore's rerun could delete the live database. A finished restore now renames `.replaced` to `.discarded` inside its protected section, so a failure keeps the marker and `.replaced` never outlives its restore; only removing `.discarded` may fail quietly, and preparation always clears it. Dropping a candidate in preparation now counts as destruction, and a refusal over a preexisting marker no longer claims the database was not modified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…online success Review round 7 on #2965: the "rerun restore_backup to recover" wrapper now carries the original status code (a 507 refusal on a rerun used to lose it), and a unit test restores a valid backup through the online path, which the integration test cannot reach until #3120. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e binding The review-coverage policy test requires every production module that imports a storage binding to be listed in framing_paths; dataLayer/restoreStaging.ts imports @harperfast/rocksdb-js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gemini review on #3121: format a thrown non-Error with String() and read its code with optional chaining, at the staging, rerun-wrapper and symlink-probe sites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rerun of an unfinished publication removes the candidate at the database path while preparing staging, which runs before the close broadcast and the closure check. A component holding that candidate would have its files unlinked underneath it. The online path now verifies nothing holds the database before preparing such a rerun; offline already probes first. DESIGN.md also narrows the mount-point refusal to what it detects: a same-device bind mount passes and fails at the first rename. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Online staging holds the restore marker for the whole copy, and a schema rescan in that window skipped the marked database and then dropped it from the map without closing its root store. The restore's close broadcast could no longer reach it, so verifyDatabaseClosed refused with a misleading 409 and the database stopped serving for the rest of the copy. The scan now keeps a marked root this thread already has open; one it has not opened is still skipped. The startup-scan test now closes its database first, as a real startup has nothing open. A .replaced that no restore marker accounts for is now refused (409) instead of being trusted as an unfinished publication, which would have dropped the live database before the backup was verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The online closure pre-check ran whenever .replaced existed, so a stray one under a fresh marker waited out the close deadline and refused with "held open by a loaded component", sending the operator offline for the wrong reason. It now runs only for a rerun under a preexisting marker, leaving the refusal that names .replaced to answer the stray case; the test matches that message. Also tidies comments the review flagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rving A rerun under a preexisting marker no longer claims the database is still incomplete: a crash during staging leaves that marker over an untouched database, so the refusal now says it may be incomplete. The rescan's "in progress; not loading it" warning is skipped for a database this thread keeps serving during online staging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp
left a comment
There was a problem hiding this comment.
I'm skeptical of this approach, as it significantly increases the disk space required to do a restore. If the remaining free space is less than the database size, it will now fail. Before this PR, there is some slight chance that this would detect a failure ahead of time, but if there is too little disk space, there is now a 100% chance of failure. Reporting the failure loudly ahead of time seems like a poor consolation in the midst of an emergency restore, if the reporting was the actual cause of the failure. I would think most of the likely failures could be checked in place, and others could still be remedied after the main database was deleted in a failed restore (if you are doing a restore, you are probably pretty committed to replacing the main database).
🤖 Reviewed with Codex
Staging needed disk for a third copy (database, repository, staging), so a restore on a tight disk was always refused. Both restore paths now run backups.verify with checksums and verifyTables against the repository before the purge, which catches the same unreadable backups (corrupt file, unsupported log header, unsupported table format or compression) with no extra disk. The staging, .replaced recovery, space check and symlink/mount-point refusals go away with it. verify_backup checks tables too, so it agrees with what restore will accept. Requires the @harperfast/rocksdb-js release that adds verifyTables (HarperFast/rocksdb-js#925). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full checksum read can take minutes. Holding the restoring marker across it meant a crash left an intact database flagged incomplete and unloadable, and the rerun's refusal told the operator to restore over newer data. Verification now runs before the lock, marker and pin; the claim's findBackup re-checks under the repository lock that the backup is still there. With no marker held while the database serves, the rescan exemption in databases.ts is no longer needed and is reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s blocked Verification runs unpinned, and deleting the newest backup frees its id for the next create_backup, so the claim now checks it is the same backup (timestamp, size, file count, metadata), not just the same id. A refusal over an interrupted restore says the database stays unloadable instead of "was not modified". The restore JSDoc now lists verification first, matching the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b snapshot Holding the repository lock from verification through the claim keeps a delete plus create_backup from swapping a different backup in under the same id, so the identity comparison is no longer needed. The lock is already held this long behind a create_backup. Verification still runs before the marker, so a crash mid-verify leaves the database loadable. Restore now shares verify_backup's file checks, including the missing blob snapshot a manifest declares, which restoreBlobSnapshot would otherwise only warn about after the purge. A failure reading the marker while reporting a refusal no longer masks the verification error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manifest read, the blob-root check and the engine-only decision ran before the lock, so a delete plus create_backup reusing the id could leave them describing a different backup than the one verified and pinned. All of selection now runs inside the lock with verification and the claim. A refusal says this restore did not modify the database, which stays true even when the database's restore state is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t status Restore opens every table; verify_backup keeps its existing default rather than changing cost and results for every caller. A refusal caused by a client error (a missing declared blob snapshot) keeps its status instead of surfacing as a server error, and a non-Error rejection can no longer replace the diagnosis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ackup's blob snapshot missing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verifying while the database still served let it acknowledge writes for the length of a full checksum read that the purge then discarded; before this PR the close came first and those writes failed instead. Online verification now runs after the close and the closure check, under the claim, with the pin protecting the selected backup. Offline, with no live writers, keeps verifying under the repository lock before the claim, so a crash mid-verify leaves the database loadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verification can run for minutes after verifyDatabaseClosed. The marker keeps Harper from reopening the database, but a component opening the path directly is not held off by it, so the closure check runs again right before the purge. The offline comment no longer claims it mirrors online, and the design note states the crash-mid-verify cost plainly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fresh restore claim now takes the lock and the pin but writes no marker until immediately before its first destructive step (markRestoreDestructive). The held lock alone already keeps every open off the database, so a crash while claimed — online, that is the whole verification read — leaves the intact database loadable instead of marked incomplete, where a rerun of an unreadable backup could only be escaped by restoring older data over it. Pins count while their restore holds the lock, not only while a marker exists, so the source stays protected before the marker. An offline create_backup treats a held claim as an active restore. Clearing a claim that never marked is a no-op on the marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tore Online verification now runs after the closure check, so the insert that added `note` (a redeclare, which leaks handles until harper#3120 lands) made the online refusal a closure 409 instead of a verification refusal. Declaring the attribute at create_table avoids the redeclare, which also lets the success path run online. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks Kris. That was the open "your call" on disk cost, and your point about the emergency restore on a tight disk carries it. Staging is gone; the restore now checks the backup in place and needs no extra disk. Before anything is deleted, Ordering, since it is the subtle part:
The cost is time rather than disk: the check reads the whole backup once, and online that read is inside the downtime. 🤖 Generated by Anthropic Claude (Opus); posted via @cb1kenobi. |
⊙ Problem
restore_backuppurged the database before anything checked that this build could read the backup (#2965: restore_backup purges the destination before anything verifies the backup can be read). A corrupt file, a table in a newerformat_version, or an unreadable transaction log destroyed the database it was meant to restore.💡 Solution
Verify the backup in place, in its repository, before the purge, with no staging copy and no extra disk. Online, the check runs after the database is closed, so no write is acknowledged and then lost. The restoring marker is now published only immediately before the purge, so a crash during the check leaves the database loadable.
⚖️ Alternatives
🔧 Changes
dataLayer/rocksdbBackup.ts).assertBackupReadablerunsverifyBackupFiles:backups.verifywith checksums andverifyTables(rocksdb-js 2.12.0), plus a check that a declared blob snapshot exists. A refusal keeps a client error's status and says this restore did not modify the database, or that an interrupted restore stays blocked.verify_backupis unchanged.dataLayer/restoreMarker.ts).claimRestoretakes the lock and runs the pin callback but writes no marker for a fresh claim.markRestoreDestructivepublishes it before the purge, andbeginRestoreis both together.checkRestoreState's marker-first contract still holds.restoreLockHeldExclusivelyis factored out of it, and clearing a never-marked claim is a no-op.claimRestore, pins count while the lock is held, and an offlinecreate_backuptreats a held claim as an active restore.dataLayer/DESIGN.md, pin lifetime, and its index line.✅ Verification
End-to-end route: the new integration test, through the Operations API and the CLI.
restore-backup-preflight.test.ts: an online corrupt-backup refusal leaves the database writable, an offline refusal survives a restart, and a valid backup restores offline and online. It declares its table up front so no insert redeclares it (#3120 leaks handles on a redeclare).rocksdbBackup.test.js, 20 cases on both paths:format_version99, an unreadable log, and a missing blob snapshot;restoreMarker.test.js: a claim holds opens off without a marker, an abandoned claim leaves the database loadable, and a recovery claim keeps its marker.dist/and fails its test. On rocksdb-js 2.12.0:test:unit:backup182 passing,test:unit:resources4283 passing; typecheck, lint, format andcheck:design-docsclean.test:unit:main(it opens the local install) and the integration test (port in use); both run in CI.qa822-purge-log-invariantsfails onmaintoo.Docs: HarperFast/documentation#721: Document that
restore_backupchecks a backup before replacing the database.🤖 Generated by Anthropic Claude (Opus); posted via @cb1kenobi.
Related PRs: #2648 independent, #2649 independent, #3107 independent, #2155 independent
Complexity: complicated
Release target: v5.4 — a behavior change to restore, milestoned for the next minor
Framing-Verdict: chosen-approach-sound (round roll-up)
Review-Coverage: authored=claude; ran=gemini,codex,cursor-composer,cursor-muse,cursor-grok; adjudicated=domain; declined=cursor-kimi; rounds=17; full=8 @ dd51b03
Review-Attention: deep ~40m (critical: backupRepository.ts, restoreMarker.ts +1; decisions: markerless-claim, online-verify-after-close, verify-backup-skips-tables, checksum-on-every-restore, do-less-alternative, release-target-vs-evidence) @ dd51b03