From bc0179d6812e9667b451e6cd8facadb44d66f69a Mon Sep 17 00:00:00 2001 From: TimeToBuildBob Date: Mon, 26 Oct 2026 19:21:43 +0000 Subject: [PATCH 1/2] test(datastore): Close keeps every write acknowledged to racing writers aw-tauri closes the shared datastore at shutdown while tray and API clones may still be writing. Race four writers against Close on a real SQLite file and check with an independent reader that each writer's last acknowledged value survived, that Close is not starved, and that the closed worker rejects requests. Git-Session-Id: 290bb976-cfe5-490b-a9de-ae87ca85565a --- aw-datastore/tests/close_durability.rs | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 aw-datastore/tests/close_durability.rs diff --git a/aw-datastore/tests/close_durability.rs b/aw-datastore/tests/close_durability.rs new file mode 100644 index 000000000..5e7437985 --- /dev/null +++ b/aw-datastore/tests/close_durability.rs @@ -0,0 +1,83 @@ +//! `Datastore::close` racing active writers on the shared worker. +//! +//! aw-tauri shuts down by closing the server's datastore while tray and API +//! clones may still be writing. Every write the worker acknowledged before +//! Close must be durable afterwards, and Close must not be starved by them. +//! Close does not drain in-flight HTTP handlers; this covers worker requests. + +use aw_datastore::Datastore; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Barrier}; +use std::thread; +use std::time::Duration; + +const WRITERS: usize = 4; +const ROUNDS: usize = 3; + +#[test] +fn close_keeps_every_acknowledged_write_from_racing_writers() { + for round in 0..ROUNDS { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(format!("close-{round}.db")); + let path = path.to_str().unwrap().to_owned(); + let ds = Datastore::new(path.clone(), false); + ds.set_key_value_and_commit("initial", "ready").unwrap(); + + let barrier = Arc::new(Barrier::new(WRITERS + 1)); + let closing = Arc::new(AtomicBool::new(false)); + let writers: Vec<_> = (0..WRITERS) + .map(|id| { + let worker = ds.clone(); + let barrier = barrier.clone(); + let closing = closing.clone(); + thread::spawn(move || { + let key = format!("writer-{id}"); + // Each writer holds an acknowledged value before Close races it. + worker.set_key_value(&key, "0").unwrap(); + let mut last_ok = 0; + barrier.wait(); + for n in 1..10_000 { + match worker.set_key_value(&key, &n.to_string()) { + Ok(()) => last_ok = n, + Err(_) => { + assert!( + closing.load(Ordering::SeqCst), + "write failed before Close" + ); + break; + } + } + } + (key, last_ok) + }) + }) + .collect(); + + barrier.wait(); + closing.store(true, Ordering::SeqCst); + let closer = ds.clone(); + let (done_tx, done_rx) = mpsc::channel(); + thread::spawn(move || { + closer.close(); + done_tx.send(()).unwrap(); + }); + done_rx + .recv_timeout(Duration::from_secs(5)) + .expect("active writers starved Close"); + + let acknowledged: Vec<_> = writers.into_iter().map(|w| w.join().unwrap()).collect(); + let reader = Datastore::open_reader(path).unwrap(); + for (key, value) in acknowledged { + assert_eq!( + reader.get_key_value(&key).unwrap(), + value.to_string(), + "round {round}: acknowledged write to {key} lost by Close" + ); + } + reader.close(); + assert!( + ds.get_buckets().is_err(), + "closed worker still served a request" + ); + } +} From d7217b822fcf149adca8cb8264e65ab7859325f3 Mon Sep 17 00:00:00 2001 From: TimeToBuildBob Date: Mon, 26 Oct 2026 19:36:44 +0000 Subject: [PATCH 2/2] test(datastore): bound writer completion after Close Git-Session-Id: 25d5f566-67ab-5baf-bdb1-e98d6a142cfa --- aw-datastore/tests/close_durability.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/aw-datastore/tests/close_durability.rs b/aw-datastore/tests/close_durability.rs index 5e7437985..af3f0f516 100644 --- a/aw-datastore/tests/close_durability.rs +++ b/aw-datastore/tests/close_durability.rs @@ -9,7 +9,7 @@ use aw_datastore::Datastore; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{mpsc, Arc, Barrier}; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; const WRITERS: usize = 4; const ROUNDS: usize = 3; @@ -25,11 +25,13 @@ fn close_keeps_every_acknowledged_write_from_racing_writers() { let barrier = Arc::new(Barrier::new(WRITERS + 1)); let closing = Arc::new(AtomicBool::new(false)); + let (writer_done_tx, writer_done_rx) = mpsc::channel(); let writers: Vec<_> = (0..WRITERS) .map(|id| { let worker = ds.clone(); let barrier = barrier.clone(); let closing = closing.clone(); + let writer_done_tx = writer_done_tx.clone(); thread::spawn(move || { let key = format!("writer-{id}"); // Each writer holds an acknowledged value before Close races it. @@ -48,6 +50,7 @@ fn close_keeps_every_acknowledged_write_from_racing_writers() { } } } + writer_done_tx.send(()).unwrap(); (key, last_ok) }) }) @@ -65,6 +68,13 @@ fn close_keeps_every_acknowledged_write_from_racing_writers() { .recv_timeout(Duration::from_secs(5)) .expect("active writers starved Close"); + drop(writer_done_tx); + let deadline = Instant::now() + Duration::from_secs(5); + for _ in 0..WRITERS { + writer_done_rx + .recv_timeout(deadline.saturating_duration_since(Instant::now())) + .expect("writer did not finish after Close"); + } let acknowledged: Vec<_> = writers.into_iter().map(|w| w.join().unwrap()).collect(); let reader = Datastore::open_reader(path).unwrap(); for (key, value) in acknowledged {