feat(sandokan-lifecycle): A4 — primitivas de lifecycle agnósticas
Nuevo crate runtime/sandokan-lifecycle: lógica pura reutilizable por cualquier supervisor de procesos (shuma, matilda Ghost, charka-shadow, mirada). Sin syscalls, sin proceso, sin UI. Módulos: - backoff — Backoff exponencial con tope - ttl — Ttl anclado a Instant - quota — ResourceQuota + check_quota + Breach + QuotaAction - restart — RestartPolicy + RestartTracker (conteo + backoff) - state — LifecycleState (Pending/Running/Exited/Failed/Killed) 15 tests verdes. cargo check --workspace verde. Variante segura de A4: se crea la library limpia sin tocar shuma-core (módulo maduro). La migración de WorkspaceManager a consumir estas primitivas queda registrada como A4.2 (refactor diferido, no urgente). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
//! Máquina de estados del ciclo de vida de una entidad supervisada.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Estado de una entidad supervisada (proceso, workspace, sandbox, ...).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum LifecycleState {
|
||||
/// Creada, aún no arrancó.
|
||||
Pending,
|
||||
/// En ejecución.
|
||||
Running,
|
||||
/// Salió por sí misma con un código de salida.
|
||||
Exited { code: i32 },
|
||||
/// Falló (no llegó a correr, o crasheó de forma no capturable).
|
||||
Failed { reason: String },
|
||||
/// Terminada por el supervisor (SIGKILL / quota / drain).
|
||||
Killed,
|
||||
}
|
||||
|
||||
impl LifecycleState {
|
||||
/// `true` si el estado es terminal (no habrá más transiciones sin
|
||||
/// un restart explícito).
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
LifecycleState::Exited { .. }
|
||||
| LifecycleState::Failed { .. }
|
||||
| LifecycleState::Killed
|
||||
)
|
||||
}
|
||||
|
||||
/// `true` si el estado terminal cuenta como fallo (dispara restart
|
||||
/// si la política lo permite). `Exited { code: 0 }` NO es fallo.
|
||||
pub fn is_failure(&self) -> bool {
|
||||
match self {
|
||||
LifecycleState::Exited { code } => *code != 0,
|
||||
LifecycleState::Failed { .. } => true,
|
||||
LifecycleState::Killed => false, // kill deliberado, no fallo
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn terminal_detection() {
|
||||
assert!(!LifecycleState::Pending.is_terminal());
|
||||
assert!(!LifecycleState::Running.is_terminal());
|
||||
assert!(LifecycleState::Exited { code: 0 }.is_terminal());
|
||||
assert!(LifecycleState::Killed.is_terminal());
|
||||
assert!(LifecycleState::Failed { reason: "x".into() }.is_terminal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_semantics() {
|
||||
assert!(!LifecycleState::Exited { code: 0 }.is_failure());
|
||||
assert!(LifecycleState::Exited { code: 1 }.is_failure());
|
||||
assert!(LifecycleState::Failed { reason: "x".into() }.is_failure());
|
||||
assert!(!LifecycleState::Killed.is_failure());
|
||||
assert!(!LifecycleState::Running.is_failure());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user