Give the event store a restore path, and prove it by destroying one - #582
Merged
Conversation
CORA is described as the system of record for the experiment, and the Postgres event store is where that record lives. Until now there was no backup of any kind: no tool, no WAL archive, no restore path. That is the one claim about CORA a facility reviewer can disprove in a single question, so it blocks the 2-BM pilot ahead of the modelling backlog. Picks pgBackRest, closing the deferred row. The candidates were pgBackRest, WAL-G and managed; managed is unavailable on-premises, and between the other two the deciding difference is who runs the restore. That person is a beamline scientist at an awkward hour. pgBackRest generates the recovery configuration itself; WAL-G asks them to hand-author recovery.signal and restore_command at the moment they are least equipped to. pg_dump was not a candidate: it is a snapshot, so it cannot answer a row about point-in-time recovery at all. The drill is the point. A backup nobody has restored is a belief, so scripts/restore_drill.py deletes the contents of a real PGDATA and brings it back, verifying at the domain level rather than at "Postgres started". It writes three known Run streams around the backup and the recovery target: one inside the base backup, one after it (recoverable only from archived WAL), one after the target (which must not return). It then compares each surviving stream's ordered fold digest, so a restore that dropped or reordered an event inside a stream fails even with the row count intact. It also asserts the damaged stream's WAL segment reached the repository first, since otherwise its absence would prove a truncated archive rather than a working target. Executing it, rather than writing it, is what found the real defects. The documented restore command aborted on any populated PGDATA, which is every real restore, and needed --delta. Backup and stanza commands need a live cluster connection, not just the data directory. pgBackRest selects a backup set at whole-second resolution. Archiving failures before stanza-create are expected and made a naive failed_count check useless. Repository target stays deferred, deliberately: repo1-type is one config key, so local disk today swaps for a facility share or object store with no operator-facing command change. What does NOT defer is repository encryption, which is fixed at stanza creation, and the limits section now says so alongside the rest of what is missing: nothing schedules backups, so nothing expires them either; nothing alerts on a failing archive; nothing runs verify; and the drill proves the mechanism, not 2-BM's host. Also records the interaction with out-of-band Atlas migrations. Nothing asserts the schema version at boot, so restoring a pre-migration backup under a newer image fails silently. The restore procedure carries that check until the application does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
CORA is described as the system of record for the experiment, and the Postgres event store is where that record lives. There was no backup of any kind: no tool, no WAL archive, no restore path. That is the one claim about CORA a facility reviewer can disprove in a single question, so it blocks the 2-BM pilot ahead of the modelling backlog.
The decision
Closes the
Backup / PITRrow indocs/stack/deferred.md. pgBackRest. Managed Postgres is unavailable on-premises, and between the two remaining candidates the deciding difference is who runs the restore: a beamline scientist at an awkward hour. pgBackRest generates the recovery configuration itself; WAL-G asks that person to hand-authorrecovery.signalandrestore_command.pg_dumpwas not a candidate, being a snapshot, so it cannot answer a point-in-time row at all.The repository TARGET stays deferred deliberately, and cheaply:
repo1-typeis one config key and no operator-facing command changes with it.The drill is the point
scripts/restore_drill.pydeletes the contents of a real PGDATA and brings it back, verifying at the domain level rather than at "Postgres started".Run B is recoverable only from archived WAL, so it separates a working archive from a working backup. Run C must not return, which separates point-in-time recovery from restore. Each surviving stream's
version-ordered fold digest is compared across the restore, so an event dropped or reordered inside a stream fails even with the row count intact.It also asserts that Run C's WAL segment reached the repository BEFORE the restore, since otherwise its absence would prove a truncated archive rather than a working recovery target.
Executed 2026-07-28 against
pgvector/pgvector:pg18and pgBackRest 2.58.0: 8 of 8 checks pass, schema at revision20260713000000.Executing it, rather than writing it, is what found the defects
ERROR: [040]), which is every real restore. Needed--delta. The drill now runs the identical command so the two cannot drift.stanza-createare expected, which makes a naivefailed_count = 0health check useless.Gate review (4 lenses) also forced an evidence-honesty correction: only 3 of the 8 checks can actually fail in a state the drill constructs. The doc now says which three carry the claim. It further caught
positiondescribed as the projection watermark, which this repo's ownevent_store.pycontradicts (the cursor is the(transaction_id, position)tuple).Limits, all stated in the doc
Local disk does not survive host loss. Nothing schedules backups, so nothing expires them either and the repository grows unbounded. Nothing alerts on a failing archive and nothing runs
verify. The drill proves the mechanism, not 2-BM's host.One deferred item has a hidden deadline, now recorded: repository encryption is fixed at stanza creation and cannot be added to an existing repository, so it must be decided before the first backup at any shared or off-site target.
Also records the interaction with out-of-band Atlas migrations: nothing asserts the schema version at boot, so restoring a pre-migration backup under a newer image fails silently. The restore procedure carries that check until the application does.
Test plan
make restore-drill(executed, 8/8;CORA_DRILL_PORToverride available)uv run pytest tests/architecture(29601 passed, 590 skipped)make docs-build(strict, clean)🤖 Generated with Claude Code