Interrupted metadata export leaves a snapshot silently treated as complete but unrestorable #177

Open
opened 2026-09-22 09:37:46 +02:00 by clawbot · 0 comments
Collaborator

Split from issue #148 (its chosen option b) and scenario 2 of issue #72.

Issue #148 closed the interrupted blob upload window: a run now deduplicates only against chunks held by an uploaded blob, and drops un-uploaded blob rows at startup. Its option b also names a second window that #148 did not close and that this issue covers.

The window

A backup's final phase (finalizeSnapshotMetadata in internal/vaultik/snapshot.go) marks the snapshot complete in the local index (CompleteSnapshot -> MarkComplete, which sets completed_at) and only then exports the snapshot metadata to the destination (ExportSnapshotMetadata: the blob manifest and the encrypted database). If the run dies after MarkComplete but before or during that export -- the "blobs uploaded, manifest never written" state, i.e. scenario 2 of issue #72 (kill -9 between the manifest write and the db.zst.age upload) -- the local index shows the snapshot as complete while the destination holds no manifest or database for it.

On the next run, PruneDatabase deletes only snapshots whose completed_at is NULL, so this one survives and is treated as complete. snapshot list shows it as local-only "drift", but nothing detects or repairs it, and only snapshot verify against the destination would surface the missing metadata.

CleanupIncompleteSnapshots already performs the correct remote check (stat the metadata key; drop the record when it is absent), but only for snapshots still marked incomplete, and it is not on the backup path (PruneDatabase is used instead). Reconciling these two is part of this work.

Definition of done

  1. A snapshot is never silently treated as complete when its metadata was never exported. Either:
    • order completion so the snapshot is marked complete only after its metadata export succeeds (an interrupted export then leaves it incomplete and the next run's prune removes it); or
    • at the start of a run, detect the "complete locally, no metadata at the destination" state and repair it (re-export or drop), with snapshot list and snapshot verify reporting it honestly rather than crashing or silently ignoring it.
  2. A test injects a fault during the metadata export (blobs uploaded, manifest/database not written), re-runs the backup, and asserts the observable end state: the destination and the local index agree, and the snapshot either restores or is honestly reported as absent -- never silently listed as a good backup. This uses the fault-injection seam from issue #72 (unmerged as of this writing); coordinate with that branch.
  3. make check green.

Scenario 2, verbatim from issue #72

kill -9 between manifest write and db.zst.age upload. This is the dangerous window -- the destination has blobs and a manifest but no index. Assert the next run detects and repairs the state, and that snapshot list and verify both report it honestly rather than crashing or silently ignoring it.

Model: opus-4-8

Split from [issue #148](https://git.eeqj.de/sneak/vaultik/issues/148) (its chosen option b) and scenario 2 of [issue #72](https://git.eeqj.de/sneak/vaultik/issues/72). [Issue #148](https://git.eeqj.de/sneak/vaultik/issues/148) closed the interrupted *blob upload* window: a run now deduplicates only against chunks held by an uploaded blob, and drops un-uploaded blob rows at startup. Its option b also names a second window that #148 did not close and that this issue covers. ## The window A backup's final phase (`finalizeSnapshotMetadata` in `internal/vaultik/snapshot.go`) marks the snapshot complete in the local index (`CompleteSnapshot` -> `MarkComplete`, which sets `completed_at`) and only then exports the snapshot metadata to the destination (`ExportSnapshotMetadata`: the blob manifest and the encrypted database). If the run dies after `MarkComplete` but before or during that export -- the "blobs uploaded, manifest never written" state, i.e. scenario 2 of [issue #72](https://git.eeqj.de/sneak/vaultik/issues/72) (kill -9 between the manifest write and the `db.zst.age` upload) -- the local index shows the snapshot as complete while the destination holds no manifest or database for it. On the next run, `PruneDatabase` deletes only snapshots whose `completed_at` is NULL, so this one survives and is treated as complete. `snapshot list` shows it as local-only "drift", but nothing detects or repairs it, and only `snapshot verify` against the destination would surface the missing metadata. `CleanupIncompleteSnapshots` already performs the correct remote check (stat the metadata key; drop the record when it is absent), but only for snapshots still marked incomplete, and it is not on the backup path (`PruneDatabase` is used instead). Reconciling these two is part of this work. ## Definition of done 1. A snapshot is never silently treated as complete when its metadata was never exported. Either: - order completion so the snapshot is marked complete only after its metadata export succeeds (an interrupted export then leaves it incomplete and the next run's prune removes it); or - at the start of a run, detect the "complete locally, no metadata at the destination" state and repair it (re-export or drop), with `snapshot list` and `snapshot verify` reporting it honestly rather than crashing or silently ignoring it. 2. A test injects a fault during the metadata export (blobs uploaded, manifest/database not written), re-runs the backup, and asserts the observable end state: the destination and the local index agree, and the snapshot either restores or is honestly reported as absent -- never silently listed as a good backup. This uses the fault-injection seam from [issue #72](https://git.eeqj.de/sneak/vaultik/issues/72) (unmerged as of this writing); coordinate with that branch. 3. `make check` green. ## Scenario 2, verbatim from issue #72 > kill -9 between manifest write and db.zst.age upload. This is the dangerous window -- the destination has blobs and a manifest but no index. Assert the next run detects and repairs the state, and that snapshot list and verify both report it honestly rather than crashing or silently ignoring it. Model: opus-4-8
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/vaultik#177