SQLite engine open: SqliteOpts, connection architecture, spawn_blocking seam posture (task sqlite-engine-open-opts)
- SqliteOpts (poll_interval: Option<Duration>, None = 1 ms shipping default ADR-023 §4; max_readers, DEFAULT_MAX_READERS = 8; opts exemption from non_exhaustive per ADR-017 §3) - open(path, opts) -> Box<dyn Store>: writer slot, reader pool, SharedUpdateWatcher with fallible spawn mapped W-2-style into Error::Database; plain-path posture (ADR-023 §3) pinned by test - Substrate delta D-30: open_conn_bootstrapped — every connection (writer and pooled readers) carries the full bootstrap surface (pragmas, notify, alkstore functions, schema) per engine-sqlite.md; lineage opened readers pragmas-only. Registered in PROVENANCE.md - spawn_blocking seam helper + error mappings (string/rusqlite -> Database, source chains preserved); helper cfg(test)-gated until sqlite-engine-seam-tx wires the trait impls - Store trait stubbed with Database errors (no panics); close()/Drop join the watcher, clear subscribers (death-guard), close pool+writer - 15 new tests (open boot, watcher fanout/death-close, cadence accounting, plain-path literal filename, :memory:, pool bounding, drop teardown, seam smoke incl. panic mapping); gates green
This commit is contained in:
1 parent
5474141f2b
commit
7d400906f5
11 files changed
+937
-13
No files matched your search
@@ -0,0 +1,220 @@
|
||||
//! The SQLite engine's store: the `open` constructor, the connection
|
||||
//! architecture (engine-sqlite.md's "Connection architecture"), and
|
||||
//! the [`Store`] trait wiring.
|
||||
//!
|
||||
//! Connection architecture: one writer connection behind the
|
||||
//! substrate's `Writer` slot (WAL single-writer modeled honestly), a
|
||||
//! pool of reader connections (fresh opens on demand up to
|
||||
//! [`SqliteOpts::max_readers`]), and one `SharedUpdateWatcher` poll
|
||||
//! thread over the db path. Every connection — writer and pooled
|
||||
//! readers alike — runs the substrate's full bootstrap at open:
|
||||
//! pragmas (WAL, `synchronous=NORMAL`, busy timeout), the notify
|
||||
//! machinery, the substrate's SQL function set, and the schema
|
||||
//! bootstrap.
|
||||
//!
|
||||
//! Watcher spawn is fallible in the substrate (ADR-012 §4's W-2); the
|
||||
//! failure maps into [`Error::Database`](alkstore::Error::Database)
|
||||
//! engine-side and `open` fails if the watcher cannot start. On
|
||||
//! watcher death the substrate's `WatcherDeathGuard` closes every
|
||||
//! subscriber (ADR-006) — the death-guard plumbing exists from open
|
||||
//! onward; the consumer-visible receiver bridging arrives with
|
||||
//! `listen`.
|
||||
//!
|
||||
//! Drop/close: the watcher's poll thread is joined (releasing its
|
||||
//! connection and the db file handle) and the writer slot + reader
|
||||
//! pool are closed; [`Drop`](SqliteStore::drop) delegates to
|
||||
//! [`SqliteStore::close`].
|
||||
//!
|
||||
//! Every engine call round-trips the `spawn_blocking` seam
|
||||
//! ([`crate::seam`]); rusqlite connections are not
|
||||
//! `Send`-across-await. Single-host by nature: file-backed, one
|
||||
//! machine; NFS two-writers-unsupported (the lineage's honesty
|
||||
//! posture, inherited — ADR-016).
|
||||
|
||||
use alkstore::Store;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::opts::SqliteOpts;
|
||||
use crate::seam::{database_error, sqlite_error};
|
||||
use crate::substrate::{Readers, SharedUpdateWatcher, Writer};
|
||||
|
||||
/// The SQLite engine's store handle. Consumers hold
|
||||
/// [`alkstore::Store`] — the trait is the contract surface; the type
|
||||
/// exists so the engine can own its machinery and so tests can reach
|
||||
/// the close path directly.
|
||||
pub struct SqliteStore {
|
||||
writer: Arc<Writer>,
|
||||
readers: Arc<Readers>,
|
||||
watcher: Arc<SharedUpdateWatcher>,
|
||||
db_path: PathBuf,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SqliteStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SqliteStore")
|
||||
.field("db_path", &self.db_path)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl SqliteStore {
|
||||
/// Explicit close: stops the watcher, disconnects every subscriber
|
||||
/// (the death-guard close — receivers see them close), closes the
|
||||
/// reader pool, and closes the writer slot. Idempotent. [`Drop`]
|
||||
/// delegates here.
|
||||
pub fn close(&self) {
|
||||
let _ = self.watcher.close();
|
||||
self.readers.close();
|
||||
self.writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SqliteStore {
|
||||
fn drop(&mut self) {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a SQLite-backed store at `path`.
|
||||
///
|
||||
/// The path is a **plain filesystem path** (ADR-023 §3): no URI
|
||||
/// interpretation — a `?name=value` suffix is part of the literal
|
||||
/// filename (`:memory:` works without URI mode). The full connection
|
||||
/// architecture boots: the writer slot, the reader pool, and the
|
||||
/// update watcher (whose spawn is fallible — open fails with
|
||||
/// [`Error::Database`](alkstore::Error::Database) if it cannot start).
|
||||
pub fn open(path: &str, opts: SqliteOpts) -> alkstore::Result<Box<dyn Store>> {
|
||||
Ok(Box::new(open_store(path, opts)?))
|
||||
}
|
||||
|
||||
fn open_store(path: &str, opts: SqliteOpts) -> alkstore::Result<SqliteStore> {
|
||||
let config = opts.watcher_config()?;
|
||||
|
||||
let writer_conn = crate::substrate::open_conn_bootstrapped(path)
|
||||
.map_err(|e| match e {
|
||||
crate::substrate::SchemaError::Sqlite(inner) => sqlite_error(inner),
|
||||
})
|
||||
.map_err(|e| database_error(format!("failed to open/bootstrap writer connection: {e}")))?;
|
||||
|
||||
let watcher = SharedUpdateWatcher::new_with_config(PathBuf::from(path), config)
|
||||
.map_err(|e| database_error(format!("failed to spawn update watcher: {e}")))?;
|
||||
|
||||
Ok(SqliteStore {
|
||||
writer: Arc::new(Writer::new(writer_conn)),
|
||||
readers: Arc::new(Readers::new(path.to_string(), opts.max_readers)),
|
||||
watcher: Arc::new(watcher),
|
||||
db_path: PathBuf::from(path),
|
||||
})
|
||||
}
|
||||
|
||||
impl Store for SqliteStore {
|
||||
fn begin_tx(
|
||||
&self,
|
||||
) -> alkstore::BoxedFuture<'_, alkstore::Result<Box<dyn alkstore::TxHandle + Send>>> {
|
||||
Box::pin(async { Err(database_error("begin_tx wiring lands with the seam task")) })
|
||||
}
|
||||
|
||||
fn notify<'a>(
|
||||
&'a self,
|
||||
_channel: &str,
|
||||
_payload: serde_json::Value,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"notify wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn listen<'a>(
|
||||
&'a self,
|
||||
_channel: &str,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::WakeReceiver>>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"listen wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn stream<'a>(
|
||||
&'a self,
|
||||
_name: &str,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::StreamHandle>>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"stream wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn queue<'a>(
|
||||
&'a self,
|
||||
_name: &str,
|
||||
_opts: alkstore::QueueOpts,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Queue>>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"queue wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn outbox<'a>(
|
||||
&'a self,
|
||||
_name: &str,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Outbox>>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"outbox wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn try_lock<'a>(
|
||||
&'a self,
|
||||
_name: &str,
|
||||
_owner: &str,
|
||||
_ttl: i64,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<Option<Box<dyn alkstore::Lock>>>> {
|
||||
Box::pin(async { Err(database_error("lock wiring lands with the mechanism tasks")) })
|
||||
}
|
||||
|
||||
fn schedule<'a>(
|
||||
&'a self,
|
||||
_name: &str,
|
||||
_spec: &str,
|
||||
_queue: &str,
|
||||
_payload: serde_json::Value,
|
||||
_opts: alkstore::ScheduleOpts,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<alkstore::Schedule>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"schedule wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn unschedule<'a>(&'a self, _name: &str) -> alkstore::BoxedFuture<'a, alkstore::Result<bool>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"unschedule wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn run_schedules<'a>(
|
||||
&'a self,
|
||||
_stop: alkstore::StopToken,
|
||||
) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
|
||||
Box::pin(async {
|
||||
Err(database_error(
|
||||
"scheduler wiring lands with the mechanism tasks",
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod open_tests;
|
||||
Reference in new issue
Block a user