Skip to content

sync: snapshot the index on every successful sync and ride a copy to the destination#91

Merged
mbertschler merged 3 commits into
mainfrom
claude/tender-cori-eLPja
Jun 4, 2026
Merged

sync: snapshot the index on every successful sync and ride a copy to the destination#91
mbertschler merged 3 commits into
mainfrom
claude/tender-cori-eLPja

Conversation

@mbertschler
Copy link
Copy Markdown
Owner

What

Implements #75. The catalog should be as redundant as the data it describes, so after every successful sync squirrel now:

  1. Snapshot-on-sync (local). Takes one VACUUM INTO snapshot of the global index (reusing store.Backup) to a local tier (default <dir of db>/backups/), named index-<ISO8601>-run-<id>.db, with keep-N rotation.
  2. Cloud ride-along (destination syncs only). Rides the same snapshot file along to <dest.root>/<volume>/.squirrel-index/index-<ISO8601>-run-<id>.db via the rclone wrapper (copyto), then rotates that dir to cloud_keep (rclone lsf newest-N + deletefile).

One VACUUM per squirrel sync invocation, fanned out to the local tier and every destination — never one VACUUM per pair.

Decisions honoured

Config

[backups]
enabled    = true   # local snapshot-on-sync (default true)
dir        = ""     # default: <dir of db>/backups
keep       = 7      # local rotation
cloud      = true   # ride-along to destination buckets (default true)
cloud_keep = 7      # snapshots kept per <dest>/<volume>/.squirrel-index/

enabled = false disables both halves; cloud = false keeps the local snapshot but uploads nothing.

Guards (non-negotiable)

  • .squirrel-index/ added to all three reserved-path filters: the --filter lists in buildRcloneArgs and buildRestoreArgs, plus isReservedSyncPath/isReservedFolderPath in peer-sync.
  • A snapshot/upload failure surfaces on Report.SnapshotErr (mirroring FinishErr) and is logged — it never flips a successful sync to failed or mutates rep.Status.
  • Snapshot is taken after the run's terminal row is committed (SyncNode was split into runNodeSession so its deferred finishRun fires first).
  • No peer ride-along; restore runs take no snapshot. Scheduled syncs on the agent (each node) snapshot too.

Tests

  • Cloud ride-along: snapshot present under .squirrel-index/, is the global catalog (carries rows for a volume never synced to that destination), opens cleanly + passes integrity check, local-tier copy present with matching name, filename carries the run id.
  • .squirrel-index/ excluded from sync (source-leak test), restore + sync arg builders (filter assertion), and peer-sync (isReservedSyncPath/isReservedFolderPath).
  • Injected backup error → run status unchanged, surfaced on SnapshotErr.
  • enabled=false (nil Snapshotter → no-op) and cloud=false (local snapshot, no upload).
  • Local + cloud rotation bound to N.
  • Config: defaults when absent, overrides, enabled=false/cloud=false, negative-keep rejection, unknown-field rejection.

go vet ./..., go test ./..., and go test -race ./sync/... all pass.

Out of scope (per ticket)

Litestream; peer ride-along; snapshot on restore; encrypting the snapshot at rest (docs note recommends a private bucket / SSE — added to the README).

Closes #75


Generated by Claude Code

…the destination

After a sync run reaches a terminal success/partial state, take one
VACUUM INTO snapshot of the global index (reusing store.Backup) to a
local tier and, for destination syncs, ride the same file along to
<dest>/<volume>/.squirrel-index/ so the catalog inherits the same
redundancy as the data it describes.

- New [backups] config table (enabled/dir/keep/cloud/cloud_keep),
  on-by-default and zero-config: an absent table resolves to defaults.
- Snapshotter coordinates one VACUUM per `squirrel sync` invocation and
  fans the single file out to the local tier and every destination;
  rotation bounds the local dir (keep) and each cloud .squirrel-index/
  dir (cloud_keep, via rclone lsf + deletefile).
- Snapshot/upload failures surface on Report.SnapshotErr and are logged;
  they never flip a successful sync to failed or mutate rep.Status.
- Trigger sites are Sync and SyncNode after the run's terminal row is
  committed; peer-sync takes the local snapshot only (no ride-along).
  Scheduled syncs on the agent snapshot too.
- .squirrel-index/ added to all three reserved-path filters: the sync
  and restore --filter lists and isReservedSyncPath/isReservedFolderPath
  in peer-sync, so a snapshot is never treated as user content.

Closes #75
Copilot AI review requested due to automatic review settings June 4, 2026 21:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements issue #75 by adding automatic SQLite index snapshots after successful sync runs, storing them locally with rotation and (for destination syncs) uploading the same snapshot as a “ride-along” into each destination volume’s .squirrel-index/, also with rotation. It also ensures .squirrel-index/ is treated as a reserved path and excluded from sync/restore transfers and peer-sync discovery.

Changes:

  • Add Snapshotter to take one VACUUM INTO snapshot per squirrel sync invocation and fan it out to local + destination copies, surfacing failures via Report.SnapshotErr without failing the sync.
  • Exclude .squirrel-index/ in rclone sync/restore filters and in peer-sync reserved-path checks.
  • Add [backups] config block with defaults (enabled by default) and tests; wire snapshot behavior into CLI sync and agent scheduled syncs.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sync/sync.go Adds .squirrel-index constant, Options.Snapshot, Report.SnapshotErr, and filter exclusions for sync/restore.
sync/snapshot.go Implements Snapshotter (local snapshot, cloud ride-along upload, local/cloud rotation).
sync/snapshot_test.go Adds tests covering ride-along, disable flags, error surfacing without status flip, rotation bounds, and reserved-path guards.
sync/rclone.go Adds “plain” rclone helpers (copyto, lsf, deletefile) to support snapshot ride-along and rotation.
sync/node.go Ensures snapshot hook runs after peer-sync terminal run state is committed; expands reserved-path predicates.
README.md Documents snapshot-on-sync behavior and destination layout including .squirrel-index/.
config/config.go Adds resolved Backups config to Config and resolves it during load.
config/backups.go Introduces [backups] config with defaults and validation.
config/backups_test.go Tests defaults, overrides, disabling, negative retention rejection, and unknown-field rejection.
cmd/squirrel/sync.go Wires Snapshotter creation into squirrel sync (shared across pairs).
cmd/squirrel/agent.go Enables snapshot-on-sync for scheduled sync kicks via agent.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sync/snapshot.go Outdated
Comment thread README.md Outdated
…amp example

Address Copilot review on #91:
- rotateSnapshots sorted by modtime only; coarse-resolution filesystems
  or same-tick writes could order equal modtimes non-deterministically
  and rotate away a newer snapshot. Break ties by filename (which embeds
  a sortable timestamp) so rotation stays deterministic and chronological.
- README destination-layout example used an extended ISO8601 timestamp
  with dashes/colons; the code uses the compact 20060102T150405.000Z
  layout. Match the example to what users actually see.
@mbertschler mbertschler merged commit 5c9fbd2 into main Jun 4, 2026
2 checks passed
@mbertschler mbertschler deleted the claude/tender-cori-eLPja branch June 4, 2026 21:30
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.

sync: snapshot the index on every successful sync and ride a copy along to the destination

3 participants