Skip to content

feat(repo-sync): mirror backups + WAL between repositories - #9

Merged
gliatti merged 2 commits into
mainfrom
feat/repo-sync
Jul 4, 2026
Merged

gliatti merged 2 commits into
mainfrom
feat/repo-sync

Conversation

@gliatti

@gliatti gliatti commented Jun 2, 2026

Copy link
Copy Markdown
Owner

repo-sync (Layer 1): repo-to-repo mirroring of backups + WAL

What

A new repo-sync command and an inline --repo-sync option on backup that
mirror completed backups and archived WAL from the active repository to every
other configured repository.

Intended topology: the primary archives to repo1 only; repo1 then
synchronises to repo2..N — run where repo1 is local (e.g. on the repository
host), so no bytes round-trip through the primary.

How

Repos in a multi-repo set are configured as byte-identical mirrors:
compression is global (already identical), and bundling / block-incremental /
cipher-type are validated identical, with a shared cipher sub-key. Mirroring
is therefore a pure raw byte copy of the stored objects (standalone files,
bundles, block maps, manifests, WAL segments) at the same repo paths — no decode
/ re-encode / re-bundle / re-block / size recompute.

repo-sync validates this consistency up front (refusing mismatched repos) and,
for a fresh empty encrypted target, aligns its sub-key to the source's so the
raw-copied bytes decrypt; a populated target under a divergent sub-key is a hard
error.

  • Engine (crates/pgbr-command/src/sync/):
    • sync_backup_to_repo — diff/incr ancestor backfill, manifest-sentinel
      idempotency, verbatim backup.info entry merge (never overwrites the
      target's own backups), missing db-history copy.
    • sync_archive_to_repo / per-segment copy — idempotent WAL mirroring
      (repo_has_segment skip across compression suffixes).
    • repo-sync command — --type=all|wal|backup (default all), --set=LABEL,
      best-effort across targets, holds the stanza backup lock.
  • Inline: backup --repo-sync mirrors the just-completed backup to the other
    repositories before expire-auto.
  • Config: the repo-sync command + global boolean option, plus the
    stanza / repo / repo-bundle / repo-block command associations the
    command needs to resolve storage, cipher, and mirror settings.

Tests

  • 28 unit tests (sync/{mod,backup,archive}.rs): raw copy, encrypted-mirror
    sub-key alignment + restorability, consistency rejection (bundle/block/cipher),
    diff ancestor backfill, WAL idempotency, --type/--set.
  • Docker integration: new 12-repo-sync.sh — encrypted + bundle + block,
    archive-to-repo1-then-sync, byte-identical object verification, restore from
    the mirror. All 12 Docker scenarios pass.
  • Vagrant: new scenario 25; full suite 211 passed / 0 failed.
  • Gate: cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, cargo test --workspace — all green.

Scope

Layer 1 only (engine + command + inline option). Layer 2 — automatic
server-triggered fan-out on WAL receipt over the TLS server topology — is a
documented follow-up (touches the remote/server subsystem).

🤖 Generated with Claude Code

gliatti and others added 2 commits June 3, 2026 00:26
Add repo-sync (Layer 1): byte-identical mirroring of completed backups and
archived WAL from the active repository to every other configured repository,
plus an inline `--repo-sync` option on `backup`.

Repositories in a multi-repo set are configured as identical mirrors (global
compression; identical bundling / block-incremental / cipher-type; shared cipher
sub-key), so syncing is a pure raw byte copy of the stored objects at the same
repo paths -- no decode / re-encode / re-bundle / re-block / size recompute.
Consistency is validated up front: repo-sync refuses mismatched repositories and
aligns a fresh encrypted target's sub-key to the source's so the copied bytes
decrypt. Intended topology: the primary archives to repo1 only; repo1 then
synchronises to repo2..N, run where repo1 is local.

- crates/pgbr-command/src/sync/{mod,backup,archive}.rs: the engine --
  assert_consistent, sync_backup_to_repo (diff/incr ancestor backfill,
  manifest-sentinel idempotency, verbatim backup.info merge), sync_archive_to_repo
  (idempotent WAL copy), and the `repo-sync` command (--type=all|wal|backup,
  --set=LABEL).
- backup: new `repo-sync` boolean mirrors the just-completed backup to the other
  repositories before expire-auto.
- config.yaml / help.xml: the `repo-sync` command + global option, and the
  stanza / repo / repo-bundle / repo-block command associations they require.
- integration: new Docker scenario 12-repo-sync.sh (encrypted, bundle + block,
  archive-to-repo1-then-sync, byte-identical verification, restore from mirror)
  and Vagrant scenario 25; a repo-sync smoke check in 08-multi-repo.sh.

Layer 2 (automatic server-triggered fan-out on WAL receipt over the TLS server
topology) is a follow-up.

Validated: fmt, clippy --all-targets -D warnings, cargo test --workspace (28 new
sync tests); all 12 Docker scenarios pass; full Vagrant suite 211 passed / 0
failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…nfig faults

Review fixes on the repo-sync engine:

- copy integrity: shared size-checked copy_object (sync/mod.rs) re-copies
  torn objects instead of sealing them behind the manifest sentinel
- ordering: destination-initialized check runs before any copy, and the
  sentinel path still reconciles the backup.info entry (merge_backup_info)
- CLI: backup builds all repo backends only when --repo-sync is enabled
- inline sync: best-effort across targets; expire-auto always runs, the
  first sync error is surfaced after retention
- WAL: source archive.info decrypted with the src_index passphrase (not
  the lowest configured index); destination listed once instead of
  per-segment exists probes
- consistency: assert_consistent enforces db identity (archive-id +
  system-id); dead repo-block-size entry replaced with real options
- align_sub_key preserves prior info files as .presync before rewrite
- reuse archive::COMPRESS_SUFFIXES (now pub(crate)); CLAUDE.md updated

Co-Authored-By: Claude Fable 5 <[email protected]>
@gliatti
gliatti merged commit e80e138 into main Jul 4, 2026
2 checks passed
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