use std::time::Instant;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum MaintenanceState {
#[default]
Active,
EnteringMaintenance {
since: Instant,
deadline: Option<Instant>,
},
Maintenance {
since: Instant,
},
ExitingMaintenance {
since: Instant,
},
DrainFailed {
since: Instant,
reason: String,
},
Recovery {
since: Instant,
},
}
impl MaintenanceState {
#[allow(dead_code)]
fn rank(&self) -> u8 {
match self {
MaintenanceState::Active => 0,
MaintenanceState::EnteringMaintenance { .. } => 1,
MaintenanceState::Maintenance { .. } => 2,
MaintenanceState::ExitingMaintenance { .. } => 3,
MaintenanceState::DrainFailed { .. } => 3,
MaintenanceState::Recovery { .. } => 4,
}
}
pub fn is_valid_successor(&self, new: &MaintenanceState) -> bool {
use MaintenanceState::*;
if std::mem::discriminant(self) == std::mem::discriminant(new) {
return true;
}
match (self, new) {
(Active, EnteringMaintenance { .. }) => true,
(EnteringMaintenance { .. }, Maintenance { .. }) => true,
(EnteringMaintenance { .. }, DrainFailed { .. }) => true,
(Maintenance { .. }, ExitingMaintenance { .. }) => true,
(Maintenance { .. }, DrainFailed { .. }) => true,
(ExitingMaintenance { .. }, Recovery { .. }) => true,
(DrainFailed { .. }, ExitingMaintenance { .. }) => true,
(Recovery { .. }, Active) => true,
_ => false,
}
}
pub fn is_non_active(&self) -> bool {
!matches!(self, MaintenanceState::Active)
}
pub fn is_steady_maintenance(&self) -> bool {
matches!(self, MaintenanceState::Maintenance { .. })
}
pub fn since(&self) -> Option<Instant> {
match self {
MaintenanceState::Active => None,
MaintenanceState::EnteringMaintenance { since, .. }
| MaintenanceState::Maintenance { since }
| MaintenanceState::ExitingMaintenance { since }
| MaintenanceState::DrainFailed { since, .. }
| MaintenanceState::Recovery { since } => Some(*since),
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn default_is_active() {
let s = MaintenanceState::default();
assert!(matches!(s, MaintenanceState::Active));
assert!(!s.is_non_active());
assert!(!s.is_steady_maintenance());
assert_eq!(s.since(), None);
}
#[test]
fn predicates_reflect_state_shape() {
let base = Instant::now();
let entering = MaintenanceState::EnteringMaintenance {
since: base,
deadline: Some(base + Duration::from_secs(60)),
};
assert!(entering.is_non_active());
assert!(!entering.is_steady_maintenance());
assert_eq!(entering.since(), Some(base));
let steady = MaintenanceState::Maintenance { since: base };
assert!(steady.is_non_active());
assert!(steady.is_steady_maintenance());
let recovery = MaintenanceState::Recovery { since: base };
assert!(recovery.is_non_active());
assert!(!recovery.is_steady_maintenance());
}
#[test]
fn is_valid_successor_accepts_forward_arcs_only() {
let base = Instant::now();
let active = MaintenanceState::Active;
let entering = MaintenanceState::EnteringMaintenance {
since: base,
deadline: None,
};
let maintenance = MaintenanceState::Maintenance { since: base };
let exiting = MaintenanceState::ExitingMaintenance { since: base };
let drain_failed = MaintenanceState::DrainFailed {
since: base,
reason: "deadline".into(),
};
let recovery = MaintenanceState::Recovery { since: base };
assert!(active.is_valid_successor(&entering));
assert!(entering.is_valid_successor(&maintenance));
assert!(entering.is_valid_successor(&drain_failed));
assert!(maintenance.is_valid_successor(&exiting));
assert!(drain_failed.is_valid_successor(&exiting));
assert!(exiting.is_valid_successor(&recovery));
assert!(recovery.is_valid_successor(&active));
assert!(entering.is_valid_successor(&entering));
assert!(recovery.is_valid_successor(&recovery));
assert!(!entering.is_valid_successor(&active));
assert!(!maintenance.is_valid_successor(&entering));
assert!(!recovery.is_valid_successor(&maintenance));
assert!(!exiting.is_valid_successor(&entering));
assert!(!drain_failed.is_valid_successor(&entering));
assert!(!maintenance.is_valid_successor(&active));
assert!(!entering.is_valid_successor(&active));
}
#[test]
fn is_valid_successor_rejects_exiting_to_drain_failed() {
let base = Instant::now();
let exiting = MaintenanceState::ExitingMaintenance { since: base };
let drain_failed = MaintenanceState::DrainFailed {
since: base,
reason: "late replay".into(),
};
assert!(
!exiting.is_valid_successor(&drain_failed),
"operator force-exit must not be regressed by a late DrainFailed observation",
);
}
}