use crate::{
ids::{WasmStoreGcMode, WasmStoreGcStatus},
storage::stable::template::{WasmStoreGcStateRecord, WasmStoreGcStateStore},
};
use canic_core::dto::error::Error;
pub struct WasmStoreGcOps;
impl WasmStoreGcOps {
#[must_use]
pub fn status() -> WasmStoreGcStateRecord {
WasmStoreGcStateStore::get()
}
#[must_use]
pub fn snapshot() -> WasmStoreGcStatus {
let current = Self::status();
WasmStoreGcStatus {
mode: current.mode,
changed_at: current.changed_at,
prepared_at: current.prepared_at,
started_at: current.started_at,
completed_at: current.completed_at,
runs_completed: current.runs_completed,
}
}
pub fn prepare(changed_at: u64) -> Result<(), Error> {
Self::transition_to(WasmStoreGcMode::Prepared, changed_at)
}
pub fn begin(changed_at: u64) -> Result<(), Error> {
Self::transition_to(WasmStoreGcMode::InProgress, changed_at)
}
pub fn complete(changed_at: u64) -> Result<(), Error> {
Self::transition_to(WasmStoreGcMode::Complete, changed_at)
}
fn transition_to(next: WasmStoreGcMode, changed_at: u64) -> Result<(), Error> {
let current = Self::status();
let updated = transition_record(¤t, next, changed_at)?;
WasmStoreGcStateStore::set(updated);
Ok(())
}
}
fn transition_record(
current: &WasmStoreGcStateRecord,
next: WasmStoreGcMode,
changed_at: u64,
) -> Result<WasmStoreGcStateRecord, Error> {
if current.mode == next {
return Ok(current.clone());
}
match (current.mode, next) {
(WasmStoreGcMode::Normal, WasmStoreGcMode::Prepared)
| (WasmStoreGcMode::Prepared, WasmStoreGcMode::InProgress)
| (WasmStoreGcMode::InProgress, WasmStoreGcMode::Complete) => {
let mut updated = current.clone();
updated.mode = next;
updated.changed_at = changed_at;
match next {
WasmStoreGcMode::Prepared => {
updated.prepared_at = Some(changed_at);
updated.started_at = None;
updated.completed_at = None;
}
WasmStoreGcMode::InProgress => {
updated.started_at = Some(changed_at);
updated.completed_at = None;
}
WasmStoreGcMode::Complete => {
updated.completed_at = Some(changed_at);
updated.runs_completed = updated.runs_completed.saturating_add(1);
}
WasmStoreGcMode::Normal => {}
}
Ok(updated)
}
_ => Err(Error::conflict(format!(
"wasm store gc transition {:?} -> {:?} is not allowed",
current.mode, next
))),
}
}
#[cfg(test)]
mod tests {
use super::{WasmStoreGcOps, transition_record};
use crate::{
ids::WasmStoreGcMode,
storage::stable::template::{WasmStoreGcStateRecord, WasmStoreGcStateStore},
};
use canic_core::dto::error::ErrorCode;
#[test]
fn transition_record_advances_monotonically() {
let prepared = transition_record(
&WasmStoreGcStateRecord::default(),
WasmStoreGcMode::Prepared,
10,
)
.expect("normal -> prepared must succeed");
assert_eq!(prepared.mode, WasmStoreGcMode::Prepared);
assert_eq!(prepared.changed_at, 10);
assert_eq!(prepared.prepared_at, Some(10));
assert_eq!(prepared.started_at, None);
assert_eq!(prepared.completed_at, None);
assert_eq!(prepared.runs_completed, 0);
let in_progress = transition_record(&prepared, WasmStoreGcMode::InProgress, 20)
.expect("prepared -> in progress must succeed");
assert_eq!(in_progress.mode, WasmStoreGcMode::InProgress);
assert_eq!(in_progress.changed_at, 20);
assert_eq!(in_progress.prepared_at, Some(10));
assert_eq!(in_progress.started_at, Some(20));
assert_eq!(in_progress.completed_at, None);
assert_eq!(in_progress.runs_completed, 0);
let complete = transition_record(&in_progress, WasmStoreGcMode::Complete, 30)
.expect("in progress -> complete must succeed");
assert_eq!(complete.mode, WasmStoreGcMode::Complete);
assert_eq!(complete.changed_at, 30);
assert_eq!(complete.prepared_at, Some(10));
assert_eq!(complete.started_at, Some(20));
assert_eq!(complete.completed_at, Some(30));
assert_eq!(complete.runs_completed, 1);
}
#[test]
fn transition_record_is_idempotent_for_same_mode() {
let current = WasmStoreGcStateRecord {
mode: WasmStoreGcMode::Prepared,
changed_at: 10,
prepared_at: Some(10),
started_at: None,
completed_at: None,
runs_completed: 0,
};
let updated = transition_record(¤t, WasmStoreGcMode::Prepared, 99)
.expect("same-mode transition must be idempotent");
assert_eq!(updated.mode, WasmStoreGcMode::Prepared);
assert_eq!(updated.changed_at, 10);
}
#[test]
fn transition_record_rejects_invalid_order() {
let err = transition_record(
&WasmStoreGcStateRecord::default(),
WasmStoreGcMode::InProgress,
10,
)
.expect_err("normal -> in progress must fail");
assert_eq!(err.code, ErrorCode::Conflict);
assert!(err.message.contains("not allowed"));
}
#[test]
fn snapshot_reflects_persisted_state() {
WasmStoreGcStateStore::clear_for_test();
WasmStoreGcOps::prepare(10).expect("prepare must succeed");
let snapshot = WasmStoreGcOps::snapshot();
assert_eq!(snapshot.mode, WasmStoreGcMode::Prepared);
assert_eq!(snapshot.prepared_at, Some(10));
WasmStoreGcStateStore::clear_for_test();
}
}