use crate::ContainerID;
use crate::lock::LockId;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BoxStatus {
Unknown,
Configured,
Running,
Stopping,
Stopped,
Paused,
}
impl BoxStatus {
pub fn is_active(&self) -> bool {
matches!(self, BoxStatus::Running | BoxStatus::Paused)
}
pub fn is_running(&self) -> bool {
matches!(self, BoxStatus::Running)
}
pub fn is_configured(&self) -> bool {
matches!(self, BoxStatus::Configured)
}
pub fn is_stopped(&self) -> bool {
matches!(self, BoxStatus::Stopped)
}
pub fn is_paused(&self) -> bool {
matches!(self, BoxStatus::Paused)
}
pub fn is_transient(&self) -> bool {
matches!(self, BoxStatus::Stopping)
}
pub fn can_start(&self) -> bool {
matches!(self, BoxStatus::Configured | BoxStatus::Stopped)
}
pub fn can_stop(&self) -> bool {
matches!(self, BoxStatus::Running | BoxStatus::Paused)
}
pub fn can_remove(&self) -> bool {
matches!(
self,
BoxStatus::Configured | BoxStatus::Stopped | BoxStatus::Unknown
)
}
pub fn can_exec(&self) -> bool {
matches!(
self,
BoxStatus::Configured | BoxStatus::Running | BoxStatus::Stopped
)
}
pub fn can_transition_to(&self, target: BoxStatus) -> bool {
use BoxStatus::*;
matches!(
(self, target),
(Unknown, _) |
(Configured, Running) |
(Configured, Stopped) |
(Configured, Unknown) |
(Running, Stopping) |
(Running, Stopped) |
(Running, Paused) |
(Running, Unknown) |
(Stopping, Stopped) |
(Stopping, Unknown) |
(Stopped, Running) |
(Stopped, Unknown) |
(Paused, Running) |
(Paused, Stopped) |
(Paused, Unknown)
)
}
pub fn as_str(&self) -> &'static str {
match self {
BoxStatus::Unknown => "unknown",
BoxStatus::Configured => "configured",
BoxStatus::Running => "running",
BoxStatus::Stopping => "stopping",
BoxStatus::Stopped => "stopped",
BoxStatus::Paused => "paused",
}
}
}
impl std::str::FromStr for BoxStatus {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"unknown" => Ok(BoxStatus::Unknown),
"configured" => Ok(BoxStatus::Configured),
"starting" => Ok(BoxStatus::Configured),
"running" => Ok(BoxStatus::Running),
"stopping" => Ok(BoxStatus::Stopping),
"stopped" => Ok(BoxStatus::Stopped),
"paused" => Ok(BoxStatus::Paused),
"snapshotting" | "restoring" | "exporting" | "cloning" => Ok(BoxStatus::Stopped),
_ => Err(()),
}
}
}
impl std::fmt::Display for BoxStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoxState {
pub status: BoxStatus,
pub pid: Option<u32>,
pub container_id: Option<ContainerID>,
pub last_updated: DateTime<Utc>,
pub lock_id: Option<LockId>,
#[serde(default)]
pub health_status: HealthStatus,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HealthStatus {
pub state: HealthState,
pub failures: u32,
pub last_check: Option<DateTime<Utc>>,
}
impl HealthStatus {
pub fn new() -> Self {
Self {
state: HealthState::None,
failures: 0,
last_check: None,
}
}
pub fn init(&mut self) {
self.state = HealthState::Starting;
self.failures = 0;
self.last_check = Some(Utc::now());
}
pub fn mark_success(&mut self) {
self.state = HealthState::Healthy;
self.failures = 0;
self.last_check = Some(Utc::now());
}
pub fn mark_failure(&mut self, retries: u32) -> bool {
self.failures += 1;
self.last_check = Some(Utc::now());
if self.failures >= retries {
self.state = HealthState::Unhealthy;
return true;
}
false
}
pub fn clear(&mut self) {
self.state = HealthState::None;
self.failures = 0;
self.last_check = None;
}
}
impl Default for HealthStatus {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthState {
None,
Starting,
Healthy,
Unhealthy,
}
impl BoxState {
pub fn new() -> Self {
Self {
status: BoxStatus::Configured,
pid: None,
container_id: None,
last_updated: Utc::now(),
lock_id: None,
health_status: HealthStatus::new(),
}
}
pub fn set_lock_id(&mut self, lock_id: LockId) {
self.lock_id = Some(lock_id);
self.last_updated = Utc::now();
}
pub fn transition_to(&mut self, new_status: BoxStatus) -> BoxliteResult<()> {
if !self.status.can_transition_to(new_status) {
return Err(BoxliteError::InvalidState(format!(
"Cannot transition from {} to {}",
self.status, new_status
)));
}
self.status = new_status;
self.last_updated = Utc::now();
Ok(())
}
pub fn force_status(&mut self, status: BoxStatus) {
self.status = status;
self.last_updated = Utc::now();
}
pub fn set_status(&mut self, status: BoxStatus) {
self.force_status(status);
}
pub fn set_pid(&mut self, pid: Option<u32>) {
self.pid = pid;
self.last_updated = Utc::now();
}
pub fn mark_stop(&mut self) {
self.status = BoxStatus::Stopped;
self.pid = None;
self.last_updated = Utc::now();
}
pub fn reset_for_reboot(&mut self) {
if self.status.is_active() {
self.status = BoxStatus::Stopped;
}
self.pid = None;
self.last_updated = Utc::now();
}
pub fn init_health_status(&mut self) {
self.health_status.init();
self.last_updated = Utc::now();
}
pub fn mark_health_check_success(&mut self) {
self.health_status.mark_success();
self.last_updated = Utc::now();
}
pub fn mark_health_check_failure(&mut self, retries: u32) -> bool {
let became_unhealthy = self.health_status.mark_failure(retries);
self.last_updated = Utc::now();
became_unhealthy
}
pub fn clear_health_status(&mut self) {
self.health_status.clear();
self.last_updated = Utc::now();
}
}
impl Default for BoxState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_is_active() {
assert!(!BoxStatus::Configured.is_active());
assert!(BoxStatus::Running.is_active());
assert!(!BoxStatus::Stopping.is_active());
assert!(!BoxStatus::Stopped.is_active());
assert!(BoxStatus::Paused.is_active());
assert!(!BoxStatus::Unknown.is_active());
}
#[test]
fn test_status_is_configured() {
assert!(BoxStatus::Configured.is_configured());
assert!(!BoxStatus::Running.is_configured());
assert!(!BoxStatus::Stopped.is_configured());
}
#[test]
fn test_status_is_paused() {
assert!(BoxStatus::Paused.is_paused());
assert!(!BoxStatus::Running.is_paused());
assert!(!BoxStatus::Stopped.is_paused());
}
#[test]
fn test_status_can_start() {
assert!(BoxStatus::Configured.can_start());
assert!(!BoxStatus::Running.can_start());
assert!(!BoxStatus::Stopping.can_start());
assert!(BoxStatus::Stopped.can_start());
assert!(!BoxStatus::Paused.can_start());
assert!(!BoxStatus::Unknown.can_start());
}
#[test]
fn test_status_can_stop() {
assert!(!BoxStatus::Configured.can_stop());
assert!(BoxStatus::Running.can_stop());
assert!(!BoxStatus::Stopping.can_stop());
assert!(!BoxStatus::Stopped.can_stop());
assert!(BoxStatus::Paused.can_stop());
assert!(!BoxStatus::Unknown.can_stop());
}
#[test]
fn test_status_can_exec() {
assert!(BoxStatus::Configured.can_exec());
assert!(BoxStatus::Running.can_exec());
assert!(!BoxStatus::Stopping.can_exec());
assert!(BoxStatus::Stopped.can_exec());
assert!(!BoxStatus::Paused.can_exec());
assert!(!BoxStatus::Unknown.can_exec());
}
#[test]
fn test_valid_transitions() {
assert!(BoxStatus::Configured.can_transition_to(BoxStatus::Running));
assert!(BoxStatus::Configured.can_transition_to(BoxStatus::Stopped));
assert!(!BoxStatus::Configured.can_transition_to(BoxStatus::Stopping));
assert!(BoxStatus::Running.can_transition_to(BoxStatus::Stopping));
assert!(BoxStatus::Running.can_transition_to(BoxStatus::Stopped));
assert!(BoxStatus::Running.can_transition_to(BoxStatus::Paused));
assert!(!BoxStatus::Running.can_transition_to(BoxStatus::Configured));
assert!(BoxStatus::Stopping.can_transition_to(BoxStatus::Stopped));
assert!(!BoxStatus::Stopping.can_transition_to(BoxStatus::Running));
assert!(BoxStatus::Stopped.can_transition_to(BoxStatus::Running));
assert!(!BoxStatus::Stopped.can_transition_to(BoxStatus::Configured));
assert!(!BoxStatus::Stopped.can_transition_to(BoxStatus::Stopping));
assert!(!BoxStatus::Stopped.can_transition_to(BoxStatus::Paused));
assert!(BoxStatus::Paused.can_transition_to(BoxStatus::Running));
assert!(BoxStatus::Paused.can_transition_to(BoxStatus::Stopped));
assert!(!BoxStatus::Paused.can_transition_to(BoxStatus::Configured));
assert!(BoxStatus::Unknown.can_transition_to(BoxStatus::Configured));
assert!(BoxStatus::Unknown.can_transition_to(BoxStatus::Running));
assert!(BoxStatus::Unknown.can_transition_to(BoxStatus::Stopped));
assert!(BoxStatus::Unknown.can_transition_to(BoxStatus::Paused));
}
#[test]
fn test_state_transition() {
let mut state = BoxState::new();
assert_eq!(state.status, BoxStatus::Configured);
assert!(state.transition_to(BoxStatus::Running).is_ok());
assert_eq!(state.status, BoxStatus::Running);
assert!(state.transition_to(BoxStatus::Paused).is_ok());
assert_eq!(state.status, BoxStatus::Paused);
assert!(state.transition_to(BoxStatus::Running).is_ok());
assert_eq!(state.status, BoxStatus::Running);
assert!(state.transition_to(BoxStatus::Stopping).is_ok());
assert!(state.transition_to(BoxStatus::Stopped).is_ok());
assert!(state.transition_to(BoxStatus::Running).is_ok());
}
#[test]
fn test_invalid_transition() {
let mut state = BoxState::new();
state.status = BoxStatus::Configured;
let result = state.transition_to(BoxStatus::Stopping);
assert!(result.is_err());
assert_eq!(state.status, BoxStatus::Configured);
}
#[test]
fn test_reset_for_reboot() {
let mut state = BoxState::new();
state.status = BoxStatus::Running;
state.pid = Some(12345);
state.reset_for_reboot();
assert_eq!(state.status, BoxStatus::Stopped);
assert_eq!(state.pid, None);
}
#[test]
fn test_reset_for_reboot_paused() {
let mut state = BoxState::new();
state.status = BoxStatus::Paused;
state.pid = Some(12345);
state.reset_for_reboot();
assert_eq!(state.status, BoxStatus::Stopped);
assert_eq!(state.pid, None);
}
#[test]
fn test_reset_for_reboot_stopped() {
let mut state = BoxState::new();
state.status = BoxStatus::Stopped;
state.reset_for_reboot();
assert_eq!(state.status, BoxStatus::Stopped);
}
#[test]
fn test_reset_for_reboot_configured() {
let mut state = BoxState::new();
assert_eq!(state.status, BoxStatus::Configured);
state.reset_for_reboot();
assert_eq!(state.status, BoxStatus::Configured);
}
#[test]
fn test_status_as_str() {
assert_eq!(BoxStatus::Unknown.as_str(), "unknown");
assert_eq!(BoxStatus::Configured.as_str(), "configured");
assert_eq!(BoxStatus::Running.as_str(), "running");
assert_eq!(BoxStatus::Stopping.as_str(), "stopping");
assert_eq!(BoxStatus::Stopped.as_str(), "stopped");
assert_eq!(BoxStatus::Paused.as_str(), "paused");
}
#[test]
fn test_status_from_str() {
assert_eq!("unknown".parse(), Ok(BoxStatus::Unknown));
assert_eq!("configured".parse(), Ok(BoxStatus::Configured));
assert_eq!("starting".parse(), Ok(BoxStatus::Configured));
assert_eq!("running".parse(), Ok(BoxStatus::Running));
assert_eq!("stopping".parse(), Ok(BoxStatus::Stopping));
assert_eq!("stopped".parse(), Ok(BoxStatus::Stopped));
assert_eq!("paused".parse(), Ok(BoxStatus::Paused));
assert_eq!("snapshotting".parse(), Ok(BoxStatus::Stopped));
assert_eq!("restoring".parse(), Ok(BoxStatus::Stopped));
assert_eq!("exporting".parse(), Ok(BoxStatus::Stopped));
assert_eq!("cloning".parse(), Ok(BoxStatus::Stopped));
assert!("invalid".parse::<BoxStatus>().is_err());
}
#[test]
fn test_health_status_new() {
let status = HealthStatus::new();
assert_eq!(status.state, HealthState::None);
assert_eq!(status.failures, 0);
assert!(status.last_check.is_none());
}
#[test]
fn test_health_status_init() {
let mut status = HealthStatus::new();
status.init();
assert_eq!(status.state, HealthState::Starting);
assert_eq!(status.failures, 0);
assert!(status.last_check.is_some());
let elapsed = Utc::now() - status.last_check.unwrap();
assert!(elapsed.num_seconds() <= 1);
}
#[test]
fn test_health_status_mark_success() {
let mut status = HealthStatus::new();
status.init();
status.mark_success();
assert_eq!(status.state, HealthState::Healthy);
assert_eq!(status.failures, 0);
assert!(status.last_check.is_some());
}
#[test]
fn test_health_status_mark_failure_within_retries() {
let mut status = HealthStatus::new();
status.init();
status.mark_success();
let became_unhealthy = status.mark_failure(3);
assert!(!became_unhealthy);
assert_eq!(status.state, HealthState::Healthy); assert_eq!(status.failures, 1);
}
#[test]
fn test_health_status_mark_failure_at_threshold() {
let mut status = HealthStatus::new();
status.mark_success();
assert!(!status.mark_failure(3)); assert_eq!(status.failures, 1);
assert!(!status.mark_failure(3)); assert_eq!(status.failures, 2);
let became_unhealthy = status.mark_failure(3); assert!(became_unhealthy);
assert_eq!(status.state, HealthState::Unhealthy);
assert_eq!(status.failures, 3);
}
#[test]
fn test_health_status_mark_failure_exceeds_threshold() {
let mut status = HealthStatus::new();
status.mark_success();
status.mark_failure(3); status.mark_failure(3); status.mark_failure(3); status.mark_failure(3);
assert_eq!(status.state, HealthState::Unhealthy);
assert_eq!(status.failures, 4);
}
#[test]
fn test_health_status_zero_retries() {
let mut status = HealthStatus::new();
status.init();
let became_unhealthy = status.mark_failure(0);
assert!(became_unhealthy);
assert_eq!(status.state, HealthState::Unhealthy);
assert_eq!(status.failures, 1);
}
#[test]
fn test_health_status_one_retry() {
let mut status = HealthStatus::new();
status.mark_success();
let became_unhealthy = status.mark_failure(1);
assert!(became_unhealthy);
assert_eq!(status.state, HealthState::Unhealthy);
assert_eq!(status.failures, 1);
}
#[test]
fn test_health_status_clear() {
let mut status = HealthStatus::new();
status.init();
status.mark_success();
status.clear();
assert_eq!(status.state, HealthState::None);
assert_eq!(status.failures, 0);
assert!(status.last_check.is_none());
}
#[test]
fn test_health_status_recovery_after_failure() {
let mut status = HealthStatus::new();
status.mark_success();
status.mark_failure(3);
status.mark_failure(3);
assert_eq!(status.failures, 2);
assert_eq!(status.state, HealthState::Healthy);
status.mark_success();
assert_eq!(status.failures, 0);
assert_eq!(status.state, HealthState::Healthy);
status.mark_failure(3);
assert_eq!(status.failures, 1);
assert_eq!(status.state, HealthState::Healthy);
}
#[test]
fn test_health_status_full_lifecycle() {
let mut status = HealthStatus::new();
assert_eq!(status.state, HealthState::None);
status.init();
assert_eq!(status.state, HealthState::Starting);
status.mark_success();
assert_eq!(status.state, HealthState::Healthy);
status.mark_failure(3);
assert_eq!(status.state, HealthState::Healthy);
assert_eq!(status.failures, 1);
status.mark_failure(3);
status.mark_failure(3);
assert_eq!(status.state, HealthState::Unhealthy);
assert_eq!(status.failures, 3);
status.clear();
assert_eq!(status.state, HealthState::None);
assert_eq!(status.failures, 0);
}
#[test]
fn test_health_status_default() {
let status = HealthStatus::default();
assert_eq!(status.state, HealthState::None);
assert_eq!(status.failures, 0);
assert!(status.last_check.is_none());
}
#[test]
fn test_health_state_equality() {
let status1 = HealthStatus::new();
let status2 = HealthStatus::new();
assert_eq!(status1, status2);
let mut status3 = HealthStatus::new();
let mut status4 = HealthStatus::new();
status3.init();
status4.mark_success();
assert_ne!(status3, status4);
assert_eq!(status3.state, HealthState::Starting);
assert_eq!(status4.state, HealthState::Healthy);
}
#[test]
fn test_box_state_init_health_status() {
let mut state = BoxState::new();
state.init_health_status();
assert_eq!(state.health_status.state, HealthState::Starting);
assert_eq!(state.health_status.failures, 0);
assert!(state.health_status.last_check.is_some());
assert!(state.last_updated > Utc::now() - chrono::Duration::seconds(1));
}
#[test]
fn test_box_state_mark_health_check_success() {
let mut state = BoxState::new();
state.init_health_status();
state.mark_health_check_success();
assert_eq!(state.health_status.state, HealthState::Healthy);
assert_eq!(state.health_status.failures, 0);
}
#[test]
fn test_box_state_mark_health_check_failure() {
let mut state = BoxState::new();
state.init_health_status();
state.mark_health_check_success();
let should_mark_unhealthy = state.mark_health_check_failure(3);
assert!(!should_mark_unhealthy);
assert_eq!(state.health_status.failures, 1);
state.mark_health_check_failure(3);
let should_mark_unhealthy = state.mark_health_check_failure(3);
assert!(should_mark_unhealthy);
assert_eq!(state.health_status.state, HealthState::Unhealthy);
assert_eq!(state.health_status.failures, 3);
}
#[test]
fn test_box_state_clear_health_status() {
let mut state = BoxState::new();
state.init_health_status();
state.mark_health_check_success();
state.clear_health_status();
assert_eq!(state.health_status.state, HealthState::None);
assert_eq!(state.health_status.failures, 0);
assert!(state.health_status.last_check.is_none());
}
#[test]
fn test_box_state_new_has_default_health_status() {
let state = BoxState::new();
assert_eq!(state.health_status.state, HealthState::None);
assert_eq!(state.health_status.failures, 0);
}
#[test]
fn deserialize_box_state_without_health_status() {
let old_json = r#"{
"status": "configured",
"pid": null,
"container_id": null,
"last_updated": "2026-02-26T00:00:00Z",
"lock_id": null
}"#;
let state: BoxState = serde_json::from_str(old_json).unwrap();
assert_eq!(state.status, BoxStatus::Configured);
assert_eq!(state.health_status.state, HealthState::None);
assert_eq!(state.health_status.failures, 0);
assert!(state.health_status.last_check.is_none());
}
}