Skip to content

feat(partitions): implement state transfer - #3808

Open
numinnex wants to merge 15 commits into
masterfrom
partition_superblock
Open

feat(partitions): implement state transfer#3808
numinnex wants to merge 15 commits into
masterfrom
partition_superblock

Conversation

@numinnex

@numinnex numinnex commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Implement the StateTransfer protocol for partition plane. Additionally use the superblock mechanism introduced in #3767 inside of the partition plane.

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

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.05922% with 413 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.58%. Comparing base (0a23e16) to head (6b5957b).

Files with missing lines Patch % Lines
core/server-ng/src/segment_recovery.rs 0.00% 75 Missing ⚠️
core/server-ng/src/dispatch.rs 47.82% 60 Missing ⚠️
core/server-ng/src/bootstrap.rs 5.08% 56 Missing ⚠️
core/server-ng/src/responses.rs 19.64% 45 Missing ⚠️
core/partitions/src/iggy_partition.rs 88.94% 39 Missing and 3 partials ⚠️
core/server-ng/src/partition_helpers.rs 72.18% 36 Missing and 6 partials ⚠️
core/server-ng/src/dispatch/authz.rs 34.28% 23 Missing ⚠️
core/server-ng/src/auth.rs 33.33% 12 Missing ⚠️
core/server-ng/src/server_error.rs 0.00% 12 Missing ⚠️
core/consensus/src/state_manifest.rs 0.00% 9 Missing ⚠️
... and 11 more
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3808      +/-   ##
============================================
- Coverage     76.40%   75.58%   -0.83%     
  Complexity     1046     1046              
============================================
  Files          1334     1337       +3     
  Lines        165228   168037    +2809     
  Branches     137583   140469    +2886     
============================================
+ Hits         126242   127007     +765     
- Misses        35271    37175    +1904     
- Partials       3715     3855     +140     
Components Coverage Δ
Rust Core 75.45% <65.05%> (-0.95%) ⬇️
Java SDK 63.67% <ø> (ø)
C# SDK 71.13% <ø> (-1.14%) ⬇️
Python SDK 88.14% <ø> (ø)
PHP SDK 82.97% <ø> (ø)
Node SDK 96.36% <ø> (+0.08%) ⬆️
Go SDK 43.08% <ø> (ø)
Files with missing lines Coverage Δ
core/binary_protocol/src/consensus/operation.rs 96.77% <100.00%> (+0.07%) ⬆️
core/consensus/src/impls.rs 83.60% <100.00%> (+0.04%) ⬆️
core/consensus/src/lib.rs 0.00% <ø> (ø)
core/journal/src/lib.rs 25.00% <ø> (ø)
core/metadata/src/impls/metadata.rs 69.20% <100.00%> (-0.36%) ⬇️
core/partitions/src/journal.rs 80.25% <100.00%> (+2.03%) ⬆️
core/partitions/src/lib.rs 0.00% <ø> (ø)
core/partitions/src/state_transfer.rs 26.60% <ø> (ø)
core/partitions/src/types.rs 34.48% <ø> (ø)
core/shard/src/lib.rs 49.36% <ø> (-8.72%) ⬇️
... and 22 more

... and 57 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.

numinnex and others added 3 commits August 3, 2026 16:02
Removing the namespace-exemption import left its #[cfg(debug_assertions)]
attribute orphaned onto the unconditional server_common import below it, so
every release build of the shard crate fails with 112 unresolved-name
errors. Debug builds keep the import and CI's -D warnings lane rides
clippy, which builds debug, so nothing upstream could observe it.

Co-Authored-By: Claude Fable 5 <[email protected]>
@numinnex numinnex changed the title feat(partitions): persist partition VSR state in a durable superblock feat(partitions): implement state transfer Aug 4, 2026
Comment thread core/shard/src/lib.rs
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/state_transfer.rs
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.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 4, 2026
@numinnex

numinnex commented Aug 4, 2026

Copy link
Copy Markdown
Contributor 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 Aug 4, 2026
Comment thread core/partitions/src/state_transfer.rs
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs
Comment thread core/shard/src/lib.rs Outdated
Comment thread core/server-ng/src/segment_recovery.rs Outdated
Comment thread core/partitions/src/iggy_partition.rs Outdated
Comment thread core/shard/src/lib.rs
Comment thread core/shard/src/lib.rs Outdated
Comment thread core/shard/src/lib.rs Outdated
Comment thread core/server-ng/src/partition_helpers.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 Aug 4, 2026
@numinnex

