refactor(hive-c0re): shared sqlite open helper with busy timeout

one db::open owns the parent-dir + connection + busy_timeout dance for
every host-side store (broker/approvals/questions/schedules/power in
broker.sqlite, build_logs, audit_log); schema + migrations stay per
store. same-file connections now wait out concurrent writers instead
of risking SQLITE_BUSY.
This commit is contained in:
müde 2026-07-06 20:46:57 +02:00
commit 380c6ad47f
8 changed files with 45 additions and 41 deletions

View file

@ -91,12 +91,7 @@ pub struct Approvals {
impl Approvals {
pub fn open(path: &Path) -> Result<Self> {
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")?;

View file

@ -157,12 +157,7 @@ pub struct Broker {
impl Broker {
pub fn open(path: &Path) -> Result<Self> {
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")?;

View file

@ -147,11 +147,8 @@ pub struct BuildLogs {
impl BuildLogs {
pub fn open(db_dir: &Path) -> Result<Self> {
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);

38
hive-c0re/src/db.rs Normal file
View file

@ -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<Connection>` — 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<Connection> {
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)
}

View file

@ -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;

View file

@ -97,13 +97,7 @@ pub struct OperatorQuestions {
impl OperatorQuestions {
pub fn open(path: &Path) -> Result<Self> {
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")?;

View file

@ -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<Self> {
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 {

View file

@ -178,13 +178,7 @@ pub struct ScheduledPrompts {
impl ScheduledPrompts {
pub fn open(path: &Path) -> Result<Self> {
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;")