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