use aion_core::{AlarmCause, InvariantAlarm, ToleranceSpec};
use aion_store::InvariantHealthState;
use chrono::{DateTime, Utc};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnconfirmedEvidence {
SampleRed,
WindowMissed,
}
impl UnconfirmedEvidence {
#[must_use]
pub const fn cause(self) -> AlarmCause {
match self {
Self::SampleRed => AlarmCause::SampleRed,
Self::WindowMissed => AlarmCause::WindowMissed,
}
}
}
pub fn observe_confirmed(state: &mut InvariantHealthState, at: DateTime<Utc>) {
state.last_confirmed_at = Some(at);
state.consecutive_unconfirmed = 0;
state.last_evidence = None;
state.alarmed = false;
}
pub fn observe_unconfirmed(state: &mut InvariantHealthState, evidence: UnconfirmedEvidence) {
state.consecutive_unconfirmed = state.consecutive_unconfirmed.saturating_add(1);
state.last_evidence = Some(evidence.cause());
}
#[must_use]
pub fn tolerance_breach(
tolerance: &ToleranceSpec,
state: &InvariantHealthState,
anchor: DateTime<Utc>,
now: DateTime<Utc>,
) -> Option<AlarmCause> {
if let Some(windows) = tolerance.consecutive_windows()
&& state.consecutive_unconfirmed > windows
{
return Some(
state
.last_evidence
.unwrap_or(AlarmCause::UnconfirmedUnknown),
);
}
if let Some(unconfirmed_for) = tolerance.unconfirmed_for() {
let since = state.last_confirmed_at.unwrap_or(anchor);
if let Ok(delta) = chrono::Duration::from_std(unconfirmed_for)
&& let Some(deadline) = since.checked_add_signed(delta)
&& now > deadline
{
return Some(
state
.last_evidence
.unwrap_or(AlarmCause::UnconfirmedUnknown),
);
}
}
None
}
#[must_use]
pub fn peek_alarm(
invariant: &str,
tolerance: &ToleranceSpec,
state: &InvariantHealthState,
anchor: DateTime<Utc>,
now: DateTime<Utc>,
window_seq: Option<u64>,
) -> Option<InvariantAlarm> {
if state.alarmed {
return None;
}
let cause = tolerance_breach(tolerance, state, anchor, now)?;
Some(InvariantAlarm {
invariant: invariant.to_owned(),
cause,
window_seq,
last_confirmed_at: state.last_confirmed_at,
consecutive_unconfirmed: state.consecutive_unconfirmed,
})
}
pub fn latch_alarm(state: &mut InvariantHealthState) {
state.alarmed = true;
}
pub fn take_alarm(
invariant: &str,
tolerance: &ToleranceSpec,
state: &mut InvariantHealthState,
anchor: DateTime<Utc>,
now: DateTime<Utc>,
window_seq: Option<u64>,
) -> Option<InvariantAlarm> {
let alarm = peek_alarm(invariant, tolerance, state, anchor, now, window_seq)?;
latch_alarm(state);
Some(alarm)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use chrono::TimeZone;
use super::*;
fn at(offset: i64) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 8, 25, 6, 0, 0)
.single()
.unwrap_or_default()
+ chrono::Duration::seconds(offset)
}
fn count_tolerance(windows: u64) -> ToleranceSpec {
ToleranceSpec::count(windows)
}
fn duration_tolerance(seconds: u64) -> Result<ToleranceSpec, Box<dyn std::error::Error>> {
Ok(ToleranceSpec::duration(Duration::from_secs(seconds))?)
}
#[test]
fn count_form_tolerates_exactly_n_and_alarms_on_the_next()
-> Result<(), Box<dyn std::error::Error>> {
let tolerance = count_tolerance(3);
let mut state = InvariantHealthState::default();
for miss in 0..3 {
observe_unconfirmed(&mut state, UnconfirmedEvidence::WindowMissed);
assert!(
take_alarm("serving", &tolerance, &mut state, at(0), at(miss), Some(1)).is_none(),
"within tolerance ({miss}) must not alarm — never before"
);
}
observe_unconfirmed(&mut state, UnconfirmedEvidence::WindowMissed);
let alarm = take_alarm("serving", &tolerance, &mut state, at(0), at(4), Some(4))
.ok_or("exceeding tolerance must alarm — always then")?;
assert_eq!(alarm.cause, AlarmCause::WindowMissed);
assert_eq!(alarm.consecutive_unconfirmed, 4);
assert_eq!(alarm.window_seq, Some(4));
assert_eq!(alarm.last_confirmed_at, None);
Ok(())
}
#[test]
fn tolerance_zero_alarms_on_the_first_unhealthy_sample()
-> Result<(), Box<dyn std::error::Error>> {
let tolerance = count_tolerance(0);
let mut state = InvariantHealthState::default();
observe_unconfirmed(&mut state, UnconfirmedEvidence::SampleRed);
let alarm = take_alarm("serving", &tolerance, &mut state, at(0), at(1), Some(1))
.ok_or("declared tolerance zero must alarm on one bad sample")?;
assert_eq!(alarm.cause, AlarmCause::SampleRed);
Ok(())
}
#[test]
fn a_sustained_breach_alarms_once_and_recovery_rearms() {
let tolerance = count_tolerance(0);
let mut state = InvariantHealthState::default();
observe_unconfirmed(&mut state, UnconfirmedEvidence::SampleRed);
assert!(take_alarm("serving", &tolerance, &mut state, at(0), at(1), None).is_some());
observe_unconfirmed(&mut state, UnconfirmedEvidence::SampleRed);
assert!(take_alarm("serving", &tolerance, &mut state, at(0), at(2), None).is_none());
observe_confirmed(&mut state, at(3));
assert!(!state.alarmed);
assert_eq!(state.consecutive_unconfirmed, 0);
observe_unconfirmed(&mut state, UnconfirmedEvidence::SampleRed);
assert!(take_alarm("serving", &tolerance, &mut state, at(0), at(4), None).is_some());
}
#[test]
fn duration_form_alarms_on_total_silence_with_unknown_cause()
-> Result<(), Box<dyn std::error::Error>> {
let tolerance = duration_tolerance(100)?;
let mut state = InvariantHealthState::default();
assert!(take_alarm("serving", &tolerance, &mut state, at(0), at(100), None).is_none());
let alarm = take_alarm("serving", &tolerance, &mut state, at(0), at(101), None)
.ok_or("duration expiry with zero samples must alarm (R2.4a)")?;
assert_eq!(alarm.cause, AlarmCause::UnconfirmedUnknown);
assert_eq!(alarm.consecutive_unconfirmed, 0);
assert_eq!(alarm.last_confirmed_at, None);
assert_eq!(alarm.window_seq, None);
Ok(())
}
#[test]
fn duration_form_measures_from_the_last_confirmation() -> Result<(), Box<dyn std::error::Error>>
{
let tolerance = duration_tolerance(100)?;
let mut state = InvariantHealthState::default();
observe_confirmed(&mut state, at(50));
assert!(take_alarm("serving", &tolerance, &mut state, at(0), at(150), None).is_none());
let alarm = take_alarm("serving", &tolerance, &mut state, at(0), at(151), None)
.ok_or("unconfirmed past D after the last confirmation must alarm")?;
assert_eq!(alarm.last_confirmed_at, Some(at(50)));
Ok(())
}
#[test]
fn duration_expiry_names_the_accrued_evidence_when_there_is_some()
-> Result<(), Box<dyn std::error::Error>> {
let tolerance = ToleranceSpec::both(10, Duration::from_secs(100))?;
let mut state = InvariantHealthState::default();
observe_unconfirmed(&mut state, UnconfirmedEvidence::SampleRed);
observe_unconfirmed(&mut state, UnconfirmedEvidence::SampleRed);
let alarm = take_alarm("serving", &tolerance, &mut state, at(0), at(101), Some(2))
.ok_or("duration expiry must alarm")?;
assert_eq!(alarm.cause, AlarmCause::SampleRed);
assert_eq!(alarm.consecutive_unconfirmed, 2);
Ok(())
}
}