numinnex commented on code in PR #4025:
URL: https://github.com/apache/iggy/pull/4025#discussion_r3906533537


##########
core/partitions/src/poll_plan.rs:
##########
@@ -1032,6 +1034,54 @@ struct ChunkWalk {
     corrupt: bool,
 }
 
+/// A fragment sliced from a disk chunk keeps the WHOLE chunk allocation
+/// alive until the reply frame is written out, and a reply can sit in a
+/// per-connection mailbox for a while. Copy the matched bytes out when they
+/// cover less than this fraction of the chunk, so a sparse match (a
+/// `count=1` poll off a cold partition) cannot pin ~1 MiB per queued reply;
+/// a dense catch-up read keeps the zero-copy path, where retained bytes stay
+/// within this factor of shipped bytes.
+const SPARSE_CHUNK_PIN_DIVISOR: usize = 4;

Review Comment:
   Disk tier verified fixed — the bound composes across chunks and segments, 
the grow-and-re-read path is a provable no-op (`consumed == 0` means nothing 
was pushed, so `borrowed == 0`), and `chunk.len()` is the right denominator 
since `read_chunk_with_retry` never slices the buffer. Byte-for-byte parity 
survives the in-place rewrite. Two things about the framing, though.
   
   **The resident tier keeps the same pin.** `try_push_resident_entry` 
(journal.rs, `Fragment::slice(prepare.clone(), ..)`) gets no unpinning. The 
reason this was dismissed earlier — "the journal owns those bytes anyway" — 
does not hold: `Journal::evict_prefix` (journal.rs:553) calls 
`inner.storage.drain()`, which is `std::mem::take(entries)` 
(journal.rs:282-291), on the routine commit-flush path. The journal releases 
its refcount and a still-queued reply becomes sole owner of the whole prepare 
allocation. That window is unbounded on single-replica groups, where the 
`evicted_ring` backstop is gated off by `repair_retention` 
(iggy_partition.rs:504-506).
   
   This is introduced here, not pre-existing: on master 
`build_polled_messages_body` consumed `PollFragments` by value and dropped 
every refcount before the reply was enqueued, so no fragment outlived 
`handle_poll_messages` on either tier.
   
   Magnitude is workload-dependent and mostly benign — `batch_length` defaults 
to 1000 on both producer and consumer, so the default path selects whole 
batches, `borrowed` is approximately the whole prepare, and the predicate would 
not copy anyway. The bad case is a small `count` against a large-batch 
producer, where a partial selection ships ~1.3 KiB and pins the entire batch.
   
   One caveat if you reuse this helper there: `execute_resident()` runs inline 
on the shard pump (the disk path is detached through `spawn_poll_io`), so a 
ratio-gated copy could put a ~1 MiB memcpy on the pump. An absolute byte floor 
(copy when the selected bytes are under ~64 KiB, regardless of ratio) keeps the 
copy bounded and fires only on the at-most-two partial batches per poll.
   
   **The stated bound is not what the predicate delivers.** "retained bytes 
stay within this factor of shipped bytes" (and "4x shipped bytes" in the commit 
message) overstates it three ways: it is disk-tier only; `borrowed` excludes 
the owned rewritten-header fragments that are also shipped, so the denominator 
is not shipped bytes; and the copy allocates 4096-aligned, so a chunk 
contributing 300 B still retains a page. Suggested replacement:
   
   > On the disk tier, the chunk allocations a poll reply keeps alive are 
bounded by `SPARSE_CHUNK_PIN_DIVISOR` times the record bytes it serves from 
disk, plus one page per compacted chunk. This is a ratio, not an absolute cap — 
a grown chunk can still retain tens of MiB — and it does not cover the resident 
tier or the reply's absolute size.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to