numinnex commented Aug 5, 2026

Copy link
Copy Markdown
Contributor 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 Aug 5, 2026
mmodzelewski
mmodzelewski previously approved these changes Aug 5, 2026
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs
Comment thread core/partitions/src/state_transfer.rs
Comment thread core/partitions/src/iggy_partition.rs
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/server-ng/src/partition_helpers.rs
Comment thread core/shard/src/lib.rs
@github-actions github-actions Bot removed the S-waiting-on-review PR is waiting on a reviewer label Aug 5, 2026
@github-actions github-actions Bot added the S-waiting-on-author PR is waiting on author response label Aug 5, 2026
@numinnex

numinnex commented Aug 5, 2026

Copy link
Copy Markdown
Contributor 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 Aug 5, 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.

the offset/op frontier is claimed in four places that disagree about what the claim means (boot re-derivation, the install fence, the sequencer after clear_all, the unlink window). since replicas re-stamp base_offset from their local counter, a wrong claim becomes divergent persisted bytes rather than an inconvenience. the partition superblock already spends a field on a commit_max its own comment calls a dead write - giving the frontier a durable home there closes most of the family at once.

two decisions sit above the individual comments: the StateTransferTargetHeader mid-struct layout change in the published binary_protocol crate needs an explicit call (tail-append plus a real version signal if mixed-version links should ever work)

Comment thread core/server-ng/src/bootstrap.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/binary_protocol/src/consensus/header.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/shard/src/lib.rs
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.rs Outdated
Comment thread core/partitions/src/state_transfer.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 Aug 5, 2026
@numinnex

numinnex commented Aug 5, 2026

Copy link
Copy Markdown
Contributor 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 Aug 5, 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.

shard/lib.rs:5046 (pre-existing): a manifest whose first entry isn't METADATA_SNAPSHOT - unknown kind, or a checksum mismatch ahead of it - leaves generation = None, so :5116 short-circuits, burn_decode_failure never charges, and :5138 re-arms the same peer: an unbounded full-manifest re-pull loop. it needs a hostile or buggy peer today (partition kinds route to the partition arm, which refuses unknown kinds and backs off), but the fix is cheap - the accept-time generation at :4588-4591 is computed and then discarded; store it on the session and the charge becomes unconditional.

// would pass the serve gate and offer that emptiness onward, making a peer
// unlink its own chain. Leaving it `None` costs one spurious full
// re-transfer on the legitimate empty-install restart; a false caught-up
// claim is not recoverable. A durable home for the frontier (the partition

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.

this comment is right, and it's the blocker: the frontier's only durable carrier is a segment file name. three routes leave the counter at 0 while the group is at N - the unsynced empty-install dirent (state_transfer.rs:1966), a crash in the swap window (state_transfer.rs:1875), and the ConvergeFailed fence-and-rebuild path which needs no crash at all. after any of them, repaired_window_is_offsets_only accepts a floor with no batch anchor and the next append stamps base_offset 0 and re-derives batch_checksum (send_messages2.rs:819-822) - this replica durably persists different bytes than its group for the same committed op, silently. the anti-rewind check can't catch it either, it's gated on local_next_offset > 0.

