Align Go and Rust reader positions with libsystemd

KT
1d ago
+2656-683

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_entries and the tail entry-array fields from a dedicated header mapping, so an unchanged file costs no system call. ReadAt keeps one header pread.
  • 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/... and cargo test --workspace pass.
  • Interoperability against systemd 255:
    • live matrix 8/8;
    • directory matrix PASS;
    • mixed-directory matrix 42/42.
  • run_journalctl_query_matrix.py against 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 Previous after Close at 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.