feat(shipote): collision detection + stats history server-side (fase O)
- Flow socket names usan pipeline_id full (ULID 26 chars) + edge_idx. Cero colisiones entre pipelines (ULID es único global). Fallback con suffix -N si el path existe (cap 1000 retries). - WorkspaceState.stats_history (VecDeque cap 64) — workspace_stats appendea cada call. API workspace_stats_history(id, tail). Protocol WorkspaceStatsHistory. Shell pide history al primer probe → sparkline hidratada al boot, sobrevive restart del shell. 84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 25, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,14 @@ pub struct WorkspaceState {
|
||||
/// Última muestra de `(wall_instant, cpu_usec)` usada para calcular
|
||||
/// `cpu_percent` en la próxima medición. None hasta el primer measure.
|
||||
pub last_cpu_sample: Option<(Instant, u64)>,
|
||||
/// Ring buffer de samples recientes para sparklines. Se popula cada
|
||||
/// vez que `workspace_stats` se llama (típicamente desde el shell).
|
||||
/// Cap 64 samples = ~2 minutos a 2s/sample.
|
||||
pub stats_history: std::collections::VecDeque<stats::WorkspaceStats>,
|
||||
}
|
||||
|
||||
const STATS_HISTORY_CAP: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandState {
|
||||
pub id: Ulid,
|
||||
@@ -562,9 +568,28 @@ impl WorkspaceManager {
|
||||
}
|
||||
ws.last_cpu_sample = Some((now, cpu_now));
|
||||
}
|
||||
// Append a history (ring buffer cap).
|
||||
if ws.stats_history.len() >= STATS_HISTORY_CAP {
|
||||
ws.stats_history.pop_front();
|
||||
}
|
||||
ws.stats_history.push_back(s.clone());
|
||||
Some(s)
|
||||
}
|
||||
|
||||
/// Retorna las últimas N samples de stats (servidas desde el ring
|
||||
/// buffer interno). Sobrevive restart del shell.
|
||||
pub async fn workspace_stats_history(
|
||||
&self,
|
||||
id: WorkspaceId,
|
||||
tail: usize,
|
||||
) -> Option<Vec<stats::WorkspaceStats>> {
|
||||
let g = self.inner.lock().await;
|
||||
let ws = g.workspaces.get(&id)?;
|
||||
let take = if tail == 0 { ws.stats_history.len() } else { tail };
|
||||
let skip = ws.stats_history.len().saturating_sub(take);
|
||||
Some(ws.stats_history.iter().skip(skip).cloned().collect())
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
self: &Arc<Self>,
|
||||
spec: WorkspaceSpec,
|
||||
@@ -604,6 +629,7 @@ impl WorkspaceManager {
|
||||
commands: HashMap::new(),
|
||||
started: Instant::now(),
|
||||
last_cpu_sample: None,
|
||||
stats_history: std::collections::VecDeque::with_capacity(STATS_HISTORY_CAP),
|
||||
};
|
||||
self.inner.lock().await.workspaces.insert(id, state);
|
||||
info!(%id, ?ttl, "workspace created");
|
||||
@@ -1364,6 +1390,38 @@ mod tests {
|
||||
panic!("quota enforce kill never triggered");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_stats_history_accumulates() {
|
||||
let mgr = Arc::new(WorkspaceManager::new(IncarnatorConfig::default()));
|
||||
let spec = WorkspaceSpec {
|
||||
label: "history".into(),
|
||||
soma: Default::default(),
|
||||
permissions: Default::default(),
|
||||
ttl: None,
|
||||
flow_dirs: vec![],
|
||||
on_exit: shipote_card::ExitPolicy::Reap,
|
||||
quota_enforce: Default::default(),
|
||||
};
|
||||
let (id, _) = mgr.create(spec).await.unwrap();
|
||||
// Necesitamos al menos un comando vivo para que `measure` no
|
||||
// retorne source=none (que igual se appendea, pero con stats vacíos).
|
||||
let _ = mgr
|
||||
.run(id, "/bin/sleep".into(), vec!["5".into()], vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
// Llamar stats 5 veces.
|
||||
for _ in 0..5 {
|
||||
let _ = mgr.workspace_stats(id).await;
|
||||
}
|
||||
let history = mgr.workspace_stats_history(id, 0).await.unwrap();
|
||||
assert_eq!(history.len(), 5, "history debería tener 5 samples");
|
||||
// tail=3 retorna los últimos 3.
|
||||
let tail3 = mgr.workspace_stats_history(id, 3).await.unwrap();
|
||||
assert_eq!(tail3.len(), 3);
|
||||
// Cleanup.
|
||||
let _ = mgr.stop_with_grace(id, std::time::Duration::ZERO).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_true_in_workspace() {
|
||||
let mgr = Arc::new(WorkspaceManager::new(IncarnatorConfig::default()));
|
||||
|
||||
Reference in New Issue
Block a user