Skip to content

Commit e32fc4a

Browse files
authored
snapshot: Provide streaming snapshot verification. (#2691)
1 parent 69a2ab7 commit e32fc4a

File tree

5 files changed

+372
-76
lines changed

5 files changed

+372
-76
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/snapshot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ spacetimedb-schema = { path = "../schema" }
3434
anyhow.workspace = true
3535
env_logger.workspace = true
3636
pretty_assertions = { workspace = true, features = ["unstable"] }
37+
rand.workspace = true

crates/snapshot/src/lib.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ use std::{
4848
ops::{Add, AddAssign},
4949
path::PathBuf,
5050
};
51+
use tokio::task::spawn_blocking;
5152

5253
pub mod remote;
54+
use remote::verify_snapshot;
5355

5456
#[derive(Debug, Copy, Clone)]
5557
/// An object which may be associated with an error during snapshotting.
@@ -543,6 +545,7 @@ impl fmt::Debug for SnapshotSize {
543545
}
544546

545547
/// A repository of snapshots of a particular database instance.
548+
#[derive(Clone)]
546549
pub struct SnapshotRepository {
547550
/// The directory which contains all the snapshots.
548551
root: SnapshotsPath,
@@ -752,6 +755,7 @@ impl SnapshotRepository {
752755
});
753756
}
754757

758+
let snapshot_dir = self.snapshot_dir_path(tx_offset);
755759
let object_repo = Self::object_repo(&snapshot_dir)?;
756760

757761
let blob_store = snapshot.reconstruct_blob_store(&object_repo)?;
@@ -769,6 +773,60 @@ impl SnapshotRepository {
769773
})
770774
}
771775

776+
/// Read the [`Snapshot`] metadata at `tx_offset` and verify the integrity
777+
/// of all objects it refers to.
778+
///
779+
/// Fails if:
780+
///
781+
/// - No snapshot exists in `self` for `tx_offset`
782+
/// - The snapshot is incomplete, as detected by its lockfile still existing.
783+
/// - The snapshot file's magic number does not match [`MAGIC`].
784+
/// - Any object file (page or large blob) referenced by the snapshot file
785+
/// is missing or corrupted.
786+
///
787+
/// The following conditions are not detected or considered as errors:
788+
///
789+
/// - The snapshot file's version does not match [`CURRENT_SNAPSHOT_VERSION`].
790+
/// - The snapshot file's database identity or instance ID do not match
791+
/// those in `self`.
792+
/// - The snapshot file's module ABI version does not match
793+
/// [`CURRENT_MODULE_ABI_VERSION`].
794+
/// - The snapshot file's recorded transaction offset does not match
795+
/// `tx_offset`.
796+
///
797+
/// Callers may want to inspect the returned [`Snapshot`] and ensure its
798+
/// contents match their expectations.
799+
pub async fn verify_snapshot(&self, tx_offset: TxOffset) -> Result<Snapshot, SnapshotError> {
800+
let snapshot_dir = self.snapshot_dir_path(tx_offset);
801+
let snapshot = spawn_blocking({
802+
let snapshot_dir = snapshot_dir.clone();
803+
move || {
804+
let lockfile = Lockfile::lock_path(&snapshot_dir);
805+
if lockfile.try_exists()? {
806+
return Err(SnapshotError::Incomplete { tx_offset, lockfile });
807+
}
808+
809+
let snapshot_file_path = snapshot_dir.snapshot_file(tx_offset);
810+
let (snapshot, _compress_type) = Snapshot::read_from_file(&snapshot_file_path)?;
811+
812+
if snapshot.magic != MAGIC {
813+
return Err(SnapshotError::BadMagic {
814+
tx_offset,
815+
magic: snapshot.magic,
816+
});
817+
}
818+
Ok(snapshot)
819+
}
820+
})
821+
.await
822+
.unwrap()?;
823+
let object_repo = Self::object_repo(&snapshot_dir)?;
824+
verify_snapshot(object_repo, self.root.clone(), snapshot.clone())
825+
.await
826+
.map(drop)?;
827+
Ok(snapshot)
828+
}
829+
772830
/// Open a repository at `root`, failing if the `root` doesn't exist or isn't a directory.
773831
///
774832
/// Calls [`Path::is_dir`] and requires that the result is `true`.

0 commit comments

Comments
 (0)