Problem
Segment rolls are currently reactive: the data plane seals when size_bytes >= limit, checked on commit. The roll then travels the metadata log — seal → RollSegment Raft commit → new-segment assignment delivered to the data plane — before the range can serve the next offset.
That opens a write-availability gap at every roll boundary. While the roll is in flight the range's write head is in limbo: the old segment is full/sealing and the successor isn't ready yet (producers hit the retriable "segment not ready / not write leader" window — see d6). A caught-up tail consumer inherits the gap: it drains the sealing segment to its end, then waits out the roll.
To be precise about what this is not: it is not a disk cold read — the data on both sides of the boundary is hot in cache, nothing loads from a segment file. The stall is purely the wait for the roll to materialize (a Raft round-trip to the shard group's majority + assignment propagation). It shows up as periodic latency jitter at each ~1 GB roll, which matters for latency-sensitive tailing.
Root cause / tradeoff
EastGuard places each segment independently (the successor can land on different nodes) — the property that buys flexible scaling and the D5 repair "lottery." That independence is exactly what forces coordination on a roll. Same situation as Pulsar (per-ledger ensemble) and Northguard (segments assigned to potentially new brokers). Kafka avoids the gap entirely by not placing segments independently: a partition's segments share its replica set, so a roll is a local file open with no coordination.
Proposed fix — anticipatory rolling
Decouple the roll trigger from the write-head switch:
- Trigger the roll at a soft high-water mark (e.g. ~90% of the limit), not at the hard cap.
- Keep the current segment writable while the roll is in flight. The
RollSegment commit + assignment delivery overlap with continued writes to the current segment; by the time it would reach the hard cap, the successor's tracker, placement, and connections are already live.
- The seal still fixes the old segment's end at its real committed end, and the switch is gated on "successor ready" — which has already happened. The tail consumer flows into the successor with no gap.
Size the slack between the soft mark and the hard cap to cover the roll latency at the segment's write rate.
Offset-continuity wrinkle (vs Pulsar)
EastGuard keeps continuous offsets across a range's segment chain (segment[N].end_offset + 1 == segment[N+1].start_offset), and the old segment's end isn't known until it actually seals. So we can't fully pre-bake the successor's start offset the way Pulsar can (per-ledger entry IDs restart at 0). Hence the early-trigger + write-headroom shape rather than pure pre-creation: the expensive coordination (placement decision, assignment, data-plane readiness) is hidden under the tail of the current segment, while the start-offset binding still happens at the real seal.
Scope
- Server-side, data-plane concern. The client / consumer prefetch path cannot fix this — it's the roll latency itself, not a client resolution cost.
- Relevant docs:
diagrams/data-plane/d3_segment_roll_integration.md, and the size-based-seal note in CLAUDE.md.
Prior art
Problem
Segment rolls are currently reactive: the data plane seals when
size_bytes >= limit, checked on commit. The roll then travels the metadata log — seal →RollSegmentRaft commit → new-segment assignment delivered to the data plane — before the range can serve the next offset.That opens a write-availability gap at every roll boundary. While the roll is in flight the range's write head is in limbo: the old segment is full/sealing and the successor isn't ready yet (producers hit the retriable "segment not ready / not write leader" window — see
d6). A caught-up tail consumer inherits the gap: it drains the sealing segment to its end, then waits out the roll.To be precise about what this is not: it is not a disk cold read — the data on both sides of the boundary is hot in cache, nothing loads from a segment file. The stall is purely the wait for the roll to materialize (a Raft round-trip to the shard group's majority + assignment propagation). It shows up as periodic latency jitter at each ~1 GB roll, which matters for latency-sensitive tailing.
Root cause / tradeoff
EastGuard places each segment independently (the successor can land on different nodes) — the property that buys flexible scaling and the D5 repair "lottery." That independence is exactly what forces coordination on a roll. Same situation as Pulsar (per-ledger ensemble) and Northguard (segments assigned to potentially new brokers). Kafka avoids the gap entirely by not placing segments independently: a partition's segments share its replica set, so a roll is a local file open with no coordination.
Proposed fix — anticipatory rolling
Decouple the roll trigger from the write-head switch:
RollSegmentcommit + assignment delivery overlap with continued writes to the current segment; by the time it would reach the hard cap, the successor's tracker, placement, and connections are already live.Size the slack between the soft mark and the hard cap to cover the roll latency at the segment's write rate.
Offset-continuity wrinkle (vs Pulsar)
EastGuard keeps continuous offsets across a range's segment chain (
segment[N].end_offset + 1 == segment[N+1].start_offset), and the old segment's end isn't known until it actually seals. So we can't fully pre-bake the successor's start offset the way Pulsar can (per-ledger entry IDs restart at 0). Hence the early-trigger + write-headroom shape rather than pure pre-creation: the expensive coordination (placement decision, assignment, data-plane readiness) is hidden under the tail of the current segment, while the start-offset binding still happens at the real seal.Scope
diagrams/data-plane/d3_segment_roll_integration.md, and the size-based-seal note inCLAUDE.md.Prior art