feat(shipote): pipeline backoff + quota card + logs follow (fase M)
- PipelineSpec.restart_backoff_ms + restart_max_backoff_ms + restart_max: backoff exponencial entre relaunches (anti-thrash). take_pending_restarts aplica restart_max (0 = infinito); excedido = supervisor descartado con warning. Daemon hace tokio::sleep(backoff) antes del relaunch y escala current_backoff x2 hasta el cap. - shipote-shell card "Quota breaches": probe extiende con WorkspaceQuota por workspace. Color rojo si hay breaches, verde si no. - shipote logs --follow: poll cada 200ms al daemon, imprime suffix nuevo hasta que el comando termine. Sin cambios al protocolo. Best-effort: si el ring rota más rápido que el poll, se pierden bytes. 83 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8, shipote-core 24, shipote-discern 5, yahweh-provider-fs 3). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,9 @@ enum Cmd {
|
||||
/// Stream a leer: stdout | stderr | both.
|
||||
#[arg(long, default_value = "both")]
|
||||
stream: String,
|
||||
/// Seguir el log en vivo (poll cada 200ms hasta que el comando termine).
|
||||
#[arg(short = 'f', long)]
|
||||
follow: bool,
|
||||
},
|
||||
|
||||
/// Pipeline DAG con flujo tipado.
|
||||
@@ -457,28 +460,82 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
Cmd::Logs { workspace, command, tail, stream: which_stream } => {
|
||||
Cmd::Logs { workspace, command, tail, stream: which_stream, follow } => {
|
||||
let ws = parse_ws_id(&workspace)?;
|
||||
let cmd_id = Ulid::from_string(&command).map_err(|e| anyhow!("invalid command id: {e}"))?;
|
||||
let resp = round_trip(
|
||||
&mut stream,
|
||||
Request::CommandLogs {
|
||||
workspace: ws,
|
||||
command: cmd_id,
|
||||
tail_bytes: tail,
|
||||
stream: which_stream,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match resp {
|
||||
Response::CommandLogs { bytes } => {
|
||||
// stdout raw, sin decoding — el log puede tener bytes binarios.
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().write_all(&bytes);
|
||||
if !follow {
|
||||
let resp = round_trip(
|
||||
&mut stream,
|
||||
Request::CommandLogs {
|
||||
workspace: ws,
|
||||
command: cmd_id,
|
||||
tail_bytes: tail,
|
||||
stream: which_stream,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match resp {
|
||||
Response::CommandLogs { bytes } => {
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().write_all(&bytes);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
Response::Error { message } => return Err(anyhow!(message)),
|
||||
other => print_unexpected(&other),
|
||||
}
|
||||
} else {
|
||||
// Follow mode: poll cada 200ms. Mantenemos el último buffer
|
||||
// visto; cada round imprimimos el delta (suffix nuevo).
|
||||
// Limitación: si el ring rota más rápido que el poll, perdemos
|
||||
// bytes — pero el comportamiento es "best effort".
|
||||
use std::io::Write;
|
||||
let mut prev: Vec<u8> = Vec::new();
|
||||
loop {
|
||||
let resp = round_trip(
|
||||
&mut stream,
|
||||
Request::CommandLogs {
|
||||
workspace: ws,
|
||||
command: cmd_id,
|
||||
tail_bytes: 0,
|
||||
stream: which_stream.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let bytes = match resp {
|
||||
Response::CommandLogs { bytes } => bytes,
|
||||
Response::Error { message } => return Err(anyhow!(message)),
|
||||
other => {
|
||||
print_unexpected(&other);
|
||||
break;
|
||||
}
|
||||
};
|
||||
// Imprimir suffix nuevo si bytes es extension de prev.
|
||||
if bytes.len() >= prev.len() && bytes[..prev.len()] == prev[..] {
|
||||
let _ = std::io::stdout().write_all(&bytes[prev.len()..]);
|
||||
} else {
|
||||
// Ring rotó — reset y print todo.
|
||||
let _ = std::io::stdout().write_all(&bytes);
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
prev = bytes;
|
||||
|
||||
// Si el comando terminó, salir tras un último read.
|
||||
let list_resp = round_trip(
|
||||
&mut stream,
|
||||
Request::CommandList { workspace: ws },
|
||||
)
|
||||
.await?;
|
||||
let mut still_alive = false;
|
||||
if let Response::CommandList { items } = list_resp {
|
||||
if let Some(c) = items.iter().find(|c| c.id == cmd_id) {
|
||||
still_alive = c.alive;
|
||||
}
|
||||
}
|
||||
if !still_alive {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
Response::Error { message } => return Err(anyhow!(message)),
|
||||
other => print_unexpected(&other),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user