Skip to content

fix(partitions): reserve offsets before confirming them - #3975

Open
krishvishal wants to merge 6 commits into
masterfrom
durable-offset-watermark
Open

fix(partitions): reserve offsets before confirming them#3975
krishvishal wants to merge 6 commits into
masterfrom
durable-offset-watermark

Conversation

@krishvishal

@krishvishal krishvishal commented Aug 27, 2026

Copy link
Copy Markdown
Member

The defect

A solo node ACKs sends from its in-memory journal. The client receives a concrete base offset before the threshold-gated flush writes it to a segment. A SIGKILL in this window loses the only record that the offset was issued.

LIFE 1  threshold = 4 messages
        offsets 0 1 2 3 | 4 5 6 7 8 9    all ACKed
        disk:   [0..3]  | RAM only         lost on SIGKILL

BOOT    counter := highest segment offset + 1 = 4

LIFE 2  next send ACKed at offset 4         already issued for another message

Two messages now share offset 4. Consumers positioned above the reissued range also miss the new messages.

offset_frontier already existed in the superblock, but stable-view traffic never persisted it because its write gate only ran on view changes.

The fix

Add offset_reserved, a monotonic ceiling on offsets that may have been issued. Before an offset can escape, the append fence reserves its block in the superblock. On boot, minting starts at this ceiling.

send -> mint -> reserve_offsets_through -> journal -+-> commit -> ACK
                   superblock write                 +-> poll tier
                                                    +-> prepare to peers

The reservation covers both primary mints and backup re-stamps. It requires one write per block, not per batch. With the default 64 Ki lease at 100,000 messages per second, this is about three fsyncs per second. A crash wastes at most one block of the u64 offset space.

The lease is configurable through [partition] offset_reservation_lease. A failed reservation rejects the append.

Why a second field?

segments  [0 ..... 3]
journal            [4 ..... 9]   ACKed, RAM only
                              ^10                              ^65537
                   offset_frontier                     offset_reserved

                   offsets known to exist              offsets that may
                                                       have been issued

Combining the fields would make valid transfer offers inside the reserved block look like rewinds. The rewind guard must instead compare against stored data: sized segments plus the resident journal. The append counter may be one lease block ahead after recovery.

Segment re-anchoring

A gap inside a segment is not recoverable. recover_segment_bounds expects contiguous offsets and truncates everything after a gap, allowing another crash to reissue confirmed offsets.

BEFORE                              AFTER BOOT RE-ANCHORING

00000000000000000000.log [0..3]     00000000000000000000.log [0..3] SEALED
  append 65537 inside it             00000000000000065537.log [empty]
  next boot truncates suffix         gap lies on the segment boundary

An empty tail that falsely claims a range is removed. A sized tail is sealed, and a new segment is created at the frontier. Graceful shutdown collapses the reservation to the frontier, so only crashes spend offsets.

This applies only to solo groups. A backup rejects prepares whose base_offset does not continue its counter.

Upgrading and rolling back

The superblock record grows from 66 to 74 bytes. Upgrading is transparent: a 66-byte record decodes with the reservation seeded from the frontier it carries. Rolling back is not, since a build that predates the field rejects a 74-byte record, so a downgrade needs the data directory wiped on every node.

Anyone running a build from an EARLIER push of this branch must also wipe their data directory before running this one. Those builds planted re-anchor gaps without the .anchor record the chain guard now requires, so this build reads such a chain as a lost segment and the solo arm tombstones the partition. master plants no gaps and edge images come only from master, so nothing deployed is affected.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Aug 27, 2026
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.35088% with 110 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.07%. Comparing base (831b2a6) to head (b10db89).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
core/partitions/src/iggy_partition.rs 90.44% 52 Missing and 10 partials ⚠️
core/shard/src/lib.rs 69.04% 13 Missing ⚠️
core/server/src/segment_recovery.rs 93.71% 9 Missing and 2 partials ⚠️
core/server/src/bootstrap.rs 78.37% 3 Missing and 5 partials ⚠️
core/partitions/src/segment_anchor.rs 94.89% 1 Missing and 4 partials ⚠️
core/partitions/src/log.rs 73.33% 1 Missing and 3 partials ⚠️
core/partitions/src/state_transfer.rs 40.00% 3 Missing ⚠️
core/configs/src/server_config/partition.rs 60.00% 2 Missing ⚠️
core/consensus/src/vsr_state.rs 97.56% 1 Missing ⚠️
core/server/src/partition_helpers.rs 98.52% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3975      +/-   ##
============================================
- Coverage     85.13%   85.07%   -0.06%     
  Complexity     1402     1402              
============================================
  Files          1226     1227       +1     
  Lines        181308   182290     +982     
  Branches     147612   148593     +981     
