//! Host-side vacuum of every per-agent events.sqlite. The harness //! writes to `/state/hyperhive-events.sqlite` (bind-mounted from //! `/var/lib/hyperhive/agents//state/`); we open the same file //! from the host every hour and apply the same two-stage delete //! (drop rows older than `keep_secs`, then trim to `keep_rows` //! newest). Keeping retention on the host means agents don't need any //! cleanup wiring of their own, and a misbehaving harness can't //! disable its own vacuum. use std::path::Path; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use rusqlite::{Connection, Result, params}; use crate::coordinator::Coordinator; const VACUUM_INTERVAL: Duration = Duration::from_secs(3600); const KEEP_SECS: i64 = 7 * 24 * 3600; const KEEP_ROWS: i64 = 2000; /// Background loop: sweep every existing agent state dir hourly, run /// the vacuum SQL against its events.sqlite if present. Errors are /// logged but don't tear the loop down. pub fn spawn(coord: Arc) { tokio::spawn(async move { loop { sweep_once(); // touching coord keeps the type wired in case future sweeps // need approvals/etc.; the ref is otherwise unused today. let _ = &coord; tokio::time::sleep(VACUUM_INTERVAL).await; } }); } fn sweep_once() { for name in Coordinator::kept_state_names() { let path = Coordinator::agent_notes_dir(&name).join("hyperhive-events.sqlite"); if !path.exists() { continue; } match vacuum_file(&path) { Ok(0) => {} Ok(n) => tracing::info!(agent = %name, removed = n, "events vacuum"), Err(e) => tracing::warn!(agent = %name, error = ?e, "events vacuum failed"), } } } fn vacuum_file(path: &Path) -> Result { let conn = Connection::open(path)?; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .ok() .and_then(|d| i64::try_from(d.as_secs()).ok()) .unwrap_or(0); let cutoff = now - KEEP_SECS; let by_age = conn.execute("DELETE FROM events WHERE ts < ?1", params![cutoff])?; let by_count = conn.execute( "DELETE FROM events WHERE id NOT IN ( SELECT id FROM events ORDER BY id DESC LIMIT ?1 )", params![KEEP_ROWS], )?; Ok(u64::try_from(by_age + by_count).unwrap_or(0)) }