Skip to main content

fakecloud_persistence/
snapshot.rs

1use std::future::Future;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::pin::Pin;
5use std::sync::Arc;
6
7/// A type-erased, owned closure that persists one service's whole state as a
8/// snapshot when invoked. Built by a service from its own `state` / store /
9/// serializing lock (see each service's `snapshot_hook()`), so the
10/// serialization stays in the owning crate.
11///
12/// The CloudFormation resource provisioner mutates services' shared state
13/// directly and cannot reach their private `save_snapshot()` paths. After a
14/// stack op it invokes the hook for each touched service to write that state
15/// through to disk -- the same persistence a direct API mutation would
16/// trigger. A `None` hook (memory mode / no store) is simply never collected,
17/// so invoking a present hook is always a real persist.
18pub type SnapshotHook = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
19
20/// Generic opaque-blob snapshot store used by services that persist their
21/// whole state as a single serialized document (DynamoDB tables, SQS queues,
22/// etc.). Unlike the fine-grained [`crate::s3::S3Store`] which tracks
23/// individual objects and streams bodies to disk, this trait is designed for
24/// services whose state is small enough to fit in memory and can be written
25/// as one atomic file.
26pub trait SnapshotStore: Send + Sync {
27    /// Load the latest snapshot, if one exists. Returns `Ok(None)` when
28    /// there is nothing on disk yet (first boot).
29    fn load(&self) -> io::Result<Option<Vec<u8>>>;
30
31    /// Persist the given bytes as the new snapshot. Implementations must
32    /// ensure the write is atomic (crash-safe) and durable.
33    fn save(&self, bytes: &[u8]) -> io::Result<()>;
34}
35
36/// No-op store used in `StorageMode::Memory`. `load` always returns `None`
37/// and `save` is a noop.
38pub struct MemorySnapshotStore;
39
40impl MemorySnapshotStore {
41    pub fn new() -> Self {
42        Self
43    }
44}
45
46impl Default for MemorySnapshotStore {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl SnapshotStore for MemorySnapshotStore {
53    fn load(&self) -> io::Result<Option<Vec<u8>>> {
54        Ok(None)
55    }
56
57    fn save(&self, _bytes: &[u8]) -> io::Result<()> {
58        Ok(())
59    }
60}
61
62/// Disk-backed snapshot store. Writes are atomic via the `.tmp` + rename
63/// dance in [`crate::atomic::write_atomic_bytes`], with the parent directory
64/// fsynced on success.
65pub struct DiskSnapshotStore {
66    path: PathBuf,
67}
68
69impl DiskSnapshotStore {
70    pub fn new(path: PathBuf) -> Self {
71        Self { path }
72    }
73
74    pub fn path(&self) -> &Path {
75        &self.path
76    }
77}
78
79impl SnapshotStore for DiskSnapshotStore {
80    fn load(&self) -> io::Result<Option<Vec<u8>>> {
81        match std::fs::read(&self.path) {
82            Ok(bytes) => Ok(Some(bytes)),
83            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
84            Err(err) => Err(err),
85        }
86    }
87
88    fn save(&self, bytes: &[u8]) -> io::Result<()> {
89        if let Some(parent) = self.path.parent() {
90            std::fs::create_dir_all(parent)?;
91        }
92        crate::atomic::write_atomic_bytes(&self.path, bytes)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn memory_store_is_noop() {
102        let store = MemorySnapshotStore::new();
103        assert!(store.load().unwrap().is_none());
104        store.save(b"anything").unwrap();
105        assert!(store.load().unwrap().is_none());
106    }
107
108    #[test]
109    fn disk_store_round_trips() {
110        let tmp = tempfile::tempdir().unwrap();
111        let store = DiskSnapshotStore::new(tmp.path().join("sub/dir/snapshot.json"));
112        assert!(store.load().unwrap().is_none());
113        store.save(b"hello world").unwrap();
114        assert_eq!(store.load().unwrap().unwrap(), b"hello world");
115        store.save(b"second write").unwrap();
116        assert_eq!(store.load().unwrap().unwrap(), b"second write");
117    }
118}