Files
brahman/crates/runtime/sandokan-lifecycle/src/backoff.rs
T
sergio 545dd59c72 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>
2026-05-20 00:32:52 +00:00

69 lines
2.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Backoff exponencial con tope.
use std::time::Duration;
/// Calculador de backoff exponencial. Cada `next_delay()` devuelve el
/// delay actual y luego lo duplica, hasta saturar en `max`.
#[derive(Debug, Clone)]
pub struct Backoff {
base: Duration,
max: Duration,
current: Duration,
}
impl Backoff {
/// Crea un backoff que arranca en `base` y satura en `max`.
/// Si `base > max`, `base` se clampa a `max`.
pub fn new(base: Duration, max: Duration) -> Self {
let base = base.min(max);
Self { base, max, current: base }
}
/// Devuelve el delay actual y escala el siguiente (×2, capeado a `max`).
pub fn next_delay(&mut self) -> Duration {
let delay = self.current;
self.current = (self.current * 2).min(self.max);
delay
}
/// Vuelve al delay base (tras un éxito).
pub fn reset(&mut self) {
self.current = self.base;
}
/// Delay que devolvería el próximo `next_delay()` sin consumirlo.
pub fn peek(&self) -> Duration {
self.current
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escalates_then_caps() {
let mut b = Backoff::new(Duration::from_millis(100), Duration::from_millis(800));
assert_eq!(b.next_delay(), Duration::from_millis(100));
assert_eq!(b.next_delay(), Duration::from_millis(200));
assert_eq!(b.next_delay(), Duration::from_millis(400));
assert_eq!(b.next_delay(), Duration::from_millis(800));
assert_eq!(b.next_delay(), Duration::from_millis(800)); // capeado
}
#[test]
fn reset_returns_to_base() {
let mut b = Backoff::new(Duration::from_millis(100), Duration::from_secs(30));
b.next_delay();
b.next_delay();
b.reset();
assert_eq!(b.next_delay(), Duration::from_millis(100));
}
#[test]
fn base_clamped_to_max() {
let mut b = Backoff::new(Duration::from_secs(10), Duration::from_secs(1));
assert_eq!(b.next_delay(), Duration::from_secs(1));
}
}