use crate::core::{Disposition, EffectKey, StepId};
use super::{Record, RecordKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FailedEffect {
pub step: Option<StepId>,
pub key: EffectKey,
pub kind: String,
pub disposition: Disposition,
pub permanent: bool,
pub code: Option<String>,
pub account: bool,
pub detail: String,
}
impl FailedEffect {
#[must_use]
pub const fn refuses_the_account(&self) -> bool {
self.account
}
}
#[must_use]
pub fn failed_effect(records: &[Record]) -> Option<FailedEffect> {
let mut standing: Option<FailedEffect> = None;
for record in records {
let body = &record.body;
if !body.phase.is_forward() {
continue;
}
match &body.kind {
RecordKind::EffectFailed {
error,
disposition,
permanent,
code,
account,
..
} => {
let Some(key) = body.effect_key else {
continue;
};
standing = Some(FailedEffect {
step: body.step,
key,
kind: started_kind(records, key).unwrap_or_default(),
disposition: *disposition,
permanent: *permanent,
code: code.clone(),
account: *account,
detail: error.clone(),
});
}
RecordKind::EffectDone { .. } | RecordKind::EffectReconciled { .. }
if standing.as_ref().is_some_and(|f| f.step == body.step) =>
{
standing = None;
}
RecordKind::StepFinished { outcome }
if outcome == "succeeded"
&& standing.as_ref().is_some_and(|f| f.step == body.step) =>
{
standing = None;
}
_ => {}
}
}
standing
}
fn started_kind(records: &[Record], key: EffectKey) -> Option<String> {
records.iter().find_map(|r| match &r.body.kind {
RecordKind::EffectStarted { descriptor, .. } if r.body.effect_key == Some(key) => {
Some(descriptor.kind.clone())
}
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::failed_effect;
use crate::core::{
Digest, Disposition, EffectDescriptor, EffectKey, Recovery, RunId, Spend, StepId,
};
use crate::journal::{Append, Record, RecordKind};
fn key(n: u8) -> EffectKey {
EffectKey::from_hex(&Digest::of(&[n]).to_hex()).unwrap()
}
fn records(entries: Vec<(StepId, Option<EffectKey>, RecordKind)>) -> Vec<Record> {
let run = RunId::generate();
let mut prev = Digest::ZERO;
let mut out = Vec::new();
for (i, (step, k, kind)) in entries.into_iter().enumerate() {
let mut a = Append::new(run, kind).step(step);
if let Some(k) = k {
a = a.effect(k);
}
let r = Record::seal(a.into_body(i as u64 + 1, 1), prev).unwrap();
prev = r.hash;
out.push(r);
}
out
}
fn started() -> RecordKind {
RecordKind::EffectStarted {
descriptor: EffectDescriptor::nullary("model.complete"),
recovery: Recovery::Retry,
mutates: false,
attempt: 1,
backoff_ms: 0,
outbound_label: None,
outbound_bytes: None,
content_rules: None,
credential: None,
}
}
fn failed(code: &str, account: bool) -> RecordKind {
RecordKind::EffectFailed {
account,
error: "no".into(),
spend: Spend::default(),
disposition: Disposition::DidNotHappen,
permanent: false,
unreproducible: None,
elapsed_ms: None,
code: Some(code.into()),
}
}
#[test]
fn a_failure_its_step_got_past_is_not_the_runs() {
let s0 = StepId(0);
let s1 = StepId(1);
let history = records(vec![
(s0, Some(key(1)), started()),
(s0, Some(key(1)), failed("overloaded", false)),
(s0, Some(key(2)), started()),
(
s0,
Some(key(2)),
RecordKind::EffectDone {
declared: crate::core::DeclaredOutput::untrusted(),
output: serde_json::json!(1),
source: None,
by: None,
spend: Spend::default(),
content: None,
elapsed_ms: None,
},
),
(s1, Some(key(3)), started()),
(s1, Some(key(3)), failed("billing_error", true)),
]);
let failed = failed_effect(&history).expect("the standing failure");
assert_eq!(failed.key, key(3));
assert_eq!(failed.code.as_deref(), Some("billing_error"));
assert!(
failed.refuses_the_account(),
"the record's verdict was not read"
);
assert_eq!(failed.detail, "no");
assert_eq!(failed.kind, "model.complete");
assert!(
failed_effect(&history[..4]).is_none(),
"an answered retry left a failure standing"
);
}
}