Skip to content

Give the event store a restore path, and prove it by destroying one - #582

Merged
xmap merged 1 commit into
mainfrom
claude/elastic-heisenberg-b94ca5
Jul 28, 2026
Merged

Give the event store a restore path, and prove it by destroying one#582
xmap merged 1 commit into
mainfrom
claude/elastic-heisenberg-b94ca5

Conversation

@xmap

@xmap xmap commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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 / PITR row in docs/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-author recovery.signal and restore_command. pg_dump was 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-type is one config key and no operator-facing command changes with it.

The drill is the point

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

Run A  ->  full backup  ->  Run B  ->  [target]  ->  Run C

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:pg18 and pgBackRest 2.58.0: 8 of 8 checks pass, schema at revision 20260713000000.

Executing it, rather than writing it, is what found the defects

  • The documented restore command aborted on any populated PGDATA (ERROR: [040]), which is every real restore. Needed --delta. The drill now runs the identical command so the two cannot drift.
  • Backup and stanza commands need a live cluster connection, not just the data directory.
  • pgBackRest selects a backup set at whole-second resolution, strictly-less-than.
  • Archiving failures before stanza-create are expected, which makes a naive failed_count = 0 health 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 position described as the projection watermark, which this repo's own event_store.py contradicts (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_PORT override available)
  • uv run pytest tests/architecture (29601 passed, 590 skipped)
  • make docs-build (strict, clean)
  • ruff check + format clean on the new script

🤖 Generated with Claude Code

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>
@github-actions

Copy link
Copy Markdown

Coverage report

This PR does not seem to contain any modification to coverable code.

@xmap
xmap merged commit afbaa9c into main Jul 28, 2026
16 checks passed
@xmap
xmap deleted the claude/elastic-heisenberg-b94ca5 branch July 28, 2026 12:55
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