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,115 @@
|
||||
//! Política de restart con conteo + backoff exponencial.
|
||||
|
||||
use crate::backoff::Backoff;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Política declarativa de restart.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RestartPolicy {
|
||||
/// Si reintentar tras una salida con fallo.
|
||||
pub on_failure: bool,
|
||||
/// Máximo de restarts. `0` = infinito.
|
||||
pub max_restarts: u32,
|
||||
}
|
||||
|
||||
impl Default for RestartPolicy {
|
||||
fn default() -> Self {
|
||||
Self { on_failure: false, max_restarts: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Estado mutable de restart de una entidad supervisada. Combina la
|
||||
/// política con un `Backoff` y el conteo de intentos consumidos.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RestartTracker {
|
||||
policy: RestartPolicy,
|
||||
backoff: Backoff,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl RestartTracker {
|
||||
pub fn new(policy: RestartPolicy, backoff: Backoff) -> Self {
|
||||
Self { policy, backoff, count: 0 }
|
||||
}
|
||||
|
||||
/// Registra un fallo. Devuelve `Some(delay)` con el backoff a esperar
|
||||
/// antes del próximo intento, o `None` si no se debe reintentar
|
||||
/// (política desactivada o `max_restarts` agotado).
|
||||
pub fn on_failure(&mut self) -> Option<Duration> {
|
||||
if !self.policy.on_failure {
|
||||
return None;
|
||||
}
|
||||
if self.policy.max_restarts != 0 && self.count >= self.policy.max_restarts {
|
||||
return None;
|
||||
}
|
||||
self.count += 1;
|
||||
Some(self.backoff.next_delay())
|
||||
}
|
||||
|
||||
/// Registra un éxito: resetea conteo y backoff.
|
||||
pub fn on_success(&mut self) {
|
||||
self.count = 0;
|
||||
self.backoff.reset();
|
||||
}
|
||||
|
||||
/// Cantidad de restarts consumidos.
|
||||
pub fn count(&self) -> u32 {
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn backoff() -> Backoff {
|
||||
Backoff::new(Duration::from_millis(100), Duration::from_secs(30))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_policy_never_restarts() {
|
||||
let mut t = RestartTracker::new(
|
||||
RestartPolicy { on_failure: false, max_restarts: 0 },
|
||||
backoff(),
|
||||
);
|
||||
assert!(t.on_failure().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_max_restarts() {
|
||||
let mut t = RestartTracker::new(
|
||||
RestartPolicy { on_failure: true, max_restarts: 3 },
|
||||
backoff(),
|
||||
);
|
||||
assert!(t.on_failure().is_some());
|
||||
assert!(t.on_failure().is_some());
|
||||
assert!(t.on_failure().is_some());
|
||||
assert!(t.on_failure().is_none()); // 4º agota la cuota
|
||||
assert_eq!(t.count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infinite_when_max_zero() {
|
||||
let mut t = RestartTracker::new(
|
||||
RestartPolicy { on_failure: true, max_restarts: 0 },
|
||||
backoff(),
|
||||
);
|
||||
for _ in 0..100 {
|
||||
assert!(t.on_failure().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backoff_escalates_then_success_resets() {
|
||||
let mut t = RestartTracker::new(
|
||||
RestartPolicy { on_failure: true, max_restarts: 0 },
|
||||
backoff(),
|
||||
);
|
||||
assert_eq!(t.on_failure(), Some(Duration::from_millis(100)));
|
||||
assert_eq!(t.on_failure(), Some(Duration::from_millis(200)));
|
||||
t.on_success();
|
||||
assert_eq!(t.count(), 0);
|
||||
assert_eq!(t.on_failure(), Some(Duration::from_millis(100)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user