1use std::sync::Arc;
11use std::time::Duration;
12
13use chrono::Utc;
14use tokio::sync::Mutex as AsyncMutex;
15
16use fakecloud_persistence::SnapshotStore;
17
18use crate::persistence::save_snapshot;
19use crate::state::SharedDsqlState;
20
21const TRANSITION_AFTER: chrono::Duration = chrono::Duration::milliseconds(400);
25
26const TICK: Duration = Duration::from_millis(200);
28
29pub fn spawn(
32 state: SharedDsqlState,
33 store: Option<Arc<dyn SnapshotStore>>,
34 lock: Arc<AsyncMutex<()>>,
35) {
36 tokio::spawn(async move {
37 loop {
38 tokio::time::sleep(TICK).await;
39 let changed = advance(&state);
40 if changed {
41 save_snapshot(&state, store.clone(), &lock).await;
42 }
43 }
44 });
45}
46
47pub fn advance(state: &SharedDsqlState) -> bool {
50 let now = Utc::now();
51 let mut changed = false;
52 let mut accounts = state.write();
53 for (_id, acct) in accounts.iter_mut() {
54 let mut to_remove = Vec::new();
55 for (cid, cluster) in acct.clusters.iter_mut() {
56 let elapsed = now.signed_duration_since(cluster.creation_time);
57 match cluster.status.as_str() {
58 "CREATING" | "PENDING_SETUP" if elapsed >= TRANSITION_AFTER => {
59 cluster.status = "ACTIVE".to_string();
60 changed = true;
61 }
62 "DELETING" | "PENDING_DELETE" => {
63 let deleting_for = cluster
67 .deleted_at
68 .map(|t| now.signed_duration_since(t))
69 .unwrap_or(TRANSITION_AFTER);
70 if deleting_for >= TRANSITION_AFTER {
71 to_remove.push(cid.clone());
72 changed = true;
73 }
74 }
75 _ => {}
76 }
77 let mut streams_to_remove = Vec::new();
79 for (sid, stream) in cluster.streams.iter_mut() {
80 match stream.status.as_str() {
81 "CREATING"
82 if now.signed_duration_since(stream.creation_time) >= TRANSITION_AFTER =>
83 {
84 stream.status = "ACTIVE".to_string();
85 changed = true;
86 }
87 "DELETING" => {
88 let deleting_for = stream
89 .deleted_at
90 .map(|t| now.signed_duration_since(t))
91 .unwrap_or(TRANSITION_AFTER);
92 if deleting_for >= TRANSITION_AFTER {
93 streams_to_remove.push(sid.clone());
94 changed = true;
95 }
96 }
97 _ => {}
98 }
99 }
100 for sid in streams_to_remove {
101 cluster.streams.remove(&sid);
102 }
103 }
104 for cid in to_remove {
105 acct.clusters.remove(&cid);
106 }
107 }
108 changed
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::state::{cluster_arn, cluster_endpoint, Cluster, EncryptionDetails};
115 use fakecloud_core::multi_account::MultiAccountState;
116 use parking_lot::RwLock;
117 use std::collections::BTreeMap;
118
119 fn cluster(status: &str, age_ms: i64) -> Cluster {
120 Cluster {
121 identifier: "abcdefghij0123456789klmnop".into(),
122 arn: cluster_arn("us-east-1", "000000000000", "abcdefghij0123456789klmnop"),
123 status: status.into(),
124 creation_time: Utc::now() - chrono::Duration::milliseconds(age_ms),
125 deletion_protection_enabled: true,
126 multi_region_properties: None,
127 encryption_details: EncryptionDetails {
128 encryption_type: "AWS_OWNED_KMS_KEY".into(),
129 kms_key_arn: None,
130 encryption_status: "ENABLED".into(),
131 },
132 endpoint: cluster_endpoint("abcdefghij0123456789klmnop", "us-east-1"),
133 tags: BTreeMap::new(),
134 policy: None,
135 policy_version: 0,
136 client_token: None,
137 deleted_at: None,
138 streams: BTreeMap::new(),
139 }
140 }
141
142 fn state_with(c: Cluster) -> SharedDsqlState {
143 let mut accounts: MultiAccountState<crate::state::DsqlState> =
144 MultiAccountState::new("000000000000", "us-east-1", "");
145 accounts
146 .default_mut()
147 .clusters
148 .insert(c.identifier.clone(), c);
149 Arc::new(RwLock::new(accounts))
150 }
151
152 #[test]
153 fn creating_advances_to_active_after_window() {
154 let state = state_with(cluster("CREATING", 1000));
155 assert!(advance(&state));
156 assert_eq!(
157 state.read().get("000000000000").unwrap().clusters["abcdefghij0123456789klmnop"].status,
158 "ACTIVE"
159 );
160 }
161
162 #[test]
163 fn creating_stays_within_window() {
164 let state = state_with(cluster("CREATING", 0));
165 assert!(!advance(&state));
166 }
167
168 #[test]
169 fn deleting_removed_after_grace_window() {
170 let mut c = cluster("DELETING", 0);
172 c.deleted_at = Some(Utc::now() - chrono::Duration::milliseconds(1000));
173 let state = state_with(c);
174 assert!(advance(&state));
175 assert!(state
176 .read()
177 .get("000000000000")
178 .unwrap()
179 .clusters
180 .is_empty());
181 }
182
183 #[test]
184 fn deleting_survives_within_grace_window() {
185 let mut c = cluster("DELETING", 0);
187 c.deleted_at = Some(Utc::now());
188 let state = state_with(c);
189 assert!(!advance(&state));
190 assert_eq!(
191 state.read().get("000000000000").unwrap().clusters["abcdefghij0123456789klmnop"].status,
192 "DELETING"
193 );
194 }
195}