Align Go and Rust reader positions with libsystemd
KT
1d ago+2656+2656+0123456789012345678901234567890123456789-683-683-012345678901234567890123456789
Stacked on #13; merge #13 first. Completes SOW-0045 (.agents/sow/done/SOW-0045-20260528-go-reader-alignment-optimization.md).
Problem
A healthy Go Refresh() or QueryUnique() at EOF, or on an empty file, skipped the first newly appended entry. Related gaps:
- directory readers exposed a stale entry after a realtime seek;
- realtime-search ENTRY read errors were swallowed;
- reader positions after boundary moves differed from libsystemd.
Changes (Go and Rust)
- File, directory and facade readers follow the libsystemd position table:
- an explicit head or tail has no current entry;
- a failed move keeps the last returned entry current, in directory readers too;
- a directory realtime seek clears the current entry.
- Live appends after tail, after EOF and under installed filters are observed exactly once. Filters rebuild their posting state when the entry population grows.
- Realtime-search ENTRY read errors are returned.
- Go mmap live checks read
n_entriesand the tail entry-array fields from a dedicated header mapping, so an unchanged file costs no system call. ReadAt keeps one headerpread. - Rust filters record their source matches without an extra copy, so they can be rebuilt after the file grows.
Validation
- New position-contract tests in both languages:
go/journal/reader_position_contract_test.go, a 576-cell libsystemd table;rust/src/journal/src/tests/position_contract.rs.
CGO_ENABLED=0 go test ./...,CGO_ENABLED=1 go test ./cmd/...andcargo test --workspacepass.- Interoperability against systemd 255:
- live matrix 8/8;
- directory matrix PASS;
- mixed-directory matrix 42/42.
run_journalctl_query_matrix.pyagainst a pinned systemd v262 journalctl: 3586/3586 PASS.- Benchmarks:
- Rust restart paths execute no more instructions than the baseline.
- Go live backward restart adds about 23 instructions per restart (+0.4%); this cost was accepted and is recorded in the SOW.
Summary by cubic
Fixes a regression where a healthy Go Refresh() or QueryUnique() at EOF skipped the first newly appended entry, and aligns Go and Rust journal reader positions with libsystemd.
- File, directory, and facade readers follow the libsystemd position contract: an explicit head or tail has no current entry, and a failed move keeps the last returned entry current.
- Live appends after tail, after EOF, and under installed filters are observed exactly once; filters rebuild their posting state when the entry population grows, and directional cursor seeks check for growth first.
- Realtime-search ENTRY read errors are returned instead of swallowed.
- Go mmap live checks read header fields from a dedicated header mapping so an unchanged file costs no system call; Rust filters keep their source matches so a grown population can rebuild them.
- Go
PreviousafterCloseat an explicit tail returns an error instead of panicking, and the DATA hash-table guard moved into filter match resolution so refreshes reject a missing table. - Adds shared position-contract tests in both languages, including cursor-seek live growth coverage for directory and facade readers.