Skip to content
zit One writer at a time, and tests that know it
Login

One writer at a time, and tests that know it

0.3.32 fixes one command that wrote to the repository without its lock, moves that lock into the storage layer, and adds the tests that should have caught the bug in the first place.

A zit repository is one append-only file. Every writer takes an exclusive lock on it, finds the end, and appends there. Two writers that are not serialised both find the same end and both append at it, so the second writes over the first. The overwriting frames are complete and pass every checksum, so neither zit verify nor crash recovery can tell anything happened. One of the two writes is simply gone.

zit purge undo restores buried artifacts by appending them, and it opened the store without that lock. Run while a commit, a pull or a server push was writing the same repository, it could lose its own restore or the other writer's check-in without a trace. It now opens the store the way every other writer does and waits its turn.

That is the bug. The reason it existed is the more useful part. The lock was taken by each command when it opened the file, so one command that opened the file differently skipped it, and nothing noticed. The log now takes the lock itself: opening a store for appending locks first, and only then reads the file's length and scans for its end. A store opened for reading cannot append at all, so a writer that opens the file the wrong way fails loudly. A source check in the test suite also fails on any code that opens the repository for writing outside the one shared opener.

The deterministic simulation already swept crashes, torn writes and faulty disks, always with a single writer. A lock nobody contends for is never exercised, so the whole class was invisible to it. A new sweep runs two to four writers against one simulated log, interleaved step by step, through a file that models the lock as the operating system provides it. Each writer follows a real command's order, so another writer can run between finding the file's length, opening the log, appending and committing. The file reports an unlocked append, or one that does not land at the end, the moment it happens. Each writer must see everything committed before it took the lock and read back what it just wrote, including deltas against other writers' artifacts. Removing the lock fails this sweep, and so does reading the file's length before taking it.

An end-to-end test adds the real-process version: commits from two worktrees, tag writes and user-table writes racing on one store, then a count of what landed and a clean verify.