J-HowHuang commented on code in PR #19069:
URL: https://github.com/apache/pinot/pull/19069#discussion_r3669603170


##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -812,27 +816,105 @@ private RebalanceResult doRebalance(TableConfig 
tableConfig, RebalanceConfig reb
       idealState.setNumPartitions(nextAssignment.size());
       
idealState.setReplicas(Integer.toString(nextAssignment.values().iterator().next().size()));
 
-      // Check version and update IdealState
-      try {
-        Preconditions.checkState(_helixDataAccessor.getBaseDataAccessor()
-                .set(idealStatePropertyKey.getPath(), idealStateRecord, 
expectedVersion, AccessOption.PERSISTENT),
-            "Failed to update IdealState");
+      // Segments this batch changes relative to the current assignment it was 
computed against. Captured before the
+      // compare-and-set so that, on a version conflict, we can tell whether a 
concurrent write touched any segment
+      // this batch moves.
+      List<String> batchMovedSegments = 
SegmentAssignmentUtils.getSegmentsToMove(currentAssignment, nextAssignment);
+
+      // 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.
+      boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, 
providedTierToSegmentsMap);

Review Comment:
   Should this be 
   
   ```suggestion
         boolean rebasable = !isStrictRealtimeSegmentAssignment || 
isMovingOnlyTierSegments(batchMovedSegments, providedTierToSegmentsMap);
   ```



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -812,27 +816,105 @@ private RebalanceResult doRebalance(TableConfig 
tableConfig, RebalanceConfig reb
       idealState.setNumPartitions(nextAssignment.size());
       
idealState.setReplicas(Integer.toString(nextAssignment.values().iterator().next().size()));
 
-      // Check version and update IdealState
-      try {
-        Preconditions.checkState(_helixDataAccessor.getBaseDataAccessor()
-                .set(idealStatePropertyKey.getPath(), idealStateRecord, 
expectedVersion, AccessOption.PERSISTENT),
-            "Failed to update IdealState");
+      // Segments this batch changes relative to the current assignment it was 
computed against. Captured before the
+      // compare-and-set so that, on a version conflict, we can tell whether a 
concurrent write touched any segment
+      // this batch moves.
+      List<String> batchMovedSegments = 
SegmentAssignmentUtils.getSegmentsToMove(currentAssignment, nextAssignment);
+
+      // 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.
+      boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, 
providedTierToSegmentsMap);
+      boolean updated = false;
+      int rebaseAttempts = 0;
+      while (true) {
+        try {
+          Preconditions.checkState(_helixDataAccessor.getBaseDataAccessor()
+                  .set(idealStatePropertyKey.getPath(), idealStateRecord, 
expectedVersion, AccessOption.PERSISTENT),
+              "Failed to update IdealState");
+          updated = true;
+          break;
+        } catch (ZkBadVersionException e) {
+          if (!rebasable || rebaseAttempts >= 
MAX_IDEAL_STATE_UPDATE_REBASE_ATTEMPTS) {
+            break;
+          }
+          IdealState latestIdealState;
+          try {
+            latestIdealState = 
_helixDataAccessor.getProperty(idealStatePropertyKey);
+          } catch (Exception re) {
+            tableRebalanceLogger.warn("Failed to re-read IdealState for 
rebasing after a version conflict", re);
+            break;
+          }
+          if (latestIdealState == null) {
+            break;
+          }
+          Map<String, Map<String, String>> latestAssignment = 
latestIdealState.getRecord().getMapFields();
+          boolean concurrentChangeTouchesBatch = false;
+          for (String segment : batchMovedSegments) {
+            if (!Objects.equals(currentAssignment.get(segment), 
latestAssignment.get(segment))) {
+              concurrentChangeTouchesBatch = true;
+              break;
+            }
+          }
+          if (concurrentChangeTouchesBatch) {
+            break;
+          }
+          // Rebase the batch onto the latest IdealState, preserving 
concurrent changes to the other segments.
+          Map<String, Map<String, String>> rebasedAssignment = new 
TreeMap<>(latestAssignment);
+          for (String segment : batchMovedSegments) {
+            rebasedAssignment.put(segment, nextAssignment.get(segment));
+          }
+          nextAssignment = rebasedAssignment;
+          idealState = latestIdealState;
+          idealStateRecord = latestIdealState.getRecord();
+          idealStateRecord.setMapFields(nextAssignment);
+          idealState.setNumPartitions(nextAssignment.size());
+          
idealState.setReplicas(Integer.toString(nextAssignment.values().iterator().next().size()));
+          expectedVersion = idealStateRecord.getVersion();
+          rebaseAttempts++;
+          tableRebalanceLogger.info("Rebasing IdealState update onto version 
{} after a concurrent change that does "
+              + "not affect the segments being moved (attempt {})", 
expectedVersion, rebaseAttempts);
+        } catch (Exception e) {
+          onReturnFailure("Caught exception while updating IdealState, 
aborting the rebalance", e,
+              tableRebalanceLogger);
+          return new RebalanceResult(rebalanceJobId, 
RebalanceResult.Status.FAILED,
+              "Caught exception while updating IdealState: " + e, 
instancePartitionsMap, tierToInstancePartitionsMap,
+              targetAssignment, preChecksResult, summaryResult);
+        }
+      }
+
+      if (updated) {
         expectedVersion++;
         currentAssignment = nextAssignment;
+        if (rebaseAttempts > 0) {
+          // A rebase adopts the latest IdealState, which may have segments 
added (e.g. new consuming segments) or
+          // removed (e.g. by retention) concurrently that the target 
assignment, computed earlier, does not match.
+          // Since a rebase is only done when the rebalance moves only tier 
segments, added segments keep their current
+          // placement, so rebuild the target from the adopted current 
assignment while preserving the target only for
+          // the segments still moving that still exist. This keeps the 
convergence check
+          // (currentAssignment.equals(targetAssignment)) well-defined and its 
key set aligned with the current
+          // assignment.
+          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;

Review Comment:
   we may also reuse the `rebaseTargetAssignment` method if we have one here.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -812,27 +816,105 @@ private RebalanceResult doRebalance(TableConfig 
tableConfig, RebalanceConfig reb
       idealState.setNumPartitions(nextAssignment.size());
       
idealState.setReplicas(Integer.toString(nextAssignment.values().iterator().next().size()));
 
-      // Check version and update IdealState
-      try {
-        Preconditions.checkState(_helixDataAccessor.getBaseDataAccessor()
-                .set(idealStatePropertyKey.getPath(), idealStateRecord, 
expectedVersion, AccessOption.PERSISTENT),
-            "Failed to update IdealState");
+      // Segments this batch changes relative to the current assignment it was 
computed against. Captured before the
+      // compare-and-set so that, on a version conflict, we can tell whether a 
concurrent write touched any segment
+      // this batch moves.
+      List<String> batchMovedSegments = 
SegmentAssignmentUtils.getSegmentsToMove(currentAssignment, nextAssignment);
+
+      // 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.
+      boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, 
providedTierToSegmentsMap);
+      boolean updated = false;
+      int rebaseAttempts = 0;
+      while (true) {
+        try {
+          Preconditions.checkState(_helixDataAccessor.getBaseDataAccessor()
+                  .set(idealStatePropertyKey.getPath(), idealStateRecord, 
expectedVersion, AccessOption.PERSISTENT),
+              "Failed to update IdealState");
+          updated = true;
+          break;
+        } catch (ZkBadVersionException e) {
+          if (!rebasable || rebaseAttempts >= 
MAX_IDEAL_STATE_UPDATE_REBASE_ATTEMPTS) {
+            break;
+          }
+          IdealState latestIdealState;
+          try {
+            latestIdealState = 
_helixDataAccessor.getProperty(idealStatePropertyKey);
+          } catch (Exception re) {
+            tableRebalanceLogger.warn("Failed to re-read IdealState for 
rebasing after a version conflict", re);
+            break;
+          }
+          if (latestIdealState == null) {
+            break;
+          }
+          Map<String, Map<String, String>> latestAssignment = 
latestIdealState.getRecord().getMapFields();
+          boolean concurrentChangeTouchesBatch = false;
+          for (String segment : batchMovedSegments) {
+            if (!Objects.equals(currentAssignment.get(segment), 
latestAssignment.get(segment))) {
+              concurrentChangeTouchesBatch = true;
+              break;
+            }
+          }
+          if (concurrentChangeTouchesBatch) {
+            break;
+          }
+          // Rebase the batch onto the latest IdealState, preserving 
concurrent changes to the other segments.
+          Map<String, Map<String, String>> rebasedAssignment = new 
TreeMap<>(latestAssignment);
+          for (String segment : batchMovedSegments) {
+            rebasedAssignment.put(segment, nextAssignment.get(segment));
+          }
+          nextAssignment = rebasedAssignment;

Review Comment:
   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



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -812,27 +816,105 @@ private RebalanceResult doRebalance(TableConfig 
tableConfig, RebalanceConfig reb
       idealState.setNumPartitions(nextAssignment.size());
       
idealState.setReplicas(Integer.toString(nextAssignment.values().iterator().next().size()));
 
-      // Check version and update IdealState
-      try {
-        Preconditions.checkState(_helixDataAccessor.getBaseDataAccessor()
-                .set(idealStatePropertyKey.getPath(), idealStateRecord, 
expectedVersion, AccessOption.PERSISTENT),
-            "Failed to update IdealState");
+      // Segments this batch changes relative to the current assignment it was 
computed against. Captured before the
+      // compare-and-set so that, on a version conflict, we can tell whether a 
concurrent write touched any segment
+      // this batch moves.
+      List<String> batchMovedSegments = 
SegmentAssignmentUtils.getSegmentsToMove(currentAssignment, nextAssignment);
+
+      // 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.
+      boolean rebasable = isMovingOnlyTierSegments(segmentsToMove, 
providedTierToSegmentsMap);
+      boolean updated = false;
+      int rebaseAttempts = 0;
+      while (true) {

Review Comment:
   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.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to