This is an automated email from the ASF dual-hosted git repository.

raghavyadav01 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 89b84bad2e2 Resume a pauseless commit start after its IdealState 
update fails (#19740)
89b84bad2e2 is described below

commit 89b84bad2e2fcefcb7dac4ec0bbdb148fdb0e023
Author: Suvodeep Pyne <[email protected]>
AuthorDate: Tue Oct 20 16:28:33 2026 -0700

    Resume a pauseless commit start after its IdealState update fails (#19740)
    
    * Resume a pauseless commit start after its IdealState update fails
    
    The pauseless commit start moves the committing segment to COMMITTING in ZK
    (step 1), creates the next segment's ZK metadata (step 2), then updates the
    IdealState (step 3). When step 3 fails, e.g. after losing every retry to
    IdealState version conflicts, the segment stays COMMITTING, and every retry
    of the commit failed with "should be IN_PROGRESS, found: COMMITTING" until
    RealtimeSegmentValidationManager repaired the partition after
    max.segment.completion.time.millis. The same happened when the controller
    stopped between step 1 and step 3.
    
    A commit start for a segment that is already COMMITTING with the same end
    offset now resumes the earlier attempt from step 2, until the max segment
    completion time, after which the validation manager still owns the repair.
    Steps 2 and 3 are made idempotent so that the attempts of a commit start,
    including attempts on different controllers around a lead change, converge:
    - A resumed attempt uses the new segment created by an earlier attempt, if
      any. The new segment is named after its creation time with minute
      granularity, and is created between step 1 and the max segment completion
      time, so the resumed attempt probes the names of the minutes in between.
      Otherwise it creates the new segment with the current time, as the first
      attempt does, so the segment is never named after an older time.
    - New segment ZK metadata is created atomically only if absent and is never
      overwritten, since the segment might be consuming or committed already.
    - The IdealState updater skips an update that is already applied (all
      replicas of the committing segment ONLINE and the new segment present).
      Previously a retry after a write that succeeded but was reported as failed
      failed every remaining attempt, and every update batched with it, on
      "Failed to find instance in CONSUMING state".
    - When step 3 fails, the controller re-reads the IdealState and proceeds if
      the update was applied. Otherwise it keeps the new segment's ZK metadata 
for
      the next attempt, or for the validation manager, which adds such a segment
      to the IdealState. It removes the metadata only when the new segment is 
not
      in the IdealState and the committing segment is no longer CONSUMING, the
      only state in which no attempt can add the segment anymore.
    
    Attempts on two controllers, or a kept new segment and a validation repair
    that started before it was created, can still create two new segments with
    the same sequence number. When several segments of a partition have the
    highest sequence number, the validation manager now takes the one in the
    IdealState as the latest segment, instead of whichever is listed first, 
which
    could make it fail every run with "Segment ... is a duplicate of existing
    segment ...".
    
    No ZK metadata is rolled back, so the IdealState version check of step 3
    remains the only fence against a concurrent repair by the validation 
manager.
    
    * Remove a commit start test that duplicates another one
    
    testCommitStartFailureKeepsNewSegmentAddedConcurrently exercised the same
    branch as testCommitStartSucceedsWhenFailedIdealStateUpdateWasApplied: after
    a failed IdealState update, the controller re-reads the IdealState, finds 
the
    completion applied and proceeds. The failure handler does not depend on
    whether the attempt was resumed.
    
    * Document assumptions of the commit start resume
    
    Note that probing for the new segment of an earlier attempt relies on the 
committing segment ZK metadata not being rewritten while it stays COMMITTING, 
and which segment is the latest one when several segments with the highest 
sequence number are missing from the IdealState.
---
 .../realtime/BlockingSegmentCompletionFSM.java     |   5 +-
 .../realtime/PinotLLCRealtimeSegmentManager.java   | 252 +++++++++--
 .../PinotLLCRealtimeSegmentManagerTest.java        | 460 ++++++++++++++++++++-
 3 files changed, 686 insertions(+), 31 deletions(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/BlockingSegmentCompletionFSM.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/BlockingSegmentCompletionFSM.java
index 381bebfdb30..c8c4df32241 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/BlockingSegmentCompletionFSM.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/BlockingSegmentCompletionFSM.java
@@ -141,8 +141,9 @@ public class BlockingSegmentCompletionFSM implements 
SegmentCompletionFSM {
     _maxTimeAllowedToCommitMs = _startTimeMs + _initialCommitTimeMs;
     _controllerVipUrl = segmentCompletionManager.getControllerVipUrl();
 
-    // NOTE: If segment ZK status is COMMITTING, The current behaviour expects 
the segment protocol calls to fail and
-    // abort leaving it to realtime segment validation job to fix it.
+    // NOTE: If segment ZK status is COMMITTING, a pauseless commit start with 
the same end offset resumes the commit
+    // (see PinotLLCRealtimeSegmentManager). Otherwise the segment protocol 
calls fail and abort, leaving it to realtime
+    // segment validation job to fix it.
     if (segmentMetadata.getStatus().isCompleted()) {
       _state = BlockingSegmentCompletionFSMState.COMMITTED;
       String endOffset = segmentMetadata.getEndOffset();
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java
index 9303c6e5bcb..b4add6ac69e 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java
@@ -659,9 +659,15 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     // Step-1: Update PROPERTYSTORE
     LOGGER.info("Committing segment metadata for segment: {}", 
committingSegmentName);
     long startTimeNs1 = System.nanoTime();
-    SegmentZKMetadata committingSegmentZKMetadata = toCommitting
-        ? updateSegmentZKMetadataToCommitting(realtimeTableName, 
committingSegmentDescriptor)
-        : updateSegmentZKMetadataToDone(realtimeTableName, 
committingSegmentDescriptor, Status.IN_PROGRESS);
+    boolean resumed = toCommitting && 
isCommitStartResumable(realtimeTableName, committingSegmentDescriptor);
+    SegmentZKMetadata committingSegmentZKMetadata;
+    if (resumed) {
+      committingSegmentZKMetadata = getSegmentZKMetadata(realtimeTableName, 
committingSegmentName);
+    } else {
+      committingSegmentZKMetadata = toCommitting
+          ? updateSegmentZKMetadataToCommitting(realtimeTableName, 
committingSegmentDescriptor)
+          : updateSegmentZKMetadataToDone(realtimeTableName, 
committingSegmentDescriptor, Status.IN_PROGRESS);
+    }
 
     preProcessNewSegmentZKMetadata();
 
@@ -669,7 +675,7 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     long startTimeNs2 = System.nanoTime();
     String newConsumingSegmentName =
         createNewSegmentMetadata(tableConfig, committingSegmentDescriptor, 
committingSegmentZKMetadata,
-            instancePartitions);
+            instancePartitions, toCommitting, resumed);
 
     preProcessCommitIdealStateUpdate();
 
@@ -677,33 +683,41 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     LOGGER.info("Updating Idealstate for previous: {} and new segment: {}", 
committingSegmentName,
         newConsumingSegmentName);
     long startTimeNs3 = System.nanoTime();
-    Map<String, Map<String, String>> instanceStatesMapAfterStep3;
-    boolean newConsumingSegmentInIdealState = false;
+    IdealState idealState;
 
     // When multiple segments of the same table complete around the same time 
it is possible that
     // the idealstate update fails due to contention. We serialize the updates 
to the idealstate
     // to reduce this contention. We may still contend with RetentionManager, 
or other updates
     // to idealstate from other controllers, but then we have the retry 
mechanism to get around that.
     try {
-      IdealState idealState =
+      idealState =
           updateIdealStateForSegments(tableConfig, committingSegmentName, 
newConsumingSegmentName, instancePartitions);
-      instanceStatesMapAfterStep3 = idealState.getRecord().getMapFields();
-      if (newConsumingSegmentName != null) {
-        newConsumingSegmentInIdealState = 
instanceStatesMapAfterStep3.containsKey(newConsumingSegmentName);
-        if (!newConsumingSegmentInIdealState) {
-          LOGGER.info(
-              "Cleaning up segment ZK metadata for new consuming segment {} of 
table {} because it was not added to "
-                  + "IdealState. This can happen when table/topic consumption 
is paused.",
-              newConsumingSegmentName, realtimeTableName);
+    } catch (RuntimeException e) {
+      if (!toCommitting) {
+        if (newConsumingSegmentName != null) {
           removeSegmentZKMetadataBestEffort(realtimeTableName, 
newConsumingSegmentName);
-          newConsumingSegmentName = null;
         }
+        throw e;
       }
-    } catch (RuntimeException e) {
-      if (newConsumingSegmentName != null) {
+      idealState =
+          handleCommitStartIdealStateUpdateFailure(realtimeTableName, 
committingSegmentName, newConsumingSegmentName);
+      if (idealState == null) {
+        throw e;
+      }
+    }
+
+    Map<String, Map<String, String>> instanceStatesMapAfterStep3 = 
idealState.getRecord().getMapFields();
+    boolean newConsumingSegmentInIdealState = false;
+    if (newConsumingSegmentName != null) {
+      newConsumingSegmentInIdealState = 
instanceStatesMapAfterStep3.containsKey(newConsumingSegmentName);
+      if (!newConsumingSegmentInIdealState) {
+        LOGGER.info(
+            "Cleaning up segment ZK metadata for new consuming segment {} of 
table {} because it was not added to "
+                + "IdealState. This can happen when table/topic consumption is 
paused.",
+            newConsumingSegmentName, realtimeTableName);
         removeSegmentZKMetadataBestEffort(realtimeTableName, 
newConsumingSegmentName);
+        newConsumingSegmentName = null;
       }
-      throw e;
     }
 
     long endTimeNs = System.nanoTime();
@@ -806,11 +820,65 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     return segmentZKMetadata;
   }
 
+  /// Returns `true` if a pauseless commit start can resume an earlier attempt 
that moved the segment to COMMITTING
+  /// with the same end offset, but did not complete, e.g. because the 
IdealState update failed or the controller
+  /// stopped. Steps 2 and 3 are idempotent for the same segment and end 
offset: the resumed attempt uses the new
+  /// segment created by an earlier attempt if any (see 
findNewSegmentOfEarlierAttempt), new segment ZK metadata is only
+  /// created if absent, and the IdealState update is skipped when it is 
already applied. Once the max segment
+  /// completion time is exceeded, RealtimeSegmentValidationManager repairs 
the segment instead.
+  private boolean isCommitStartResumable(String realtimeTableName,
+      CommittingSegmentDescriptor committingSegmentDescriptor) {
+    String segmentName = committingSegmentDescriptor.getSegmentName();
+    SegmentZKMetadata segmentZKMetadata = 
getSegmentZKMetadata(realtimeTableName, segmentName);
+    if (segmentZKMetadata.getStatus() != Status.COMMITTING) {
+      return false;
+    }
+    String endOffset = segmentZKMetadata.getEndOffset();
+    if (!committingSegmentDescriptor.getNextOffset().equals(endOffset)) {
+      LOGGER.warn("Cannot resume commit start for segment: {} with end offset: 
{}, segment is COMMITTING with end "
+          + "offset: {}", segmentName, 
committingSegmentDescriptor.getNextOffset(), endOffset);
+      return false;
+    }
+    if (isExceededMaxSegmentCompletionTime(realtimeTableName, segmentName, 
getCurrentTimeMs())) {
+      LOGGER.warn("Cannot resume commit start for segment: {} after the max 
segment completion time", segmentName);
+      return false;
+    }
+    LOGGER.info("Resuming commit start for segment: {} which is COMMITTING 
with end offset: {}", segmentName,
+        endOffset);
+    return true;
+  }
+
+  /// Returns the new segment created by an earlier attempt of a resumed 
commit start, or `null` if there is none.
+  ///
+  /// The new segment is named after its creation time with minute 
granularity, and is created between step 1 of the
+  /// first attempt (the modification time of the committing segment ZK 
metadata) and now, which is bounded by the max
+  /// segment completion time. Probes the names for every minute in between, 
with one minute of slack on both ends for
+  /// clock differences between controllers and ZK. This relies on nothing 
rewriting the committing segment ZK metadata
+  /// while it stays COMMITTING, as a later modification time would skip the 
minutes in which the new segment was
+  /// created.
+  @Nullable
+  private String findNewSegmentOfEarlierAttempt(String realtimeTableName, 
LLCSegmentName committingLLCSegment) {
+    Stat stat = new Stat();
+    getSegmentZKMetadata(realtimeTableName, 
committingLLCSegment.getSegmentName(), stat);
+    long oneMinuteMs = TimeUnit.MINUTES.toMillis(1);
+    long startTimeMs = Math.floorDiv(stat.getMtime(), oneMinuteMs) * 
oneMinuteMs - oneMinuteMs;
+    long endTimeMs = Math.floorDiv(getCurrentTimeMs(), oneMinuteMs) * 
oneMinuteMs + oneMinuteMs;
+    for (long timeMs = startTimeMs; timeMs <= endTimeMs; timeMs += 
oneMinuteMs) {
+      String segmentName = new 
LLCSegmentName(committingLLCSegment.getTableName(),
+          committingLLCSegment.getPartitionGroupId(), 
committingLLCSegment.getSequenceNumber() + 1,
+          timeMs).getSegmentName();
+      if (segmentZKMetadataExists(realtimeTableName, segmentName)) {
+        return segmentName;
+      }
+    }
+    return null;
+  }
+
   // Step 2: Create new segment metadata
   @Nullable
   private String createNewSegmentMetadata(TableConfig tableConfig,
       CommittingSegmentDescriptor committingSegmentDescriptor, 
SegmentZKMetadata committingSegmentZKMetadata,
-      InstancePartitions instancePartitions) {
+      InstancePartitions instancePartitions, boolean createIfAbsent, boolean 
resumed) {
     String committingSegmentName = 
committingSegmentDescriptor.getSegmentName();
 
     String realtimeTableName = tableConfig.getTableName();
@@ -836,17 +904,35 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
             + "paused.", committingSegmentName);
         return null;
       }
+      // Another attempt of the same commit start might have created the new 
segment already, and the segment might be
+      // consuming or even committed, so never overwrite its ZK metadata
+      if (resumed) {
+        String existingNewSegmentName = 
findNewSegmentOfEarlierAttempt(realtimeTableName, committingLLCSegment);
+        if (existingNewSegmentName != null) {
+          LOGGER.info("Using existing segment metadata for new segment: {}", 
existingNewSegmentName);
+          return existingNewSegmentName;
+        }
+      }
       String rawTableName = 
TableNameBuilder.extractRawTableName(realtimeTableName);
       long newSegmentCreationTimeMs = getCurrentTimeMs();
       LLCSegmentName newLLCSegment = new LLCSegmentName(rawTableName, 
committingSegmentPartitionGroupId,
           committingLLCSegment.getSequenceNumber() + 1, 
newSegmentCreationTimeMs);
+      newConsumingSegmentName = newLLCSegment.getSegmentName();
 
       StreamConfig streamConfig =
           
IngestionConfigUtils.getStreamConfigFromPinotPartitionId(streamConfigs, 
committingSegmentPartitionGroupId);
-      createNewSegmentZKMetadata(tableConfig, streamConfig, newLLCSegment, 
newSegmentCreationTimeMs,
-          committingSegmentDescriptor, committingSegmentZKMetadata, 
instancePartitions, partitionIds.size(),
-          numReplicas);
-      newConsumingSegmentName = newLLCSegment.getSegmentName();
+      SegmentZKMetadata newSegmentZKMetadata =
+          buildNewSegmentZKMetadata(tableConfig, streamConfig, newLLCSegment, 
newSegmentCreationTimeMs,
+              committingSegmentDescriptor, committingSegmentZKMetadata, 
instancePartitions, partitionIds.size(),
+              numReplicas);
+      if (createIfAbsent) {
+        if (!createSegmentZKMetadataIfAbsent(realtimeTableName, 
newSegmentZKMetadata)) {
+          LOGGER.info("Using existing segment metadata for new segment: {}", 
newConsumingSegmentName);
+          return newConsumingSegmentName;
+        }
+      } else {
+        persistSegmentZKMetadata(realtimeTableName, newSegmentZKMetadata, -1);
+      }
       LOGGER.info("Created new segment metadata for segment: {} with status: 
{}.", newConsumingSegmentName,
           Status.IN_PROGRESS);
     } else {
@@ -873,6 +959,73 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     }
   }
 
+  @VisibleForTesting
+  boolean segmentZKMetadataExists(String realtimeTableName, String 
segmentName) {
+    String segmentMetadataPath =
+        
ZKMetadataProvider.constructPropertyStorePathForSegment(realtimeTableName, 
segmentName);
+    return _propertyStore.exists(segmentMetadataPath, AccessOption.PERSISTENT);
+  }
+
+  /// Atomically creates the segment ZK metadata. Returns `false` if it 
already exists.
+  @VisibleForTesting
+  boolean createSegmentZKMetadataIfAbsent(String realtimeTableName, 
SegmentZKMetadata segmentZKMetadata) {
+    String segmentName = segmentZKMetadata.getSegmentName();
+    String segmentMetadataPath =
+        
ZKMetadataProvider.constructPropertyStorePathForSegment(realtimeTableName, 
segmentName);
+    LOGGER.info("Creating segment ZK metadata for segment: {}", segmentName);
+    try {
+      if (_propertyStore.create(segmentMetadataPath, 
segmentZKMetadata.toZNRecord(), AccessOption.PERSISTENT)) {
+        return true;
+      }
+      Preconditions.checkState(_propertyStore.exists(segmentMetadataPath, 
AccessOption.PERSISTENT),
+          "Failed to create segment ZK metadata for segment: %s of table: %s", 
segmentName, realtimeTableName);
+      return false;
+    } catch (Exception e) {
+      _controllerMetrics.addMeteredTableValue(realtimeTableName, 
ControllerMeter.LLC_ZOOKEEPER_UPDATE_FAILURES, 1L);
+      throw e;
+    }
+  }
+
+  /// Handles a failed IdealState update (step 3) of a pauseless commit start. 
Re-reads the IdealState to find out
+  /// whether the update was applied (e.g. the write succeeded but the ZK 
connection was lost before the response
+  /// arrived). If it was, returns the IdealState so that the commit can 
proceed. Otherwise returns `null`, and the
+  /// committing segment stays COMMITTING so that the next attempt of the 
commit can resume it (see
+  /// isCommitStartResumable).
+  ///
+  /// The ZK metadata of the new consuming segment is kept for the next 
attempt, or for
+  /// RealtimeSegmentValidationManager to add the segment to the IdealState. 
It is only removed when the segment can no
+  /// longer be added, i.e. it is not in the IdealState and the committing 
segment is no longer CONSUMING (e.g. the
+  /// validation manager repaired the partition with another segment). Adding 
the segment requires the committing
+  /// segment to be CONSUMING, and the IdealState version check prevents a 
concurrent attempt from adding it based on
+  /// an older IdealState.
+  @Nullable
+  private IdealState handleCommitStartIdealStateUpdateFailure(String 
realtimeTableName, String committingSegmentName,
+      @Nullable String newConsumingSegmentName) {
+    IdealState idealState;
+    try {
+      idealState = getIdealState(realtimeTableName);
+    } catch (Exception e) {
+      LOGGER.warn("Failed to read IdealState of table: {} after failing to 
update it for segment: {}",
+          realtimeTableName, committingSegmentName, e);
+      return null;
+    }
+    if (isSegmentCompletionApplied(idealState, committingSegmentName, 
newConsumingSegmentName)) {
+      LOGGER.info("IdealState of table: {} already reflects the completion of 
segment: {}, proceeding with the commit",
+          realtimeTableName, committingSegmentName);
+      return idealState;
+    }
+    if (newConsumingSegmentName != null && 
idealState.getInstanceStateMap(newConsumingSegmentName) == null) {
+      Map<String, String> committingSegmentInstanceStateMap = 
idealState.getInstanceStateMap(committingSegmentName);
+      if (committingSegmentInstanceStateMap == null || 
!committingSegmentInstanceStateMap.containsValue(
+          SegmentStateModel.CONSUMING)) {
+        LOGGER.info("Removing segment ZK metadata for new segment: {} because 
segment: {} is no longer CONSUMING",
+            newConsumingSegmentName, committingSegmentName);
+        removeSegmentZKMetadataBestEffort(realtimeTableName, 
newConsumingSegmentName);
+      }
+    }
+    return null;
+  }
+
   // Step 3: Update IdealState
   private IdealState updateIdealStateForSegments(TableConfig tableConfig, 
String committingSegmentName,
       String newConsumingSegmentName, InstancePartitions instancePartitions) {
@@ -977,6 +1130,17 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
       LLCSegmentName newLLCSegmentName, long creationTimeMs, 
CommittingSegmentDescriptor committingSegmentDescriptor,
       @Nullable SegmentZKMetadata committingSegmentZKMetadata, 
InstancePartitions instancePartitions, int numPartitions,
       int numReplicas) {
+    SegmentZKMetadata newSegmentZKMetadata =
+        buildNewSegmentZKMetadata(tableConfig, streamConfig, 
newLLCSegmentName, creationTimeMs,
+            committingSegmentDescriptor, committingSegmentZKMetadata, 
instancePartitions, numPartitions, numReplicas);
+    persistSegmentZKMetadata(tableConfig.getTableName(), newSegmentZKMetadata, 
-1);
+  }
+
+  /// Builds segment ZK metadata for the new CONSUMING segment.
+  private SegmentZKMetadata buildNewSegmentZKMetadata(TableConfig tableConfig, 
StreamConfig streamConfig,
+      LLCSegmentName newLLCSegmentName, long creationTimeMs, 
CommittingSegmentDescriptor committingSegmentDescriptor,
+      @Nullable SegmentZKMetadata committingSegmentZKMetadata, 
InstancePartitions instancePartitions, int numPartitions,
+      int numReplicas) {
     String realtimeTableName = tableConfig.getTableName();
     String segmentName = newLLCSegmentName.getSegmentName();
 
@@ -1027,8 +1191,7 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     flushThresholdUpdater.updateFlushThreshold(streamConfig, 
newSegmentZKMetadata,
         getMaxNumPartitionsPerInstance(instancePartitions, numPartitions, 
numReplicas));
     updateFlushThresholdGauge(streamConfig, 
newSegmentZKMetadata.getSizeThresholdToFlushSegment());
-
-    persistSegmentZKMetadata(realtimeTableName, newSegmentZKMetadata, -1);
+    return newSegmentZKMetadata;
   }
 
   private void updateCommittingSegmentSizeGauge(StreamConfig streamConfig, 
long segmentSize) {
@@ -1384,9 +1547,15 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
 
   /// Returns the latest LLC realtime segment ZK metadata for each partition.
   ///
+  /// When several segments of a partition have the highest sequence number, 
e.g. the new segment of a failed commit
+  /// start and the new segment of a concurrent repair, the one in the 
IdealState is the latest one. If none of them is
+  /// in the IdealState, whichever is listed first is the latest one.
+  ///
   /// @param realtimeTableName Realtime table name
+  /// @param instanceStatesMap Instance states map of the IdealState
   /// @return Map from partition group id to the latest LLC realtime segment 
ZK metadata
-  private Map<Integer, SegmentZKMetadata> getLatestSegmentZKMetadataMap(String 
realtimeTableName) {
+  private Map<Integer, SegmentZKMetadata> getLatestSegmentZKMetadataMap(String 
realtimeTableName,
+      Map<String, Map<String, String>> instanceStatesMap) {
     List<String> segments = getLLCSegments(realtimeTableName);
 
     Map<Integer, LLCSegmentName> latestLLCSegmentNameMap = new HashMap<>();
@@ -1398,6 +1567,11 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
         } else {
           if (llcSegmentName.getSequenceNumber() > 
latestLLCSegmentName.getSequenceNumber()) {
             return llcSegmentName;
+          } else if (llcSegmentName.getSequenceNumber() == 
latestLLCSegmentName.getSequenceNumber()) {
+            LOGGER.warn("Found segments: {} and {} with the same sequence 
number in partition: {} of table: {}",
+                latestLLCSegmentName.getSegmentName(), segmentName, 
partitionId, realtimeTableName);
+            return 
!instanceStatesMap.containsKey(latestLLCSegmentName.getSegmentName())
+                && instanceStatesMap.containsKey(segmentName) ? llcSegmentName 
: latestLLCSegmentName;
           } else {
             return latestLLCSegmentName;
           }
@@ -1503,6 +1677,13 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
         throw new HelixHelper.PermanentUpdaterException(
             "Exceeded max segment completion time for segment " + 
committingSegmentName);
       }
+      // A previous attempt might have been applied even though it was 
reported as failed, e.g. when the write
+      // succeeded but the ZK connection was lost before the response arrived
+      if (isSegmentCompletionApplied(idealState, committingSegmentName, 
newSegmentName)) {
+        LOGGER.info("IdealState of table: {} already reflects the completion 
of segment: {}, skipping the update",
+            realtimeTableName, committingSegmentName);
+        return idealState;
+      }
       
updateInstanceStatesForNewConsumingSegment(idealState.getRecord().getMapFields(),
 committingSegmentName,
           isTablePaused(idealState) || isTopicPaused(idealState, 
committingSegmentName), newSegmentName,
           segmentAssignment, instancePartitionsMap);
@@ -1517,6 +1698,20 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
         false);
   }
 
+  /// Returns `true` if the IdealState already reflects the completion of the 
committing segment, i.e. all its replicas
+  /// are ONLINE, and the new consuming segment (if one is expected) is 
present.
+  @VisibleForTesting
+  static boolean isSegmentCompletionApplied(IdealState idealState, String 
committingSegmentName,
+      @Nullable String newSegmentName) {
+    Map<String, String> committingSegmentInstanceStateMap = 
idealState.getInstanceStateMap(committingSegmentName);
+    if (committingSegmentInstanceStateMap == null || 
committingSegmentInstanceStateMap.isEmpty()
+        || 
!committingSegmentInstanceStateMap.values().stream().allMatch(SegmentStateModel.ONLINE::equals))
 {
+      return false;
+    }
+    return newSegmentName == null || isTablePaused(idealState) || 
isTopicPaused(idealState, committingSegmentName)
+        || idealState.getInstanceStateMap(newSegmentName) != null;
+  }
+
   public static boolean isTablePaused(IdealState idealState) {
     PauseState pauseState = extractTablePauseState(idealState);
     if (pauseState != null) {
@@ -1735,7 +1930,8 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
         
StreamConsumerFactoryProvider.create(streamConfigs.get(0)).createStreamMsgOffsetFactory();
 
     // Get the latest segment ZK metadata for each partition
-    Map<Integer, SegmentZKMetadata> latestSegmentZKMetadataMap = 
getLatestSegmentZKMetadataMap(realtimeTableName);
+    Map<Integer, SegmentZKMetadata> latestSegmentZKMetadataMap =
+        getLatestSegmentZKMetadataMap(realtimeTableName, instanceStatesMap);
 
     // Create a map from partition id to start offset
     // TODO: Directly return map from StreamMetadataProvider
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManagerTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManagerTest.java
index 34deec66251..2f943077ccb 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManagerTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManagerTest.java
@@ -30,6 +30,7 @@ import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -46,9 +47,12 @@ import javax.annotation.Nullable;
 import javax.ws.rs.core.Response;
 import org.apache.commons.io.FileUtils;
 import org.apache.helix.AccessOption;
+import org.apache.helix.BaseDataAccessor;
 import org.apache.helix.ClusterMessagingService;
 import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixDataAccessor;
 import org.apache.helix.HelixManager;
+import org.apache.helix.PropertyKey;
 import org.apache.helix.model.ExternalView;
 import org.apache.helix.model.IdealState;
 import org.apache.helix.model.InstanceConfig;
@@ -1486,6 +1490,403 @@ public class PinotLLCRealtimeSegmentManagerTest {
         AccessOption.PERSISTENT);
   }
 
+  /// Sets up a table for pauseless commit start tests: the committing 
segments list can be created, and removing a
+  /// segment ZK metadata removes it from the fake segment manager.
+  private FakePinotLLCRealtimeSegmentManager setUpTableForCommitStart() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = new 
FakePinotLLCRealtimeSegmentManager();
+    setUpNewTable(segmentManager, 2, 5, 4);
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
segmentManager._mockResourceManager.getPropertyStore();
+    when(propertyStore.create(anyString(), any(), 
eq(AccessOption.PERSISTENT))).thenReturn(true);
+    when(propertyStore.remove(anyString(), 
eq(AccessOption.PERSISTENT))).thenAnswer(invocation -> {
+      String path = invocation.getArgument(0);
+      String segmentName = path.substring(path.lastIndexOf('/') + 1);
+      segmentManager._segmentZKMetadataMap.remove(segmentName);
+      segmentManager._segmentZKMetadataVersionMap.remove(segmentName);
+      return true;
+    });
+    return segmentManager;
+  }
+
+  /// Simulates another controller completing the commit start of the segment: 
the new segment is created (with the
+  /// given status), the committing segment becomes ONLINE and the new segment 
CONSUMING in the IdealState.
+  private static void 
completeCommitStartElsewhere(FakePinotLLCRealtimeSegmentManager segmentManager,
+      String committingSegment, String newSegment, Status newSegmentStatus) {
+    SegmentZKMetadata newSegmentZKMetadata = new SegmentZKMetadata(newSegment);
+    newSegmentZKMetadata.setStatus(newSegmentStatus);
+    newSegmentZKMetadata.setStartOffset(NEXT_OFFSET);
+    segmentManager.persistSegmentZKMetadata(REALTIME_TABLE_NAME, 
newSegmentZKMetadata, -1);
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    instanceStatesMap.put(newSegment, new 
TreeMap<>(instanceStatesMap.get(committingSegment)));
+    instanceStatesMap.get(committingSegment).replaceAll((instance, state) -> 
SegmentStateModel.ONLINE);
+  }
+
+  @Test
+  public void testCommitStartResumesAfterIdealStateUpdateFailure() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    CommittingSegmentDescriptor committingSegmentDescriptor = 
createCommittingSegmentDescriptor(committingSegment);
+
+    segmentManager._numFailedIdealStateUpdates = 1;
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+    // The committing segment stays COMMITTING, and the new segment is kept 
for the next attempt
+    SegmentZKMetadata committingSegmentZKMetadata = 
segmentManager._segmentZKMetadataMap.get(committingSegment);
+    assertEquals(committingSegmentZKMetadata.getStatus(), Status.COMMITTING);
+    assertEquals(committingSegmentZKMetadata.getEndOffset(), NEXT_OFFSET);
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(newConsumingSegment).getStatus(),
 Status.IN_PROGRESS);
+    int newSegmentZKMetadataVersion = 
segmentManager._segmentZKMetadataVersionMap.get(newConsumingSegment);
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+    assertFalse(instanceStatesMap.containsKey(newConsumingSegment));
+
+    // The retry resumes the commit start with the same new segment, although 
the current time moved on
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(2);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    committingSegmentZKMetadata = 
segmentManager._segmentZKMetadataMap.get(committingSegment);
+    assertEquals(committingSegmentZKMetadata.getStatus(), Status.COMMITTING);
+    assertEquals(committingSegmentZKMetadata.getEndOffset(), NEXT_OFFSET);
+    assertEquals(segmentManager._segmentZKMetadataMap.keySet().stream()
+        .filter(segment -> new LLCSegmentName(segment).getPartitionGroupId() 
== 0)
+        .collect(Collectors.toSet()), Set.of(committingSegment, 
newConsumingSegment));
+    assertEquals((int) 
segmentManager._segmentZKMetadataVersionMap.get(newConsumingSegment),
+        newSegmentZKMetadataVersion);
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()), 
Set.of(SegmentStateModel.ONLINE));
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(newConsumingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+  }
+
+  @Test
+  public void 
testCommitStartResumeCreatesNewSegmentWithCurrentTimeWhenEarlierAttemptCreatedNone()
 {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    CommittingSegmentDescriptor committingSegmentDescriptor = 
createCommittingSegmentDescriptor(committingSegment);
+
+    // The first attempt fails after step 1, before creating the new segment
+    segmentManager._beforeNewSegmentZKMetadata = () -> {
+      throw new RuntimeException("Injected failure before creating the new 
segment");
+    };
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+    segmentManager._beforeNewSegmentZKMetadata = () -> {
+    };
+
+    // The resumed attempt creates the new segment with its own creation time, 
but fails to update the IdealState
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(6);
+    long newSegmentCreationTimeMs = segmentManager._currentTimeMs;
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
newSegmentCreationTimeMs).getSegmentName();
+    segmentManager._numFailedIdealStateUpdates = 1;
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(newConsumingSegment).getCreationTime(),
+        newSegmentCreationTimeMs);
+
+    // The next resumed attempt finds that new segment
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(1);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    assertEquals(segmentManager._segmentZKMetadataMap.keySet().stream()
+        .filter(segment -> new LLCSegmentName(segment).getPartitionGroupId() 
== 0)
+        .collect(Collectors.toSet()), Set.of(committingSegment, 
newConsumingSegment));
+    assertEquals(new 
HashSet<>(segmentManager._idealState.getInstanceStateMap(newConsumingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+  }
+
+  @Test
+  public void testCommitStartResumeFindsNewSegmentAtEdgeOfClockSkewAllowance() 
{
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    CommittingSegmentDescriptor committingSegmentDescriptor = 
createCommittingSegmentDescriptor(committingSegment);
+    long minuteStartMs = CURRENT_TIME_MS / 60_000 * 60_000;
+
+    // Step 1 at second 59 of a minute, then the first attempt fails before 
creating the new segment
+    segmentManager._currentTimeMs = minuteStartMs + 59_000;
+    segmentManager._beforeNewSegmentZKMetadata = () -> {
+      throw new RuntimeException("Injected failure before creating the new 
segment");
+    };
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+    segmentManager._beforeNewSegmentZKMetadata = () -> {
+    };
+
+    // Another controller, with a clock less than a minute ahead, created the 
new segment in the next minute
+    segmentManager._currentTimeMs = minuteStartMs + 
TimeUnit.MINUTES.toMillis(10) + 1_000;
+    String newConsumingSegment =
+        new LLCSegmentName(RAW_TABLE_NAME, 0, 1, minuteStartMs + 
TimeUnit.MINUTES.toMillis(11)).getSegmentName();
+    SegmentZKMetadata newSegmentZKMetadata = new 
SegmentZKMetadata(newConsumingSegment);
+    newSegmentZKMetadata.setStatus(Status.IN_PROGRESS);
+    newSegmentZKMetadata.setStartOffset(NEXT_OFFSET);
+    segmentManager.persistSegmentZKMetadata(REALTIME_TABLE_NAME, 
newSegmentZKMetadata, -1);
+
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    assertEquals(segmentManager._segmentZKMetadataMap.keySet().stream()
+        .filter(segment -> new LLCSegmentName(segment).getPartitionGroupId() 
== 0)
+        .collect(Collectors.toSet()), Set.of(committingSegment, 
newConsumingSegment));
+    assertEquals(new 
HashSet<>(segmentManager._idealState.getInstanceStateMap(newConsumingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+  }
+
+  @Test
+  public void testCommitStartSucceedsWhenFailedIdealStateUpdateWasApplied() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+
+    segmentManager._numAppliedButFailedIdealStateUpdates = 1;
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+        createCommittingSegmentDescriptor(committingSegment));
+
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(committingSegment).getStatus(),
 Status.COMMITTING);
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(newConsumingSegment).getStatus(),
 Status.IN_PROGRESS);
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()), 
Set.of(SegmentStateModel.ONLINE));
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(newConsumingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
segmentManager._mockResourceManager.getPropertyStore();
+    verify(propertyStore, never()).remove(anyString(), 
eq(AccessOption.PERSISTENT));
+  }
+
+  @Test
+  public void 
testCommitStartRetryAfterCompletedCommitStartDoesNotOverwriteNewSegment() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    CommittingSegmentDescriptor committingSegmentDescriptor = 
createCommittingSegmentDescriptor(committingSegment);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    int newSegmentZKMetadataVersion = 
segmentManager._segmentZKMetadataVersionMap.get(newConsumingSegment);
+    Map<String, Map<String, String>> expectedInstanceStatesMap =
+        
cloneInstanceStatesMap(segmentManager._idealState.getRecord().getMapFields());
+
+    // E.g. the server retries the commit after the commit end failed. The new 
segment might be consuming already.
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    assertEquals((int) 
segmentManager._segmentZKMetadataVersionMap.get(newConsumingSegment),
+        newSegmentZKMetadataVersion);
+    assertEquals(segmentManager._idealState.getRecord().getMapFields(), 
expectedInstanceStatesMap);
+  }
+
+  @Test
+  public void 
testCommitStartDoesNotOverwriteNewSegmentCreatedByConcurrentAttempt() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+
+    // After step 1, another controller (e.g. the new lead controller) resumes 
and completes the commit start, and the
+    // new segment is committed already
+    segmentManager._beforeNewSegmentZKMetadata =
+        () -> completeCommitStartElsewhere(segmentManager, committingSegment, 
newConsumingSegment, Status.DONE);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+        createCommittingSegmentDescriptor(committingSegment));
+
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(newConsumingSegment).getStatus(),
 Status.DONE);
