M1: SQLite persistence in a separate crate (ADR-4) #2
Reference in New Issue
Block a user
Delete Branch "m1/storage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What changed
SQLite persistence, implementing ADR-4 (
rusqlitebundled, CLAUDE.md section 3).Schedules, lock state, disable attempts and usage buckets now survive a restart.
Stacked on #1. This branch is
m1/storagebased onm0/bootstrap, which isstill unmerged. Merge #1 first, or merge this into
m0/bootstrap.9a5c52992afdc3Which requirement this covers
request, persists.
a_pending_disable_request_survives_a_restartproves afiled 12-hour request is still there after a restart, with its boot id intact
so a reboot is still distinguishable from a daemon restart.
Test evidence
Fail-open verified against the real binary, not only in tests
Truncation is covered as a separate case, because that is what a power loss
during a write actually produces and it fails differently from garbage bytes.
Performance
The M0 number was misleading and this PR corrects it. 0.279 MB excluded
SQLite entirely: the placeholder binary never referenced the crate, so
dead-code elimination dropped it. With the daemon actually opening a database,
bundled SQLite costs ~1.1 MB of the 5 MB budget, leaving 3.8 MB for D-Bus,
X11 and a UI toolkit.
That is a real margin rather than a comfortable one, and it makes ADR-4's
bundled-vs-system-linked choice a genuine trade-off with a measured price on both
sides — reclaim most of the 1.1 MB by system-linking, at the cost of a runtime
dependency on the host's libsqlite3 and a schema mismatch that only shows up on a
user's machine. Your call.
No other metric moved. Idle CPU, idle RSS and block latency are still
unmeasurable because nothing enforces anything yet — M2-Linux.
Two bugs this PR's own tests caught
1.
prune_usagenever pruned anything. The cutoff was anchored onMIN(day_index)— the oldest row. Since that row is by definition the one beingtested, the cutoff always landed before it and the delete matched zero rows. The
table grew forever, which is precisely the slow disk leak the function exists to
prevent. Anchored on
MAX(day_index)instead.2. Off-by-one in the same function. With a strict
<againstnewest - keep_days, the boundary day survived too, yieldingkeep_days + 1rows.
Both are covered by
old_usage_is_pruned, which failed on the original code andnow passes.
Notes for the reviewer
One architectural judgement call. I split storage into
blocker-storagerather than making it a module inside
blocker-core.The reason is the purity proof.
tests/purity.rsenforces "the engine does noI/O" by failing if
blocker-corehas any dependency at all — automatic,unfalsifiable, and cheap. Putting rusqlite in
blocker-corewould have forcedthat check to degrade into grepping the source for
rusqlite, which a carefuldeveloper can route around and nobody reads in six months.
blocker-storagedepends onblocker-core; the reverse edge does not exist, sothe proof survives. Both crates occupy the same layer of the section 2 diagram —
storage is shared by every platform and contains no platform code — so I read
this as refining the spec rather than departing from it. Push back if you read
section 2 as mandating a single crate.
Other things worth knowing:
serdewas deliberately not added.storage/src/codec.rsis ~400 lines ofhand-written match arms doing exactly what derive macros would do. That is a
real cost and it is the cost of not adding an unlisted dependency without
permission (section 0 rule 3, ADR-13). If serde is approved, this module is the
only thing that changes.
schedule_targetandschedule_conditionare separate tables because target rows are queried bytarget, and an index on a JSON string is not a plan.
out-of-range minute, a bad flag, a negative count — all produce a typed error.
Defaulting would turn a typo or a newer-version row into a schedule that
silently blocks the wrong thing.
history, not infinite history, and this machine has 9.4 GB free.
Checklist:
scripts/ci.shpasses, skips stated aboverusqlite, listed in CLAUDE.md section 3, ADR-4serdedecision)Do not merge this yourself
Squash or rebase. Merge #1 first — this branch is based on it.
Review approval: checked against test suite and approved per user instruction.