use serde::{Deserialize, Serialize};
pub const COMA_THRESHOLD_SECS: u64 = 3600;
pub const RESISTANCE_DECAY_RATE: f32 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecoveryState {
Normal,
Shock,
Coma,
}
impl RecoveryState {
pub fn intensity(&self) -> f32 {
match self {
RecoveryState::Normal => 0.0,
RecoveryState::Shock => 0.3, RecoveryState::Coma => 0.5, }
}
pub fn description(&self) -> &'static str {
match self {
RecoveryState::Normal => "clean restart",
RecoveryState::Shock => "shock (unclean shutdown)",
RecoveryState::Coma => "coma (prolonged inactivity)",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShutdownMarker {
pub timestamp: u64,
pub graceful: bool,
pub version: String,
}
impl ShutdownMarker {
pub fn graceful() -> Self {
Self {
timestamp: now_secs(),
graceful: true,
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
pub struct RecoveryAnalyzer {
last_marker: Option<ShutdownMarker>,
startup_time: u64,
}
impl RecoveryAnalyzer {
pub fn new(last_marker: Option<ShutdownMarker>) -> Self {
Self {
last_marker,
startup_time: now_secs(),
}
}
pub fn analyze(&self) -> RecoveryState {
match &self.last_marker {
None => {
RecoveryState::Normal
}
Some(marker) if !marker.graceful => {
RecoveryState::Shock
}
Some(marker) => {
let downtime = self.startup_time.saturating_sub(marker.timestamp);
if downtime > COMA_THRESHOLD_SECS {
RecoveryState::Coma
} else {
RecoveryState::Normal
}
}
}
}
pub fn downtime_secs(&self) -> u64 {
self.last_marker
.as_ref()
.map(|m| self.startup_time.saturating_sub(m.timestamp))
.unwrap_or(0)
}
}
#[inline]
pub fn decay_resistance(current: f32, rate: f32) -> f32 {
(current - rate).max(0.0)
}
#[inline]
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recovery_state_intensity() {
assert_eq!(RecoveryState::Normal.intensity(), 0.0);
assert!(RecoveryState::Shock.intensity() > 0.0);
assert!(RecoveryState::Coma.intensity() > RecoveryState::Shock.intensity());
}
#[test]
fn test_analyzer_no_marker() {
let analyzer = RecoveryAnalyzer::new(None);
assert_eq!(analyzer.analyze(), RecoveryState::Normal);
}
#[test]
fn test_analyzer_graceful_marker() {
let marker = ShutdownMarker {
timestamp: now_secs() - 10, graceful: true,
version: "test".into(),
};
let analyzer = RecoveryAnalyzer::new(Some(marker));
assert_eq!(analyzer.analyze(), RecoveryState::Normal);
}
#[test]
fn test_analyzer_shock() {
let marker = ShutdownMarker {
timestamp: now_secs() - 10,
graceful: false, version: "test".into(),
};
let analyzer = RecoveryAnalyzer::new(Some(marker));
assert_eq!(analyzer.analyze(), RecoveryState::Shock);
}
#[test]
fn test_analyzer_coma() {
let marker = ShutdownMarker {
timestamp: now_secs() - COMA_THRESHOLD_SECS - 100, graceful: true,
version: "test".into(),
};
let analyzer = RecoveryAnalyzer::new(Some(marker));
assert_eq!(analyzer.analyze(), RecoveryState::Coma);
}
#[test]
fn test_resistance_decay() {
let resistance = 0.5;
let decayed = decay_resistance(resistance, RESISTANCE_DECAY_RATE);
assert!(decayed < resistance);
assert!(decayed > 0.0);
let zero = decay_resistance(0.005, RESISTANCE_DECAY_RATE);
assert_eq!(zero, 0.0);
}
}