Skip to content

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

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

fix(partitions): reserve offsets before confirming them#3975
krishvishal wants to merge 9 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 91.81102% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.13%. Comparing base (ca0b578) to head (6ba8a2b).

Files with missing lines Patch % Lines
core/shard/src/lib.rs 69.76% 13 Missing ⚠️
core/server/src/segment_recovery.rs 93.92% 10 Missing and 1 partial ⚠️
core/server/src/partition_helpers.rs 91.26% 3 Missing and 6 partials ⚠️
core/partitions/src/segment_anchor.rs 95.00% 3 Missing and 4 partials ⚠️
core/partitions/src/log.rs 73.33% 1 Missing and 3 partials ⚠️
core/partitions/src/state_transfer.rs 70.00% 3 Missing ⚠️
core/server/src/boot/recovery.rs 95.00% 2 Missing ⚠️
core/simulator/src/lib.rs 83.33% 2 Missing ⚠️
core/consensus/src/vsr_state.rs 97.56% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #3975       +/-   ##
=============================================
- Coverage     85.17%   73.13%   -12.05%     
  Complexity     1402     1402               
=============================================
  Files          1237     1237               
  Lines        181722   167455    -14267     
  Branches     148015   133749    -14266     
=============================================
- Hits         154783   122463    -32320     
- Misses        22882    40961    +18079     
+ Partials       4057     4031       -26     
Components Coverage Δ
Rust Core 71.03% <91.81%> (-15.08%) ⬇️
Java SDK 67.29% <ø> (ø)
C# SDK 75.37% <ø> (-0.03%) ⬇️
Python SDK 90.06% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 94.48% <ø> (-1.77%) ⬇️
Go SDK 69.36% <ø> (+0.07%) ⬆️
Files with missing lines Coverage Δ
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/configs/src/server_config/partition.rs 97.16% <100.00%> (+1.28%) ⬆️
core/consensus/src/impls.rs 91.49% <100.00%> (+0.04%) ⬆️
core/journal/src/superblock.rs 93.49% <ø> (+0.88%) ⬆️
core/metadata/src/impls/recovery.rs 84.77% <100.00%> (+0.01%) ⬆️
core/partitions/src/iggy_partition.rs 89.92% <ø> (+0.85%) ⬆️
core/partitions/src/lib.rs 0.00% <ø> (ø)
core/consensus/src/vsr_state.rs 94.40% <97.56%> (+1.38%) ⬆️
... and 8 more

... and 264 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
@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 Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
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 Outdated
Comment thread core/integration/tests/cluster/crash_offset_reuse.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
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.

Comment thread core/configs/src/server_config/partition.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
@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
@krishvishal
krishvishal force-pushed the durable-offset-watermark branch from b10db89 to 37efb37 Compare September 2, 2026 04:31
@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 2, 2026
@krishvishal

Copy link
Copy Markdown
Member Author

The three out-of-diff items from the review body are fixed too:

  • partition_helpers.rs:528 and shard/lib.rs:3677 no longer name should_increment_offset; they say OffsetSpace / OffsetSpace::append_live.
  • state_transfer.rs:2172 chains anchor_cleanup_path after .log and .index, with a comment recording that it is unreachable today because anchors are solo-only.

All six round-3 items are in the working tree, not in a push yet, so this head still shows none of them. Verification after the six: fmt, sort --no-format, clippy --all-features --all-targets -D warnings clean; 1005 unit tests across partitions, configs, server, shard and consensus; cluster::crash_offset_reuse 6/6 on a rebuilt server.

@krishvishal
krishvishal force-pushed the durable-offset-watermark branch from a3de9df to 71a0886 Compare September 2, 2026 13:29
`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 71a0886 to c701c7b Compare September 2, 2026 17:36
@krishvishal

Copy link
Copy Markdown
Member Author

/ready

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review PR is waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants