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:
parent
604e1c2557
commit
380c6ad47f
8 changed files with 45 additions and 41 deletions
|
|
@ -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")?;
|
||||
|
|
|
|||
|
|
@ -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")?;
|
||||
|
|
|
|||
|
|
@ -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
38
hive-c0re/src/db.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")?;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;")
|
||||
|
|
|
|||
Loading…
Reference in a new issue