SQLite commit-error-arm coverage (task sqlite-commit-error-arm): the wave-3 review gate's deferred test landed — failed_commit_replenishes_the_writer_slot drives a failing COMMIT through the engine's commit path and pins the review's code-read fix end-to-end: the error surfaces as the opaque Database carrying the SQLITE_FULL-shaped source chain, the failed connection is dropped and the writer slot replenished via the handle's reopen closure (the next begin_tx proceeds within a bounded timeout, no parking — the store-wide-livelock posture), no partial-commit residue (the dropped connection's uncommitted writes read back None), the post-failure commit is a real clean commit (the fault disarms on consumption), and auto-commit notify works afterward. Injection is a cfg(test) commit-fault seam in seam.rs — a per-store Arc<AtomicBool> arm (born disarmed, armed via arm_commit_fault, take() disarms on first consumption so exactly one commit faults) whose fabricated rusqlite SqliteFailure feeds the production commit error arm rather than replicating it; the PRAGMA max_page_count route was probed live against both WAL and DELETE journal modes first and rejected: SQLite checks the page-count limit at page-allocation time, so the squeeze always fails the growth statement (SQLITE_FULL/DiskFull on the first INSERT) and leaves no transaction active for COMMIT to fail — the arm is unreachable through PRAGMA-space (also probed: the pragma is per-connection, so pre-begin arming on the writer conn would have ridden into the tx conn; the route failed on error placement, not delivery). Mechanism choice and probes documented in the seam doc comment and the task Notes. Cross-test safety is per-store scoping; parallel stores never see the arm. Replay-proofed live: with the error arm's writer_reopen replenish temporarily removed the test fails (begin_tx parks past the 5 s timeout — the stranding the review identified), reverted it passes. Plumbing follows the pg-fix-forwarder-reconnect cfg(test) precedent: fields on SqliteStore/SqliteTxHandle and a begin param are cfg-gated, production builds compile the plain path. The waves-1-2 review's optional watcher reconnect-success add rides here (taken — recorded in Notes): reconnect_success_resumes_wake_delivery drives run_poll_loop through its existing open_conn_fn seam (same instrument as the W-1 failure test), with the db file present throughout because the vanished-file route cannot reach the success body (file reappearance trips the dead-man's identity switch first): initial open + first two reconnects fail by injection, the third reconnect succeeds, and a subsequent commit wakes on_change — the success arm's data_version re-baseline and restored delivery pinned. Watcher shape untouched. Verified: cargo test -p alkstore-sqlite green server-less (191 lib + 25 suite), workspace cargo test 399/0, clippy -D warnings, fmt clean
This commit is contained in:
1 parent
36023914b2
commit
0c7744977c
6 files changed
+320
-8
No files matched your search
@@ -820,6 +820,94 @@ mod watcher_tests {
|
||||
assert_eq!(idx, 0);
|
||||
}
|
||||
|
||||
/// The reconnect loop's *success* body (the coverage doc's open
|
||||
/// line 187–202 — `shared_update_watcher`'s W-1 test drives only
|
||||
/// the failure path, and a vanished-file drive cannot test the
|
||||
/// success body: after the file reappears the identity check reads
|
||||
/// a fresh `(dev, ino)` and kills the watcher through the
|
||||
/// dead-man's switch). Driven here without the vanished-file trick
|
||||
/// and without touching the engine's watcher shape: the db file
|
||||
/// exists for the whole run (identity stable), and the
|
||||
/// `run_poll_loop`'s own `open_conn_fn` seam — the same mechanism
|
||||
/// the W-1 test instruments — fails the initial open and the first
|
||||
/// two reconnects, with the third reconnect succeeding. A commit
|
||||
/// from a separate writer connection after that success must wake
|
||||
/// `on_change` again, proving the success arm re-baselines
|
||||
/// `data_version` and restores wake delivery (and that injected
|
||||
/// failed opens do not poison the re-established connection).
|
||||
#[test]
|
||||
fn reconnect_success_resumes_wake_delivery() {
|
||||
static OPENS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
const FAIL_FIRST: u64 = 3;
|
||||
fn instrumented_open(path: &Path) -> rusqlite::Result<Connection> {
|
||||
let n = OPENS.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if n <= FAIL_FIRST {
|
||||
return Err(rusqlite::Error::InvalidPath(path.to_path_buf()));
|
||||
}
|
||||
open_watcher_conn(path)
|
||||
}
|
||||
|
||||
let tmp = temp_db("reconnect-success");
|
||||
wal_db(&tmp);
|
||||
let writer = Connection::open(&tmp).unwrap();
|
||||
writer
|
||||
.execute("CREATE TABLE IF NOT EXISTS _test_reconnect(x INTEGER)", [])
|
||||
.unwrap();
|
||||
|
||||
let wakes = Arc::new(AtomicU64::new(0));
|
||||
let wakes_t = wakes.clone();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_t = stop.clone();
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<()>(1);
|
||||
let db_path = tmp.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
run_poll_loop(
|
||||
db_path,
|
||||
move || {
|
||||
wakes_t.fetch_add(1, Ordering::Relaxed);
|
||||
},
|
||||
stop_t,
|
||||
ready_tx,
|
||||
Duration::from_millis(5),
|
||||
instrumented_open,
|
||||
)
|
||||
});
|
||||
ready_rx
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("watcher loop baseline capture");
|
||||
|
||||
// The reconnect schedule retries every MAX_RECONNECT_TICKS
|
||||
// ticks — 500 ms apart at the 5 ms cadence. Let the first two
|
||||
// reconnect attempts fail (opens 2 and 3) and the third
|
||||
// reconnect succeed (open 4).
|
||||
std::thread::sleep(Duration::from_millis(1_800));
|
||||
let opens = OPENS.load(Ordering::Relaxed);
|
||||
assert!(
|
||||
opens > FAIL_FIRST,
|
||||
"a reconnect open must have succeeded after the injected failures, saw {opens}"
|
||||
);
|
||||
|
||||
writer
|
||||
.execute("INSERT INTO _test_reconnect(x) VALUES (1)", [])
|
||||
.unwrap();
|
||||
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(3);
|
||||
while wakes.load(Ordering::Relaxed) == 0 && std::time::Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
assert!(
|
||||
wakes.load(Ordering::Relaxed) > 0,
|
||||
"no wake delivered after the successful reconnect — the success \
|
||||
body must re-baseline data_version and restore delivery"
|
||||
);
|
||||
|
||||
stop.store(true, Ordering::Release);
|
||||
handle.join().unwrap();
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
let _ = std::fs::remove_file(format!("{}-wal", tmp.display()));
|
||||
let _ = std::fs::remove_file(format!("{}-shm", tmp.display()));
|
||||
}
|
||||
|
||||
/// W-1: with the watcher connection down and the db file vanished,
|
||||
/// the reconnect path is throttled — not one open attempt per
|
||||
/// poll tick. Driven directly: a missing db path fails every open,
|
||||
|
||||
Reference in new issue
Block a user