feat(shipote): throughput card + rate-limit + snapshot incremental (fase Q)
- shipote-shell Flow channels card extiende con bytes_total + bytes/s por socket. Lookup helper evita borrows en closures. - DiscernPolicy.max_bytes_per_sec: splitter task hace sleep proporcional al tamaño de chunk tras cada broadcast. Token-bucket simple v1. - WorkspaceManager.dirty: AtomicBool. mark_dirty() en mutaciones que afectan al snapshot. save_snapshot skip si clean y path existe. restore_snapshot resetea dirty=false (hidratación no es mutation). 85 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 26, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,10 @@ pub enum LogStream {
|
||||
pub struct WorkspaceManager {
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
incarnator: Arc<Incarnator>,
|
||||
/// True si hubo alguna mutación desde el último `save_snapshot`.
|
||||
/// `save_snapshot` skip si false (snapshot incremental — evita
|
||||
/// re-serialize cuando nada cambió, ej. SIGTERM tras un período idle).
|
||||
dirty: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
@@ -238,9 +242,23 @@ impl WorkspaceManager {
|
||||
pending_pipeline_restarts: Vec::new(),
|
||||
})),
|
||||
incarnator: Arc::new(Incarnator::new(cfg)),
|
||||
dirty: std::sync::atomic::AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Marca el manager como dirty. Cualquier mutación que afecta al
|
||||
/// snapshot debería llamar esto.
|
||||
#[inline]
|
||||
fn mark_dirty(&self) {
|
||||
self.dirty.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// True si hubo cambios desde el último `save_snapshot`. Útil para
|
||||
/// chequeos cooperativos (ej. monitoring que pollea cada N).
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Registra un supervisor para un pipeline con `restart_on_failure=true`.
|
||||
/// El daemon llama esto tras `run_pipeline` para que `reap_dead` agregue
|
||||
/// el pipeline a la cola de restart cuando algún command falle.
|
||||
@@ -267,6 +285,8 @@ impl WorkspaceManager {
|
||||
current_backoff_ms: initial_backoff,
|
||||
},
|
||||
);
|
||||
drop(g);
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Variante que preserva backoff/count del supervisor anterior (para
|
||||
@@ -480,6 +500,7 @@ impl WorkspaceManager {
|
||||
/// Guarda (o reemplaza) un PipelineSpec bajo `name`.
|
||||
pub async fn save_pipeline(&self, name: String, spec: PipelineSpec) {
|
||||
self.inner.lock().await.saved_pipelines.insert(name, spec);
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Devuelve los nombres de los pipelines guardados.
|
||||
@@ -497,7 +518,11 @@ impl WorkspaceManager {
|
||||
|
||||
/// Elimina un saved pipeline.
|
||||
pub async fn drop_saved_pipeline(&self, name: &str) -> bool {
|
||||
self.inner.lock().await.saved_pipelines.remove(name).is_some()
|
||||
let existed = self.inner.lock().await.saved_pipelines.remove(name).is_some();
|
||||
if existed {
|
||||
self.mark_dirty();
|
||||
}
|
||||
existed
|
||||
}
|
||||
|
||||
/// Label del workspace, si existe.
|
||||
@@ -648,6 +673,7 @@ impl WorkspaceManager {
|
||||
stats_history: std::collections::VecDeque::with_capacity(STATS_HISTORY_CAP),
|
||||
};
|
||||
self.inner.lock().await.workspaces.insert(id, state);
|
||||
self.mark_dirty();
|
||||
info!(%id, ?ttl, "workspace created");
|
||||
|
||||
// Si tiene TTL, programar auto-stop. El task captura un weak ref
|
||||
@@ -698,6 +724,7 @@ impl WorkspaceManager {
|
||||
// También limpiamos flow_channels del workspace si los hubiera —
|
||||
// por workspace lo retenemos por pipeline, no por workspace.
|
||||
drop(g);
|
||||
self.mark_dirty();
|
||||
|
||||
// 1) SIGTERM (o SIGKILL si grace=0) a todos vivos.
|
||||
let initial_signal = if grace.is_zero() { Signal::SIGKILL } else { Signal::SIGTERM };
|
||||
|
||||
Reference in New Issue
Block a user