Upstream: rosedblabs/wal#51
Repo: rosedblabs/wal (tested on v1.3.8, Sync: true). This affects rosedblabs/rosedb v2, which replays the WAL on Open.
Severity: acknowledged, fsynced writes are lost to replay, and rosedb's Open fails with invalid crc after a single crash during a write.
Searched upstream: no matching issue in rosedblabs/wal or rosedblabs/rosedb. rosedb#56 ("Crash resilience", 2021) got the answer that rosedb "always starts normally whatever the crash".
What happens
- With
Sync: true,WAL.Writefsyncs before returning, so a crash can only tear the chunk being written, which was never acknowledged. - On restart,
Openopens the last segment andopenSegmentFileseeks to the end of the file (segment.go:116, :126). Nothing checks the tail, so the torn chunk stays. - New writes are appended after the torn bytes, fsynced and acknowledged.
- On the next restart, sequential replay (
NewReader().Next()) hits the torn chunk and returnsErrInvalidCRC(segment.go:453). Everything written after it is unreachable to replay, even though it was fsynced and acknowledged.
rosedb v2.4.0 loadIndexFromWAL (db.go:686-691) returns any non-EOF reader error from Open. In rosedb, the same crash therefore makes the database unopenable rather than silently losing data.
A related hazard: readInternal slices block[chunkOffset : chunkOffset+chunkHeaderSize] and then block[start:start+length] (segment.go:440-447) without checking either range against the segment size. On a torn tail, those reads see stale buffer bytes, and a large garbage length can panic with slice bounds out of range. Compare rosedb#315, which reports that panic from segment.go readInternal.
Reproduction
The harness is an append-only server that replays the WAL on startup. It stops at the first replay error and keeps serving, which is the most lenient behavior a consumer could choose. A client appends 200 entries, then verifies every acknowledged one.
cd examples/wals
seedfall replay -c rosedb.json -seed 1
427ms crash server during disk op #56 <- torn chunk, never acknowledged
server#2| replay stopped after 26 entries: invalid crc, the data may be corrupted
1.705s crash server <- any later crash
server#3| replay stopped after 26 entries: invalid crc …
FAIL client#1: acknowledged entry 27 lost
It shrinks to 2 faults: one crash during a write, then any crash.
Strict POSIX directories
rotateActiveSegment (wal.go:316-329) creates the next segment file but never fsyncs the WAL directory. Under seedfall's strict_dirs model, one crash after a rotation loses the whole new segment (rosedb.strict.json, seed 4, one plain crash, acknowledged entry 116 lost). This has the same ext4/XFS caveat as the hashicorp/raft and tidwall/wal directory findings: those filesystems usually persist the entry when the file is fsynced.
Verified fix
These changes are applied to a local copy in .prof/rosedb-wal:
segment.repairTail(), called fromOpenon the active segment, scans the segment with its own reader, truncates and fsyncs at the end of the last valid chunk, and resetscurrentBlockNumberandcurrentBlockSize.readInternalreturnsErrInvalidCRCwhen the header or the payload would extend past the segment size, instead of reading stale buffer bytes.rotateActiveSegmentfsyncs the WAL directory after creating the new segment.
| Variant | rosedb.json |
rosedb.strict.json |
|---|---|---|
| v1.3.8 | FAIL seed 1 (2 faults) | FAIL |
| + tail repair + bounds check | ok, 1000 seeds | FAIL seed 4 (segment lost) |
| + directory fsync on rotation | ok, 1000 seeds | ok, 1000 seeds |
Not covered: a torn record that spans blocks (ChunkTypeFirst with no Last). Reading it returns io.EOF instead of an error, so repair wouldn't truncate it. The test payloads are under 32 KB, so this path wasn't exercised.
When the failure becomes inevitable
seedfall cause re-randomizes the run from many moments and measures how often it still fails:
0s 15% █████
427.268ms 10% ███
427.268ms 75% ███████████████████████ <- after 427.268ms crash server during disk op #56, restart at 791.388ms
1.7052s 70% █████████████████████
1.7052s 100% ██████████████████████████████ <- after 1.7052s crash server, restart at 1.942589s
biggest jump: 10% -> 75% at 427.268ms
The crash in the middle of a write is what plants the bug: from that moment, three futures in four lose acknowledged data. The later crash only triggers the replay that exposes it.