viewer: no false "database differs" while the books are being written
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
New concurrency test: one thread writes entries, drafts and receipts as
fast as it can while a Snapshot (the viewer) refreshes in a loop. At
every refresh the snapshot's books must balance, never go backward, and
never see the writer's database as differing from the journal; at the
end it must equal the writer in full. It failed 30 runs in 50:
check_disk read the applied position and the tables in separate
statements, so a write landing between them made a consistent database
look like it "differs", which the viewer shows as a problem in Needs
attention.
- check_disk reads in one read transaction (one consistent view).
- A database that has applied lines the viewer hasn't read yet is now
DiskCheck::Ahead ("viewer catching up"), not Behind.
- Without the transaction the test fails 14 runs in 20; with it, 50/50.
Also: members' equity spec 3b states the exact bound (a member's period
income is within two cents of their share, one cent of rounding at each
end; the running balance chains exactly), and the spec-blind property
checks that bound. docs/testing.md gains the concurrency layer and item
12: each close into members' capital still rounds on its own (pending).
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
37824e55d3
commit
f4381f170c
6 files changed
+165
-10
No files matched your search
@@ -40,6 +40,14 @@ pub enum DiskCheck {
|
||||
/// Journal lines there are.
|
||||
journal: u64,
|
||||
},
|
||||
/// The database has applied journal lines this snapshot hasn't read
|
||||
/// yet (normal while the writer is busy; the next refresh catches up).
|
||||
Ahead {
|
||||
/// Journal lines the database has applied.
|
||||
applied: u64,
|
||||
/// Journal lines this snapshot has read.
|
||||
journal: u64,
|
||||
},
|
||||
/// No database file (the writer rebuilds it on start).
|
||||
Missing,
|
||||
/// The database differs from the journal: what differs.
|
||||
@@ -192,19 +200,29 @@ impl Snapshot {
|
||||
|
||||
/// Compare the writer's SQLite database (opened read-only) with this
|
||||
/// snapshot's own projection of the journal, every table, row for row.
|
||||
///
|
||||
/// The applied position and the tables are read in one read
|
||||
/// transaction, so a write landing between the two reads can't make
|
||||
/// them disagree (the writer is often busy while a viewer checks: found
|
||||
/// by the concurrency test, 2026-10-13, as a false "differs").
|
||||
pub fn check_disk(&self) -> DiskCheck {
|
||||
use rusqlite::OpenFlags;
|
||||
let path = self.data_dir.join(DB_FILE);
|
||||
if !path.exists() {
|
||||
return DiskCheck::Missing;
|
||||
}
|
||||
let conn = match Connection::open_with_flags(
|
||||
let mut conn = match Connection::open_with_flags(
|
||||
&path,
|
||||
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return DiskCheck::Unreadable(e.to_string()),
|
||||
};
|
||||
// One consistent view of the database for every read below.
|
||||
let conn = match conn.transaction() {
|
||||
Ok(t) => t,
|
||||
Err(e) => return DiskCheck::Unreadable(e.to_string()),
|
||||
};
|
||||
let applied: u64 = match conn.query_row(
|
||||
"SELECT value FROM meta WHERE key='last_applied_seq'",
|
||||
[],
|
||||
@@ -214,9 +232,12 @@ impl Snapshot {
|
||||
Err(e) => return DiskCheck::Unreadable(e.to_string()),
|
||||
};
|
||||
let journal = self.journal_seq();
|
||||
if applied != journal {
|
||||
if applied < journal {
|
||||
return DiskCheck::Behind { applied, journal };
|
||||
}
|
||||
if applied > journal {
|
||||
return DiskCheck::Ahead { applied, journal };
|
||||
}
|
||||
let (disk, ours) = match (projection_rows(&conn), projection_rows(&self.conn)) {
|
||||
(Ok(d), Ok(o)) => (d, o),
|
||||
(Err(e), _) | (_, Err(e)) => return DiskCheck::Unreadable(e.to_string()),
|
||||
|
||||
Reference in new issue
Block a user