+    assertEquals(new 
HashSet<>(segmentManager._idealState.getInstanceStateMap(committingSegment).values()),
+        Set.of(SegmentStateModel.ONLINE));
+  }
+
+  @Test
+  public void 
testCommitStartFailureRemovesNewSegmentAfterPartitionRepairedWithAnotherSegment()
 {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    String repairSegment =
+        new LLCSegmentName(RAW_TABLE_NAME, 0, 1, CURRENT_TIME_MS + 
TimeUnit.MINUTES.toMillis(2)).getSegmentName();
+
+    // Before the IdealState update, RealtimeSegmentValidationManager repairs 
the partition with another new segment,
+    // so the new segment of this attempt can no longer be added
+    segmentManager._beforeCommitIdealStateUpdate =
+        () -> completeCommitStartElsewhere(segmentManager, committingSegment, 
repairSegment, Status.IN_PROGRESS);
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+            createCommittingSegmentDescriptor(committingSegment)));
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    assertFalse(instanceStatesMap.containsKey(newConsumingSegment));
+    assertEquals(new HashSet<>(instanceStatesMap.get(repairSegment).values()), 
Set.of(SegmentStateModel.CONSUMING));
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(committingSegment).getStatus(),
 Status.COMMITTING);
+  }
+
+  @Test
+  public void 
testValidationAfterCommitStartAndConcurrentRepairCreatedDifferentNewSegments() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    String repairSegment =
+        new LLCSegmentName(RAW_TABLE_NAME, 0, 1, CURRENT_TIME_MS + 
TimeUnit.MINUTES.toMillis(30)).getSegmentName();
+
+    // The commit start keeps its new segment after its IdealState update 
fails, then a concurrent repair that did not
+    // see that segment adds another new segment with the same sequence number
+    segmentManager._numFailedIdealStateUpdates = 1;
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+            createCommittingSegmentDescriptor(committingSegment)));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    completeCommitStartElsewhere(segmentManager, committingSegment, 
repairSegment, Status.IN_PROGRESS);
+    Map<String, Map<String, String>> expectedInstanceStatesMap =
+        
cloneInstanceStatesMap(segmentManager._idealState.getRecord().getMapFields());
+
+    // Validation keeps the segment in the IdealState as the latest segment, 
whichever order the segments are listed in
+    segmentManager._exceededMaxSegmentCompletionTime = true;
+    for (String lastSegment : List.of(newConsumingSegment, repairSegment)) {
+      Map<String, SegmentZKMetadata> segmentZKMetadataMap = new 
LinkedHashMap<>(segmentManager._segmentZKMetadataMap);
+      segmentZKMetadataMap.put(lastSegment, 
segmentZKMetadataMap.remove(lastSegment));
+      segmentManager._segmentZKMetadataMap = segmentZKMetadataMap;
+      segmentManager.ensureAllPartitionsConsuming();
+      assertEquals(segmentManager._idealState.getRecord().getMapFields(), 
expectedInstanceStatesMap);
+    }
+  }
+
+  @Test
+  public void 
testCommitStartUsesNewSegmentCreatedConcurrentlyByAnotherAttempt() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    segmentManager._useProductionCreateIfAbsent = true;
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegmentPath =
+        
ZKMetadataProvider.constructPropertyStorePathForSegment(REALTIME_TABLE_NAME, 
newConsumingSegment);
+    // Another attempt creates the new segment ZK metadata between the 
existence check and the creation
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
segmentManager._mockResourceManager.getPropertyStore();
+    when(propertyStore.create(eq(newConsumingSegmentPath), any(), 
eq(AccessOption.PERSISTENT))).thenReturn(false);
+    when(propertyStore.exists(newConsumingSegmentPath, 
AccessOption.PERSISTENT)).thenReturn(true);
+
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+        createCommittingSegmentDescriptor(committingSegment));
+    verify(propertyStore).create(eq(newConsumingSegmentPath), any(), 
eq(AccessOption.PERSISTENT));
+    verify(propertyStore, never()).set(eq(newConsumingSegmentPath), any(), 
anyInt(), anyInt());
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    assertEquals(new 
HashSet<>(segmentManager._idealState.getInstanceStateMap(newConsumingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+  }
+
+  @Test
+  public void testCreateSegmentZKMetadataIfAbsentFailsWhenNotCreated() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    segmentManager._useProductionCreateIfAbsent = true;
+    ZkHelixPropertyStore<ZNRecord> propertyStore = 
segmentManager._mockResourceManager.getPropertyStore();
+    when(propertyStore.create(anyString(), any(), 
eq(AccessOption.PERSISTENT))).thenReturn(false);
+    SegmentZKMetadata segmentZKMetadata =
+        new SegmentZKMetadata(new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName());
+    assertThrows(IllegalStateException.class,
+        () -> 
segmentManager.createSegmentZKMetadataIfAbsent(REALTIME_TABLE_NAME, 
segmentZKMetadata));
+  }
+
+  @Test
+  public void testCommitStartDoesNotResumeWithDifferentEndOffset() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    segmentManager._numFailedIdealStateUpdates = 1;
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+            createCommittingSegmentDescriptor(committingSegment)));
+
+    String otherOffset = new LongMsgOffset(PARTITION_OFFSET.getOffset() + 
NUM_DOCS + 1L).toString();
+    IllegalStateException e = expectThrows(IllegalStateException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+            createCommittingSegmentDescriptor(committingSegment, 
otherOffset)));
+    assertTrue(e.getMessage().contains("should be IN_PROGRESS, found: 
COMMITTING"));
+  }
+
+  @Test
+  public void testCommitStartDoesNotResumeAfterMaxSegmentCompletionTime() {
+    FakePinotLLCRealtimeSegmentManager segmentManager = 
setUpTableForCommitStart();
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    CommittingSegmentDescriptor committingSegmentDescriptor = 
createCommittingSegmentDescriptor(committingSegment);
+    segmentManager._numFailedIdealStateUpdates = 1;
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+
+    // RealtimeSegmentValidationManager owns the repair once the max segment 
completion time is exceeded
+    segmentManager._exceededMaxSegmentCompletionTime = true;
+    assertThrows(IllegalStateException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+    
assertFalse(segmentManager._idealState.getRecord().getMapFields().containsKey(newConsumingSegment));
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(committingSegment).getStatus(),
 Status.COMMITTING);
+  }
+
+  /// Runs the production IdealState update with a write that is applied but 
reported as failed, for both the group
+  /// commit and the single commit.
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testIdealStateUpdateOnSegmentCompletionSkipsAppliedUpdate() {
+    for (boolean groupCommitEnabled : new boolean[]{true, false}) {
+      ControllerConf controllerConf = new ControllerConf();
+      controllerConf.setDataDir(TEMP_DIR.toString());
+      
controllerConf.setProperty(ControllerConf.CONTROLLER_SEGMENT_COMPLETION_GROUP_COMMIT_ENABLED,
+          groupCommitEnabled);
+      PinotHelixResourceManager resourceManager = 
FakePinotLLCRealtimeSegmentManager.createMockedResourceManager();
+      when(resourceManager.getPropertyStore().create(anyString(), any(), 
eq(AccessOption.PERSISTENT))).thenReturn(
+          true);
+      FakePinotLLCRealtimeSegmentManager segmentManager =
+          new FakePinotLLCRealtimeSegmentManager(resourceManager, 
controllerConf);
+      setUpNewTable(segmentManager, 2, 5, 4);
+      segmentManager._useProductionIdealStateUpdate = true;
+
+      HelixDataAccessor dataAccessor = mock(HelixDataAccessor.class);
+      
when(resourceManager.getHelixZkManager().getHelixDataAccessor()).thenReturn(dataAccessor);
+      when(dataAccessor.keyBuilder()).thenReturn(new 
PropertyKey.Builder(CLUSTER_NAME));
+      int[] version = {0};
+      
when(dataAccessor.getProperty(any(PropertyKey.class))).thenAnswer(invocation -> 
{
+        IdealState idealState = new 
IdealState(segmentManager._idealState.getRecord());
+        idealState.getRecord().setVersion(version[0]);
+        return idealState;
+      });
+      BaseDataAccessor<ZNRecord> baseDataAccessor = 
mock(BaseDataAccessor.class);
+      when(dataAccessor.getBaseDataAccessor()).thenReturn(baseDataAccessor);
+      when(baseDataAccessor.set(anyString(), any(ZNRecord.class), anyInt(), 
eq(AccessOption.PERSISTENT))).thenAnswer(
+          invocation -> {
+            assertEquals((int) invocation.getArgument(2), version[0]);
+            segmentManager._idealState = new IdealState((ZNRecord) 
invocation.getArgument(1));
+            version[0]++;
+            // The write is applied, but the response is lost
+            return false;
+          });
+
+      String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+      String newConsumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+      segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME,
+          createCommittingSegmentDescriptor(committingSegment));
+
+      // The retry finds the update applied and does not write again
+      verify(baseDataAccessor, times(1)).set(anyString(), any(ZNRecord.class), 
anyInt(), eq(AccessOption.PERSISTENT));
+      verify(dataAccessor, times(2)).getProperty(any(PropertyKey.class));
+      Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+      assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()),
+          Set.of(SegmentStateModel.ONLINE));
+      assertEquals(new 
HashSet<>(instanceStatesMap.get(newConsumingSegment).values()),
+          Set.of(SegmentStateModel.CONSUMING));
+    }
+  }
+
+  @Test
+  public void testIsSegmentCompletionApplied() {
+    String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, 
CURRENT_TIME_MS).getSegmentName();
+    String newSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
CURRENT_TIME_MS).getSegmentName();
+    IdealState idealState = new IdealState(REALTIME_TABLE_NAME);
+
+    // Committing segment missing
+    
assertFalse(PinotLLCRealtimeSegmentManager.isSegmentCompletionApplied(idealState,
 committingSegment, newSegment));
