Skip to content

Rebase and retry the IdealState update in place on a version conflict during tier-relocation rebalance - #19069

Draft
Jackie-Jiang wants to merge 1 commit into
apache:masterfrom
Jackie-Jiang:rebalance-rebase-on-version-conflict
Draft

Jackie-Jiang wants to merge 1 commit into
apache:masterfrom
Jackie-Jiang:rebalance-rebase-on-version-conflict

Conversation

@Jackie-Jiang

@Jackie-Jiang Jackie-Jiang commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

PR flow

IdealState update loop with rebase on version conflict for tier-only moves

flowchart TD
  N0["Initialize#58; compute batchMovedSegments#44; rebasable flag #40;F1#41;"]:::stAdded
  N1["Retry loop start #40;F1#41;"]:::stAdded
  N2["Attempt CAS update IdealState #40;F1#41;"]:::stAdded
  N3["CAS success#58; set updated#61;true#44; break loop #40;F1#41;"]:::stAdded
  N4["CAS failure #40;ZkBadVersionException#41;#58; check rebasable#44; re#45;read latest#44; test touch #40;F1#41;"]:::stAdded
  N5["Other exception#58; return FAILED #40;F1#41;"]:::stAdded
  N6["Post#45;loop success#58; update currentAssignment#44; maybe refresh target#44; recompute#44;log #40;F1#41;"]:::stAdded
  N7["Post#45;loop failure#58; log version changed#44; call onRollback #40;F1#41;"]:::stAdded
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N2 --> N4
  N2 --> N5
  N4 -->|"rebase and continue"| N1
  N4 -->|"break loop #40;no rebase#41;"| N7
  N3 --> N6
  N5 --> N7
  classDef stAdded fill:#dafbe1,stroke:#1a7f37,color:#1f2328,stroke-width:2px
  classDef stModified fill:#fff8c5,stroke:#9a6700,color:#1f2328,stroke-width:2px
  classDef stRemoved fill:#ffebe9,stroke:#cf222e,color:#1f2328,stroke-width:2px
  classDef stUnchanged fill:#f6f8fa,stroke:#656d76,color:#1f2328,stroke-width:1px
Loading

AI-generated · Green: added · Yellow: modified · Red: removed · Gray: existing

Diff evidence
  • F1: pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java — before · after
  • Regenerate PR flow

Summary

Follow-up to #19054. That PR reduces the cost of the version-checked IdealState update losing the compare-and-set during a tier-relocation rebalance of a strict realtime table (upsert/dedup); this PR reduces the cost of recovering from a lost compare-and-set.

Each batch of segment moves is applied with a version-checked IdealState update. On a continuously-ingesting table, consuming-segment commits bump the IdealState version between the read and the write, so the update loses the compare-and-set. Previously every lost compare-and-set fell back to the top of the convergence loop — another ExternalView-convergence wait (~hundreds of ms) plus a target recompute — so the rebalance could make very little progress while ingestion continued.

Change

When the rebalance moves only tier segments (the base placements of the partitions are unchanged — the same condition #19054 uses to skip the target recompute), a concurrent write that does not touch the segments this batch moves cannot invalidate the batch. So on a ZkBadVersionException:

  1. Re-read the IdealState.
  2. If the concurrent change is disjoint from the segments this batch moves, rebase the batch onto the latest IdealState (reapply the batch's moves, preserving the concurrent changes) and retry the compare-and-set in place — no ExternalView wait, no rebalanceTable.
  3. On overlap, re-read failure, or exhausting a bounded number of attempts (MAX_IDEAL_STATE_UPDATE_REBASE_ATTEMPTS), fall back to the convergence loop as before.

On a successful rebase, the target assignment is refreshed from the adopted IdealState so the currentAssignment.equals(targetAssignment) convergence check stays well-defined when a rebase pulls in concurrently added (e.g. new consuming) or removed (e.g. retention) segments.

Net effect: disjoint version churn is absorbed in place, so a tier relocation makes progress against a steady stream of consuming-segment commits instead of repeatedly restarting.

Notes

@codecov-commenter

codecov-commenter commented Jul 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 15.09434% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.05%. Comparing base (ef7b1b7) to head (47762a4).

Files with missing lines Patch % Lines
...ntroller/helix/core/rebalance/TableRebalancer.java 15.09% 43 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19069      +/-   ##
============================================
- Coverage     68.07%   68.05%   -0.03%     
  Complexity     1450     1450              
============================================
  Files          3516     3516              
  Lines        228011   228057      +46     
  Branches      36077    36087      +10     
============================================
- Hits         155213   155194      -19     
- Misses        60613    60680      +67     
+ Partials      12185    12183       -2     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 68.05% <15.09%> (-0.03%) ⬇️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 68.05% <15.09%> (-0.03%) ⬇️
unittests 68.04% <15.09%> (-0.03%) ⬇️
unittests1 58.14% <ø> (-0.02%) ⬇️
unittests2 39.74% <15.09%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jackie-Jiang
Jackie-Jiang force-pushed the rebalance-rebase-on-version-conflict branch from ddcba5e to 931f4af Compare July 23, 2026 23:35
@Jackie-Jiang Jackie-Jiang added enhancement Improvement to existing functionality segment-rebalance Related to segment rebalancing across servers labels Jul 23, 2026
@yashmayya
yashmayya requested review from J-HowHuang and Copilot July 28, 2026 21:04

Copilot AI 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.

Pull request overview

This PR improves TableRebalancer’s behavior during strict-realtime tier-relocation rebalances by handling IdealState compare-and-set (ZK version) conflicts more efficiently. When a batch moves only tier segments and a concurrent IdealState write is disjoint from the segments in the batch, the rebalance now rebases the batch onto the latest IdealState and retries in place, avoiding an expensive restart of the convergence loop.

Changes:

  • Add a bounded “rebase and retry” loop for IdealState updates on ZkBadVersionException when the concurrent write does not touch the batch’s moved segments.
  • Refresh targetAssignment after a successful rebase so the convergence check remains well-defined if segments were concurrently added/removed.
  • Introduce MAX_IDEAL_STATE_UPDATE_REBASE_ATTEMPTS to bound in-place retries.
Comments suppressed due to low confidence (1)

pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:918

  • When the in-place rebase loop gives up (updated == false), restore expectedVersion to the value associated with the still-current currentAssignment so the next convergence-loop iteration will reliably detect the IdealState version mismatch and refresh currentAssignment/targetAssignment. Without this, a fallback after exhausting rebase attempts can leave expectedVersion equal to the latest ZK version while currentAssignment is stale.
      } else {
        tableRebalanceLogger.info("Version changed while updating IdealState");
        // Since IdealState wasn't updated, rollback the stats changes made
        _tableRebalanceObserver.onRollback();
      }

Comment on lines +831 to +834
boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, providedTierToSegmentsMap);
boolean updated = false;
int rebaseAttempts = 0;
while (true) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think in this case it would go to https://github.com/apache/pinot/pull/19069/changes#diff-d4962fcb9ad5591bb650b16bec8858ae3d9aa357aadc5cf2940a0524216898aaR651, so it should be fine? Please confirm this as well.

