Add snapshots - #215
Add snapshots#215
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds first-class “snapshots” metadata to the SQLite persistence layer so database backups can be recorded and used to prevent the orphan/unpin loop from unpinning objects that are still referenced by a backup (Issue #202).
Changes:
- Replace the store-level
Backuphook withCreateSnapshot, which both creates a SQLite backup file and records referencedsia_object_ids as a snapshot. - Add
snapshots/snapshot_objectstables (migration + init schema) and update orphan selection to exclude objects referenced by any snapshot. - Add unit/integration tests validating snapshot creation, listing, deletion, and orphan-withholding behavior; add a changeset entry.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
sia/sia.go |
Store interface updated to CreateSnapshot; backup entrypoint now records snapshots. |
sia/persist/sqlite/snapshots.go |
Implements snapshot creation/listing/deletion and ties snapshot creation to DB backup. |
sia/persist/sqlite/snapshots_test.go |
New tests for snapshot lifecycle and orphan withholding. |
sia/persist/sqlite/objects.go |
Orphan selection now excludes objects referenced by snapshots. |
sia/persist/sqlite/objects_test.go |
Extends orphan tests to cover snapshot-based withholding. |
sia/persist/sqlite/migrations.go |
Adds migration creating snapshots and snapshot_objects tables + index. |
sia/persist/sqlite/init.sql |
Adds the same tables/index to fresh DB initialization. |
sia/objects/objects.go |
Adds objects.Snapshot model used by listing. |
s3/s3_test.go |
Backup endpoint test now asserts the backup is recorded as a snapshot. |
.changeset/snapshot_orphan_guard.md |
Documents the behavior change in backups/orphan handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b08ef35 to
72a45c5
Compare
|
(still working on exploring the trade-offs between backup API and vacuum into) |
|
I explored several strategies and ended up reverting it to the simplest approach. The main reason why I think it's not trivial, is because of An alternative approach I considered is to use The most correct approach would be to gate our single connection and use a transaction queue. All transactions flow through that as well as the backup and essentially what it does is it interleaves "regular operations" with backup steps properly. In a way that you are also guaranteed that the backup process makes progress at fixed intervals and doesn't have to fight for the connection. I was implementing that but it felt over-engineered and figured I'd keep it super simple and propose the approach first. I would say it's 200 lines or something, you'd have a txQueue and a txJob and if there's an active backup you step every couple of jobs or after a fixed amount of time... It comes with some footguns and needless overhead because 99.99% of the time you are not backing up. We could also forget about FIFO entirely and just rely that the backup will eventually finish because after releasing the connection, it'll fight for the connection again with other concurrent write processes and eventually get it and make progress. In practice that might work really well. I wrote that out and tested it and in my benchmark the longest wait was 30s, which I found unacceptable.. At the same time though you eventually finish and you don't block writers during the backup process. In short:
edit: I have a bunch of benchmark output but wanted to type it out instead. SQLite backup strategy comparison
edit 2: One thing to note is that whatever strategy we choose, we should probably do the following items because it's little cost and real benefit:
|
47f1da8 to
3bedb63
Compare
257301e to
fa5969b
Compare
chris124567
left a comment
There was a problem hiding this comment.
Nothing to add to Chris's comments
|
going to push one more commit that adds the |
|
@peterjan vibed this up. Seems like the fix is somewhat difficult... |
Thank you, I'll look into it. There's many many edges with snapshot recovery, at least this one is retaining the object which is definitely preferable over unpinning an object that we want to keep. But I'll see what the issue is and try and come up with a good fix for it. |
There was a problem hiding this comment.
🔵 Needs a closer look
Backup cleanup can delete a pre-existing temporary file, and the API omits a critical recovery limitation.
Review details
Suppressed comments (2)
sia/persist/sqlite/backup.go:38
- This cleanup can delete a file that existed before the backup call.
VACUUM INTOrejects an existingdestPath + ".tmp"; the named error then triggers this unconditional removal, so an unrelated adjacent file is lost. Allocate a unique temporary path owned by this invocation (preferably in the destination directory) and only clean up that owned file.
_ = os.Remove(tmpPath)
openapi.yml:86
- The snapshot can be created while objects are still buffered only on local disk, but those bytes are not included in the uploaded database backup. After restoring elsewhere, those database rows reference files that do not exist and cannot be uploaded or read. Preserve the prior API warning and tell callers to flush pending objects before taking a recoverable snapshot.
description: >
Backs up the SQLite metadata database and uploads it to the Sia network
as a tagged, pinned object so it can be recovered later. Objects that a
snapshot references are not unpinned until the snapshot is deleted.
The backup does not block database reads or writes.
- Files reviewed: 25/25 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
@peterjan Courtesy of Claude: |
Yeah I'm aware of this one, I have one more commit locally but I'm still soak testing it. It includes the fix you mentioned. |
There was a problem hiding this comment.
🟡 Changes recommended
Stale synchronization state can allow orphan or snapshot deletion before newly published snapshot events are processed.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
sia/sia.go:592
syncedremains true between the one-minute metadata polls, so this check does not ensure that the event stream is drained when orphan deletion starts. For example, a snapshot event published after the last successful poll but before the hourly orphan pass is still unknown locally, and this pass can unpin an object referenced by that snapshot—the corruption this gate is intended to prevent. Serialize orphan processing with a fresh successful metadata drain (or use an indexer watermark/other protocol that proves no snapshot can be published before deletion) instead of treating one past sync as permanently current.
if !s.synced.Load() {
s.logger.Debug("deferring orphan processing until object metadata is synced")
return
}
- Files reviewed: 25/25 changed files
- Comments generated: 1
- Review effort level: Balanced
I'm sure an astra run finds 5 more race conditions that 'll never play out. |
|
edit: nvm |
This PR introduces snapshots. A snapshot is a backup of the sqlite database that gets gzipped and uploaded to the Sia network as a tagged, pinned object, so the full database can be retrieved and restored to its state at the time the snapshot was taken.
References #202