============================================
+ Hits         154359   155088     +729     
- Misses        22902    23102     +200     
- Partials       4047     4100      +53     
Components Coverage Δ
Rust Core 85.98% <90.35%> (-0.08%) ⬇️
Java SDK 67.29% <ø> (ø)
C# SDK 75.35% <ø> (-0.03%) ⬇️
Python SDK 90.06% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 96.24% <ø> (ø)
Go SDK 69.35% <ø> (+0.03%) ⬆️
Files with missing lines Coverage Δ
core/common/src/types/options/mod.rs 86.66% <ø> (ø)
core/configs/src/server_config/cluster.rs 99.09% <ø> (ø)
core/configs/src/server_config/defaults.rs 100.00% <100.00%> (ø)
core/configs/src/server_config/displays.rs 100.00% <ø> (ø)
core/consensus/src/impls.rs 91.49% <100.00%> (+<0.01%) ⬆️
core/journal/src/superblock.rs 92.60% <ø> (ø)
core/metadata/src/impls/recovery.rs 84.77% <100.00%> (+0.01%) ⬆️
core/partitions/src/lib.rs 0.00% <ø> (ø)
core/consensus/src/vsr_state.rs 94.40% <97.56%> (+1.38%) ⬆️
core/server/src/partition_helpers.rs 78.93% <98.52%> (+1.43%) ⬆️
... and 8 more

... and 39 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/server/src/segment_recovery.rs Outdated
@krishvishal
krishvishal force-pushed the durable-offset-watermark branch from 6a90640 to d21c33a Compare August 31, 2026 07:52

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the crash shapes reproduce on this branch with curl against a solo node: a crash below the flush threshold then a clean restart re-mints offset 0 or tombstones the partition, and a crash, one send, and a clean restart tombstones on the next boot. inline comments have the details.

outside the diff: init_partition in core/shard/src/lib.rs:3636 arms the floor on every simulator restart with no re-anchor, so the simulator diverges from the server here. the poll ceiling at core/partitions/src/iggy_partition.rs:1788-1806 serves an uncommitted offset 0 before the first commit - pre-existing, but it is how the spent-floor re-mint reaches consumers. the primary apply error at iggy_partition.rs:2676-2688 has no rollback_pipelined_prepare on the partitions plane - pre-existing, this fence is the new trigger. the doc on persist_offset_frontier_at at iggy_partition.rs:1122 still describes the install use it lost.

Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/server/src/partition_helpers.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/server/src/segment_recovery.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/server/src/segment_recovery.rs
@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Aug 31, 2026
`restore_offset_frontier` is public, so its intra-doc link to the
private `mint_floor` is a hard rustdoc error under `-D warnings`. It
failed the pre-merge Rust lane before clippy, machete or any test job
got a runner, hiding two more denials behind it: the lease default
widened with an `as` cast, and `armed_mint_floor` reads only Copy
state, so both trip the pedantic set.
`reanchor_to_offset_frontier` seals the recovered tail and plants the
next segment at the restored frontier, betting that "every reader copes"
with a hole on a segment boundary. `ensure_contiguous_chain` does not:
it walks consecutive planned bounds and demands `end + 1 == next start`,
so the boot AFTER the one that stopped the re-mint refused its own chain
and tombstoned the partition, which sends then time out against.

`offset_reserved` is what separates the two shapes. It already means
"offsets this replica may have minted", so a gap ending inside it names
offsets no file was ever meant to hold. A gap reaching past the claimed
ceiling is still a stray file, and still refuses.

Threading the ceiling in put `load_persisted_segments` one argument over
the lint ceiling and `load_partition` one line over it, so the namespace
triple collapses into the `IggyNamespace` every caller already holds and
the offset-counter restore moves out to its own function.
@krishvishal
krishvishal force-pushed the durable-offset-watermark branch from 2bfc229 to 7b66cfc Compare September 1, 2026 14:30
Comment thread core/server/src/bootstrap.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs
Comment thread core/partitions/src/segment_anchor.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/consensus/src/vsr_state.rs Outdated
Comment thread core/partitions/src/state_transfer.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/configs/src/server_config/partition.rs
Comment thread core/integration/tests/cluster/crash_offset_reuse.rs
Comment thread core/partitions/src/iggy_partition.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
@krishvishal

Copy link
Copy Markdown
Member Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Sep 1, 2026

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

round 3 against ccf5adc (head b10db89): 21 of the 22 threads check out, the lease knob tests are the one reply that is not in the push, and the backoff bypass in reserve_offsets_through needs one more change - inline. out of the diff: should_increment_offset is still named in the build_partition_fresh doc (partition_helpers.rs:528) and the shard restore comment (shard/lib.rs:3677), and the install unlink at state_transfer.rs:2172 still drops only .log and .index - unreachable for an anchor today since anchors are solo-only, so hygiene.

);
return Err(ConfigurationError::InvalidConfigurationValue);
}
if self.offset_reservation_lease == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still no zero, above-ceiling or at-ceiling case for this knob in mod tests - the reply says they were added, but ccf5adc does not touch this file.

/// rejected with nothing externalised, exactly as a failed view persist
/// withholds its sends.
///
/// BYPASSES the retry backoff, like `record_frontier_before_quarantine` and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superblock_wedged assumes one failure per backoff step (2m / 1s cap = 120), and now every refused send adds one - a few producers retrying through a short disk fault exit the node in seconds. count a failure only outside the backoff window, or make the wedge wall-clock.

// is fully synchronous, so the single load cannot drift mid-plan.
let commit_offset = self.offsets().commit_offset;
if !self.should_increment_offset || args.count == 0 {
if !self.offset_space.append_live || args.count == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_live is the wrong bit here - the comment above says this reads the committed frontier, and on a reservation-only boot it is live with offset at 0 naming nothing. gate on committed_seeded.

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants