Mark a snapshot complete only after its metadata export succeeds (closes #177)
check / check (pull_request) Successful in 1m42s
check / check (push) Successful in 3m35s

finalizeSnapshotMetadata marked the snapshot complete and then exported its metadata. A crash after completion but before/during the export left the local index showing the snapshot complete while the destination had no manifest or database, and PruneDatabase (which drops only NULL completed_at rows) kept it: a silently unrestorable snapshot.

Reorder so completion is recorded last. CompleteSnapshot is split into PopulateSnapshotBlobs (before the export) and MarkSnapshotComplete (after it). An interrupted export now leaves the snapshot incomplete, so the next run PruneDatabase drops it and re-backs-up the data; the reverse tiny window leaves a restorable snapshot the index reports honestly as remote-only. Update REPOSTRUCTURE.md guarantee 4 and the ARCHITECTURE.md flow. Add a fault-injection test driving the full create path.

Model: opus-4-8
This commit was merged in pull request #200.
This commit is contained in:
2026-09-22 20:00:41 +02:00
parent 1548c0f933
commit dd7a610c23
5 changed files with 263 additions and 22 deletions
+20 -3
View File
@@ -69,6 +69,9 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error {
// Prune the database before starting: delete incomplete snapshots and orphaned data.
// This ensures the database is consistent before we start a new snapshot.
// Since we use locking, only one vaultik instance accesses the DB at a time.
// A snapshot whose metadata export was interrupted is left incomplete by
// finalizeSnapshotMetadata, so it is among the incomplete snapshots dropped
// here (https://git.eeqj.de/sneak/vaultik/issues/177).
_, err = v.PruneDatabase()
if err != nil {
return fmt.Errorf("prune database: %w", err)
@@ -324,7 +327,12 @@ func (v *Vaultik) collectUploadStats(scanner *snapshot.Scanner, stats *snapshotS
}
}
// finalizeSnapshotMetadata updates stats, marks complete, and exports metadata
// finalizeSnapshotMetadata updates stats, exports metadata, and only then
// marks the snapshot complete. Recording completion last is deliberate: an
// export interrupted by a crash leaves the snapshot incomplete rather than
// looking complete with no manifest or database at the destination. The next
// run's PruneDatabase drops the incomplete snapshot and re-backs-up its data.
// See https://git.eeqj.de/sneak/vaultik/issues/177.
func (v *Vaultik) finalizeSnapshotMetadata(
snapshotID string, stats *snapshotStats,
) error {
@@ -346,9 +354,11 @@ func (v *Vaultik) finalizeSnapshotMetadata(
return fmt.Errorf("updating snapshot stats: %w", err)
}
err = v.SnapshotManager.CompleteSnapshot(v.ctx, snapshotID)
// snapshot_blobs must be populated before the export, which builds the
// manifest and the trimmed metadata database from it.
err = v.SnapshotManager.PopulateSnapshotBlobs(v.ctx, snapshotID)
if err != nil {
return fmt.Errorf("completing snapshot: %w", err)
return fmt.Errorf("populating snapshot blobs: %w", err)
}
err = v.SnapshotManager.ExportSnapshotMetadata(
@@ -357,6 +367,13 @@ func (v *Vaultik) finalizeSnapshotMetadata(
return fmt.Errorf("exporting snapshot metadata: %w", err)
}
// Record completion last, so an interrupted export never leaves a
// snapshot marked complete without its metadata at the destination.
err = v.SnapshotManager.MarkSnapshotComplete(v.ctx, snapshotID)
if err != nil {
return fmt.Errorf("marking snapshot complete: %w", err)
}
return nil
}