//! Connection open posture, notify machinery, bootstrap, and the //! writer/reader connection architecture — ported from honker-core //! `lib.rs` @ `f4e53c6` (upstream names preserved; the table family //! renamed `_honker_*` → `__alkstore_*` per ADR-011; the bootstrap's //! duplicate-column race swallow re-keyed to `pragma_table_info` //! per ADR-012 §5; the at-attach notifications pruning cap per //! ADR-010 §6 / register D-16). use parking_lot::{Condvar, Mutex}; use rusqlite::functions::FunctionFlags; use rusqlite::{Connection, OpenFlags}; use std::sync::atomic::{AtomicBool, Ordering}; #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Database error: {0}")] Sqlite(#[from] rusqlite::Error), } pub(crate) const DEFAULT_PRAGMAS: &str = "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON; PRAGMA cache_size = -32000; PRAGMA temp_store = MEMORY; PRAGMA wal_autocheckpoint = 10000;"; pub(crate) fn apply_default_pragmas(conn: &Connection) -> rusqlite::Result<()> { conn.execute_batch("PRAGMA busy_timeout = 5000;")?; set_journal_mode_wal(conn)?; conn.execute_batch(DEFAULT_PRAGMAS) } fn set_journal_mode_wal(conn: &Connection) -> rusqlite::Result<()> { let already_wal = |conn: &Connection| -> rusqlite::Result { let mode: String = conn.query_row("PRAGMA journal_mode", [], |row| row.get(0))?; Ok(mode.eq_ignore_ascii_case("wal")) }; if already_wal(conn)? { return Ok(()); } let deadline = std::time::Instant::now() + std::time::Duration::from_millis(5000); let mut backoff = std::time::Duration::from_millis(1); loop { match conn.execute_batch("PRAGMA journal_mode = WAL;") { Ok(()) => return Ok(()), Err(e) => { if already_wal(conn).unwrap_or(false) { return Ok(()); } if std::time::Instant::now() >= deadline { return Err(e); } std::thread::sleep(backoff); backoff = (backoff * 2).min(std::time::Duration::from_millis(50)); } } } } /// Upper bound on `__alkstore_notifications` rows before the next /// attach prunes oldest-first past it (register D-16 — ADR-010 §6's /// at-attach cap; engine-internal hygiene, not consumer-visible). /// /// Not upstream. Upstream's `attach_notify` never prunes ("no magic /// timer" — callers invoke `prune_notifications` explicitly); this /// substrate's postures pins hygiene at attach so the engine grows /// nothing unbounded by deferring to a caller who never prunes. /// Rows are only trimmed while their senders' transactions are /// committed — pruning runs on a fresh connection before its /// `notify()` function is registered, so no in-flight wake row can /// race the DELETE. pub(crate) const NOTIFY_ATTACH_MAX_ROWS: i64 = 10_000; /// Install the `__alkstore_notifications` table and the /// `notify(channel, payload)` SQL scalar function on `conn`. /// Idempotent. pub(crate) fn attach_notify(conn: &Connection) -> Result<(), Error> { conn.execute_batch( "CREATE TABLE IF NOT EXISTS __alkstore_notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel TEXT NOT NULL, payload TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE INDEX IF NOT EXISTS __alkstore_notifications_recent ON __alkstore_notifications(channel, id);", )?; conn.execute( "DELETE FROM __alkstore_notifications WHERE id IN ( SELECT id FROM __alkstore_notifications ORDER BY id DESC LIMIT -1 OFFSET ?1 )", [NOTIFY_ATTACH_MAX_ROWS], )?; conn.create_scalar_function("notify", 2, FunctionFlags::SQLITE_UTF8, |ctx| { let channel: String = ctx.get(0)?; let payload: String = ctx.get(1)?; let db = unsafe { ctx.get_connection() }?; let mut ins = db.prepare_cached( "INSERT INTO __alkstore_notifications (channel, payload) VALUES (?1, ?2)", )?; let id = ins.insert(rusqlite::params![channel, payload])?; Ok(id) })?; Ok(()) } pub(crate) const BOOTSTRAP_ALKSTORE_SQL: &str = " CREATE TABLE IF NOT EXISTS __alkstore_live ( id INTEGER PRIMARY KEY AUTOINCREMENT, queue TEXT NOT NULL, payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending', priority INTEGER NOT NULL DEFAULT 0, run_at INTEGER NOT NULL DEFAULT (unixepoch()), worker_id TEXT, claim_expires_at INTEGER, attempts INTEGER NOT NULL DEFAULT 0, max_attempts INTEGER NOT NULL DEFAULT 3, created_at INTEGER NOT NULL DEFAULT (unixepoch()), expires_at INTEGER, claimed_at INTEGER, visibility_timeout_s INTEGER NOT NULL DEFAULT 300, backoff_base_s INTEGER NOT NULL DEFAULT 5, dead_letter_retention_s INTEGER ); CREATE INDEX IF NOT EXISTS __alkstore_live_claim ON __alkstore_live(queue, priority DESC, run_at, id) WHERE state IN ('pending', 'processing'); CREATE INDEX IF NOT EXISTS __alkstore_live_pending_deadline ON __alkstore_live(queue, run_at) WHERE state = 'pending'; CREATE INDEX IF NOT EXISTS __alkstore_live_processing_deadline ON __alkstore_live(queue, claim_expires_at) WHERE state = 'processing'; CREATE TABLE IF NOT EXISTS __alkstore_dead ( id INTEGER PRIMARY KEY, queue TEXT NOT NULL, payload TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, run_at INTEGER NOT NULL DEFAULT 0, attempts INTEGER NOT NULL DEFAULT 0, max_attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT, created_at INTEGER NOT NULL DEFAULT (unixepoch()), died_at INTEGER NOT NULL DEFAULT (unixepoch()), visibility_timeout_s INTEGER NOT NULL DEFAULT 300, backoff_base_s INTEGER NOT NULL DEFAULT 5, dead_letter_retention_s INTEGER, claimed_at INTEGER, expires_at INTEGER ); CREATE INDEX IF NOT EXISTS __alkstore_dead_retention ON __alkstore_dead(queue, died_at); CREATE TABLE IF NOT EXISTS __alkstore_locks ( name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS __alkstore_scheduler_tasks ( name TEXT PRIMARY KEY, queue TEXT NOT NULL, spec TEXT NOT NULL, payload TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, expires_s INTEGER, next_fire_at INTEGER NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, max_attempts INTEGER NOT NULL DEFAULT 3, visibility_timeout_s INTEGER NOT NULL DEFAULT 300, backoff_base_s INTEGER NOT NULL DEFAULT 5, dead_letter_retention_s INTEGER ); CREATE TABLE IF NOT EXISTS __alkstore_stream ( offset INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, key TEXT, payload TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE INDEX IF NOT EXISTS __alkstore_stream_topic ON __alkstore_stream(topic, offset); CREATE TABLE IF NOT EXISTS __alkstore_stream_consumers ( name TEXT NOT NULL, topic TEXT NOT NULL, offset INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (name, topic) ); "; fn column_present(conn: &Connection, table: &str, column: &str) -> bool { let mut stmt = match conn.prepare(&format!( "SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?1" )) { Ok(stmt) => stmt, Err(_) => return false, }; stmt.query_row([column], |_| Ok(true)).unwrap_or(false) } fn add_column_if_absent( conn: &Connection, table: &str, column: &str, ddl: &str, ) -> Result<(), Error> { if column_present(conn, table, column) { return Ok(()); } match conn.execute(&format!("ALTER TABLE {table} ADD COLUMN {ddl}"), []) { Ok(_) => Ok(()), Err(e) => { if column_present(conn, table, column) { Ok(()) } else { Err(e.into()) } } } } /// Install the alkstore queue schema on `conn`. Idempotent. /// /// Fresh bootstrap only (ADR-012 §5): a file carrying honker's legacy /// `_honker_*` tables gets inert orphans — never touched, never read, /// never migrated. The migrations below exist for the same-schema /// case: a database this bootstrap already created in an earlier /// revision, missing a column appended since. pub(crate) fn bootstrap_schema(conn: &Connection) -> Result<(), Error> { conn.execute_batch(BOOTSTRAP_ALKSTORE_SQL)?; add_column_if_absent( conn, "__alkstore_scheduler_tasks", "enabled", "enabled INTEGER NOT NULL DEFAULT 1", )?; add_column_if_absent( conn, "__alkstore_scheduler_tasks", "max_attempts", "max_attempts INTEGER NOT NULL DEFAULT 3", )?; add_column_if_absent( conn, "__alkstore_scheduler_tasks", "visibility_timeout_s", "visibility_timeout_s INTEGER NOT NULL DEFAULT 300", )?; add_column_if_absent( conn, "__alkstore_scheduler_tasks", "backoff_base_s", "backoff_base_s INTEGER NOT NULL DEFAULT 5", )?; add_column_if_absent( conn, "__alkstore_scheduler_tasks", "dead_letter_retention_s", "dead_letter_retention_s INTEGER", )?; add_column_if_absent(conn, "__alkstore_live", "claimed_at", "claimed_at INTEGER")?; add_column_if_absent( conn, "__alkstore_live", "visibility_timeout_s", "visibility_timeout_s INTEGER NOT NULL DEFAULT 300", )?; add_column_if_absent( conn, "__alkstore_live", "backoff_base_s", "backoff_base_s INTEGER NOT NULL DEFAULT 5", )?; add_column_if_absent( conn, "__alkstore_live", "dead_letter_retention_s", "dead_letter_retention_s INTEGER", )?; add_column_if_absent( conn, "__alkstore_dead", "visibility_timeout_s", "visibility_timeout_s INTEGER NOT NULL DEFAULT 300", )?; add_column_if_absent( conn, "__alkstore_dead", "backoff_base_s", "backoff_base_s INTEGER NOT NULL DEFAULT 5", )?; add_column_if_absent( conn, "__alkstore_dead", "dead_letter_retention_s", "dead_letter_retention_s INTEGER", )?; add_column_if_absent(conn, "__alkstore_dead", "claimed_at", "claimed_at INTEGER")?; add_column_if_absent(conn, "__alkstore_dead", "expires_at", "expires_at INTEGER")?; Ok(()) } pub(crate) fn open_conn(path: &str, install_notify: bool) -> Result { // No SQLITE_OPEN_URI (ADR-023 §3 delta): the path argument is a // plain filesystem path — URI-interpreted `?name=value` suffixes // would mutate connection semantics outside the engine // constructor's opts. let conn = Connection::open_with_flags( path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, )?; apply_default_pragmas(&conn)?; if install_notify { attach_notify(&conn)?; } Ok(conn) } /// Open a connection carrying the engine's full per-connection /// bootstrap: pragmas, notify machinery, this substrate's function /// set, and schema bootstrap. The engine's `open` constructor runs /// this on the writer and each pooled reader (engine-sqlite.md's /// connection architecture: "each connection runs the substrate's /// bootstrap at open"). /// /// D-30: the lineage's pools open readers through bare `open_conn` /// (pragmas + notify only), the function set + schema living only on /// the writer. This substrate extends the pool open path with /// `attach_alkstore_functions` + `bootstrap_schema` — the engine's /// reader-slot operations (claim/ack, stream reads, lock ops, /// scheduler checks) run this substrate's SQL functions on pooled /// connections, so every pooled connection must carry them. pub(crate) fn open_conn_bootstrapped(path: &str) -> Result { let conn = open_conn(path, true)?; super::ops::attach_alkstore_functions(&conn)?; bootstrap_schema(&conn)?; Ok(conn) } pub(crate) struct Writer { slot: Mutex>, available: Condvar, closed: AtomicBool, path: String, } impl Writer { pub(crate) fn new(path: String, conn: Connection) -> Self { Self { slot: Mutex::new(Some(conn)), available: Condvar::new(), closed: AtomicBool::new(false), path, } } /// The abandonment arm (the auto-commit seam's panic-safety armor, /// review 004 item 1): a connection that cannot return to the slot /// — a panicked op leaves its statement state unknown — is dropped, /// not released, and the slot is replenished with a fresh /// bootstrapped open from the writer's own path. The release's slot /// notify re-opens the waiters. A failed replenish leaves the slot /// empty (the established arms' tolerance — the tick's /// poisoned-disposition arm and the tx seam's `ConnLease` drop arm /// both take `if let Ok(fresh)`), with the failure printed for /// diagnosability. pub(crate) fn replenish(&self) { if self.closed.load(Ordering::Acquire) { return; } match open_conn_bootstrapped(&self.path) { Ok(fresh) => self.release(fresh), Err(e) => eprintln!( "alkstore: error: writer slot replenish after an abandoned \ operation failed to open a fresh connection: {e}" ), } } pub(crate) fn acquire(&self) -> Option { let mut guard = self.slot.lock(); loop { if self.closed.load(Ordering::Acquire) { return None; } if let Some(c) = guard.take() { return Some(c); } self.available.wait(&mut guard); } } pub(crate) fn release(&self, conn: Connection) { if self.closed.load(Ordering::Acquire) { return; } let mut guard = self.slot.lock(); *guard = Some(conn); self.available.notify_one(); } pub(crate) fn close(&self) { self.closed.store(true, Ordering::Release); let mut guard = self.slot.lock(); guard.take(); self.available.notify_all(); } } pub(crate) struct Readers { pool: Mutex>, outstanding: Mutex, available: Condvar, path: String, max: usize, closed: AtomicBool, } impl Readers { pub(crate) fn new(path: String, max: usize) -> Self { Self { pool: Mutex::new(Vec::new()), outstanding: Mutex::new(0), available: Condvar::new(), path, max: max.max(1), closed: AtomicBool::new(false), } } pub(crate) fn acquire(&self) -> Result { loop { if self.closed.load(Ordering::Acquire) { return Err(closed_err()); } let mut pool = self.pool.lock(); if let Some(c) = pool.pop() { return Ok(c); } let mut out = self.outstanding.lock(); if *out < self.max { *out += 1; drop(out); drop(pool); match open_conn_bootstrapped(&self.path) { Ok(conn) => { if self.closed.load(Ordering::Acquire) { *self.outstanding.lock() -= 1; drop(conn); return Err(closed_err()); } return Ok(conn); } Err(e) => { *self.outstanding.lock() -= 1; return Err(e); } } } drop(out); self.available.wait(&mut pool); } } pub(crate) fn release(&self, conn: Connection) { if self.closed.load(Ordering::Acquire) { return; } let mut pool = self.pool.lock(); pool.push(conn); self.available.notify_one(); } /// The abandonment arm (the auto-commit seam's panic-safety /// armor, review 004 item 1): the abandoned connection is dropped, /// not released. `outstanding` counts connections in existence — /// every connection carries its fresh-open increment for life, and /// release/pop never decrement — so the abandoned connection's /// decrement restores the pool's open-budget, and a waiter wakes /// under the pool lock (the release's notify discipline) to /// re-evaluate: pool miss + under `max` ⇒ fresh open. pub(crate) fn abandon(&self) { { let mut out = self.outstanding.lock(); *out = out.saturating_sub(1); } let _pool = self.pool.lock(); self.available.notify_one(); } pub(crate) fn close(&self) { self.closed.store(true, Ordering::Release); self.pool.lock().clear(); self.available.notify_all(); } } fn closed_err() -> Error { Error::Sqlite(rusqlite::Error::SqliteFailure( rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_MISUSE), Some("Database is closed".to_string()), )) } #[cfg(test)] mod writer_reader_tests { use super::*; use std::sync::Arc; fn mem() -> Connection { Connection::open_in_memory().unwrap() } fn temp_db(name: &str) -> std::path::PathBuf { let p = std::env::temp_dir().join(format!( "alkstore-{name}-{}-{:?}.db", std::process::id(), std::thread::current().id() )); let _ = std::fs::remove_file(&p); let _ = std::fs::remove_file(format!("{}-wal", p.display())); let _ = std::fs::remove_file(format!("{}-shm", p.display())); p } #[test] fn writer_close_drops_idle_connection() { let w = Writer::new(":memory:".to_string(), mem()); w.close(); assert!(w.acquire().is_none()); } #[test] fn writer_close_drops_returned_connection() { let w = Writer::new(":memory:".to_string(), mem()); let conn = w.acquire().unwrap(); w.close(); w.release(conn); assert!(w.acquire().is_none()); } #[test] fn readers_close_returns_closed_err() { let tmp = temp_db("readers-close"); let _ = std::fs::remove_file(&tmp); let c = Connection::open(&tmp).unwrap(); c.execute_batch("PRAGMA journal_mode=WAL;").unwrap(); drop(c); let r = Readers::new(tmp.to_string_lossy().into_owned(), 4); let c = r.acquire().unwrap(); r.release(c); r.close(); match r.acquire() { Err(Error::Sqlite(rusqlite::Error::SqliteFailure(_, Some(msg)))) => { assert!(msg.contains("Database is closed")); } other => panic!("expected closed err, got {other:?}"), } let _ = std::fs::remove_file(&tmp); } #[test] fn readers_open_failure_does_not_leak_capacity() { let r = Arc::new(Readers::new( "/this/parent/does/not/exist/alkstore-readers-leak.db".into(), 2, )); for i in 0..5 { let r = r.clone(); let handle = std::thread::spawn(move || r.acquire()); let result = handle.join().unwrap(); assert!( result.is_err(), "attempt {i}: expected open failure, got Ok" ); } } #[test] fn open_conn_applies_wal_and_default_pragmas() { let tmp = temp_db("open-pragmas"); let conn = open_conn(tmp.to_str().unwrap(), false).unwrap(); let mode: String = conn .pragma_query_value(None, "journal_mode", |r| r.get(0)) .unwrap(); assert!(mode.eq_ignore_ascii_case("wal")); let busy: i64 = conn .pragma_query_value(None, "busy_timeout", |r| r.get(0)) .unwrap(); assert_eq!(busy, 5000); drop(conn); let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(format!("{}-wal", tmp.display())); let _ = std::fs::remove_file(format!("{}-shm", tmp.display())); } #[test] fn open_conn_treats_uri_shaped_path_as_plain_filename() { // ADR-023 §3: no SQLITE_OPEN_URI — a `?name=value` suffix must // be a literal filename (the file exists with that name), not // URI interpretation (which for a bare-relative "file"-less // string with query params would error or mutate semantics). let dir = std::env::temp_dir().join(format!( "alkstore-uri-literal-{}-{:?}", std::process::id(), std::thread::current().id() )); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("plain.db?mode=memory"); let conn = open_conn(path.to_str().unwrap(), false).unwrap(); conn.execute("CREATE TABLE t (x)", []).unwrap(); drop(conn); let plain = dir.join("plain.db?mode=memory"); assert!(plain.exists(), "literal-named file must exist on disk"); let _ = std::fs::remove_dir_all(&dir); } #[test] fn open_conn_bootstrapped_carries_the_full_surface_on_every_connection() { let dir = std::env::temp_dir().join(format!( "alkstore-bootstrapped-{}-{:?}", std::process::id(), std::thread::current().id() )); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("bootstrapped.db"); let first = open_conn_bootstrapped(path.to_str().unwrap()).unwrap(); drop(first); for i in 0..3 { let conn = open_conn_bootstrapped(path.to_str().unwrap()) .unwrap_or_else(|e| panic!("bootstrapped open {i} failed: {e}")); let mode: String = conn .pragma_query_value(None, "journal_mode", |r| r.get(0)) .unwrap(); assert!(mode.eq_ignore_ascii_case("wal"), "open {i}: not WAL"); let notify_fn: i64 = conn .query_row("SELECT notify('__alkstore_probe', 'x')", [], |r| r.get(0)) .unwrap_or(0); assert!(notify_fn >= 1, "open {i}: notify function missing"); let tables: i64 = conn .query_row( "SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name LIKE '__alkstore_%'", [], |r| r.get(0), ) .unwrap(); assert!( tables >= 7, "open {i}: bootstrap schema missing (saw {tables} tables)" ); let publish_ready: i64 = conn .query_row( "SELECT alkstore_stream_publish('__alkstore_probe', NULL, '{}')", [], |r| r.get(0), ) .unwrap_or(0); assert!(publish_ready >= 1, "open {i}: function set missing"); drop(conn); } let _ = std::fs::remove_dir_all(&dir); } #[test] fn concurrent_opens_never_return_database_is_locked() { const ROUNDS: usize = 8; const OPENERS: usize = 16; for round in 0..ROUNDS { let dir = std::env::temp_dir().join(format!( "alkstore-open-race-{}-{round}-{:?}", std::process::id(), std::thread::current().id() )); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("pressure.db"); let barrier = Arc::new(std::sync::Barrier::new(OPENERS)); let handles: Vec<_> = (0..OPENERS) .map(|_| { let path = path.clone(); let barrier = Arc::clone(&barrier); std::thread::spawn(move || { let conn = Connection::open(&path)?; barrier.wait(); apply_default_pragmas(&conn)?; let mode: String = conn.query_row("PRAGMA journal_mode", [], |row| row.get(0))?; assert!( mode.eq_ignore_ascii_case("wal"), "expected WAL after open, got {mode}" ); Ok::<_, rusqlite::Error>(()) }) }) .collect(); let failures: Vec = handles .into_iter() .filter_map(|h| h.join().unwrap().err().map(|e| e.to_string())) .collect(); std::fs::remove_dir_all(&dir).ok(); assert!( failures.is_empty(), "round {round}: concurrent opens failed: {failures:?}" ); } } }