+
+    // Committing segment still CONSUMING on one replica
+    idealState.setPartitionState(committingSegment, "Server_0", 
SegmentStateModel.ONLINE);
+    idealState.setPartitionState(committingSegment, "Server_1", 
SegmentStateModel.CONSUMING);
+    
assertFalse(PinotLLCRealtimeSegmentManager.isSegmentCompletionApplied(idealState,
 committingSegment, newSegment));
+
+    // Committing segment ONLINE, new segment missing
+    idealState.setPartitionState(committingSegment, "Server_1", 
SegmentStateModel.ONLINE);
+    
assertFalse(PinotLLCRealtimeSegmentManager.isSegmentCompletionApplied(idealState,
 committingSegment, newSegment));
+    
assertTrue(PinotLLCRealtimeSegmentManager.isSegmentCompletionApplied(idealState,
 committingSegment, null));
+
+    // Committing segment ONLINE, new segment present
+    idealState.setPartitionState(newSegment, "Server_0", 
SegmentStateModel.CONSUMING);
+    
assertTrue(PinotLLCRealtimeSegmentManager.isSegmentCompletionApplied(idealState,
 committingSegment, newSegment));
+
+    // Table paused, so no new segment is expected
+    idealState.getRecord().getMapFields().remove(newSegment);
+    
idealState.getRecord().setSimpleField(PinotLLCRealtimeSegmentManager.PAUSE_STATE,
+        new PauseState(true, PauseState.ReasonCode.ADMINISTRATIVE, 
"pause-for-test", Long.toString(CURRENT_TIME_MS),
+            List.of()).toJsonString());
+    
assertTrue(PinotLLCRealtimeSegmentManager.isSegmentCompletionApplied(idealState,
 committingSegment, newSegment));
+  }
+
   /// Test cases for fixing LLC segment by uploading to segment store if 
missing
   @Test
   public void testUploadToSegmentStore()
@@ -2436,10 +2837,26 @@ public class PinotLLCRealtimeSegmentManagerTest {
     InstancePartitions _consumingInstancePartitions;
     Map<String, SegmentZKMetadata> _segmentZKMetadataMap = new HashMap<>();
     Map<String, Integer> _segmentZKMetadataVersionMap = new HashMap<>();
+    Map<String, Long> _segmentZKMetadataModifiedTimeMap = new HashMap<>();
+    long _currentTimeMs = CURRENT_TIME_MS;
     IdealState _idealState;
     int _numPartitions;
     List<StreamMetadata> _streamMetadataList = null;
     boolean _exceededMaxSegmentCompletionTime = false;
+    // Number of upcoming IdealState updates on segment completion that fail 
without being applied
+    int _numFailedIdealStateUpdates = 0;
+    // Number of upcoming IdealState updates on segment completion that are 
applied but reported as failed
+    int _numAppliedButFailedIdealStateUpdates = 0;
+    // Whether to update the IdealState on segment completion through the 
production code path
+    boolean _useProductionIdealStateUpdate = false;
+    // Whether to create segment ZK metadata if absent through the production 
code path
+    boolean _useProductionCreateIfAbsent = false;
+    // Runs before creating the new segment ZK metadata (step 2) of a segment 
commit
+    Runnable _beforeNewSegmentZKMetadata = () -> {
+    };
+    // Runs before updating the IdealState (step 3) of a segment commit
+    Runnable _beforeCommitIdealStateUpdate = () -> {
+    };
     FileUploadDownloadClient _mockedFileUploadDownloadClient;
     PinotHelixResourceManager _mockResourceManager;
 
@@ -2556,6 +2973,7 @@ public class PinotLLCRealtimeSegmentManagerTest {
       Preconditions.checkState(_segmentZKMetadataMap.containsKey(segmentName));
       if (stat != null) {
         stat.setVersion(_segmentZKMetadataVersionMap.get(segmentName));
+        
stat.setMtime(_segmentZKMetadataModifiedTimeMap.getOrDefault(segmentName, 
CURRENT_TIME_MS));
       }
       return new SegmentZKMetadata(new 
ZNRecord(_segmentZKMetadataMap.get(segmentName).toZNRecord()));
     }
@@ -2569,6 +2987,34 @@ public class PinotLLCRealtimeSegmentManagerTest {
       }
       _segmentZKMetadataMap.put(segmentName, segmentZKMetadata);
       _segmentZKMetadataVersionMap.put(segmentName, version + 1);
+      _segmentZKMetadataModifiedTimeMap.put(segmentName, _currentTimeMs);
+    }
+
+    @Override
+    boolean segmentZKMetadataExists(String realtimeTableName, String 
segmentName) {
+      return _segmentZKMetadataMap.containsKey(segmentName);
+    }
+
+    @Override
+    boolean createSegmentZKMetadataIfAbsent(String realtimeTableName, 
SegmentZKMetadata segmentZKMetadata) {
+      if (_useProductionCreateIfAbsent) {
+        return super.createSegmentZKMetadataIfAbsent(realtimeTableName, 
segmentZKMetadata);
+      }
+      if 
(_segmentZKMetadataMap.containsKey(segmentZKMetadata.getSegmentName())) {
+        return false;
+      }
+      persistSegmentZKMetadata(realtimeTableName, segmentZKMetadata, -1);
+      return true;
+    }
+
+    @Override
+    protected void preProcessNewSegmentZKMetadata() {
+      _beforeNewSegmentZKMetadata.run();
+    }
+
+    @Override
+    protected void preProcessCommitIdealStateUpdate() {
+      _beforeCommitIdealStateUpdate.run();
     }
 
     @Override
@@ -2585,6 +3031,14 @@ public class PinotLLCRealtimeSegmentManagerTest {
     IdealState updateIdealStateOnSegmentCompletion(String realtimeTableName, 
String committingSegmentName,
         String newSegmentName, SegmentAssignment segmentAssignment,
         Map<InstancePartitionsType, InstancePartitions> instancePartitionsMap) 
{
+      if (_useProductionIdealStateUpdate) {
+        return super.updateIdealStateOnSegmentCompletion(realtimeTableName, 
committingSegmentName, newSegmentName,
+            segmentAssignment, instancePartitionsMap);
+      }
+      if (_numFailedIdealStateUpdates > 0) {
+        _numFailedIdealStateUpdates--;
+        throw new RuntimeException("Injected IdealState update failure");
+      }
       Map<String, String> committingSegmentInstanceStateMap = 
_idealState.getInstanceStateMap(committingSegmentName);
       Preconditions.checkState(
           committingSegmentInstanceStateMap != null && 
committingSegmentInstanceStateMap.containsValue(
@@ -2593,6 +3047,10 @@ public class PinotLLCRealtimeSegmentManagerTest {
       
updateInstanceStatesForNewConsumingSegment(_idealState.getRecord().getMapFields(),
 committingSegmentName,
           isTablePaused(_idealState) || isTopicPaused(_idealState, 
committingSegmentName) ? null : newSegmentName,
           segmentAssignment, instancePartitionsMap);
+      if (_numAppliedButFailedIdealStateUpdates > 0) {
+        _numAppliedButFailedIdealStateUpdates--;
+        throw new RuntimeException("Injected IdealState update failure after 
applying the update");
+      }
       return _idealState;
     }
 
@@ -2633,7 +3091,7 @@ public class PinotLLCRealtimeSegmentManagerTest {
 
     @Override
     long getCurrentTimeMs() {
-      return CURRENT_TIME_MS;
+      return _currentTimeMs;
     }
   }
 


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

Reply via email to