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 {
|
impl Approvals {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
if let Some(parent) = path.parent() {
|
let conn = crate::db::open(path, "approvals")?;
|
||||||
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()))?;
|
|
||||||
conn.execute_batch(SCHEMA)
|
conn.execute_batch(SCHEMA)
|
||||||
.context("apply approvals schema")?;
|
.context("apply approvals schema")?;
|
||||||
ensure_kind_column(&conn).context("migrate approvals.kind")?;
|
ensure_kind_column(&conn).context("migrate approvals.kind")?;
|
||||||
|
|
|
||||||
|
|
@ -157,12 +157,7 @@ pub struct Broker {
|
||||||
|
|
||||||
impl Broker {
|
impl Broker {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
if let Some(parent) = path.parent() {
|
let conn = crate::db::open(path, "broker")?;
|
||||||
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()))?;
|
|
||||||
conn.execute_batch(SCHEMA).context("apply broker schema")?;
|
conn.execute_batch(SCHEMA).context("apply broker schema")?;
|
||||||
ensure_message_columns(&conn).context("migrate messages columns")?;
|
ensure_message_columns(&conn).context("migrate messages columns")?;
|
||||||
ensure_reminder_columns(&conn).context("migrate reminders columns")?;
|
ensure_reminder_columns(&conn).context("migrate reminders columns")?;
|
||||||
|
|
|
||||||
|
|
@ -147,11 +147,8 @@ pub struct BuildLogs {
|
||||||
|
|
||||||
impl BuildLogs {
|
impl BuildLogs {
|
||||||
pub fn open(db_dir: &Path) -> Result<Self> {
|
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 path = db_dir.join("build_logs.sqlite");
|
||||||
let conn = Connection::open(&path)
|
let conn = crate::db::open(&path, "build_logs")?;
|
||||||
.with_context(|| format!("open build_logs db {}", path.display()))?;
|
|
||||||
conn.execute_batch(SCHEMA)
|
conn.execute_batch(SCHEMA)
|
||||||
.context("apply build_logs schema")?;
|
.context("apply build_logs schema")?;
|
||||||
let (notify_tx, _) = broadcast::channel(NOTIFY_CAP);
|
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 crash_watch;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
pub mod dashboard_events;
|
pub mod dashboard_events;
|
||||||
|
pub mod db;
|
||||||
pub mod flake_check;
|
pub mod flake_check;
|
||||||
pub mod forge;
|
pub mod forge;
|
||||||
pub mod gateway_nginx;
|
pub mod gateway_nginx;
|
||||||
|
|
|
||||||
|
|
@ -97,13 +97,7 @@ pub struct OperatorQuestions {
|
||||||
|
|
||||||
impl OperatorQuestions {
|
impl OperatorQuestions {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
if let Some(parent) = path.parent() {
|
let conn = crate::db::open(path, "operator_questions")?;
|
||||||
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()))?;
|
|
||||||
conn.execute_batch(SCHEMA)
|
conn.execute_batch(SCHEMA)
|
||||||
.context("apply operator_questions schema")?;
|
.context("apply operator_questions schema")?;
|
||||||
ensure_columns(&conn).context("migrate operator_questions columns")?;
|
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
|
/// `agent_power` table exists. `db_path` is the same sqlite file
|
||||||
/// the broker / approvals / questions stores open.
|
/// the broker / approvals / questions stores open.
|
||||||
pub fn open(db_path: &Path) -> Result<Self> {
|
pub fn open(db_path: &Path) -> Result<Self> {
|
||||||
if let Some(parent) = db_path.parent() {
|
let conn = crate::db::open(db_path, "agent_power")?;
|
||||||
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")?;
|
|
||||||
conn.execute_batch(SCHEMA)
|
conn.execute_batch(SCHEMA)
|
||||||
.context("apply agent_power schema")?;
|
.context("apply agent_power schema")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
|
||||||
|
|
@ -178,13 +178,7 @@ pub struct ScheduledPrompts {
|
||||||
|
|
||||||
impl ScheduledPrompts {
|
impl ScheduledPrompts {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
if let Some(parent) = path.parent() {
|
let conn = crate::db::open(path, "scheduled_prompts")?;
|
||||||
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()))?;
|
|
||||||
// Required for ON DELETE CASCADE to actually fire — sqlite
|
// Required for ON DELETE CASCADE to actually fire — sqlite
|
||||||
// ships with FKs disabled per connection by default.
|
// ships with FKs disabled per connection by default.
|
||||||
conn.execute_batch("PRAGMA foreign_keys = ON;")
|
conn.execute_batch("PRAGMA foreign_keys = ON;")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue