Jackie-Jiang commented on code in PR #19916:
URL: https://github.com/apache/pinot/pull/19916#discussion_r4342830085


##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/RealtimeSegmentAssignment.java:
##########
@@ -158,8 +178,100 @@ protected List<String> assignConsumingSegment(int 
segmentPartitionId, InstancePa
     }
   }
 
+  /// Assigns a CONSUMING segment of a partition-pinned table to the instances 
of its partition, see
+  /// [BaseSegmentAssignment#getOrCreatePartitionInstances].
+  private List<String> assignPinnedConsumingSegment(String segmentName, 
IdealState idealState,
+      InstancePartitions instancePartitions) {
+    Integer partitionId =
+        SegmentUtils.getSegmentPartitionId(segmentName, _tableNameWithType, 
_helixManager, _partitionColumn);
+    Preconditions.checkState(partitionId != null, "Failed to find partition id 
for segment: %s of table: %s",

Review Comment:
   Fixed in 7df9c15: a segment without a derivable partition id now falls back 
to the non-pinned assignment (partition id derived from the name) with a 
warning, both on assignment and on rebalance. Upsert and dedup tables still 
reject it, as before this PR. Covered in 
`PartitionPinnedRealtimeSegmentAssignmentTest`.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/BaseSegmentAssignment.java:
##########
@@ -82,21 +101,95 @@ public abstract class BaseSegmentAssignment implements 
SegmentAssignment {
   @Nullable
   private Map<String, String> _segmentToTierName;
 
+  // Cache of segment name to partition id (null value means the partition id 
cannot be determined) for the segments
+  // whose partition id is not in the segment name, loaded with a single bulk 
ZK read on first use. Same scope and
+  // limitations as _segmentToTierName; a segment absent from the cache is 
resolved on demand and merged in.
+  @Nullable
+  private Map<String, Integer> _segmentToPartitionId;
+
   @Override
   public void init(HelixManager helixManager, TableConfig tableConfig, 
@Nullable ControllerMetrics controllerMetrics) {
     _helixManager = helixManager;
     _tableNameWithType = tableConfig.getTableName();
     _tableConfig = tableConfig;
     _replication = tableConfig.getReplication();
     _partitionColumn = TableConfigUtils.getPartitionColumn(_tableConfig);
-    _controllerMetrics = controllerMetrics;
+    _partitionPinned = isPartitionPinned(tableConfig);
     if (_partitionColumn == null) {
-      _logger.info("Initialized with replication: {} without partition column 
for table: {} ", _replication,
-          _tableNameWithType);
+      _logger.info("Initialized with replication: {} without partition column, 
partitionPinned: {} for table: {}",
+          _replication, _partitionPinned, _tableNameWithType);
     } else {
-      _logger.info("Initialized with replication: {} and partition column: {} 
for table: {}", _replication,
-          _partitionColumn, _tableNameWithType);
+      _logger.info("Initialized with replication: {} and partition column: {}, 
partitionPinned: {} for table: {}",
+          _replication, _partitionColumn, _partitionPinned, 
_tableNameWithType);
+    }
+  }
+
+  /// Returns whether the segments of the table are pinned to their partition 
(see the class Javadoc): the table uses
+  /// strict replica-group routing. Subclasses add the conditions they need to 
pin the segments, and log a warning
+  /// when such a table cannot be pinned, in which case its segments are 
assigned with the segment assignment strategy.
+  protected boolean isPartitionPinned(TableConfig tableConfig) {
+    return SegmentAssignmentUtils.isStrictReplicaGroupRouting(tableConfig);
+  }
+
+  /// Returns the instances of the given partition of a partition-pinned 
table, creating its entry in the partition
+  /// assignment of the given ideal state when the partition has no entry yet, 
or an empty one (a rebalance in low disk
+  /// mode without minimum available replicas empties the entry while it drops 
the partition from its current
+  /// instances). The entry is created from the existing assignment of the 
partition (see [#getExistingAssignment])
+  /// when there is one, else from the given candidate instances computed from 
the instance partitions. An existing
+  /// entry wins over the candidate instances, so that the segments of the 
partition stay collocated when the instance
+  /// partitions change: the partition moves on the next rebalance.
+  protected List<String> getOrCreatePartitionInstances(IdealState idealState, 
int partitionId,
+      List<String> candidateInstances) {
+    List<String> instances = PartitionAssignmentUtils.getInstances(idealState, 
partitionId);
+    if (CollectionUtils.isEmpty(instances)) {

Review Comment:
   Kept as is and documented in the description: an old controller does not 
maintain the entries, so a rebalance run on the old version (rolling upgrade or 
rollback) leaves them stale until the first rebalance by the new version. 
Validating an entry against the partition's CONSUMING segment would bring back 
the IdealState scan on every new segment, and disagrees with rebalances that 
exclude consuming segments, where the entry legitimately differs from the 
CONSUMING segment.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/assignment/segment/BaseSegmentAssignment.java:
##########
@@ -82,21 +101,95 @@ public abstract class BaseSegmentAssignment implements 
SegmentAssignment {
   @Nullable
   private Map<String, String> _segmentToTierName;
 
+  // Cache of segment name to partition id (null value means the partition id 
cannot be determined) for the segments
+  // whose partition id is not in the segment name, loaded with a single bulk 
ZK read on first use. Same scope and
+  // limitations as _segmentToTierName; a segment absent from the cache is 
resolved on demand and merged in.
+  @Nullable
+  private Map<String, Integer> _segmentToPartitionId;
+
   @Override
   public void init(HelixManager helixManager, TableConfig tableConfig, 
@Nullable ControllerMetrics controllerMetrics) {
     _helixManager = helixManager;
     _tableNameWithType = tableConfig.getTableName();
     _tableConfig = tableConfig;
     _replication = tableConfig.getReplication();
     _partitionColumn = TableConfigUtils.getPartitionColumn(_tableConfig);
-    _controllerMetrics = controllerMetrics;
+    _partitionPinned = isPartitionPinned(tableConfig);
     if (_partitionColumn == null) {
-      _logger.info("Initialized with replication: {} without partition column 
for table: {} ", _replication,
-          _tableNameWithType);
+      _logger.info("Initialized with replication: {} without partition column, 
partitionPinned: {} for table: {}",
+          _replication, _partitionPinned, _tableNameWithType);
     } else {
-      _logger.info("Initialized with replication: {} and partition column: {} 
for table: {}", _replication,
-          _partitionColumn, _tableNameWithType);
+      _logger.info("Initialized with replication: {} and partition column: {}, 
partitionPinned: {} for table: {}",
+          _replication, _partitionColumn, _partitionPinned, 
_tableNameWithType);
+    }
+  }
+
+  /// Returns whether the segments of the table are pinned to their partition 
(see the class Javadoc): the table uses
+  /// strict replica-group routing. Subclasses add the conditions they need to 
pin the segments, and log a warning
+  /// when such a table cannot be pinned, in which case its segments are 
assigned with the segment assignment strategy.
+  protected boolean isPartitionPinned(TableConfig tableConfig) {
+    return SegmentAssignmentUtils.isStrictReplicaGroupRouting(tableConfig);

Review Comment:
   Deliberate, see the description: `multiStageReplicaGroup` will be removed. 
Added in 7df9c15 a warning at assignment initialization for an upsert/dedup 
table that is not pinned.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -1622,174 +1656,280 @@ private static void handleErrorInstance(String 
segmentName, String instanceName,
     }
   }
 
+  /// Returns the target assignment for the ideal state changed while waiting 
for the external view to converge, when
+  /// the segments to be moved are unchanged: the current assignment with the 
target of the segments to be moved. For
+  /// a partition-pinned table (strict replica-group routing), a partition 
added while waiting keeps its current
+  /// instances, and a segment added while waiting, which is assigned to the 
current instances of its partition, is
+  /// moved to the target instances of its partition, unless it is a CONSUMING 
segment and CONSUMING segments are not
+  /// included in the rebalance. Returns `null` when the target assignment 
must be recalculated instead: when the
+  /// partition id of such a segment is not in its name, or when CONSUMING 
segments are not included and an ONLINE
+  /// segment was added to a pauseless table, because it can still be 
committing (which only its ZK metadata tells),
+  /// in which case it is not moved either.
+  @Nullable
+  @VisibleForTesting
+  static TableAssignment getTargetAssignmentForNewIdealState(Map<String, 
Map<String, String>> currentAssignment,
+      Map<String, Map<String, String>> targetAssignment, Map<Integer, 
List<String>> currentPartitionAssignment,
+      Map<Integer, List<String>> targetPartitionAssignment, List<String> 
segmentsToMove,
+      boolean enableStrictReplicaGroup, boolean includeConsuming, boolean 
pauseless, Logger tableRebalanceLogger) {
+    // Other instance assignment code returns a TreeMap to keep it sorted, 
doing the same here
+    Map<String, Map<String, String>> newTargetAssignment = new 
TreeMap<>(currentAssignment);
+    for (String segment : segmentsToMove) {
+      newTargetAssignment.put(segment, targetAssignment.get(segment));
+    }
+    if (!enableStrictReplicaGroup) {
+      return new TableAssignment(newTargetAssignment, 
targetPartitionAssignment);
+    }
+    Map<Integer, List<String>> newTargetPartitionAssignment = new 
TreeMap<>(targetPartitionAssignment);
+    for (Map.Entry<Integer, List<String>> entry : 
currentPartitionAssignment.entrySet()) {
+      newTargetPartitionAssignment.putIfAbsent(entry.getKey(), 
entry.getValue());
+    }
+    for (Map.Entry<String, Map<String, String>> entry : 
currentAssignment.entrySet()) {
+      String segment = entry.getKey();
+      if (targetAssignment.containsKey(segment)) {
+        continue;
+      }
+      Map<String, String> currentInstanceStateMap = entry.getValue();
+      String state;
+      if (currentInstanceStateMap.containsValue(SegmentStateModel.ONLINE)) {
+        if (!includeConsuming && pauseless) {
+          tableRebalanceLogger.info("Found new ONLINE segment: {} of pauseless 
table while CONSUMING segments are not "
+              + "included, re-calculating the target assignment based on the 
new IdealState", segment);
+          return null;
+        }
+        state = SegmentStateModel.ONLINE;
+      } else if 
(currentInstanceStateMap.containsValue(SegmentStateModel.CONSUMING)) {
+        if (!includeConsuming) {

Review Comment:
   Fixed in 7df9c15: the entries are stepped with the segments only when the 
CONSUMING segments of a real-time table are included. Otherwise they are set to 
the target at the first write, so a segment created mid-rebalance goes straight 
to the target instances, as before this PR.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/rebalance/TableRebalancer.java:
##########
@@ -1803,71 +1943,63 @@ private static void 
checkIfAnyServersAssignedMoreSegmentsThanBatchSize(int batch
     }
   }
 
-  /// Create a mapping of Pair(currentInstances, targetInstances) to 
partitionId to the current assignment of segments.
-  /// This is to be used for batching purposes for StrictReplicaGroup routing, 
for all segment assignment types:
-  /// RealtimeSegmentAssignment, StrictRealtimeSegmentAssignment and 
OfflineSegmentAssignment
-  /// @param currentAssignment the current assignment
-  /// @param targetAssignment the target assignment
-  /// @param segmentPartitionIdMap cache to store the partition ids to avoid 
fetching ZK segment metadata
-  /// @param partitionIdFetcher function to fetch the partition id
-  /// @return a mapping from Pair(currentInstances, targetInstances) to the 
partitionId to the segment assignment map of
-  ///         all segments that fall in that category
-  private static Map<Pair<Set<String>, Set<String>>, Map<Integer, Map<String, 
Map<String, String>>>>
-      getCurrentAndTargetInstancesToPartitionIdToCurrentAssignmentMap(
-          Map<String, Map<String, String>> currentAssignment, Map<String, 
Map<String, String>> targetAssignment,
-          Object2IntOpenHashMap<String> segmentPartitionIdMap, 
PartitionIdFetcher partitionIdFetcher) {
-    Map<Pair<Set<String>, Set<String>>, Map<Integer, Map<String, Map<String, 
String>>>>
-        currentAndTargetInstancesToPartitionIdToCurrentAssignmentMap = new 
HashMap<>();
-
-    for (Map.Entry<String, Map<String, String>> assignment : 
currentAssignment.entrySet()) {
-      String segmentName = assignment.getKey();
-      Map<String, String> currentInstanceStateMap = assignment.getValue();
-      Map<String, String> targetInstanceStateMap = 
targetAssignment.get(segmentName);
-
-      int partitionId = segmentPartitionIdMap.computeIfAbsent(segmentName, v 
-> partitionIdFetcher.fetch(segmentName));
-      Pair<Set<String>, Set<String>> currentAndTargetInstances =
-          Pair.of(currentInstanceStateMap.keySet(), 
targetInstanceStateMap.keySet());
-      
currentAndTargetInstancesToPartitionIdToCurrentAssignmentMap.computeIfAbsent(currentAndTargetInstances,
-              k -> new TreeMap<>())
-          .computeIfAbsent(partitionId, k -> new TreeMap<>())
-          .put(segmentName, currentInstanceStateMap);
-    }
-
-    return currentAndTargetInstancesToPartitionIdToCurrentAssignmentMap;
-  }
-
-  @VisibleForTesting
-  @FunctionalInterface
-  interface PartitionIdFetcher {
-    int fetch(String segmentName);
+  /// Segments and partitions with the same current and target instances, 
which are moved together for strict
+  /// replica-group routing.
+  private static class StrictReplicaGroup {
+    final Map<String, Map<String, String>> _segments = new TreeMap<>();
+    final List<Integer> _partitionIds = new ArrayList<>();
   }
 
-  private static class PartitionIdFetcherImpl implements PartitionIdFetcher {
-    private final String _tableNameWithType;
-    private final String _partitionColumn;
-    private final HelixManager _helixManager;
-    private final boolean _isStrictRealtimeSegmentAssignment;
-
-    private PartitionIdFetcherImpl(String tableNameWithType, @Nullable String 
partitionColumn,
-        HelixManager helixManager, boolean isStrictRealtimeSegmentAssignment) {
-      _tableNameWithType = tableNameWithType;
-      _partitionColumn = partitionColumn;
-      _helixManager = helixManager;
-      _isStrictRealtimeSegmentAssignment = isStrictRealtimeSegmentAssignment;
-    }
-
-    @Override
-    public int fetch(String segmentName) {
-      Integer partitionId =
-          SegmentUtils.getSegmentPartitionId(segmentName, _tableNameWithType, 
_helixManager, _partitionColumn);
-      if (partitionId != null) {
-        return partitionId;
+  /// Groups the segments and the partitions by their current and target 
instances for batching with strict
+  /// replica-group routing, for all table types. Within a pair of current and 
target instances, the segments are
+  /// further grouped by the partition id in their name, so that partitions 
sharing the same instances can be moved in
+  /// different batches. When a segment has no partition id in its name (a 
segment of an offline table, or an uploaded
+  /// segment with a custom name), the whole pair is moved together so that 
the segment stays with the other segments
+  /// of its partition. The partitions of a partition-pinned table join the 
group of their current and target
+  /// instances. Partitions absent from the target partition assignment are 
dropped.
+  private static List<StrictReplicaGroup> getStrictReplicaGroups(Map<String, 
Map<String, String>> currentAssignment,
+      Map<String, Map<String, String>> targetAssignment, Map<Integer, 
List<String>> currentPartitionAssignment,
+      Map<Integer, List<String>> targetPartitionAssignment) {
+    Map<Pair<Set<String>, Set<String>>, Map<Integer, StrictReplicaGroup>> 
groupsMap = new HashMap<>();
+    for (Map.Entry<String, Map<String, String>> entry : 
currentAssignment.entrySet()) {
+      String segmentName = entry.getKey();
+      Map<String, String> currentInstanceStateMap = entry.getValue();
+      Pair<Set<String>, Set<String>> currentAndTargetInstances =
+          Pair.of(currentInstanceStateMap.keySet(), 
targetAssignment.get(segmentName).keySet());
+      Integer partitionId = 
SegmentUtils.getPartitionIdFromSegmentName(segmentName);

Review Comment:
   Fixed in 7df9c15: `rebalanceTable` returns the partition ids it resolves 
(one bulk ZK read), and the strict replica-group batching groups by the 
name-derived id, else the resolved id, else a hash of the name. The collapse 
into one group is gone. Covered in `TableRebalancerTest`.



-- 
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