Skip to main content

query_log_page

Function query_log_page 

pub fn query_log_page(
    active: &Path,
    reads_archives: bool,
    filter: &LogFilter,
    limit: usize,
    segment_cursor: Option<&SegmentCursor>,
) -> Result<LogPage>
Expand description

Read one page across the active file and every retained archive.

This is the entry point for /api/logs and the logs/query RPC. It owns segment enumeration so callers never hold a list of their own, which could go stale the moment the writer rotates.

Segments are opened one at a time as the scan reaches them, so a query holds a single descriptor regardless of how much history is retained.

§Rotation racing the listing

Enumeration and reading are not atomic, so a rotation can land between them: the listing names the pre-rotation active file, the rename turns that file into a new archive, and a fresh, near-empty active file takes its place. The scan then reads the replacement and never sees the rotated-away content — which sits, unlisted, between two segments this page did read.

That is not recoverable on a later request. Pagination walks newest to oldest, so once this page’s cursor lands in a segment older than the rotated-away one, following that cursor only ever moves further away from it. The events are skipped permanently.

Two properties make this cheap to close. Archives are immutable and only ever added or pruned as whole files, so a scan that stops before the active file cannot be racing anything and needs no check at all. And any rotation necessarily changes the set of archives on disk.

So: read, then re-enumerate, and redo the whole page if the archive set moved. Checking afterwards rather than before is what closes the window — a check before opening the file leaves room for a rotation between the check and the open. Read-then-verify has no such gap: either the set is unchanged, in which case nothing moved beneath the read, or it changed and the page is discarded and redone against the newer listing.

The same race exists inside enumeration: the archives are listed first and the active path is stat’d second, and a rotation landing between those two steps hides the rotated data from both halves — the listing predates the rename, so the new archive is absent from it, while the stat falls in the vacuum between the rename and the next append, so the active file is absent too. A scan built on that snapshot stops short of the active file without a cursor bounding it, so the skip condition below asks not just whether the scan read the active file but whether the listing had one at all: a listing without an active segment is verified like any other, while a cursor that stops before a listed active file still needs no check, because everything such a scan read is immutable.

Rotations during the redo just go round again, bounded by MAX_SNAPSHOT_ATTEMPTS. Exhausting that bound returns an error: every page produced by those attempts is known to omit a segment, so returning one as successful would let backward pagination skip retained events permanently.