* test(wal): pin v1 entry encoding to a Clang-written fixture
store_block_entry_clang_v1.bin is one StoreBlock entry serialized by the
current wal_entry.cpp built with Clang 21, which zero-fills the 4 bytes of
trailing Header padding. The new cases decode and verify it, re-encode the
same logical entry and require identical bytes, and round-trip fresh
entries through serialize/deserialize/verifyChecksum.
GCC 16 leaves that padding uninitialized in stableHeaderBytes(), so these
cases fail there.
Signed-off-by: trvon <me@trevon.dev>
* fix(wal): encode entry header field by field, never via padding
stableHeaderBytes() built the checksummed header with `Header stable{}`
plus member assignments and then memcpy'd sizeof(Header) bytes. That is
aggregate initialization, so the 4 bytes of trailing padding (offsets
44..47) are not guaranteed to be zero. Clang 21 memsets the object first;
GCC 16 at -O0 stores only the members, so stack garbage reached both the
serialized entry and the CRC, and verifyChecksum() failed on GCC builds.
Encode and decode the header field by field into a zero-filled 48-byte
array at the existing offsets, with static_asserts pinning every offset
and size. The checksum is CRC32 over that canonical encoding plus the
payload, which is exactly what Clang builds have always written, so their
WAL files verify unchanged. deserialize() also accepts an entry whose CRC
covers nonzero padding as stored on disk (written by earlier GCC builds)
and normalizes its in-memory checksum. WALFile and WALManager recovery
read headers through the new WALEntry::decodeHeader().
TransactionData (uint64 + uint32) has 4 bytes of trailing padding and was
copied whole into the payload, relying on the same unguaranteed zeroing
(GCC 16 and Clang 21 happen to zero it today); it is now encoded field by
field too. The remaining payload structs are padding-free, which
appendObjectBytes/readObject now enforce with
has_unique_object_representations_v.
Signed-off-by: trvon <me@trevon.dev>
---------
Signed-off-by: trvon <me@trevon.dev>
getStats() is noexcept but calls a WALManager query that iterates the
WAL directory, so an inaccessible directory terminated the daemon
instead of degrading to zeroed metrics. Catch and report zeros.
WALManager::getStats() also read lastSync/lastRotation after releasing
stateMutex while the sync and rotation paths write them under it. Read
them inside the same critical section.
Signed-off-by: trvon <me@trevon.dev>
Only prune a nonempty segment after every byte is proven zero. A zero header alone is not evidence that recovery data is absent. Scan with bounded memory and preserve the file on read failure.
Signed-off-by: trvon <me@trevon.dev>
WALManager::Config defaulted walDirectory to "./wal", so any manager built
from a default config wrote segments into the process working directory.
The repo root had accumulated 43,577 such files (42,499 zero-byte, 1,078
zero-filled 1 MiB mmap preallocations from killed writers; 1.1 GB, no
entries). The daemon already passes <dataDir>/wal explicitly.
- walDirectory has no default; initialize() returns InvalidArgument when
it is empty. The default constructor is removed (it had no callers).
- initialize() still derives the sequence from every segment name, then
deletes plain segments that never received an entry: zero bytes, or a
zero magic word at offset 0 (a preallocation left by an unclean exit).
Compressed segments and anything with real bytes are untouched.
Tests: unconfigured directory is rejected; 50 empty + 1 preallocated
segments are pruned, a non-empty one survives, and the sequence still
advances past the pruned names.
Signed-off-by: trvon <me@trevon.dev>
WalMetricsProvider::getStats() was wired (attachWalManager) but never
consumed anywhere: WAL active/pending/total entries, bytes, and log-file
count were invisible to operators. Close the optics gap:
- MetricsSnapshot: add walActiveTransactions/PendingEntries/TotalEntries/
TotalBytes/LogFileCount (detailed snapshot only).
- DaemonMetrics::enrichDetailedSnapshot: populate from
ServiceManager::getWalMetricsProvider() (cheap atomic reads).
- request_dispatcher_status: emit wal_* requestCounts on detailed status.
- metric_keys.h: kWAL* keys (wal_active_transactions, ...).
- status_command --json: render a 'wal' object when counters are nonzero.
- tests: WalMetricsProvider pass-through unit tests (zeros without a
manager; forwards WALManager stats; reset on null manager).
Validated: build/debug suite (ConfigResolver 42 cases, metrics core/status,
new WAL provider tests) plus brick (0 new cycles on touched files) and fold
(0 findings on added lines).