use std::collections::BTreeSet;
use std::sync::Arc;
use crate::core::{EffectKey, RunId, RuntimeError, StoreError};
use crate::journal::{Append, JournalStore, Record, RecordKind};
use crate::runtime::{RunOutcome, RunStatus};
use crate::store::RedbStore;
#[derive(Debug, Clone)]
pub struct CrashPoint {
pub appended: usize,
pub store: Arc<RedbStore>,
prefix: Vec<Record>,
}
impl CrashPoint {
#[must_use]
pub fn done(&self) -> BTreeSet<EffectKey> {
self.prefix
.iter()
.filter(|r| matches!(r.kind(), RecordKind::EffectDone { .. }))
.filter_map(Record::effect_key)
.collect()
}
fn in_flight(&self) -> BTreeSet<EffectKey> {
let mut open = BTreeSet::new();
for r in &self.prefix {
let Some(key) = r.effect_key() else { continue };
match r.kind() {
RecordKind::EffectStarted { .. } => {
open.insert(key);
}
RecordKind::EffectDone { .. }
| RecordKind::EffectFailed { .. }
| RecordKind::EffectReconciled { .. } => {
open.remove(&key);
}
_ => {}
}
}
open
}
fn froze_a_plan(&self) -> bool {
self.prefix
.iter()
.any(|r| matches!(r.kind(), RecordKind::PlanFrozen { .. }))
}
}
pub async fn crash_points(
store: &dyn JournalStore,
run: RunId,
) -> Result<Vec<CrashPoint>, StoreError> {
let records = store.read(run, 1).await?;
let mut points = Vec::with_capacity(records.len().saturating_sub(1));
for appended in 1..records.len() {
let prefix = records[..appended].to_vec();
points.push(CrashPoint {
appended,
store: rebuilt(run, &prefix).await?,
prefix,
});
}
Ok(points)
}
async fn rebuilt(run: RunId, prefix: &[Record]) -> Result<Arc<RedbStore>, StoreError> {
let store = Arc::new(RedbStore::open_in_memory()?);
let lease = store
.acquire(run, "crash-point", std::time::Duration::from_mins(5))
.await?;
for r in prefix {
let mut a = Append::new(run, r.kind().clone()).phase(r.body.phase);
if let Some(step) = r.body.step {
a = a.step(step);
}
if let Some(case) = r.body.case {
a = a.case(case);
}
if let Some(key) = r.effect_key() {
a = a.effect(key);
}
store.append(lease.epoch, vec![a]).await?;
}
store.release_lease(run, lease.epoch).await?;
Ok(store)
}
pub async fn assert_resumed_honestly(
point: &CrashPoint,
run: RunId,
resumed: &Result<RunOutcome, RuntimeError>,
) {
let at = format!("crash after record {}", point.appended);
super::assert_replay_was_not_backstopped(&at, resumed);
point
.store
.verify(run)
.await
.unwrap_or_else(|e| panic!("{at}: the chain does not verify after the resume: {e}"));
match resumed {
Err(e) => assert!(
!point.froze_a_plan(),
"{at}: the resume was refused, though the prefix froze a plan: {e}"
),
Ok(outcome) => {
if let RunStatus::Quarantined(why) = &outcome.status {
assert!(
!point.in_flight().is_empty(),
"{at}: the run quarantined with no effect in flight at the crash: {why}"
);
}
}
}
}