use std::sync::Arc;
use std::time::Duration;
use chrono::Utc;
use tokio::sync::Mutex as AsyncMutex;
use fakecloud_persistence::SnapshotStore;
use crate::persistence::save_snapshot;
use crate::state::SharedDsqlState;
const TRANSITION_AFTER: chrono::Duration = chrono::Duration::milliseconds(400);
const TICK: Duration = Duration::from_millis(200);
pub fn spawn(
state: SharedDsqlState,
store: Option<Arc<dyn SnapshotStore>>,
lock: Arc<AsyncMutex<()>>,
) {
tokio::spawn(async move {
loop {
tokio::time::sleep(TICK).await;
let changed = advance(&state);
if changed {
save_snapshot(&state, store.clone(), &lock).await;
}
}
});
}
pub fn advance(state: &SharedDsqlState) -> bool {
let now = Utc::now();
let mut changed = false;
let mut accounts = state.write();
for (_id, acct) in accounts.iter_mut() {
let mut to_remove = Vec::new();
for (cid, cluster) in acct.clusters.iter_mut() {
let elapsed = now.signed_duration_since(cluster.creation_time);
match cluster.status.as_str() {
"CREATING" | "PENDING_SETUP" if elapsed >= TRANSITION_AFTER => {
cluster.status = "ACTIVE".to_string();
changed = true;
}
"DELETING" | "PENDING_DELETE" => {
let deleting_for = cluster
.deleted_at
.map(|t| now.signed_duration_since(t))
.unwrap_or(TRANSITION_AFTER);
if deleting_for >= TRANSITION_AFTER {
to_remove.push(cid.clone());
changed = true;
}
}
_ => {}
}
let mut streams_to_remove = Vec::new();
for (sid, stream) in cluster.streams.iter_mut() {
match stream.status.as_str() {
"CREATING"
if now.signed_duration_since(stream.creation_time) >= TRANSITION_AFTER =>
{
stream.status = "ACTIVE".to_string();
changed = true;
}
"DELETING" => {
let deleting_for = stream
.deleted_at
.map(|t| now.signed_duration_since(t))
.unwrap_or(TRANSITION_AFTER);
if deleting_for >= TRANSITION_AFTER {
streams_to_remove.push(sid.clone());
changed = true;
}
}
_ => {}
}
}
for sid in streams_to_remove {
cluster.streams.remove(&sid);
}
}
for cid in to_remove {
acct.clusters.remove(&cid);
}
}
changed
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{cluster_arn, cluster_endpoint, Cluster, EncryptionDetails};
use fakecloud_core::multi_account::MultiAccountState;
use parking_lot::RwLock;
use std::collections::BTreeMap;
fn cluster(status: &str, age_ms: i64) -> Cluster {
Cluster {
identifier: "abcdefghij0123456789klmnop".into(),
arn: cluster_arn("us-east-1", "000000000000", "abcdefghij0123456789klmnop"),
status: status.into(),
creation_time: Utc::now() - chrono::Duration::milliseconds(age_ms),
deletion_protection_enabled: true,
multi_region_properties: None,
encryption_details: EncryptionDetails {
encryption_type: "AWS_OWNED_KMS_KEY".into(),
kms_key_arn: None,
encryption_status: "ENABLED".into(),
},
endpoint: cluster_endpoint("abcdefghij0123456789klmnop", "us-east-1"),
tags: BTreeMap::new(),
policy: None,
policy_version: 0,
client_token: None,
deleted_at: None,
streams: BTreeMap::new(),
}
}
fn state_with(c: Cluster) -> SharedDsqlState {
let mut accounts: MultiAccountState<crate::state::DsqlState> =
MultiAccountState::new("000000000000", "us-east-1", "");
accounts
.default_mut()
.clusters
.insert(c.identifier.clone(), c);
Arc::new(RwLock::new(accounts))
}
#[test]
fn creating_advances_to_active_after_window() {
let state = state_with(cluster("CREATING", 1000));
assert!(advance(&state));
assert_eq!(
state.read().get("000000000000").unwrap().clusters["abcdefghij0123456789klmnop"].status,
"ACTIVE"
);
}
#[test]
fn creating_stays_within_window() {
let state = state_with(cluster("CREATING", 0));
assert!(!advance(&state));
}
#[test]
fn deleting_removed_after_grace_window() {
let mut c = cluster("DELETING", 0);
c.deleted_at = Some(Utc::now() - chrono::Duration::milliseconds(1000));
let state = state_with(c);
assert!(advance(&state));
assert!(state
.read()
.get("000000000000")
.unwrap()
.clusters
.is_empty());
}
#[test]
fn deleting_survives_within_grace_window() {
let mut c = cluster("DELETING", 0);
c.deleted_at = Some(Utc::now());
let state = state_with(c);
assert!(!advance(&state));
assert_eq!(
state.read().get("000000000000").unwrap().clusters["abcdefghij0123456789klmnop"].status,
"DELETING"
);
}
}