4.4. ADR-004: Binary WAL Format for Crash-Safe Storage Transactions¶
4.4.1. Status¶
Accepted
Date: 2026-05-19
4.4.2. Context¶
The cda-storage crate provides a crash-safe, transactional storage backend
for diagnostic data (MDD files, configuration). Mutations are journaled to a
Write-Ahead Log (WAL) before being applied to the filesystem. The WAL must:
Record operations durably before they are applied.
Support crash recovery: detect incomplete transactions and roll back partial commits.
Minimize I/O overhead on flash-based storage where write amplification matters.
The key decision is the WAL’s on-disk encoding: binary (e.g., rkyv, wincode) vs text-based (e.g., JSON, TOML).
4.4.3. Decision¶
Use a binary WAL format with rkyv (zero-copy deserialization) for operation payloads and CRC32 checksums per entry.
4.4.3.1. Commit Strategy¶
The WAL uses a one-phase commit with checksums (1PC+C) strategy:
Operations are appended to the WAL during the transaction without
fsync.Before applying, the header status is flipped from
RECORDINGtoCOMMITTINGvia an in-place write to the file header.A single
fsyncmakes both the status change and all entries durable.Operations are applied to the filesystem.
The WAL file is deleted (point of no return).
4.4.3.2. On-Disk Format¶
[u8 magic][u8 status][u16 reserved][u32 header_crc32] [u32 crc32][u32 len][payload] ...
|-------------- 8-byte file header -----------------| |------ per-entry data -----|
File header (8 bytes): magic
0xCA, status byte (0x00= recording,0x01= committing), 2 bytes reserved padding, CRC32 over the header fields.Entry envelope (8 + N bytes): CRC32 checksum of the payload,
u32payload length, followed by the rkyv-serializedOperationenum.All fields are little-endian. The 8-byte header and 8-byte entry headers maintain 4-byte alignment as required by rkyv deserialization.
4.4.3.3. Recovery¶
On startup, LocalStorage::new() inspects the WAL:
No WAL: clean state, nothing to do.
RECORDING: transaction never reached commit. discard WAL and staging.
COMMITTING: commit was in progress. read entries, undo applied operations via
.bakfile restoration and new-artifact removal.Truncated COMMITTING WAL with no evidence of application: discard WAL, no operations were applied.
Truncated COMMITTING WAL with evidence of partial application: return
StorageError::Corruptionso the caller can decide how to handle it.
4.4.4. Rationale¶
4.4.4.1. Why Binary over Text¶
Zero-copy deserialization. rkyv deserializes directly from the memory-mapped / read buffer without parsing or allocating. Text formats (JSON, TOML) require a full parse pass and allocate intermediate structures.
Simple deserialization. The serialized bytes are the payload directly. No text encoding layer (escaping, quoting, base64) sits between the raw operation data and its on-disk representation.
Compact. Typical
Operationpayloads are < 1 KiB. A JSON equivalent with escaped strings, keys, and formatting would be 2-5x larger, increasing flash write amplification for no benefit.Checksumming is simpler. CRC32 over raw bytes. With text, checksums would need to account for encoding differences (line endings, whitespace normalization).
4.4.4.2. Why u32 Payload Length¶
The payload length field is u32:
The WAL is an on-disk format. Using
usizewould make files non-portable between 32-bit and 64-bit targets.u32fits the 4-byte alignment rkyv requires.u16would need 2 bytes of padding for no benefit.u64(andusizeon 64-bit) would waste 4 bytes per entry as with the current operation sizes, the length will never even reachu16::MAX.Actual payloads are well under 1 KiB (the largest variant,
Operation::Write, contains three short strings bounded by filesystemNAME_MAX).u32provides ~6 orders of magnitude of headroom.
4.4.4.3. Why rkyv over Other Binary Formats¶
Zero-copy: unlike bincode or postcard, rkyv does not need a deserialization pass. The archived data is accessed in-place. Bincode was a contender, but is discontinued and should not be used for new projects.
Derive-based:
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]on theOperationenum. No manual codec.Deterministic layout: same input always produces the same bytes, making CRC32 checksums reliable.
Alignment-aware: produces 4-byte aligned output, matching the WAL entry header layout without additional padding logic.
4.4.5. Consequences¶
4.4.5.1. Positive¶
Single
fsyncper transaction commit minimizes flash wear.CRC32 per entry detects partial writes and corruption during recovery.
Zero-copy deserialization keeps recovery fast even with many entries.
Fixed-size headers simplify sequential reading and offset arithmetic.
4.4.5.2. Negative¶
The WAL is not human-readable. Debugging requires tooling (e.g., a
wal-dumputility or logging during recovery).rkyv’s archive format is not stable across major versions. A rkyv version upgrade may require a WAL migration or version field in the header. The reserved header bytes can be used for this purpose.
4.4.6. References¶
cda-storage/src/wal.rs: WAL implementationcda-storage/src/recovery.rs: startup recovery logic