Skip to content

Give disks a write buffer that crashes can tear - #24

Merged
dhruvl merged 3 commits into
mainfrom
disk-sync
Aug 1, 2026
Merged

Give disks a write buffer that crashes can tear#24
dhruvl merged 3 commits into
mainfrom
disk-sync

Conversation

@dhruvl

@dhruvl dhruvl commented Aug 1, 2026

Copy link
Copy Markdown
Owner

SimDisk stays durable-at-assignment by default — an unconfigured disk draws no randomness, records no trace events, and leaves every existing hash where it was; the replay guards check exactly that. net.set_disk(name, buffered=True) makes it lie the way real storage does: writes and deletes queue in order until sync() while the host reads them back immediately, a crash drops the queue, and torn=True keeps a seeded prefix of it instead — the state a machine that lost power mid-batch actually reboots into. A prefix is the whole model, deliberately: no reordering, no half-written values, and the design doc says why it stops there. The torn draw comes from its own seed-derived stream, so a torn run makes exactly the network draws it would have made untorn.

The Raft demo is the consumer. Its record — term, vote and log — goes under one key of the host's disk, and two Safeguards flags name the syncs the node owes before answering an RPC. Dropping the one before an append is acknowledged is ablation row six: seed 0 elects a term that never heard of a committed entry, and the schedule minimizes to FIFO except one step of 1,273. With the sync restored the same scenario holds across 150 seeds, and a 5,000-seed chaos campaign on buffered, torn disks with hard power cuts runs green in under a minute at jobs=8. The vote-side sync has a one-heartbeat window that 4,000 searched seeds never hit, so it is covered by deterministic unit tests and the README says exactly that rather than claiming a detection.

dhruvl added 3 commits August 1, 2026 22:56
By default nothing changes: a write is durable at assignment, sync() is
a no-op, and an unconfigured disk draws no randomness and records no
trace events, so existing runs decide everything exactly as they did.
net.set_disk(name, buffered=True) queues writes and deletes until a
sync() while the host reads them back immediately; a crash drops the
queue, and torn=True keeps a seeded prefix of it instead - the state a
machine that lost power mid-batch actually reboots into. The prefix is
drawn from a seed-derived stream of its own so a torn run makes exactly
the network draws it would have made untorn.
Storage grows a sync() and a DiskStorage that puts the whole record -
term, vote and log - under one key on the host's disk, so a torn crash
can rewind a node to an earlier record but never pair this term with the
previous vote. Two Safeguards flags name the syncs the node owes before
it answers: sync_before_vote and sync_before_ack. The unsynced ack is
ablation row six - seed 0 elects a term that never heard of a committed
entry, minimized to FIFO except one step of 1,273 - while the synced
version holds the same scenario across 150 seeds and a 5,000-seed torn
disk chaos campaign. The vote's window is one heartbeat wide and 4,000
seeds never hit it, so that flag is covered by deterministic units and
the README says exactly that.
The supported-API table gets the set_disk row and the reworked host.disk
row, the design doc records why the crash model is a prefix and nothing
more - reordering and sector corruption would need a layer nothing else
uses, while a prefix is the failure an application can defend against -
and the README and changelog carry the short form.
@dhruvl
dhruvl merged commit 8b9dde0 into main Aug 1, 2026
9 checks passed
@dhruvl
dhruvl deleted the disk-sync branch August 1, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant