xiangfu0 commented on code in PR #19916:
URL: https://github.com/apache/pinot/pull/19916#discussion_r4332397690
##########
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",
+ segmentName, _tableNameWithType);
+ return getOrCreatePartitionInstances(idealState, partitionId,
Review Comment:
**Once a partition has an entry, the placement computed from the instance
partitions is discarded, so evacuating servers via the instance partitions no
longer redirects new segments.** Example: non-upsert strictReplicaGroup
realtime table (newly pinned by this PR); partition p3's servers fail and mark
the consuming replicas OFFLINE; the operator swaps them with `replaceInstance`
without a full rebalance; ensureAllPartitionsConsuming ->
createNewConsumingSegment -> assignSegment returns the entry, so the new
CONSUMING segment goes back to the dead servers and p3 stays down until a
rebalance (base: placed on the replacement servers). Uploads to qualifying
offline tables behave the same. Consider letting the candidate win when the
entry's instances are no longer in the instance partitions.
##########
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) {
+ continue;
+ }
+ state = SegmentStateModel.CONSUMING;
+ } else {
+ // All instances are OFFLINE, and OFFLINE segments are not rebalanced
+ continue;
+ }
+ Integer partitionId =
SegmentUtils.getPartitionIdFromSegmentName(segment);
+ if (partitionId == null) {
Review Comment:
**Every OFFLINE upload during a strictReplicaGroup rebalance forces a full
recompute, which also misplaces tier-eligible uploads.** This check is gated on
routing (`enableStrictReplicaGroup`), not on the table being pinned, and
offline names never carry a partition id, so it returns null for each upload;
the base took the copy path for offline tables. (1) The recompute reruns
getInstancePartitionsMap (persisted with reassignInstances) and rebalanceTable;
for a non-pinned replica-group table (numPartitions>1) the strategy does one
uncached ZK read per segment, and with continuous pushes the versioned write
keeps losing (ZkBadVersion only rolls back and loops), so the rebalance can
stall. (2) The recompute reuses the tier cache frozen at rebalance start
(`_segmentToTierName`; TierSegmentAssignment treats absent segments as
non-tier), so a tier-eligible upload that tiered segment assignment placed on
tier servers is retargeted to the default instances, then moved back by the
next Seg
mentRelocator run.
##########
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) {
+ continue;
+ }
+ state = SegmentStateModel.CONSUMING;
+ } else {
+ // All instances are OFFLINE, and OFFLINE segments are not rebalanced
+ continue;
+ }
+ Integer partitionId =
SegmentUtils.getPartitionIdFromSegmentName(segment);
+ if (partitionId == null) {
+ tableRebalanceLogger.info("Found new segment: {} without partition id
in its name, re-calculating the target "
+ + "assignment based on the new IdealState", segment);
+ return null;
+ }
+ List<String> currentInstances =
currentPartitionAssignment.get(partitionId);
+ if (currentInstances == null || currentInstances.size() !=
currentInstanceStateMap.size()
Review Comment:
**A segment added mid-rebalance to a partition without an entry is left in
place, whereas the base always fully recomputed for strict (upsert/dedup)
tables.** Example (rolling upgrade): pre-PR upsert table with no entries; a new
controller rebalances p0 [A,B]->[C,D]; the old-version table lead
resumes/repairs p0's consumption or accepts an upload for p0 and places the new
segment on [A,B] without creating an entry; no segment in segmentsToMove
changes state, so this branch keeps it at [A,B], the rest of p0 moves to [C,D],
the final step sets entry p0=[C,D], and the rebalance reports DONE with the
upsert partition split.
##########
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",
+ segmentName, _tableNameWithType);
+ return getOrCreatePartitionInstances(idealState, partitionId,
+ assignConsumingSegment(partitionId, instancePartitions));
+ }
+
+ /// OFFLINE segments are skipped because they are not rebalanced, so their
assignment can be stale. The instances are
+ /// taken from, in order:
+ /// - The CONSUMING segment of the partition, which is always hosted by the
CONSUMING instances of the partition.
+ /// - The latest LLC segment of the partition, else an uploaded segment of
the partition, whose partition id is in
+ /// its name or in its ZK metadata. Completed segments are not used when
they are relocated to the COMPLETED
+ /// instances, or when the segment has been moved to a tier, because they
are not on the CONSUMING instances of
+ /// the partition then. The tier is read from the segment ZK metadata
whether or not the table still has tiers
+ /// configured, because removing the tier config does not move the
segments back.
+ @Nullable
@Override
- public Map<String, Map<String, String>> rebalanceTable(Map<String,
Map<String, String>> currentAssignment,
+ protected Set<String> getExistingAssignment(int partitionId, Map<String,
Map<String, String>> currentAssignment) {
+ LLCSegmentName latestLLCSegmentName = null;
+ Map<String, String> latestInstanceStateMap = null;
+ List<String> uploadedSegments = new ArrayList<>();
+ for (Map.Entry<String, Map<String, String>> entry :
currentAssignment.entrySet()) {
+ String segmentName = entry.getKey();
+ Map<String, String> instanceStateMap = entry.getValue();
+ boolean completed;
+ if (instanceStateMap.containsValue(SegmentStateModel.ONLINE)) {
+ completed = true;
+ } else if (instanceStateMap.containsValue(SegmentStateModel.CONSUMING)) {
+ completed = false;
+ } else {
+ // All instances are OFFLINE, and OFFLINE segments are not rebalanced,
so their assignment can be stale
+ continue;
+ }
+ LLCSegmentName llcSegmentName = LLCSegmentName.of(segmentName);
+ if (llcSegmentName == null) {
+ uploadedSegments.add(segmentName);
+ continue;
+ }
+ if (llcSegmentName.getPartitionGroupId() != partitionId) {
+ continue;
+ }
+ if (!completed) {
+ return instanceStateMap.keySet();
+ }
+ if (latestLLCSegmentName == null
+ || llcSegmentName.getSequenceNumber() >
latestLLCSegmentName.getSequenceNumber()) {
+ latestLLCSegmentName = llcSegmentName;
+ latestInstanceStateMap = instanceStateMap;
+ }
+ }
+ // Completed segments relocated to the COMPLETED instances are not on the
CONSUMING instances of the partition
+ if
(InstanceAssignmentConfigUtils.shouldRelocateCompletedSegments(_tableConfig)) {
+ return null;
+ }
+ String latestSegmentName = null;
+ if (latestLLCSegmentName != null) {
+ latestSegmentName = latestLLCSegmentName.getSegmentName();
+ } else {
+ for (String uploadedSegment : uploadedSegments) {
+ Integer uploadedSegmentPartitionId =
+ SegmentUtils.getSegmentPartitionId(uploadedSegment,
_tableNameWithType, _helixManager, _partitionColumn);
Review Comment:
**When a partition has no entry and no ONLINE/CONSUMING LLC segment, this
reads the ZK metadata of every custom-named segment one by one inside the
IdealState updater, now for dedup and plain strictReplicaGroup tables too**
(base: only upsert scanned; dedup skipped uploaded segments; plain tables never
scanned). Example: plain/dedup strictReplicaGroup table with N MergeRollup
`merged_*` segments gains K stream partitions; the next
ensureAllPartitionsConsuming updater does K x N sequential ZK reads per attempt
(up to 10 attempts) while holding the table's group-commit queue, delaying
segment-commit updates; an orphan segment in the IdealState without ZK metadata
makes `SegmentUtils.getSegmentPartitionId` throw and the new partitions never
start consuming. Checking relocation first and batch-reading only the needed
segments outside the updater would avoid it.
##########
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());
Review Comment:
**putIfAbsent cannot tell a partition added while waiting from one the
target deliberately drops, so leftover entries can survive the rebalance.**
Example: formerly pinned OFFLINE strictReplicaGroup table whose instance
partitions now hold 2 instances per partition (rebalanceTable returns
`Map.of()`), entries {0:[A],1:[B]}; a retention deletion of a non-moving
segment lands before the first write, this makes target == current, and the
rebalance ends DONE with the stale entries; if the instance partitions are
later set back to one instance per partition via assignInstances/PUT without a
rebalance, uploads for partition 0 go to stale server A. Only re-adding
partitions that are new since the target was computed would avoid this.
--
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]