fix(store): one writer at a time; a second is refused, not allowed to corrupt the books
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Max-testing pass on ledger-store, found during the survey: nothing
stopped two processes from opening the same books for writing. Two
agent sessions (two MCP servers) on one books dir would each extend the
hash chain from the same last line; one write from each broke the
journal ("seq gap: expected 3, found 2") and the books would not open.
The store now holds an exclusive lock on books-data/wanderbooks.lock
(std's File::try_lock, no new dependency) from before it reads the
journal until it is dropped; the OS releases it if the process dies. A
second writer gets StoreError::Locked saying another process has the
books open. The viewer reads through Snapshot and never takes the lock.
The lock file is git-ignored in the books scaffold.
Regression: a_second_writer_is_refused_while_the_first_has_the_books
(refused while open, allowed after close, books whole).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
c539004d1a
commit
8c8ba19471
5 files changed
+78
-1
No files matched your search
@@ -1088,3 +1088,29 @@ fn a_journal_line_with_a_float_is_refused() {
|
||||
canon
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression (max-testing pass on the store, 2026-10-13): nothing stopped
|
||||
/// two processes (say, two agent sessions, each with its own MCP server)
|
||||
/// from writing the same books. One write from each broke the hash chain
|
||||
/// ("seq gap") and the books would not open. Oracle (rank 1, R-11: one
|
||||
/// append-only log, one writer): a second writer is refused while the
|
||||
/// first is open, and allowed once it closes; the books always reopen.
|
||||
#[test]
|
||||
fn a_second_writer_is_refused_while_the_first_has_the_books() {
|
||||
let dir = test_dir("two-writers");
|
||||
drop(Store::create(&dir, Currency::USD).unwrap());
|
||||
let mut first = Store::open(&dir).unwrap();
|
||||
match Store::open(&dir) {
|
||||
Err(StoreError::Locked(why)) => assert!(why.contains("another"), "{why}"),
|
||||
Err(e) => panic!("expected Locked, got {e}"),
|
||||
Ok(_) => panic!("a second writer was allowed"),
|
||||
}
|
||||
first
|
||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
drop(first);
|
||||
// Released on close: the next writer gets in, and the books are whole.
|
||||
let reopened = Store::open(&dir).unwrap();
|
||||
reopened.verify().unwrap();
|
||||
assert_eq!(reopened.ledger().chart().len(), 1);
|
||||
}
|
||||
Reference in new issue
Block a user