diff --git a/crates/devkit-ports/src/run.rs b/crates/devkit-ports/src/run.rs index a9db04abc..3509f8663 100644 --- a/crates/devkit-ports/src/run.rs +++ b/crates/devkit-ports/src/run.rs @@ -1173,7 +1173,11 @@ mod tests { } std::thread::sleep(std::time::Duration::from_millis(100)); } - assert!(ready, "server never started listening"); + assert!( + ready, + "server never started listening on {port}; its log:\n{}", + std::fs::read_to_string(&tmp).unwrap_or_else(|e| format!("unreadable: {e}")) + ); // Cleanup: stop the spawned pid and release the reservation. if let Some(pid) = out[0].pid { diff --git a/crates/devkit-rules/src/cache.rs b/crates/devkit-rules/src/cache.rs index f57110d34..a59adb8d8 100644 --- a/crates/devkit-rules/src/cache.rs +++ b/crates/devkit-rules/src/cache.rs @@ -4,10 +4,10 @@ //! Every reader loads the whole [`RuleIndex`] and filters it in memory, so //! the file keeps each file and rule as JSON in index order, beside a `meta` //! row naming the source kind, repository, remote and revision it holds. A -//! refresh replaces everything in one transaction, so a reader sees the -//! whole old index or the whole new one. A file whose format, kind, -//! repository or remote is not this cache's reads as no cache, and the next -//! refresh replaces it. +//! refresh replaces everything in one WAL-mode transaction, so a reader sees +//! the whole old index or the whole new one and never waits on a refresh. +//! A file whose format, kind, repository or remote is not this cache's reads +//! as no cache, and the next refresh replaces it. use std::{ path::{Path, PathBuf}, @@ -22,8 +22,8 @@ use crate::model::{Rule, RuleFile, RuleIndex}; /// The layout this version of devkit reads and writes. const FORMAT: i64 = 1; -/// How long a connection waits on another's lock: a reader on a refresh, -/// or one refresh on another. +/// How long a connection waits on another's lock, chiefly one refresh on +/// another. const BUSY: Duration = Duration::from_secs(2); const LAYOUT: &str = " @@ -130,7 +130,7 @@ impl RuleCache { } let mut conn = Connection::open(&self.path).with_context(writing)?; conn.busy_timeout(BUSY).with_context(writing)?; - conn.pragma_update(None, "journal_mode", "DELETE") + conn.pragma_update(None, "journal_mode", "WAL") .with_context(writing)?; let tx = conn .transaction_with_behavior(TransactionBehavior::Immediate) diff --git a/crates/devkit-rules/tests/cache.rs b/crates/devkit-rules/tests/cache.rs index b0bd9daa4..819c834c6 100644 --- a/crates/devkit-rules/tests/cache.rs +++ b/crates/devkit-rules/tests/cache.rs @@ -142,6 +142,21 @@ fn at_state_dir_names_the_file_by_kind_and_repository() { ); } +/// A reader reads the whole old index straight through a refresh that has +/// not committed, rather than waiting on it. +#[test] +fn a_refresh_in_progress_does_not_hold_up_a_reader() { + let dir = tempfile::tempdir().unwrap(); + let cache = cache(dir.path(), "a"); + cache.write(Write::Replace, 1, &index()).unwrap(); + let mut refresh = rusqlite::Connection::open(cache.path()).unwrap(); + let tx = refresh + .transaction_with_behavior(rusqlite::TransactionBehavior::Exclusive) + .unwrap(); + tx.execute_batch("DROP TABLE rules").unwrap(); + assert_eq!(value(&cache.read().unwrap().unwrap()), value(&index())); +} + /// Whether `e` is SQLite giving up on a lock another writer held past the /// busy timeout, which leaves the cache as it was. fn is_busy(e: &anyhow::Error) -> bool {