posts/0004.md · 2026-05-09
ACID hardening — atomic persist, WAL fsync, graceful shutdown
Durability used to be 'mostly, probably'. Now it's 'ack means on disk', verified by two crash-test harnesses.
The persist path was `fs::write` — not atomic, not durable. It got rewritten to: write a `.tmp` file, fsync its data, atomic rename onto the canonical name, fsync the parent directory. A per-collection mutex serializes concurrent commits so two writers can't both `truncate(true).open()` the same tmp file and mangle each other's bytes.
`OXIDB_LAZY_SYNC` defaulted to true. Strict mode now fsyncs every commit; the env flag is still there for benchmark configurations that explicitly want lazy. The tx-commit path used to no-op `log_wal_batch` on the B-tree backend — now durability runs through WAL fsync at commit, not a synchronous full-image persist. `sync_writes` no longer truncates the WAL inline; the truncate moved to a new `final_checkpoint` that runs at shutdown only. The old inline truncate was racy with concurrent writers and lost about three of two thousand acks under load.
Graceful shutdown: `oxidb-server` installs a SIGTERM/SIGINT handler that calls `OxiDb::shutdown` (flush + final checkpoint) before `process::exit`. SIGPIPE is ignored — otherwise a dropped client kills the worker mid-write.
Two new harnesses verify it:
tests/crash-recovery-go: 2000 inserts → SIGKILL → reopen.
Every acked write must survive.
No `.btree.tmp` leftovers.
tests/atomicity-go: Three scenarios — pre-commit
SIGKILL, post-commit SIGKILL,
mid-tx SIGTERM. Two collections
with a foreign-key link, must
recover all-or-nothing.
Both green. Multi-collection transactions actually buy you atomicity now, not just isolation hopefully.