use crate::core::types::{
HealthCheck, HealthCheckResult, RestartPolicy, ServiceEvent, ServiceState,
};
#[derive(Debug, Clone, PartialEq)]
pub enum ServiceAction {
Start,
CheckHealth,
Restart,
Stop(String),
Wait { delay_secs: f64 },
}
pub fn plan_service_action(
state: &ServiceState,
policy: &RestartPolicy,
health_check: &HealthCheck,
) -> ServiceAction {
if state.pid.is_none() {
return ServiceAction::Start;
}
if state.restart_count > 0 && !policy.should_restart(state.restart_count) {
return ServiceAction::Stop(format!("max restarts ({}) exceeded", policy.max_restarts));
}
let retries = health_check.retries.unwrap_or(3);
if state.consecutive_failures >= retries {
return ServiceAction::Restart;
}
if state.last_check.is_none() {
ServiceAction::CheckHealth
} else {
let delay = parse_interval(health_check.interval.as_deref());
ServiceAction::Wait { delay_secs: delay }
}
}
pub fn process_health_check(
state: &ServiceState,
result: &HealthCheckResult,
) -> (ServiceState, Vec<ServiceEvent>) {
let mut events = Vec::new();
let mut new_state = state.clone();
new_state.last_check = Some(result.checked_at.clone());
if result.healthy {
new_state.healthy = true;
new_state.consecutive_failures = 0;
events.push(ServiceEvent::HealthOk {
at: result.checked_at.clone(),
});
} else {
new_state.consecutive_failures += 1;
new_state.healthy = false;
events.push(ServiceEvent::HealthFail {
exit_code: result.exit_code,
consecutive: new_state.consecutive_failures,
at: result.checked_at.clone(),
});
}
(new_state, events)
}
pub fn apply_restart(
state: &ServiceState,
new_pid: u32,
timestamp: &str,
) -> (ServiceState, ServiceEvent) {
let old_pid = state.pid.unwrap_or(0);
let restart_count = state.restart_count + 1;
let new_state = ServiceState {
pid: Some(new_pid),
healthy: false,
consecutive_failures: 0,
last_check: None,
restart_count,
};
let event = ServiceEvent::Restarted {
old_pid,
new_pid,
restart_count,
at: timestamp.to_string(),
};
(new_state, event)
}
pub fn apply_start(pid: u32, timestamp: &str) -> (ServiceState, ServiceEvent) {
let state = ServiceState {
pid: Some(pid),
healthy: false,
consecutive_failures: 0,
last_check: None,
restart_count: 0,
};
let event = ServiceEvent::Started {
pid,
at: timestamp.to_string(),
};
(state, event)
}
pub fn apply_stop(
state: &ServiceState,
reason: &str,
timestamp: &str,
) -> (ServiceState, ServiceEvent) {
let new_state = ServiceState {
pid: None,
healthy: false,
consecutive_failures: state.consecutive_failures,
last_check: state.last_check.clone(),
restart_count: state.restart_count,
};
let event = ServiceEvent::Stopped {
reason: reason.to_string(),
at: timestamp.to_string(),
};
(new_state, event)
}
pub fn restart_backoff(policy: &RestartPolicy, state: &ServiceState) -> f64 {
policy.backoff_secs(state.restart_count)
}
pub fn format_service_summary(name: &str, state: &ServiceState) -> String {
let status = match state.pid {
None => "stopped".to_string(),
Some(pid) if state.healthy => format!("healthy (pid={pid})"),
Some(pid) => format!(
"unhealthy (pid={pid}, failures={})",
state.consecutive_failures
),
};
let restarts = if state.restart_count > 0 {
format!(" restarts={}", state.restart_count)
} else {
String::new()
};
format!("service/{name}: {status}{restarts}")
}
pub(crate) fn parse_interval(interval: Option<&str>) -> f64 {
let s = match interval {
Some(s) => s,
None => return 30.0, };
if let Some(num) = s.strip_suffix('s') {
num.parse::<f64>().unwrap_or(30.0)
} else if let Some(num) = s.strip_suffix('m') {
num.parse::<f64>().unwrap_or(0.5) * 60.0
} else if let Some(num) = s.strip_suffix('h') {
num.parse::<f64>().unwrap_or(1.0) * 3600.0
} else {
s.parse::<f64>().unwrap_or(30.0)
}
}