use rusqlite::Connection;
use std::path::Path;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LedgerJobStatus {
Running,
Completed,
Failed,
Cancelled,
}
impl LedgerJobStatus {
fn from_str(s: &str) -> Self {
match s {
"completed" => Self::Completed,
"failed" => Self::Failed,
"cancelled" => Self::Cancelled,
_ => Self::Running,
}
}
}
pub struct Ledger {
conn: Mutex<Connection>,
}
impl Ledger {
pub fn open(path: &Path, graph_fingerprint: &str) -> rusqlite::Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
let conn = Connection::open(path)?;
conn.busy_timeout(std::time::Duration::from_secs(5))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS checkpoints (
node_name TEXT PRIMARY KEY,
status TEXT NOT NULL,
output_json TEXT,
completed_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS job_status (status TEXT NOT NULL, graph_fingerprint TEXT NOT NULL);",
)?;
let count: i64 = conn.query_row("SELECT COUNT(*) FROM job_status", [], |r| r.get(0))?;
if count == 0 {
conn.execute(
"INSERT INTO job_status (status, graph_fingerprint) VALUES ('running', ?1)",
[graph_fingerprint],
)?;
}
Ok(Self {
conn: Mutex::new(conn),
})
}
pub fn graph_fingerprint(&self) -> rusqlite::Result<String> {
self.lock()
.query_row("SELECT graph_fingerprint FROM job_status", [], |r| r.get(0))
}
pub fn get_completed(&self, node_name: &str) -> rusqlite::Result<Option<serde_json::Value>> {
let result: Option<(String, Option<String>)> = match self.lock().query_row(
"SELECT status, output_json FROM checkpoints WHERE node_name = ?1",
[node_name],
|r| Ok((r.get(0)?, r.get(1)?)),
) {
Ok(row) => Some(row),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => return Err(e),
};
match result {
Some((status, Some(json))) if status == "completed" => Ok(Some(
serde_json::from_str(&json).expect("ledger never stores invalid JSON"),
)),
_ => Ok(None),
}
}
pub fn is_skipped(&self, node_name: &str) -> rusqlite::Result<bool> {
let status: Option<String> = match self.lock().query_row(
"SELECT status FROM checkpoints WHERE node_name = ?1",
[node_name],
|r| r.get(0),
) {
Ok(status) => Some(status),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(e) => return Err(e),
};
Ok(status.as_deref() == Some("skipped"))
}
pub fn write_completed(
&self,
node_name: &str,
output: &serde_json::Value,
) -> rusqlite::Result<()> {
self.lock().execute(
"INSERT OR REPLACE INTO checkpoints (node_name, status, output_json, completed_at)
VALUES (?1, 'completed', ?2, ?3)",
rusqlite::params![node_name, output.to_string(), now_marker()],
)?;
Ok(())
}
pub fn write_skipped(&self, node_name: &str) -> rusqlite::Result<()> {
self.lock().execute(
"INSERT OR REPLACE INTO checkpoints (node_name, status, output_json, completed_at)
VALUES (?1, 'skipped', NULL, ?2)",
rusqlite::params![node_name, now_marker()],
)?;
Ok(())
}
pub fn job_status(&self) -> rusqlite::Result<LedgerJobStatus> {
let s: String = self
.lock()
.query_row("SELECT status FROM job_status", [], |r| r.get(0))?;
Ok(LedgerJobStatus::from_str(&s))
}
pub fn finish(&self, status: &str) -> rusqlite::Result<()> {
self.lock()
.execute("UPDATE job_status SET status = ?1", [status])?;
Ok(())
}
fn lock(&self) -> std::sync::MutexGuard<'_, Connection> {
self.conn.lock().expect("ledger connection mutex poisoned")
}
}
pub fn jobs_root() -> Option<std::path::PathBuf> {
if let Ok(dir) = std::env::var("CUTTLEFISH_JOBS_HOME") {
return Some(std::path::PathBuf::from(dir));
}
crate::catalog::cuttlefish_home().map(|h| h.join("jobs"))
}
fn now_marker() -> String {
crate::catalog::now_rfc3339()
}