remove hive and agent apis nothing calls: build-log routes, /api/compact, host.sock list, mcp.sock wake
This commit is contained in:
parent
cbfd7b2174
commit
36c7781245
1 changed files with 115 additions and 801 deletions
|
|
@ -8,10 +8,8 @@ use std::sync::{Arc, Mutex, OnceLock};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::db::Migration;
|
||||
|
||||
|
|
@ -103,65 +101,23 @@ pub enum BuildStatus {
|
|||
Fail,
|
||||
}
|
||||
|
||||
/// Header-only row returned by `list_recent_for_agent`. Carries the
|
||||
/// metadata the dashboard's agent-card chip needs (status + age +
|
||||
/// id-to-open) without the multi-MB stdout/stderr payload.
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuildLogHeader {
|
||||
pub id: i64,
|
||||
/// A stored row with its captured text, as read back by tests.
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildLogFull {
|
||||
pub agent: String,
|
||||
pub kind: String,
|
||||
pub cmdline: String,
|
||||
pub started_at: i64,
|
||||
pub finished_at: Option<i64>,
|
||||
pub status: Option<String>,
|
||||
/// Elapsed seconds from `started_at` to `finished_at`. `None` while
|
||||
/// the build is still in progress.
|
||||
pub runtime_secs: Option<i64>,
|
||||
}
|
||||
|
||||
/// Full row with stdout/stderr text inlined. Returned by `get_full`,
|
||||
/// backs the side-panel viewer's payload.
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuildLogFull {
|
||||
#[serde(flatten)]
|
||||
pub header: BuildLogHeader,
|
||||
pub finished_at: Option<i64>,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
/// Incremental text returned by `get_progress`. Carries only the new
|
||||
/// bytes since the caller's last cursor positions so the SSE stream
|
||||
/// handler can send deltas without re-transmitting the full log.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildLogProgress {
|
||||
/// New stdout bytes beyond `stdout_cursor`.
|
||||
pub stdout_append: String,
|
||||
/// New stderr bytes beyond `stderr_cursor`.
|
||||
pub stderr_append: String,
|
||||
/// `Some(unix_ts)` once the build is finished.
|
||||
pub finished_at: Option<i64>,
|
||||
/// Terminal status string (`"ok"` / `"fail"`) once finished.
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
/// Channel capacity for per-build append notifications. 64 slots is
|
||||
/// plenty — the consumer reads fast relative to line-append rate and
|
||||
/// any lag means "read now, you have new content" rather than a lost
|
||||
/// data line.
|
||||
const NOTIFY_CAP: usize = 64;
|
||||
|
||||
/// Sqlite-backed build-log store. `Arc<BuildLogs>`-friendly: all
|
||||
/// methods take `&self`, internal `Mutex<Connection>` serializes
|
||||
/// access.
|
||||
pub struct BuildLogs {
|
||||
conn: Mutex<Connection>,
|
||||
/// Broadcast channel that fires with the `id` of the row that just
|
||||
/// had a line appended or was finished. The SSE stream handler
|
||||
/// subscribes once per open panel and drives delta reads from this.
|
||||
/// `send()` is non-async and silently drops frames when there are
|
||||
/// no subscribers — safe to call from sync append/finish paths.
|
||||
notify_tx: broadcast::Sender<i64>,
|
||||
}
|
||||
|
||||
impl BuildLogs {
|
||||
|
|
@ -171,10 +127,8 @@ impl BuildLogs {
|
|||
conn.execute_batch(SCHEMA)
|
||||
.context("apply build_logs schema")?;
|
||||
crate::db::apply_versioned_migrations(&conn, "build_logs", MIGRATIONS)?;
|
||||
let (notify_tx, _) = broadcast::channel(NOTIFY_CAP);
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
notify_tx,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -202,13 +156,6 @@ impl BuildLogs {
|
|||
Ok(u64::try_from(closed).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Subscribe to per-build append/finish notifications. Each emitted
|
||||
/// value is the `id` of the row that changed. The SSE stream handler
|
||||
/// calls this once and filters for its target id.
|
||||
pub fn subscribe_notifications(&self) -> broadcast::Receiver<i64> {
|
||||
self.notify_tx.subscribe()
|
||||
}
|
||||
|
||||
/// Open a row for a new build attempt. Returns the assigned id
|
||||
/// — the caller threads it through `append_stdout` / `append_stderr`
|
||||
/// while the child runs and into `finish` once it exits.
|
||||
|
|
@ -282,14 +229,6 @@ impl BuildLogs {
|
|||
"build_logs: append failed (dropping line)"
|
||||
);
|
||||
}
|
||||
drop(conn);
|
||||
// Notify SSE stream subscribers — non-blocking, no-op when no
|
||||
// subscribers are watching (e.g. no panel is open). Lagged
|
||||
// receivers (channel full) automatically drop frames; the SSE
|
||||
// handler re-reads the full delta on the next notification it
|
||||
// does receive, so no content is lost, only an intermediate
|
||||
// wake-up is coalesced.
|
||||
let _ = self.notify_tx.send(id);
|
||||
}
|
||||
|
||||
/// Finalize a build attempt. Sets `finished_at` to now and
|
||||
|
|
@ -307,133 +246,30 @@ impl BuildLogs {
|
|||
"build_logs: finish failed"
|
||||
);
|
||||
}
|
||||
drop(conn);
|
||||
// Final notification so the SSE stream handler sees the
|
||||
// finished_at and status, closes the connection cleanly.
|
||||
let _ = self.notify_tx.send(id);
|
||||
}
|
||||
|
||||
/// Return incremental log content beyond the given byte cursors.
|
||||
/// Used by the SSE stream handler to compute deltas between polls.
|
||||
///
|
||||
/// `stdout_cursor` / `stderr_cursor` are byte offsets into the
|
||||
/// stored `stdout` / `stderr` columns from the previous read.
|
||||
/// Slicing is safe because cursors are always derived from prior
|
||||
/// `String::len()` values (valid UTF-8 boundaries).
|
||||
///
|
||||
/// Returns `None` when the row no longer exists (vacuum reap during
|
||||
/// a long-open panel).
|
||||
pub fn get_progress(
|
||||
&self,
|
||||
id: i64,
|
||||
stdout_cursor: usize,
|
||||
stderr_cursor: usize,
|
||||
) -> Result<Option<BuildLogProgress>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT stdout, stderr, finished_at, status \
|
||||
FROM build_logs WHERE id = ?1",
|
||||
)?;
|
||||
let row = stmt
|
||||
.query_row(params![id], |r| {
|
||||
Ok((
|
||||
r.get::<_, String>(0)?,
|
||||
r.get::<_, String>(1)?,
|
||||
r.get::<_, Option<i64>>(2)?,
|
||||
r.get::<_, Option<String>>(3)?,
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
match row {
|
||||
None => Ok(None),
|
||||
Some((stdout, stderr, finished_at, status)) => {
|
||||
let stdout_append = stdout.get(stdout_cursor..).unwrap_or("").to_string();
|
||||
let stderr_append = stderr.get(stderr_cursor..).unwrap_or("").to_string();
|
||||
Ok(Some(BuildLogProgress {
|
||||
stdout_append,
|
||||
stderr_append,
|
||||
finished_at,
|
||||
status,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the most recent `limit` rows for `agent`, newest first.
|
||||
/// Headers only (no stdout/stderr blobs) — keeps `/api/state`
|
||||
/// payloads light. Limit is hard-clamped to 50 to bound worst-case
|
||||
/// payload regardless of caller input.
|
||||
pub fn list_recent_for_agent(&self, agent: &str, limit: usize) -> Result<Vec<BuildLogHeader>> {
|
||||
let limit = limit.min(50);
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, agent, kind, cmdline, started_at, finished_at, status
|
||||
FROM build_logs
|
||||
WHERE agent = ?1
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?2",
|
||||
)?;
|
||||
let rows = stmt.query_map(
|
||||
params![agent, i64::try_from(limit).unwrap_or(50)],
|
||||
row_to_header,
|
||||
)?;
|
||||
let mut out = Vec::new();
|
||||
for r in rows {
|
||||
out.push(r?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Return the most recent `limit` rows across all agents, newest first.
|
||||
/// Same header-only shape as `list_recent_for_agent`. Limit clamped to 100.
|
||||
pub fn list_recent_all(&self, limit: usize) -> Result<Vec<BuildLogHeader>> {
|
||||
let limit = limit.min(100);
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, agent, kind, cmdline, started_at, finished_at, status
|
||||
FROM build_logs
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?1",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![i64::try_from(limit).unwrap_or(100)], row_to_header)?;
|
||||
let mut out = Vec::new();
|
||||
for r in rows {
|
||||
out.push(r?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Fetch a single full row (with stdout/stderr text) by id.
|
||||
/// Returns `None` when the id doesn't exist (vacuum sweep already
|
||||
/// reaped it, or the operator passed a stale id from a refresh
|
||||
/// race).
|
||||
/// Read one row back, text included. `None` when the id doesn't exist
|
||||
/// (already vacuumed).
|
||||
#[cfg(test)]
|
||||
pub fn get_full(&self, id: i64) -> Result<Option<BuildLogFull>> {
|
||||
use rusqlite::OptionalExtension;
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, agent, kind, cmdline, started_at, finished_at, status, stdout, stderr
|
||||
FROM build_logs
|
||||
WHERE id = ?1",
|
||||
)?;
|
||||
let row = stmt
|
||||
.query_row(params![id], |r| {
|
||||
let started_at: i64 = r.get(4)?;
|
||||
let finished_at: Option<i64> = r.get(5)?;
|
||||
let runtime_secs = compute_runtime_secs(started_at, finished_at);
|
||||
Ok(BuildLogFull {
|
||||
header: BuildLogHeader {
|
||||
id: r.get(0)?,
|
||||
agent: r.get(1)?,
|
||||
kind: r.get(2)?,
|
||||
cmdline: r.get(3)?,
|
||||
started_at,
|
||||
finished_at,
|
||||
status: r.get(6)?,
|
||||
runtime_secs,
|
||||
},
|
||||
stdout: r.get(7)?,
|
||||
stderr: r.get(8)?,
|
||||
})
|
||||
})
|
||||
let row = conn
|
||||
.query_row(
|
||||
"SELECT agent, kind, finished_at, status, stdout, stderr \
|
||||
FROM build_logs WHERE id = ?1",
|
||||
params![id],
|
||||
|r| {
|
||||
Ok(BuildLogFull {
|
||||
agent: r.get(0)?,
|
||||
kind: r.get(1)?,
|
||||
finished_at: r.get(2)?,
|
||||
status: r.get(3)?,
|
||||
stdout: r.get(4)?,
|
||||
stderr: r.get(5)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
Ok(row)
|
||||
}
|
||||
|
|
@ -494,26 +330,6 @@ pub fn spawn_vacuum(coord: &Arc<crate::coordinator::Coordinator>) {
|
|||
});
|
||||
}
|
||||
|
||||
fn compute_runtime_secs(started_at: i64, finished_at: Option<i64>) -> Option<i64> {
|
||||
finished_at.map(|f| f - started_at)
|
||||
}
|
||||
|
||||
fn row_to_header(r: &rusqlite::Row) -> rusqlite::Result<BuildLogHeader> {
|
||||
let started_at: i64 = r.get(4)?;
|
||||
let finished_at: Option<i64> = r.get(5)?;
|
||||
let runtime_secs = compute_runtime_secs(started_at, finished_at);
|
||||
Ok(BuildLogHeader {
|
||||
id: r.get(0)?,
|
||||
agent: r.get(1)?,
|
||||
kind: r.get(2)?,
|
||||
cmdline: r.get(3)?,
|
||||
started_at,
|
||||
finished_at,
|
||||
status: r.get(6)?,
|
||||
runtime_secs,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -536,10 +352,10 @@ mod tests {
|
|||
db.finish(id, BuildStatus::Fail);
|
||||
|
||||
let full = db.get_full(id).expect("get").expect("Some");
|
||||
assert_eq!(full.header.agent, "alice");
|
||||
assert_eq!(full.header.kind, "prebuild");
|
||||
assert_eq!(full.header.status.as_deref(), Some("fail"));
|
||||
assert!(full.header.finished_at.is_some());
|
||||
assert_eq!(full.agent, "alice");
|
||||
assert_eq!(full.kind, "prebuild");
|
||||
assert_eq!(full.status.as_deref(), Some("fail"));
|
||||
assert!(full.finished_at.is_some());
|
||||
assert!(full.stdout.contains("/nix/store/abc.drv"));
|
||||
assert!(full.stderr.contains("error: line 12"));
|
||||
assert!(full.stderr.contains("module.nix:5"));
|
||||
|
|
@ -565,46 +381,15 @@ mod tests {
|
|||
assert_eq!(db.reap_orphans().expect("reap"), 1);
|
||||
|
||||
let row = db.get_full(orphan).expect("get").expect("Some");
|
||||
assert_eq!(row.header.status.as_deref(), Some("fail"));
|
||||
assert!(row.header.finished_at.is_some());
|
||||
assert_eq!(row.status.as_deref(), Some("fail"));
|
||||
assert!(row.finished_at.is_some());
|
||||
assert!(row.stdout.contains("building"));
|
||||
assert!(row.stderr.ends_with(&format!("{ORPHAN_NOTE}\n")));
|
||||
let ok = db.get_full(done).expect("get").expect("Some");
|
||||
assert_eq!(ok.header.status.as_deref(), Some("ok"));
|
||||
assert_eq!(ok.status.as_deref(), Some("ok"));
|
||||
assert_eq!(db.reap_orphans().expect("reap again"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_recent_orders_newest_first_and_clamps() {
|
||||
let (_d, db) = tmpdb();
|
||||
// Three attempts, two for alice + one for bob. Without sleeping
|
||||
// sqlite's `INTEGER` started_at ties at 1-sec resolution, so we
|
||||
// assert id-ordering (autoincrement) is the tiebreaker — list
|
||||
// sorts by started_at DESC but the ORDER BY still produces the
|
||||
// last-inserted row first when timestamps match.
|
||||
let id_a1 = db.start("alice", "run", "cmd one", None).expect("start");
|
||||
let _id_b = db.start("bob", "run", "cmd two", None).expect("start");
|
||||
let id_a2 = db.start("alice", "run", "cmd three", None).expect("start");
|
||||
db.finish(id_a1, BuildStatus::Ok);
|
||||
|
||||
let alice_rows = db.list_recent_for_agent("alice", 10).expect("list");
|
||||
assert_eq!(alice_rows.len(), 2);
|
||||
// Without distinct started_at values both rows share `now`,
|
||||
// but list_recent already orders by `started_at DESC` then
|
||||
// sqlite's natural insertion-order tiebreak. We rely only on
|
||||
// both IDs being present + correct count + agent isolation.
|
||||
let ids: std::collections::HashSet<i64> = alice_rows.iter().map(|h| h.id).collect();
|
||||
assert!(ids.contains(&id_a1));
|
||||
assert!(ids.contains(&id_a2));
|
||||
|
||||
let bob_rows = db.list_recent_for_agent("bob", 10).expect("list");
|
||||
assert_eq!(bob_rows.len(), 1);
|
||||
|
||||
// Limit clamp at 50.
|
||||
let huge = db.list_recent_for_agent("alice", 999_999).expect("list");
|
||||
assert!(huge.len() <= 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_full_returns_none_for_missing_id() {
|
||||
let (_d, db) = tmpdb();
|
||||
|
|
|
|||
Loading…
Reference in a new issue