diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index 6fb8dc6a..8737638f 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -91,12 +91,7 @@ pub struct Approvals { impl Approvals { pub fn open(path: &Path) -> Result { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("create approvals db parent {}", parent.display()))?; - } - let conn = Connection::open(path) - .with_context(|| format!("open approvals db {}", path.display()))?; + let conn = crate::db::open(path, "approvals")?; conn.execute_batch(SCHEMA) .context("apply approvals schema")?; ensure_kind_column(&conn).context("migrate approvals.kind")?; diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs index c80d1ef4..ba0e9fc6 100644 --- a/hive-c0re/src/broker.rs +++ b/hive-c0re/src/broker.rs @@ -157,12 +157,7 @@ pub struct Broker { impl Broker { pub fn open(path: &Path) -> Result { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("create db parent {}", parent.display()))?; - } - let conn = - Connection::open(path).with_context(|| format!("open broker db {}", path.display()))?; + let conn = crate::db::open(path, "broker")?; conn.execute_batch(SCHEMA).context("apply broker schema")?; ensure_message_columns(&conn).context("migrate messages columns")?; ensure_reminder_columns(&conn).context("migrate reminders columns")?; diff --git a/hive-c0re/src/build_logs.rs b/hive-c0re/src/build_logs.rs index 76679abc..24afa74c 100644 --- a/hive-c0re/src/build_logs.rs +++ b/hive-c0re/src/build_logs.rs @@ -147,11 +147,8 @@ pub struct BuildLogs { impl BuildLogs { pub fn open(db_dir: &Path) -> Result { - std::fs::create_dir_all(db_dir) - .with_context(|| format!("create build_logs db parent {}", db_dir.display()))?; let path = db_dir.join("build_logs.sqlite"); - let conn = Connection::open(&path) - .with_context(|| format!("open build_logs db {}", path.display()))?; + let conn = crate::db::open(&path, "build_logs")?; conn.execute_batch(SCHEMA) .context("apply build_logs schema")?; let (notify_tx, _) = broadcast::channel(NOTIFY_CAP); diff --git a/hive-c0re/src/db.rs b/hive-c0re/src/db.rs new file mode 100644 index 00000000..3e86f629 --- /dev/null +++ b/hive-c0re/src/db.rs @@ -0,0 +1,38 @@ +//! Shared sqlite connection setup for hive-c0re's host-side stores. +//! +//! Several modules keep their own tables — and their own +//! `Mutex` — in the coordinator DB +//! (`db/broker.sqlite`: broker, approvals, operator questions, +//! scheduled prompts, agent power) or in a sibling file under the same +//! `db/` dir (`build_logs.sqlite`, `audit_log.sqlite`). The open dance +//! is identical everywhere: ensure the parent dir exists, open the +//! connection, set a busy timeout so concurrent same-process writers +//! wait each other out instead of surfacing `SQLITE_BUSY`. This helper +//! owns that dance; schema creation + column migrations stay with each +//! store (they're per-table concerns). + +use std::path::Path; +use std::time::Duration; + +use anyhow::{Context, Result}; +use rusqlite::Connection; + +/// How long a write waits on another connection's lock before erroring. +/// Generous relative to the stores' tiny transactions — a timeout here +/// means something is genuinely wedged, not ordinary contention. +const BUSY_TIMEOUT: Duration = Duration::from_secs(5); + +/// Open a connection to the sqlite file at `path`, creating the parent +/// directory if needed. `subsystem` labels error contexts (`"broker"`, +/// `"approvals"`, …). +pub fn open(path: &Path, subsystem: &str) -> Result { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("create {subsystem} db parent {}", parent.display()))?; + } + let conn = Connection::open(path) + .with_context(|| format!("open {subsystem} db {}", path.display()))?; + conn.busy_timeout(BUSY_TIMEOUT) + .with_context(|| format!("set {subsystem} busy_timeout"))?; + Ok(conn) +} diff --git a/hive-c0re/src/lib.rs b/hive-c0re/src/lib.rs index c02819a7..8f1309ac 100644 --- a/hive-c0re/src/lib.rs +++ b/hive-c0re/src/lib.rs @@ -27,6 +27,7 @@ pub mod coordinator; pub mod crash_watch; pub mod dashboard; pub mod dashboard_events; +pub mod db; pub mod flake_check; pub mod forge; pub mod gateway_nginx; diff --git a/hive-c0re/src/operator_questions.rs b/hive-c0re/src/operator_questions.rs index 604c43c8..1986bd8a 100644 --- a/hive-c0re/src/operator_questions.rs +++ b/hive-c0re/src/operator_questions.rs @@ -97,13 +97,7 @@ pub struct OperatorQuestions { impl OperatorQuestions { pub fn open(path: &Path) -> Result { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!("create operator_questions db parent {}", parent.display()) - })?; - } - let conn = Connection::open(path) - .with_context(|| format!("open operator_questions db {}", path.display()))?; + let conn = crate::db::open(path, "operator_questions")?; conn.execute_batch(SCHEMA) .context("apply operator_questions schema")?; ensure_columns(&conn).context("migrate operator_questions columns")?; diff --git a/hive-c0re/src/power.rs b/hive-c0re/src/power.rs index 2f23689e..e28ad2fb 100644 --- a/hive-c0re/src/power.rs +++ b/hive-c0re/src/power.rs @@ -87,17 +87,7 @@ impl PowerStore { /// `agent_power` table exists. `db_path` is the same sqlite file /// the broker / approvals / questions stores open. pub fn open(db_path: &Path) -> Result { - if let Some(parent) = db_path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("create agent_power db parent {}", parent.display()))?; - } - let conn = Connection::open(db_path) - .with_context(|| format!("open agent_power db {}", db_path.display()))?; - // Several modules hold their own connection to this file (the - // broker / approvals / questions pattern); wait out a - // concurrent writer instead of surfacing SQLITE_BUSY. - conn.busy_timeout(std::time::Duration::from_secs(5)) - .context("set agent_power busy_timeout")?; + let conn = crate::db::open(db_path, "agent_power")?; conn.execute_batch(SCHEMA) .context("apply agent_power schema")?; Ok(Self { diff --git a/hive-c0re/src/scheduled_prompts.rs b/hive-c0re/src/scheduled_prompts.rs index 9095e7b5..bec95999 100644 --- a/hive-c0re/src/scheduled_prompts.rs +++ b/hive-c0re/src/scheduled_prompts.rs @@ -178,13 +178,7 @@ pub struct ScheduledPrompts { impl ScheduledPrompts { pub fn open(path: &Path) -> Result { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!("create scheduled_prompts db parent {}", parent.display()) - })?; - } - let conn = Connection::open(path) - .with_context(|| format!("open scheduled_prompts db {}", path.display()))?; + let conn = crate::db::open(path, "scheduled_prompts")?; // Required for ON DELETE CASCADE to actually fire — sqlite // ships with FKs disabled per connection by default. conn.execute_batch("PRAGMA foreign_keys = ON;")