use std::collections::HashMap;
use std::sync::{Mutex, MutexGuard};
use chrono::{DateTime, Utc};
use aion_core::WorkflowId;
use crate::EngineError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnrecoverableRun {
pub workflow_type: String,
pub reason: String,
pub observed_at: DateTime<Utc>,
}
#[derive(Debug, Default)]
pub struct UnrecoverableRuns {
entries: Mutex<HashMap<WorkflowId, UnrecoverableRun>>,
}
impl UnrecoverableRuns {
pub fn record(
&self,
workflow_id: WorkflowId,
entry: UnrecoverableRun,
) -> Result<(), EngineError> {
self.entries()?.insert(workflow_id, entry);
Ok(())
}
pub fn get(&self, workflow_id: &WorkflowId) -> Result<Option<UnrecoverableRun>, EngineError> {
Ok(self.entries()?.get(workflow_id).cloned())
}
pub fn clear(&self, workflow_id: &WorkflowId) -> Result<bool, EngineError> {
Ok(self.entries()?.remove(workflow_id).is_some())
}
pub fn list(&self) -> Result<Vec<(WorkflowId, UnrecoverableRun)>, EngineError> {
Ok(self
.entries()?
.iter()
.map(|(id, entry)| (id.clone(), entry.clone()))
.collect())
}
fn entries(
&self,
) -> Result<MutexGuard<'_, HashMap<WorkflowId, UnrecoverableRun>>, EngineError> {
self.entries
.lock()
.map_err(|_| EngineError::RegistryPoisoned)
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn wid(n: u128) -> WorkflowId {
WorkflowId::new(Uuid::from_u128(n))
}
fn require(found: Option<UnrecoverableRun>, why: &str) -> TestResult<UnrecoverableRun> {
found.ok_or_else(|| Box::<dyn std::error::Error>::from(why.to_owned()))
}
fn entry(reason: &str) -> UnrecoverableRun {
UnrecoverableRun {
workflow_type: "rig$deadbeef".to_owned(),
reason: reason.to_owned(),
observed_at: Utc::now(),
}
}
#[test]
fn a_recorded_run_can_be_asked_about_by_id() -> TestResult {
let runs = UnrecoverableRuns::default();
let id = wid(0x0756_ecd5);
runs.record(id.clone(), entry("pinned version 13958627 is not loaded"))?;
let found = require(runs.get(&id)?, "the run just recorded must be found")?;
assert_eq!(found.reason, "pinned version 13958627 is not loaded");
assert_eq!(found.workflow_type, "rig$deadbeef");
Ok(())
}
#[test]
fn an_unrecorded_run_is_not_degraded() -> TestResult {
let runs = UnrecoverableRuns::default();
runs.record(wid(1), entry("cannot load"))?;
assert!(
runs.get(&wid(2))?.is_none(),
"a run that was never recorded must not report as degraded — this is \
the discriminating half of the query"
);
Ok(())
}
#[test]
fn observing_the_run_resident_clears_the_degraded_fact() -> TestResult {
let runs = UnrecoverableRuns::default();
let id = wid(0x0756_ecd5);
runs.record(id.clone(), entry("cannot load"))?;
assert!(
runs.clear(&id)?,
"clear must report that it removed an entry"
);
assert!(
runs.get(&id)?.is_none(),
"a run observed resident must stop reporting as unrecoverable"
);
assert!(
!runs.clear(&id)?,
"clearing an absent entry must report false, not silently succeed — \
the boolean is how a caller can tell a real recovery from a no-op"
);
Ok(())
}
#[test]
fn the_newest_boots_verdict_replaces_the_previous_one() -> TestResult {
let runs = UnrecoverableRuns::default();
let id = wid(0x0756_ecd5);
runs.record(id.clone(), entry("first boot: cannot load"))?;
runs.record(id.clone(), entry("second boot: different cause"))?;
let found = require(runs.get(&id)?, "an entry recorded twice must be present")?;
assert_eq!(
found.reason, "second boot: different cause",
"the condition is a property of THIS process's attempt, so the latest \
verdict wins rather than the first being sticky"
);
assert_eq!(runs.list()?.len(), 1, "a replace must not also append");
Ok(())
}
#[test]
fn list_reports_every_degraded_run_and_only_those() -> TestResult {
let runs = UnrecoverableRuns::default();
runs.record(wid(1), entry("cannot load a"))?;
runs.record(wid(2), entry("cannot load b"))?;
runs.clear(&wid(2))?;
runs.record(wid(3), entry("cannot load c"))?;
let mut listed: Vec<WorkflowId> = runs.list()?.into_iter().map(|(id, _)| id).collect();
listed.sort_by_key(WorkflowId::as_uuid);
assert_eq!(
listed,
vec![wid(1), wid(3)],
"list must reflect clears, not just records"
);
Ok(())
}
}