stamping the frontier into the partition superblock and re-seeding at boot closes all three. two caveats: commit_max is an op number (vsr_state.rs:60-66), so this needs either a new VsrState field (decode demands exactly ENCODED_LEN bytes, so that's a version bump) or a documented overload; and persist_superblock_if_needed runs per prepare but needs_superblock_persist only fires on view/log_view change (impls.rs:1535-1542), so the trigger needs extending too - a frontier field alone would only persist on view changes.

// artifact's frontier, exactly where rotation would have put
// it, so post-install traffic lands in a segment named for the
// offsets it holds.
self.install_empty_segment(config, offsets_wire.next_offset)

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.

install_empty_segment passes file_exists = false, which skips both writer creation fsyncs (segment_storage/mod.rs:111), and no fsync_dir follows - the per-log one at :1910 is inside the staged loop, which is empty on this path. the frontier-bearing .log dirent is page-cache only; crash after the install and boot walks an empty dir and re-derives the counter at 0.

// fewer fsyncs than this: one-per-pair would lean on intra-directory
// rename ordering POSIX does not grant.
//
// KNOWN WINDOW, above and here: the old chain's unlinks are already

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.

crash inside this window and the frontier is unrecoverable, not just unsynced: the old chain is already unlinked and fsynced, the staged logs aren't renamed yet, and the boot sweep unconditionally deletes .log.staging (segment_recovery.rs:270-273) - so the only durable copy of the frontier gets deleted at the next boot.

topic_id,
partition_id,
));
partition.set_partition_dir(partition_dir);

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 zeroing right below is reachable with no crash at all: failed install then failed converge raises ConvergeFailed, the shard fences and tombstones, and the reconciler rebuilds through here at offset 0. on a multi-replica group that's a quorum-invisible backup at counter 0; on a single-replica group restarted stays false (replica_count > 1 is part of that check), so it takes consensus.init() and claims view-0 primaryship at counter 0.


// A receiver that missed a purge must not be re-wiped by the
// reconciler right after installing post-purge data.
self.applied_purge_generation = self

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 max() has no staleness refusal anywhere in the check phase - the only generation comparison is purge_advances at :1726 and it only widens permission. a peer that hasn't applied the purge yet offers generation N-1 with pre-purge segments; install succeeds, applied stays N, and the reconciler's committed > applied gate is N > N - it never re-wipes. purged data is durably resurrected and survives until this replica restarts (applied_purge_generation is memory-only, so a restart resets it to 0 and the boot re-purge fires). the rewind check can't substitute: post-purge local_next_offset is 0 which kills the :1732 conjunct, and once there's post-purge data the stale offer's next_offset is larger, not smaller. the comment at iggy_partition.rs:3413 closed the in-flight case but this pre-armed one is open, and the test at iggy_partition.rs:5541 only covers the advancing direction.

a fix that works without persisting the generation: refuse a regressing generation at the shard call site (lib.rs:6408), comparing against the durable committed generation from the metadata plane - not this partition's applied value, which is 0 after every restart - and route it through the existing Err arm at lib.rs:6464, which already rotates the peer. worst case is one wasted pull against a peer that hasn't caught up on the purge yet.

Ok(entries) => entries
.flatten()
.map(|entry| entry.path())
.filter(|path| {

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.

same suffix predicate as :1088 (only the closure binding differs). if these get unified, parameterize the predicate and leave sweep_staging_except's staging-only filter alone - the keep-lists at :1664 and :1808 hold only staging paths (and purge passes &[]), so widening the sweep predicate to the three-suffix list would unlink every live .log/.index on the partition, worst at the :1665 sweep which runs at descriptor-accept on a serving partition. the segment_recovery walk is a stateful fold, not the same shape - leave it out.

path: index_staging.clone(),
source,
})?;
Ok(StagedSegmentMeta {

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.

field-for-field identical to the literal at :1525-1535 - worth one shared constructor.

Comment thread core/shard/src/lib.rs
"re-answering a partition state transfer request from the offer already served"
);
let (serving_view, serving_commit_max) = serving_progress(partition);
self.send_state_transfer_target(

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.

three copies of serving_progress + send_state_transfer_target in this fn (cached here, fresh at :5548, and the Err arm at :5588) differing only in the descriptor. resolving the descriptor in one match with a single send site drops ~25-30 lines.

source,
})?;
memo.hashed_len = size;
memo.checksum = memo.hasher.finish();

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.

SegmentChecksumMemo.checksum is a second source of truth - StateArtifactHasher::finish takes &self, so both reads can use memo.hasher.finish() and the field plus its two assignments can go. the checksum: 0 initializer is already quietly wrong (xxhash3 of empty input isn't 0) - inert today, but exactly the drift a duplicate invites. dropping the memo on the failure path stays safe since it's removed from the map before hashing.

}

impl ArtifactProgress {
#[must_use]

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.

this inherent complete shadows the ChunkProgress default at :82 with a semantically identical body, and call sites split silently by genericity - generic paths (next_pending_chunk, append_chunk) resolve to the trait method, concrete ones (shard/lib.rs:6309 and the test) to the inherent. no const callers exist, so delete the inherent; otherwise any future trait-side override silently misses the concrete sites.

@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 5, 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