Skip to main content

fakecloud_persistence/
snapshot.rs

1use std::io;
2use std::path::{Path, PathBuf};
3
4/// Generic opaque-blob snapshot store used by services that persist their
5/// whole state as a single serialized document (DynamoDB tables, SQS queues,
6/// etc.). Unlike the fine-grained [`crate::s3::S3Store`] which tracks
7/// individual objects and streams bodies to disk, this trait is designed for
8/// services whose state is small enough to fit in memory and can be written
9/// as one atomic file.
10pub trait SnapshotStore: Send + Sync {
11    /// Load the latest snapshot, if one exists. Returns `Ok(None)` when
12    /// there is nothing on disk yet (first boot).
13    fn load(&self) -> io::Result<Option<Vec<u8>>>;
14
15    /// Persist the given bytes as the new snapshot. Implementations must
16    /// ensure the write is atomic (crash-safe) and durable.
17    fn save(&self, bytes: &[u8]) -> io::Result<()>;
18}
19
20/// No-op store used in `StorageMode::Memory`. `load` always returns `None`
21/// and `save` is a noop.
22pub struct MemorySnapshotStore;
23
24impl MemorySnapshotStore {
25    pub fn new() -> Self {
26        Self
27    }
28}
29
30impl Default for MemorySnapshotStore {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl SnapshotStore for MemorySnapshotStore {
37    fn load(&self) -> io::Result<Option<Vec<u8>>> {
38        Ok(None)
39    }
40
41    fn save(&self, _bytes: &[u8]) -> io::Result<()> {
42        Ok(())
43    }
44}
45
46/// Disk-backed snapshot store. Writes are atomic via the `.tmp` + rename
47/// dance in [`crate::atomic::write_atomic_bytes`], with the parent directory
48/// fsynced on success.
49pub struct DiskSnapshotStore {
50    path: PathBuf,
51}
52
53impl DiskSnapshotStore {
54    pub fn new(path: PathBuf) -> Self {
55        Self { path }
56    }
57
58    pub fn path(&self) -> &Path {
59        &self.path
60    }
61}
62
63impl SnapshotStore for DiskSnapshotStore {
64    fn load(&self) -> io::Result<Option<Vec<u8>>> {
65        match std::fs::read(&self.path) {
66            Ok(bytes) => Ok(Some(bytes)),
67            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
68            Err(err) => Err(err),
69        }
70    }
71
72    fn save(&self, bytes: &[u8]) -> io::Result<()> {
73        if let Some(parent) = self.path.parent() {
74            std::fs::create_dir_all(parent)?;
75        }
76        crate::atomic::write_atomic_bytes(&self.path, bytes)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn memory_store_is_noop() {
86        let store = MemorySnapshotStore::new();
87        assert!(store.load().unwrap().is_none());
88        store.save(b"anything").unwrap();
89        assert!(store.load().unwrap().is_none());
90    }
91
92    #[test]
93    fn disk_store_round_trips() {
94        let tmp = tempfile::tempdir().unwrap();
95        let store = DiskSnapshotStore::new(tmp.path().join("sub/dir/snapshot.json"));
96        assert!(store.load().unwrap().is_none());
97        store.save(b"hello world").unwrap();
98        assert_eq!(store.load().unwrap().unwrap(), b"hello world");
99        store.save(b"second write").unwrap();
100        assert_eq!(store.load().unwrap().unwrap(), b"second write");
101    }
102}