fakecloud_efs/
persistence.rs1use std::sync::Arc;
4
5use tokio::sync::Mutex as AsyncMutex;
6
7use fakecloud_persistence::SnapshotStore;
8
9use crate::state::{EfsSnapshot, SharedEfsState, EFS_SNAPSHOT_SCHEMA_VERSION};
10
11#[derive(Debug, PartialEq, Eq)]
12pub enum LoadOutcome {
13 Empty,
14 Loaded(usize),
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum LoadError {
19 #[error("failed to read efs persistence snapshot: {0}")]
20 Io(String),
21 #[error("failed to parse efs persistence snapshot: {0}")]
22 Parse(String),
23 #[error("efs persistence schema too new: on-disk={on_disk}, max supported={supported}")]
24 SchemaTooNew { on_disk: u32, supported: u32 },
25}
26
27pub fn load_into(
28 store: &dyn SnapshotStore,
29 state: &SharedEfsState,
30) -> Result<LoadOutcome, LoadError> {
31 let Some(bytes) = store.load().map_err(|e| LoadError::Io(e.to_string()))? else {
32 return Ok(LoadOutcome::Empty);
33 };
34 let mut snapshot: EfsSnapshot =
35 serde_json::from_slice(&bytes).map_err(|e| LoadError::Parse(e.to_string()))?;
36 if snapshot.schema_version > EFS_SNAPSHOT_SCHEMA_VERSION {
37 return Err(LoadError::SchemaTooNew {
38 on_disk: snapshot.schema_version,
39 supported: EFS_SNAPSHOT_SCHEMA_VERSION,
40 });
41 }
42 for (_account_id, account) in snapshot.accounts.iter_mut() {
46 account.reconcile_lifecycle();
47 }
48 let accounts = snapshot.accounts.account_count();
49 *state.write() = snapshot.accounts;
50 Ok(LoadOutcome::Loaded(accounts))
51}
52
53pub async fn save_snapshot(
54 state: &SharedEfsState,
55 store: Option<Arc<dyn SnapshotStore>>,
56 lock: &AsyncMutex<()>,
57) {
58 let Some(store) = store else {
59 return;
60 };
61 let _guard = lock.lock().await;
62 let snapshot = EfsSnapshot {
63 schema_version: EFS_SNAPSHOT_SCHEMA_VERSION,
64 accounts: state.read().clone(),
65 };
66 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
67 let bytes = serde_json::to_vec(&snapshot)
68 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
69 store.save(&bytes)
70 })
71 .await;
72 match join {
73 Ok(Ok(())) => {}
74 Ok(Err(err)) => tracing::error!(%err, "failed to write efs snapshot"),
75 Err(err) => tracing::error!(%err, "efs snapshot task panicked"),
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::state::{EfsData, EfsSnapshot};
83 use fakecloud_core::multi_account::MultiAccountState;
84 use parking_lot::RwLock;
85 use std::sync::Mutex;
86
87 struct MemStore(Mutex<Option<Vec<u8>>>);
88 impl SnapshotStore for MemStore {
89 fn load(&self) -> std::io::Result<Option<Vec<u8>>> {
90 Ok(self.0.lock().unwrap().clone())
91 }
92 fn save(&self, bytes: &[u8]) -> std::io::Result<()> {
93 *self.0.lock().unwrap() = Some(bytes.to_vec());
94 Ok(())
95 }
96 }
97
98 fn state() -> SharedEfsState {
99 Arc::new(RwLock::new(MultiAccountState::new(
100 "000000000000",
101 "us-east-1",
102 "",
103 )))
104 }
105
106 #[test]
107 fn empty_store_is_empty() {
108 assert_eq!(
109 load_into(&MemStore(Mutex::new(None)), &state()).unwrap(),
110 LoadOutcome::Empty
111 );
112 }
113
114 #[test]
115 fn round_trip_restores_and_reconciles_lifecycle() {
116 let mut accounts: MultiAccountState<EfsData> =
117 MultiAccountState::new("000000000000", "us-east-1", "");
118 let data = accounts.get_or_create("111122223333");
119 data.file_systems.insert(
120 "fs-0123456789abcdef0".to_string(),
121 serde_json::json!({
122 "FileSystemId": "fs-0123456789abcdef0",
123 "LifeCycleState": "creating"
124 }),
125 );
126 let snap = EfsSnapshot {
127 schema_version: EFS_SNAPSHOT_SCHEMA_VERSION,
128 accounts,
129 };
130 let store = MemStore(Mutex::new(Some(serde_json::to_vec(&snap).unwrap())));
131 let restored = state();
132 assert_eq!(
133 load_into(&store, &restored).unwrap(),
134 LoadOutcome::Loaded(2)
135 );
136 let guard = restored.read();
137 let fs = guard
138 .get("111122223333")
139 .unwrap()
140 .file_systems
141 .get("fs-0123456789abcdef0")
142 .unwrap();
143 assert_eq!(fs.get("LifeCycleState").unwrap(), "available");
145 }
146
147 #[test]
148 fn rejects_future_schema() {
149 let accounts: MultiAccountState<EfsData> =
150 MultiAccountState::new("000000000000", "us-east-1", "");
151 let bytes = serde_json::to_vec(&serde_json::json!({
152 "schema_version": EFS_SNAPSHOT_SCHEMA_VERSION + 1,
153 "accounts": accounts,
154 }))
155 .unwrap();
156 let store = MemStore(Mutex::new(Some(bytes)));
157 assert!(matches!(
158 load_into(&store, &state()),
159 Err(LoadError::SchemaTooNew { .. })
160 ));
161 }
162}