book.log is a single flat file with three regions:
Offset 0 Offset 4096 Offset 8192
+-----------+ +-----------+ +-------+-------+-------+-----+
| Header A | | Header B | | Deed | Deed | Deed | ... |
| (32 bytes | | (32 bytes | | #1 | #2 | #3 | |
| + pad) | | + pad) | | | | | |
+-----------+ +-----------+ +-------+-------+-------+-----+
Each header slot is page-aligned at 4096 bytes. Deeds (events) start at byte 8192 and are appended sequentially. A companion meta.bin file (256 bytes, fixed format) stores ship identity, lifecycle, and fake-network flag separately.
| Field | Size | Purpose |
|---|---|---|
mag_w |
4B | Magic 0x424f4f4b ("BOOK") |
ver_w |
4B | Format version (1) |
fir_d |
8B | Epoch base -- the event number before the first stored event |
las_d |
8B | Last committed event number |
seq_d |
8B | Monotonic sequence counter for double-buffer selection |
crc_w |
4B | CRC32 of all preceding fields |
fir_d is write-once. Events stored in the file are numbered fir_d+1 through las_d. For an epoch starting at event 500, fir_d = 500 and the first deed corresponds to event 501.
Each deed on disk is:
+----------+-------------------+----------+
| len_d | buffer_data | let_d |
| (8 bytes)| (len_d bytes) | (8 bytes)|
+----------+-------------------+----------+
len_d: size of the opaque bufferbuffer_data: the serialized event (opaque bytes, minimum 4)let_d: echo oflen_d, enabling backward scanning from the end of the file
The deed contains no event number -- the position is implicit. The Nth deed in the file is event fir_d + N.
This is the core crash-safety mechanism, borrowed from LMDB's design.
The invariant: At all times, at least one of the two header slots contains a valid, self-consistent snapshot of the log state. A reader can always recover by picking the valid header with the higher seq_d.
A single header write is not atomic with respect to power loss. If you crash mid-write to a single header, you lose your commit marker. With two slots:
- Slot A and Slot B alternate as the "active" (most recent) header
- You only ever write to the inactive slot
- If that write is interrupted, the active slot still has the previous valid state
- Once the write +
fsync()completes, the newly-written slot becomes active (it has a higherseq_d)
The u3_book handle caches:
hed_u: the current valid header contentsact_w: which slot (0=A, 1=B) is currently activelas_d: last committed event number (mirrorshed_u.las_dafter init)off_d: the file offset where the next deed should be appended
_book_read_head() reads both slots, validates each (magic, version, CRC via _book_head_okay()), and _book_take_head() selects:
- If both valid: pick the one with higher
seq_d - If only one valid: use that one
- If neither valid: fail
This handles the crash-during-header-write case: the interrupted slot will have a bad CRC and be rejected.
_book_save_head() encapsulates the commit protocol:
- Increment
seq_d - Recompute
crc_wover the header pwrite()the header to the inactive slot (opposite ofact_w)c3_sync()(fsync) -- this single syscall makes both the deed data written earlier and the new header durable atomically- Swap
act_win memory
The key insight: deed data is written to the file before the header update. Until the fsync completes and the header is durable, the old header remains authoritative. If a crash occurs after deeds are written but before the header fsync, the old header's las_d still points to the previous last event -- the orphaned deed bytes at the end are harmless garbage that recovery will truncate.
This is the main write path.
if empty log (fir_d == 0 && las_d == 0):
first event must equal epo_d + 1 <- epoch boundary check
set fir_d = epo_d <- write-once epoch base
else:
eve_d must equal las_d + 1 <- contiguity check
Rejects gaps and overlaps. The epoch parameter (epo_d) is only meaningful for the very first save to an empty log.
The function uses scatter-gather I/O (pwritev) for efficiency. Each deed requires 3 iovec entries: [&siz_u[i], buf, &siz_u[i]] -- note that the same siz_u element serves as both the header and trailer since len_d == let_d.
Because pwritev has an IOV_MAX limit (typically 1024), deeds are chunked into groups of 340 (1020 / 3):
allocate siz_u[len_d] <- one c3_d per event (serves as both len and let)
allocate iov_u[min(len_d*3, 1020)]
for each event:
reject if size < 4 bytes
siz_u[i] = size
for each chunk of up to 340 deeds:
build 3 iovecs per deed:
[0] -> &siz_u[i], 8 bytes (length header)
[1] -> buffer, N bytes (event data)
[2] -> &siz_u[i], 8 bytes (length trailer)
pwritev(fd, iovecs, count*3, offset)
advance offset
A single pwritev call writes an entire chunk of deeds atomically at the kernel level. For the common case (batch of ~1000 events), this is just 3 syscalls: one pwritev, one pwrite (header), one fsync.
new_las_d = eve_d + len_d - 1
hed_u.las_d = new_las_d
_book_save_head(txt_u) <- increment seq, CRC, write inactive slot, fsync, swap
txt_u->las_d = new_las_d <- update cached state
txt_u->off_d = now_d <- advance append cursor
The in-memory las_d and off_d are only updated after the fsync succeeds. If _book_save_head() fails, the caller sees c3n and the cached state hasn't advanced, so a retry would attempt to write the same events again.
u3_book_init() handles three cases:
New file (st_size == 0): Parse epoch from directory name, write both header slots identically via _book_make_head(), extend file to 8192 bytes.
Corrupt file (st_size < 8192): Reject.
Existing file: Read headers via _book_read_head(), then run a two-phase recovery:
-
_book_scan_back()(fast path): Read the trailinglet_dof the last deed from the end of the file, compute where that deed should start, read it forward to verifylen_d == let_d. If valid, trust the header'slas_dand set the append offset to the file end. This is O(1) -- a single deed validation. -
_book_scan_fore()(recovery fallback): If the backward scan fails (truncated write, corruption), scan forward fromBOOK_DEED_BASE, validating each deed. Stop at the first invalid one. Truncate the file at that point, update the header via_book_save_head()to reflect the recovered event count. This is O(N) in the number of events but only runs after a detected anomaly.
Both u3_book_read() and the walk iterator scan forward from BOOK_DEED_BASE, skipping deeds until reaching the target event. This is O(N) to seek -- acceptable because reads are sequential replays, not random lookups. The skip is cheap: read 8 bytes (len_d), advance by len_d + 16.
The system guarantees that after u3_book_save() returns c3y:
- All deed bytes are on disk
- The header in the now-active slot reflects
las_dcovering those deeds - The CRC on that header is valid
If a crash occurs at any point:
- Before fsync: Old header is still active, old
las_dis authoritative. Written-but-uncommitted deed bytes are ignored or truncated by recovery. - During header pwrite: The other slot still has the previous valid header. The interrupted slot will have a bad CRC and be skipped.
- After fsync: Committed. Both slots may be valid, but the new one has higher
seq_dand wins.
Today's benchmarks on MBP '24 with M4 Pro as of cdbcb1fd8c8ef8826595d02b977fb76e7852786a:
Event batch size histogram:
Results: