Skip to content

An idle store is not read on every poll - #193

Merged
torstei merged 1 commit into
mainfrom
bugfix/polling-reads-tails
Oct 2, 2026
Merged

torstei merged 1 commit into
mainfrom
bugfix/polling-reads-tails

Conversation

@torstei

@torstei torstei commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Second of the changes from the full-read sweep: the polling loops. A follower polls every store it ships once a second, and most are idle on most polls. Each poll parsed the store's whole .rings (and serve() loaded its whole .grain) to find that there was nothing to send.

Change

  • Shipper (ship.rs): a store is left out of the read when its cursor sits exactly at the end of the flushed chunks, no write-ahead entries are pending, and no .sap.seal handoff is in flight. The read also follows the live edge (query_entries opens a LiveTail for a store with a position), so a chunk-level test alone would be wrong for a wal store; both guards are tested. Anything unreadable is read, so the error surfaces. The stores returned by poll_excluding are the ones read, because take pairs the list with the buffer.
  • Frames sender (ship_turn): skips serve() when the store has no chunk at or after the resume seq, from the index header and its last record (serve::nothing_from).
  • serve(): no longer copies the selection twice (about 64 B per chunk of the store) before looking at max_chunks; selects by position instead. It reads only the grain pages it will send (grain::read_pages: length prefixes walked with a bounded buffer) instead of loading the whole grain, which used to happen even when nothing was selected.

Measured

Three stores of 300,000 chunks each, idle under timberfs feed --follow-from end:

read per second peak RSS
before 48 MB 128 MB
after ~0 7 MB

Tests

The skip: exactly the tape end, one byte either side, pending .sap entries, a .sap.seal, an unreadable store. A shipper that has caught up leaves both stores out of the read and picks the grown one up alone. Mutating the skip to always-idle fails 4 tests (it would lose data); to never-idle fails 2 (the optimisation would vanish). read_pages equals the pages load returns for every range, stops at a torn tail, and is empty past the end. A mid-store serve with max_chunks sends the pages of its own chunks.

Not here, and not verified

  • query --follow's own poll still re-reads the index; it is entangled with the live-tail reconcile and the retention-overtaken note, so it is a separate change.
  • A poll that has data still reads the whole .rings, and a serve turn with data still walks the grain from the front to its first page. Both want the reader-side index view planned separately.
  • I could not confirm wake-up with a live feed run: the baseline binary does not deliver in my ad-hoc setup either (a consumer needs protocol progress reports), so that run says nothing about this change. The unit test above is the evidence.

Behaviour change

Shipper::poll no longer returns slices for idle stores. Nothing else consumes poll_raw / poll_excluding.

🤖 Generated with Claude Code

A follower polls every store it ships, once a second, and most are idle on
most polls. Each poll parsed the store's whole .rings, and serve() also
loaded its whole .grain, to find that there was nothing to send.

The shipper leaves a store out of the read when its cursor sits exactly at
the end of the flushed chunks, no write-ahead entries are pending and no
.sap.seal handoff is in flight: the read also follows the live edge, so a
chunk-level test alone would miss a wal store. The stores returned are the
ones read, because the parse pairs them with the buffer. The frames sender
skips serve() when the store has no chunk at or after the resume seq,
decided from the index header and its last record.

serve() no longer copies the store's selection twice before looking at
max_chunks, and reads only the grain pages it will send instead of loading
the whole grain: grain::read_pages walks the length prefixes with a bounded
buffer.

Three stores of 300,000 chunks, idle under feed: 48 MB/s read and 128 MB
peak RSS before, 0 MB/s and 7 MB after.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@torstei
torstei merged commit 621f0f9 into main Oct 2, 2026
7 checks passed
@torstei
torstei deleted the bugfix/polling-reads-tails branch October 2, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant