use std::collections::BTreeMap;
use std::time::Duration;
use crate::entry::IntentKind;
use crate::tier::TierId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContinuityState {
Healthy,
Degraded,
Unavailable,
Recovering,
Reconciling,
}
impl ContinuityState {
pub const ALL: [ContinuityState; 5] = [
Self::Healthy,
Self::Degraded,
Self::Unavailable,
Self::Recovering,
Self::Reconciling,
];
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Healthy => "healthy",
Self::Degraded => "degraded",
Self::Unavailable => "unavailable",
Self::Recovering => "recovering",
Self::Reconciling => "reconciling",
}
}
#[must_use]
pub fn is_serving(self) -> bool {
matches!(self, Self::Healthy | Self::Degraded)
}
#[must_use]
pub fn accepts_writes(self) -> bool {
matches!(self, Self::Healthy | Self::Reconciling)
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|st| st.name() == s)
}
}
impl std::fmt::Display for ContinuityState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryDirection {
Abort,
CompleteForward,
ExternalReconciliation,
}
impl RecoveryDirection {
#[must_use]
pub const fn for_kind(kind: IntentKind) -> Self {
match kind {
IntentKind::Write => Self::Abort,
IntentKind::Move => Self::ExternalReconciliation,
}
}
#[must_use]
pub const fn is_executable_here(self) -> bool {
matches!(self, Self::Abort)
}
#[must_use]
pub const fn is_success_without_the_key(self) -> bool {
matches!(self, Self::CompleteForward)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RecoveryReport {
pub recovered: usize,
pub failed: usize,
pub needs_reconciliation: usize,
pub released_for_reconciliation: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryOutcome {
Aborted {
kind: IntentKind,
},
Completed {
kind: IntentKind,
},
Failed {
kind: IntentKind,
},
}
impl RecoveryOutcome {
#[must_use]
pub const fn kind(self) -> IntentKind {
match self {
Self::Aborted { kind } | Self::Completed { kind } | Self::Failed { kind } => kind,
}
}
#[must_use]
pub const fn is_success(self) -> bool {
!matches!(self, Self::Failed { .. })
}
#[must_use]
pub const fn is_committed(self) -> bool {
matches!(self, Self::Completed { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContinuityReport {
pub states: BTreeMap<TierId, ContinuityState>,
}
impl ContinuityReport {
#[must_use]
pub fn state(&self, tier: TierId) -> ContinuityState {
self.states
.get(&tier)
.copied()
.unwrap_or(ContinuityState::Unavailable)
}
#[must_use]
pub fn serving(&self) -> Vec<TierId> {
self.states
.iter()
.filter(|(_, s)| s.is_serving())
.map(|(t, _)| *t)
.collect()
}
#[must_use]
pub fn all_healthy(&self) -> bool {
!self.states.is_empty() && self.states.values().all(|s| *s == ContinuityState::Healthy)
}
#[must_use]
pub fn summary(&self) -> String {
self.states
.iter()
.map(|(t, s)| format!("{t}={s}"))
.collect::<Vec<_>>()
.join(" ")
}
}
pub const FAILURE_THRESHOLD: u64 = 5;
pub const INTENT_RECOVERY_AGE: Duration = Duration::from_secs(5);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_contract_state_is_representable() {
for s in [
"healthy",
"degraded",
"unavailable",
"recovering",
"reconciling",
] {
assert!(ContinuityState::parse(s).is_some(), "{s} is unparseable");
}
}
#[test]
fn write_intents_abort_and_move_intents_need_the_key() {
assert_eq!(
RecoveryDirection::for_kind(IntentKind::Write),
RecoveryDirection::Abort
);
assert_eq!(
RecoveryDirection::for_kind(IntentKind::Move),
RecoveryDirection::ExternalReconciliation
);
}
#[test]
fn only_abort_is_executable_without_the_key() {
assert!(RecoveryDirection::Abort.is_executable_here());
assert!(!RecoveryDirection::CompleteForward.is_executable_here());
assert!(!RecoveryDirection::ExternalReconciliation.is_executable_here());
}
#[test]
fn recovering_is_distinguishable_from_available() {
assert!(!ContinuityState::Recovering.is_serving());
assert!(!ContinuityState::Recovering.accepts_writes());
assert!(ContinuityState::Healthy.is_serving());
assert!(ContinuityState::Degraded.is_serving());
assert!(ContinuityState::Reconciling.accepts_writes());
}
#[test]
fn an_unlisted_rung_reads_as_unavailable() {
let report = ContinuityReport {
states: BTreeMap::new(),
};
assert_eq!(report.state(TierId::L4), ContinuityState::Unavailable);
assert!(!report.all_healthy());
assert_eq!(report.serving().len(), 0);
}
#[test]
fn report_summary_names_every_rung() {
let mut states = BTreeMap::new();
for t in TierId::ALL {
states.insert(t, ContinuityState::Healthy);
}
let report = ContinuityReport { states };
assert!(report.all_healthy());
assert_eq!(report.serving().len(), TierId::ALL.len());
assert!(report.summary().contains("L0=healthy"));
}
}