This is an automated email from the ASF dual-hosted git repository.

spetz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 6a1c84c39 fix(partitions): stop reserving 16 MiB per active segment 
index (#3974)
6a1c84c39 is described below

commit 6a1c84c39f06e956699a93fbd9a25215f20840e8
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Thu Aug 27 11:04:09 2026 +0200

    fix(partitions): stop reserving 16 MiB per active segment index (#3974)
    
    ensure_indexes reserved a 16.78 MB Vec (SIZE_16MB /
    IGGY_INDEX_SIZE = 699050 entries) on the first flush of every
    active segment, for a sparse index that gains ONE entry per
    flush. At the shipped cadence a full 1 GiB segment holds about
    1024 entries, so the reservation was roughly 700x its ceiling.
    
    It stayed invisible until many partitions flushed at once.
    Graceful shutdown force-flushes every partition regardless of
    the messages_required_to_save thresholds: with 10000 partitions,
    4000 of them holding writes, peak RSS went 3043 -> 6808 MB and
    VmData 4.04 -> 64.05 GB right after SIGTERM (60.01 GB / 16.78 MB
    = 3576 reservations). Steady state is worse than the spike, since
    10000 actively written partitions would pin 168 GB.
    
    Reserve nothing and let the Vec grow: ~1024 entries over a
    segment's life is eleven reallocations and under 48 KB of memcpy,
    on a path that runs once per flush. IggyIndexCache is a bare Vec
    behind len / is_empty / push and two binary searches, and its
    lookups borrow &self, so growth cannot invalidate a held
    reference. The shutdown delta is now 0 MB / 0.00 GB, idle RSS
    unchanged.
    
    Also correct the sparse-index density in two doc comments: at
    messages_required_to_save = 1 a flush still covers a whole batch,
    so the file tracks every batch, not every message.
---
 core/partitions/src/iggy_index_reader.rs |  3 ++-
 core/partitions/src/iggy_partition.rs    |  4 +---
 core/partitions/src/log.rs               |  7 ++-----
 core/partitions/src/poll_plan.rs         | 11 ++++++-----
 4 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/core/partitions/src/iggy_index_reader.rs 
b/core/partitions/src/iggy_index_reader.rs
index 12246cb30..bccdbe27f 100644
--- a/core/partitions/src/iggy_index_reader.rs
+++ b/core/partitions/src/iggy_index_reader.rs
@@ -122,7 +122,8 @@ impl IggyIndexReader {
     /// Load every whole entry into an `IggyIndexCache` for offset / timestamp
     /// lower-bound lookups in one read. Density is one sparse entry per 
flushed
     /// chunk, so an aggressive flush cadence (`messages_required_to_save = 1`)
-    /// makes the file track every message: callers that cannot afford an
+    /// makes the file track every produced batch - every message only when
+    /// producers send one message per batch: callers that cannot afford an
     /// unbounded read must gate on [`Self::entry_count`] and fall back to the
     /// on-file lower-bound lookups. A trailing partial entry (torn write) is
     /// ignored (see [`Self::entry_count`]).
diff --git a/core/partitions/src/iggy_partition.rs 
b/core/partitions/src/iggy_partition.rs
index fb1ba35c2..17bf8aefa 100644
--- a/core/partitions/src/iggy_partition.rs
+++ b/core/partitions/src/iggy_partition.rs
@@ -3769,9 +3769,7 @@ where
         self.log.index_writers_mut()[old_segment_index] = None;
         // Drop the sealed segment's in-memory index cache: only the ACTIVE
         // segment's cache is ever read (the `commit_messages` flush staging),
-        // so a sealed cache is dead weight -- and `ensure_indexes` 
preallocates
-        // a 16 MiB-capacity `Vec` per segment, which under small-segment
-        // workloads retains hundreds of MB across thousands of sealed 
segments.
+        // so a sealed cache is dead weight.
         self.log.indexes_mut()[old_segment_index] = None;
 
         self.log
diff --git a/core/partitions/src/log.rs b/core/partitions/src/log.rs
index 6c43176f3..1dc0fcd3d 100644
--- a/core/partitions/src/log.rs
+++ b/core/partitions/src/log.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::iggy_index::{IGGY_INDEX_SIZE, IggyIndexCache};
+use crate::iggy_index::IggyIndexCache;
 use crate::iggy_index_writer::IggyIndexWriter;
 use crate::messages_writer::MessagesWriter;
 use crate::poll_plan::SealedSegmentHandle;
@@ -30,8 +30,6 @@ use std::rc::Rc;
 
 const SEGMENTS_CAPACITY: usize = 1024;
 const ACCESS_MAP_CAPACITY: usize = 8;
-const SIZE_16MB: usize = 16 * 1024 * 1024;
-
 /// Max sealed segments per partition that keep a resident read handle (fd +
 /// sparse index). Without a cap every sealed segment a reader ever touched 
pins
 /// one fd for the partition's lifetime; the server-wide budget is this cap 
times
@@ -394,8 +392,7 @@ where
             .last_mut()
             .expect("active indexes called on empty log");
         if indexes.is_none() {
-            let capacity = SIZE_16MB / IGGY_INDEX_SIZE;
-            *indexes = Some(IggyIndexCache::with_capacity(capacity));
+            *indexes = Some(IggyIndexCache::empty());
         }
     }
 
diff --git a/core/partitions/src/poll_plan.rs b/core/partitions/src/poll_plan.rs
index 03a74e2da..4fd1ea956 100644
--- a/core/partitions/src/poll_plan.rs
+++ b/core/partitions/src/poll_plan.rs
@@ -48,11 +48,12 @@ use std::sync::atomic::Ordering;
 use tracing::{error, warn};
 
 /// Byte cap for materializing a sealed segment's sparse index into its shared
-/// read-state handle. Index density is one entry per flush: at the default
-/// cadence a 1 GiB segment yields ~24 KiB of index, but
-/// `messages_required_to_save = 1` tracks every message and the same segment
-/// yields hundreds of MB, which a single poll must never read (or pin
-/// resident) in one go. At or under the cap the whole file loads once and is
+/// read-state handle. Index density is one entry per flush, never per message:
+/// at the default cadence a 1 GiB segment yields ~24 KiB of index, but
+/// `messages_required_to_save = 1` flushes once per produced batch and the 
same
+/// segment yields tens of MB (hundreds only when producers send one message 
per
+/// batch), which a single poll must never read (or pin resident) in one go.
+/// At or under the cap the whole file loads once and is
 /// cached; above it every poll binary-searches the file with single-entry
 /// preads instead, so resident index bytes per partition stay bounded by the
 /// sealed-LRU capacity times this cap.

Reply via email to