Comment on lines +824 to +830
// Check version and update the IdealState. If the compare-and-set fails only because a concurrent write bumped
// the version without touching the segments this batch moves (e.g. consuming segment commits on a continuously
// ingesting table), rebase this batch onto the latest IdealState and retry the compare-and-set in place, without
// waiting for the ExternalView to converge again (this batch never landed, so there is nothing new to wait for)
// or recomputing the full target assignment. This keeps the rebalance from live-locking against a steady stream
// of version bumps. Only attempted when this rebalance moves only tier segments: the base placements are then
// unchanged, so a segment added concurrently keeps its correct placement and can be carried over as-is.

@J-HowHuang J-HowHuang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Makes sense. I think we can probably reuse the code, see comments.

// or recomputing the full target assignment. This keeps the rebalance from live-locking against a steady stream
// of version bumps. Only attempted when this rebalance moves only tier segments: the base placements are then
// unchanged, so a segment added concurrently keeps its correct placement and can be carried over as-is.
boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, providedTierToSegmentsMap);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be

Suggested change
boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, providedTierToSegmentsMap);
boolean rebasable = !isStrictRealtimeSegmentAssignment || isMovingOnlyTierSegments(batchMovedSegments, providedTierToSegmentsMap);

for (String segment : batchMovedSegments) {
rebasedAssignment.put(segment, nextAssignment.get(segment));
}
nextAssignment = rebasedAssignment;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Can we have a method rebaseTargetAssignment(originalTargetAssignment, originalCurrentAssignment, newCurrentAssingment) that resolves rebasable and rebasedAssignment?
Then we can use the same method here: https://github.com/apache/pinot/pull/19069/changes#diff-d4962fcb9ad5591bb650b16bec8858ae3d9aa357aadc5cf2940a0524216898aaR651-R720, it's basically doing the same thing only that it rebases target assignment and here we rebase next assignment

Comment on lines +901 to +907
Map<String, Map<String, String>> refreshedTarget = new TreeMap<>(currentAssignment);
for (String segment : SegmentAssignmentUtils.getSegmentsToMove(currentAssignment, targetAssignment)) {
if (currentAssignment.containsKey(segment)) {
refreshedTarget.put(segment, targetAssignment.get(segment));
}
}
targetAssignment = refreshedTarget;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we may also reuse the rebaseTargetAssignment method if we have one here.

Comment on lines +831 to +834
boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, providedTierToSegmentsMap);
boolean updated = false;
int rebaseAttempts = 0;
while (true) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think in this case it would go to https://github.com/apache/pinot/pull/19069/changes#diff-d4962fcb9ad5591bb650b16bec8858ae3d9aa357aadc5cf2940a0524216898aaR651, so it should be fine? Please confirm this as well.

@Jackie-Jiang
Jackie-Jiang marked this pull request as draft July 29, 2026 02:21
…ier-relocation rebalance

When TableRebalancer moves segments in batches, each batch is applied with a version-checked
IdealState update. On a continuously-ingesting strict realtime table (upsert/dedup),
consuming-segment commits bump the IdealState version between the read and the write, so the
update loses the compare-and-set. Previously every lost compare-and-set fell back to the top
of the convergence loop, paying another ExternalView-convergence wait and target recompute,
so the rebalance could make very little progress while ingestion continued.

When the rebalance moves only tier segments (so the base placements of the partitions are
unchanged), a concurrent write that does not touch the segments this batch moves cannot
invalidate the batch. In that case, on a version conflict, re-read the IdealState, and if the
concurrent change is disjoint from the segments this batch moves, rebase the batch onto the
latest IdealState and retry the compare-and-set in place, without waiting for the
ExternalView to converge again or recomputing the target. The retry is bounded; on overlap,
re-read failure, or exhausting the retries, it falls back to the convergence loop as before.

This builds on the target-recompute skip for tier-relocation rebalances: it reuses the same
"moves only tier segments" condition to decide when a batch is safe to rebase, and refreshes
the target from the adopted IdealState so the convergence check stays well-defined when a
rebase pulls in concurrently added or removed segments.
@Jackie-Jiang
Jackie-Jiang force-pushed the rebalance-rebase-on-version-conflict branch from 931f4af to 47762a4 Compare September 27, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality segment-rebalance Related to segment rebalancing across servers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants