99 lines
3.8 KiB
Rust
99 lines
3.8 KiB
Rust
//! Shared connection setup for the harness's own sqlite writers.
|
|
//!
|
|
//! The default rollback journal costs an fsync per commit and takes an
|
|
//! exclusive lock, so a write that overlaps a read on another connection (the
|
|
//! stats page, the hourly vacuum) fails at once with `SQLITE_BUSY`. WAL with
|
|
//! `synchronous=NORMAL` removes the per-row fsync and lets readers and the
|
|
//! writer overlap, and the busy timeout makes the remaining writer-vs-writer
|
|
//! overlap wait instead of dropping the row.
|
|
//!
|
|
//! WAL is only for databases that nothing outside the agent reads: a
|
|
//! read-only connection to a WAL database needs the `-wal` and `-shm`
|
|
//! sidecars to exist or be creatable, and hive-c0re reads as another uid
|
|
//! across the agent-owned harness dir. Such a database takes just
|
|
//! [`BUSY_TIMEOUT`].
|
|
|
|
use std::time::Duration;
|
|
|
|
use rusqlite::Connection;
|
|
|
|
/// How long a writer waits on a locked database before giving up.
|
|
pub const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// Put `conn` into WAL mode with `synchronous=NORMAL` and a busy timeout.
|
|
///
|
|
/// The timeout is set first so the journal-mode switch, which needs a lock,
|
|
/// waits too. `journal_mode` persists in the database file; the other two are
|
|
/// per connection, so every writer connection calls this.
|
|
///
|
|
/// When sqlite answers the switch with the old mode (it does that, not an
|
|
/// error, on a filesystem that cannot do WAL), this logs a warning and leaves
|
|
/// the database in that mode: a working rollback-journal database beats no
|
|
/// database.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Propagates sqlite pragma failures.
|
|
pub fn configure_wal(conn: &Connection) -> rusqlite::Result<()> {
|
|
conn.busy_timeout(BUSY_TIMEOUT)?;
|
|
let mode =
|
|
conn.pragma_update_and_check(None, "journal_mode", "WAL", |row| row.get::<_, String>(0))?;
|
|
if !mode.eq_ignore_ascii_case("wal") {
|
|
tracing::warn!(%mode, "sqlite journal_mode stayed put instead of switching to wal");
|
|
}
|
|
conn.pragma_update(None, "synchronous", "NORMAL")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn configure_wal_sets_wal_normal_sync_and_busy_timeout() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let conn = Connection::open(dir.path().join("w.sqlite")).unwrap();
|
|
configure_wal(&conn).unwrap();
|
|
|
|
let mode: String = conn
|
|
.query_row("PRAGMA journal_mode", [], |r| r.get(0))
|
|
.unwrap();
|
|
let sync: i64 = conn
|
|
.query_row("PRAGMA synchronous", [], |r| r.get(0))
|
|
.unwrap();
|
|
let busy: i64 = conn
|
|
.query_row("PRAGMA busy_timeout", [], |r| r.get(0))
|
|
.unwrap();
|
|
assert_eq!(mode, "wal");
|
|
assert_eq!(sync, 1, "NORMAL");
|
|
assert_eq!(busy, 5000);
|
|
}
|
|
|
|
#[test]
|
|
fn configure_wal_keeps_a_database_that_cannot_switch_usable() {
|
|
// An in-memory database cannot switch to WAL; sqlite answers "memory".
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
configure_wal(&conn).unwrap();
|
|
let mode: String = conn
|
|
.query_row("PRAGMA journal_mode", [], |r| r.get(0))
|
|
.unwrap();
|
|
assert_eq!(mode, "memory");
|
|
conn.execute_batch("CREATE TABLE t(x); INSERT INTO t VALUES (1)")
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_second_connection_still_sees_committed_rows() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("w.sqlite");
|
|
let writer = Connection::open(&path).unwrap();
|
|
configure_wal(&writer).unwrap();
|
|
writer.execute_batch("CREATE TABLE t(x)").unwrap();
|
|
let reader = Connection::open(&path).unwrap();
|
|
configure_wal(&reader).unwrap();
|
|
writer.execute("INSERT INTO t VALUES (1)", []).unwrap();
|
|
let n: i64 = reader
|
|
.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
|
|
.unwrap();
|
|
assert_eq!(n, 1);
|
|
}
|
|
}
|