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

Jackie-Jiang 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 8cf16156a44 Remove the kept new segment when a resumed pauseless 
commit start completes without it (#19995)
8cf16156a44 is described below

commit 8cf16156a44eb87096f648a9832bb091d3509710
Author: Suvodeep Pyne <[email protected]>
AuthorDate: Fri Oct 23 15:34:46 2026 -0700

    Remove the kept new segment when a resumed pauseless commit start completes 
without it (#19995)
---
 .../helix/core/PinotHelixResourceManager.java      |  89 +++++++++++
 .../realtime/PinotLLCRealtimeSegmentManager.java   |  82 ++++++----
 .../PinotHelixResourceManagerStatelessTest.java    | 135 +++++++++++++++++
 .../PinotLLCRealtimeSegmentManagerTest.java        | 166 +++++++++++++++++++++
 4 files changed, 447 insertions(+), 25 deletions(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
index 67793bec133..bd2dd0388bf 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
@@ -91,6 +91,8 @@ import org.apache.helix.store.zk.ZkHelixPropertyStore;
 import org.apache.helix.zookeeper.datamodel.ZNRecord;
 import org.apache.helix.zookeeper.datamodel.serializer.ZNRecordSerializer;
 import org.apache.helix.zookeeper.impl.client.ZkClient;
+import org.apache.helix.zookeeper.zkclient.exception.ZkException;
+import org.apache.helix.zookeeper.zkclient.exception.ZkNoNodeException;
 import org.apache.pinot.common.assignment.InstanceAssignmentConfigUtils;
 import org.apache.pinot.common.assignment.InstancePartitions;
 import org.apache.pinot.common.assignment.InstancePartitionsUtils;
@@ -211,6 +213,8 @@ import org.apache.pinot.spi.utils.TimeUtils;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.apache.pinot.spi.utils.retry.RetryPolicies;
 import org.apache.pinot.spi.utils.retry.RetryPolicy;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.Op;
 import org.apache.zookeeper.data.Stat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -221,6 +225,7 @@ public class PinotHelixResourceManager {
   private static final long CACHE_ENTRY_EXPIRE_TIME_HOURS = 6L;
   private static final RetryPolicy DEFAULT_RETRY_POLICY = 
RetryPolicies.exponentialBackoffRetryPolicy(5, 1000L, 2.0f);
   private static final int DEFAULT_SEGMENT_LINEAGE_UPDATE_NUM_RETRY = 5;
+  private static final int MAX_SEGMENT_ZK_METADATA_REMOVAL_ATTEMPTS = 5;
   public static final String APPEND = "APPEND";
   private static final int DEFAULT_LINEAGE_UPDATER_LOCKERS_SIZE = 500;
   private static final String API_REQUEST_ID_PREFIX = "api-";
@@ -2278,6 +2283,90 @@ public class PinotHelixResourceManager {
     }
   }
 
+  /// Removes the ZK metadata of the given segments that are IN_PROGRESS and 
not in the IdealState of the table, i.e.
+  /// new consuming segments that were not added to the IdealState, and 
returns the segments whose ZK metadata was
+  /// removed.
+  ///
+  /// The removal is submitted in one ZK `multi()` transaction with 
version-checked deletes of the segment ZK metadata
+  /// read here, and a version-checked write of the unchanged IdealState:
+  /// - An IdealState update that read the IdealState before the transaction 
fails its version check. An updater that
+  ///   reads the segment ZK metadata again when it retries, like the one of 
RealtimeSegmentValidationManager, then no
+  ///   longer finds the removed segments.
+  /// - If the IdealState, or the ZK metadata of one of the segments, is 
updated after it was read here, the transaction
+  ///   fails and is retried.
+  ///
+  /// The data versions cannot tell a segment ZK metadata deleted and 
recreated with the same name since it was read,
+  /// e.g. by an upload under the same name, from the one that was read. As 
the IdealState is outside the property
+  /// store, the transaction uses the multi-write ZkClient directly instead of 
[ZkMultiWriteBuilder].
+  public Set<String> removeInProgressSegmentsZKMetadataNotInIdealState(String 
tableNameWithType,
+      Collection<String> segmentNames) {
+    return 
removeInProgressSegmentsZKMetadataNotInIdealState(tableNameWithType, 
segmentNames, () -> {
+    });
+  }
+
+  /// @param afterRead Runs after reading the IdealState and the segment ZK 
metadata, before removing it
+  @VisibleForTesting
+  Set<String> removeInProgressSegmentsZKMetadataNotInIdealState(String 
tableNameWithType,
+      Collection<String> segmentNames, Runnable afterRead) {
+    ZkClient zkClient = getOrBuildMultiWriteZkClient();
+    String idealStatePath = PropertyPathBuilder.idealState(_helixClusterName, 
tableNameWithType);
+    String propertyStoreRoot = 
PropertyPathBuilder.propertyStore(_helixClusterName);
+    for (int attempt = 1; ; attempt++) {
+      Stat idealStateStat = new Stat();
+      ZNRecord idealState;
+      try {
+        idealState = zkClient.readData(idealStatePath, idealStateStat);
+      } catch (ZkNoNodeException e) {
+        idealState = null;
+      }
+      if (idealState == null) {
+        // The table was deleted
+        return Set.of();
+      }
+      List<Op> segmentDeletes = new ArrayList<>();
+      Set<String> segmentsToRemove = new HashSet<>();
+      for (String segmentName : segmentNames) {
+        if (idealState.getMapFields().containsKey(segmentName)) {
+          continue;
+        }
+        String segmentPath =
+            propertyStoreRoot + 
ZKMetadataProvider.constructPropertyStorePathForSegment(tableNameWithType, 
segmentName);
+        Stat segmentStat = new Stat();
+        ZNRecord segmentRecord;
+        try {
+          segmentRecord = zkClient.readData(segmentPath, segmentStat);
+        } catch (ZkNoNodeException e) {
+          continue;
+        }
+        if (new SegmentZKMetadata(segmentRecord).getStatus() == 
CommonConstants.Segment.Realtime.Status.IN_PROGRESS) {
+          segmentDeletes.add(Op.delete(segmentPath, segmentStat.getVersion()));
+          segmentsToRemove.add(segmentName);
+        }
+      }
+      if (segmentsToRemove.isEmpty()) {
+        return segmentsToRemove;
+      }
+      afterRead.run();
+      List<Op> ops = new ArrayList<>(segmentDeletes.size() + 1);
+      ops.add(Op.setData(idealStatePath, zkClient.serialize(idealState, 
idealStatePath), idealStateStat.getVersion()));
+      ops.addAll(segmentDeletes);
+      try {
+        zkClient.multi(ops);
+        return segmentsToRemove;
+      } catch (ZkException e) {
+        // As in ZkMultiWriteBuilder#execute(), the ZkClient wraps the 
KeeperException of an atomic rollback. A version
+        // mismatch or a missing node means that the IdealState or a segment 
ZK metadata changed concurrently.
+        boolean concurrentChange = e.getCause() instanceof 
KeeperException.BadVersionException
+            || e.getCause() instanceof KeeperException.NoNodeException;
+        if (!concurrentChange || attempt == 
MAX_SEGMENT_ZK_METADATA_REMOVAL_ATTEMPTS) {
+          throw e;
+        }
+        LOGGER.info("Retrying to remove segment ZK metadata for segments: {} 
of table: {} after a concurrent change",
+            segmentsToRemove, tableNameWithType);
+      }
+    }
+  }
+
   public boolean createZKNode(String path, ZNRecord record, int accessOption, 
long ttl) {
     return _helixDataAccessor.getBaseDataAccessor().create(path, record, 
accessOption, ttl);
   }
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 56d8ce41dd3..d2339fd5ad1 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
@@ -674,9 +674,16 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
 
     // Step-2: Create new segment metadata if needed
     long startTimeNs2 = System.nanoTime();
+    // Other attempts of the same commit start might have created the new 
segment already, even several ones when
+    // attempts on 2 controllers overlap. Find them before deciding whether 
the partition still needs a new segment, so
+    // that the unused ones can be removed.
+    List<String> newSegmentsOfEarlierAttempts =
+        resumed ? findNewSegmentsOfEarlierAttempts(realtimeTableName, new 
LLCSegmentName(committingSegmentName))
+            : List.of();
     String newConsumingSegmentName =
         createNewSegmentMetadata(tableConfig, committingSegmentDescriptor, 
committingSegmentZKMetadata,
-            instancePartitions, toCommitting, resumed);
+            instancePartitions, toCommitting,
+            newSegmentsOfEarlierAttempts.isEmpty() ? null : 
newSegmentsOfEarlierAttempts.get(0));
 
     preProcessCommitIdealStateUpdate();
 
@@ -708,17 +715,20 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     }
 
     Map<String, Map<String, String>> instanceStatesMapAfterStep3 = 
idealState.getRecord().getMapFields();
-    boolean newConsumingSegmentInIdealState = false;
+    boolean newConsumingSegmentInIdealState =
+        newConsumingSegmentName != null && 
instanceStatesMapAfterStep3.containsKey(newConsumingSegmentName);
+    // The committing segment is ONLINE now, so no attempt of the commit can 
add a new segment that is not in the
+    // IdealState anymore. Remove their ZK metadata so that 
RealtimeSegmentValidationManager does not add them either.
+    Set<String> unusedNewSegments = new 
HashSet<>(newSegmentsOfEarlierAttempts);
     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;
-      }
+      unusedNewSegments.add(newConsumingSegmentName);
+    }
+    unusedNewSegments.removeIf(instanceStatesMapAfterStep3::containsKey);
+    if (!unusedNewSegments.isEmpty()) {
+      removeUnusedNewSegmentsZKMetadata(realtimeTableName, unusedNewSegments);
+    }
+    if (!newConsumingSegmentInIdealState) {
+      newConsumingSegmentName = null;
     }
 
     long endTimeNs = System.nanoTime();
@@ -849,7 +859,7 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     return true;
   }
 
-  /// Returns the new segment created by an earlier attempt of a resumed 
commit start, or `null` if there is none.
+  /// Returns the new segments created by earlier attempts of a resumed commit 
start, oldest first.
   ///
   /// 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
@@ -857,29 +867,34 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
   /// 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) {
+  private List<String> findNewSegmentsOfEarlierAttempts(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;
+    List<String> newSegments = new ArrayList<>();
     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;
+        newSegments.add(segmentName);
       }
     }
-    return null;
+    return newSegments;
   }
 
   // Step 2: Create new segment metadata
+
+  /// Returns the new consuming segment of the partition of the committing 
segment, or `null` if the partition is no
+  /// longer in the stream, or the table/topic is paused. Uses 
`newSegmentOfEarlierAttempt` if not `null`, and creates
+  /// the new segment ZK metadata otherwise.
   @Nullable
   private String createNewSegmentMetadata(TableConfig tableConfig,
       CommittingSegmentDescriptor committingSegmentDescriptor, 
SegmentZKMetadata committingSegmentZKMetadata,
-      InstancePartitions instancePartitions, boolean createIfAbsent, boolean 
resumed) {
+      InstancePartitions instancePartitions, boolean createIfAbsent, @Nullable 
String newSegmentOfEarlierAttempt) {
     String committingSegmentName = 
committingSegmentDescriptor.getSegmentName();
 
     String realtimeTableName = tableConfig.getTableName();
@@ -905,14 +920,10 @@ 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;
-        }
+      // The new segment of an earlier attempt might be consuming or even 
committed, so never overwrite its ZK metadata
+      if (newSegmentOfEarlierAttempt != null) {
+        LOGGER.info("Using existing segment metadata for new segment: {}", 
newSegmentOfEarlierAttempt);
+        return newSegmentOfEarlierAttempt;
       }
       String rawTableName = 
TableNameBuilder.extractRawTableName(realtimeTableName);
       long newSegmentCreationTimeMs = getCurrentTimeMs();
@@ -944,6 +955,27 @@ public class PinotLLCRealtimeSegmentManager implements 
PinotClusterConfigChangeL
     return newConsumingSegmentName;
   }
 
+  /// Removes the ZK metadata of new segments that are not added to the 
IdealState, e.g. when table/topic consumption is
+  /// paused, or the partition is no longer in the stream. The removal is 
fenced against concurrent IdealState updates
+  /// that add one of them, e.g. by RealtimeSegmentValidationManager (see
+  /// 
PinotHelixResourceManager#removeInProgressSegmentsZKMetadataNotInIdealState).
+  @VisibleForTesting
+  void removeUnusedNewSegmentsZKMetadata(String realtimeTableName, Set<String> 
segmentNames) {
+    LOGGER.info("Cleaning up segment ZK metadata for new segments: {} of 
table: {} which are not in the IdealState",
+        segmentNames, realtimeTableName);
+    try {
+      Set<String> removedSegments =
+          
_helixResourceManager.removeInProgressSegmentsZKMetadataNotInIdealState(realtimeTableName,
 segmentNames);
+      if (removedSegments.size() < segmentNames.size()) {
+        LOGGER.info("Removed segment ZK metadata for segments: {} of table: 
{}, the others were added to the "
+            + "IdealState or removed concurrently", removedSegments, 
realtimeTableName);
+      }
+    } catch (Exception e) {
+      LOGGER.warn("Caught exception while removing segment ZK metadata for 
segments: {} of table: {}", segmentNames,
+          realtimeTableName, e);
+    }
+  }
+
   private void removeSegmentZKMetadataBestEffort(String realtimeTableName, 
String segmentName) {
     String segmentMetadataPath =
         
ZKMetadataProvider.constructPropertyStorePathForSegment(realtimeTableName, 
segmentName);
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
index d2d2dbc757f..7601a7779c4 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManagerStatelessTest.java
@@ -31,7 +31,10 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.helix.AccessOption;
 import org.apache.helix.HelixAdmin;
+import org.apache.helix.PropertyPathBuilder;
 import 
org.apache.helix.controller.dataproviders.ResourceControllerDataProvider;
 import 
org.apache.helix.controller.rebalancer.strategy.CrushEdRebalanceStrategy;
 import org.apache.helix.model.ClusterConfig;
@@ -40,6 +43,7 @@ import org.apache.helix.model.IdealState;
 import org.apache.helix.model.InstanceConfig;
 import org.apache.helix.model.MasterSlaveSMD;
 import org.apache.helix.zookeeper.datamodel.ZNRecord;
+import org.apache.helix.zookeeper.zkclient.exception.ZkBadVersionException;
 import org.apache.pinot.common.exception.InvalidConfigException;
 import org.apache.pinot.common.exception.TableNotFoundException;
 import org.apache.pinot.common.lineage.LineageEntryState;
@@ -1896,6 +1900,137 @@ public class PinotHelixResourceManagerStatelessTest 
extends ControllerTest {
     assertTrue(ZKMetadataProvider.removeSegmentZKMetadata(_propertyStore, 
OFFLINE_TABLE_NAME, segName2));
   }
 
+  private static final String REMOVAL_TEST_TABLE = 
"removeSegmentsZKMetadataTest_REALTIME";
+
+  /// Creates the IdealState of the removal test table with the given segment, 
and IN_PROGRESS segment ZK metadata for
+  /// the given segments.
+  private void setUpRemovalTestTable(String segmentInIdealState, List<String> 
segmentsWithZKMetadata) {
+    IdealState idealState = new IdealState(REMOVAL_TEST_TABLE);
+    idealState.setStateModelDefRef(
+        
PinotHelixSegmentOnlineOfflineStateModelGenerator.PINOT_SEGMENT_ONLINE_OFFLINE_STATE_MODEL);
+    idealState.setRebalanceMode(IdealState.RebalanceMode.CUSTOMIZED);
+    idealState.setReplicas("1");
+    idealState.setNumPartitions(1);
+    idealState.setPartitionState(segmentInIdealState, "Server_unknown_0", 
Helix.StateModel.SegmentStateModel.ONLINE);
+    _helixResourceManager.getHelixAdmin().addResource(getHelixClusterName(), 
REMOVAL_TEST_TABLE, idealState);
+    for (String segmentName : segmentsWithZKMetadata) {
+      SegmentZKMetadata segmentZKMetadata = new SegmentZKMetadata(segmentName);
+      segmentZKMetadata.setStatus(Segment.Realtime.Status.IN_PROGRESS);
+      assertTrue(ZKMetadataProvider.setSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, segmentZKMetadata));
+    }
+  }
+
+  private void tearDownRemovalTestTable(List<String> segmentNames) {
+    _helixResourceManager.getHelixAdmin().dropResource(getHelixClusterName(), 
REMOVAL_TEST_TABLE);
+    for (String segmentName : segmentNames) {
+      ZKMetadataProvider.removeSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, segmentName);
+    }
+  }
+
+  /// Covers 
[PinotHelixResourceManager#removeInProgressSegmentsZKMetadataNotInIdealState]: 
removes the ZK metadata of
+  /// the IN_PROGRESS segments that are not in the IdealState, and keeps the 
others. The removal writes the unchanged
+  /// IdealState, so an IdealState update that read the IdealState before 
fails.
+  @Test
+  public void testRemoveInProgressSegmentsZKMetadataNotInIdealState() {
+    String segmentInIdealState = "segmentInIdealState";
+    String segmentNotInIdealState = "segmentNotInIdealState";
+    String doneSegmentNotInIdealState = "doneSegmentNotInIdealState";
+    List<String> segments = List.of(segmentInIdealState, 
segmentNotInIdealState, doneSegmentNotInIdealState);
+    setUpRemovalTestTable(segmentInIdealState, segments);
+    try {
+      SegmentZKMetadata doneSegmentZKMetadata = new 
SegmentZKMetadata(doneSegmentNotInIdealState);
+      doneSegmentZKMetadata.setStatus(Segment.Realtime.Status.DONE);
+      assertTrue(ZKMetadataProvider.setSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, doneSegmentZKMetadata));
+      IdealState idealStateBefore = 
_helixResourceManager.getTableIdealState(REMOVAL_TEST_TABLE);
+
+      List<String> segmentsToRemove = new ArrayList<>(segments);
+      segmentsToRemove.add("segmentWithoutZKMetadata");
+      
assertEquals(_helixResourceManager.removeInProgressSegmentsZKMetadataNotInIdealState(REMOVAL_TEST_TABLE,
+          segmentsToRemove), Set.of(segmentNotInIdealState));
+      assertNotNull(ZKMetadataProvider.getSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, segmentInIdealState));
+      assertNull(ZKMetadataProvider.getSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, segmentNotInIdealState));
+      assertNotNull(
+          ZKMetadataProvider.getSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, doneSegmentNotInIdealState));
+      IdealState idealStateAfter = 
_helixResourceManager.getTableIdealState(REMOVAL_TEST_TABLE);
+      assertEquals(idealStateAfter.getRecord().getMapFields(), 
idealStateBefore.getRecord().getMapFields());
+      String idealStatePath = 
PropertyPathBuilder.idealState(getHelixClusterName(), REMOVAL_TEST_TABLE);
+      // An IdealState update based on the IdealState read before the removal 
fails
+      assertThrows(ZkBadVersionException.class,
+          () -> 
_helixResourceManager.getHelixZkManager().getHelixDataAccessor().getBaseDataAccessor()
+              .set(idealStatePath, idealStateBefore.getRecord(), 
idealStateBefore.getRecord().getVersion(),
+                  AccessOption.PERSISTENT));
+
+      // The table was deleted
+      
assertEquals(_helixResourceManager.removeInProgressSegmentsZKMetadataNotInIdealState("deletedTable_REALTIME",
+          segments), Set.of());
+
+      // Nothing to remove: the IdealState is not written
+      
assertEquals(_helixResourceManager.removeInProgressSegmentsZKMetadataNotInIdealState(REMOVAL_TEST_TABLE,
+          segmentsToRemove), Set.of());
+      
assertEquals(_helixResourceManager.getTableIdealState(REMOVAL_TEST_TABLE).getRecord().getVersion(),
+          idealStateAfter.getRecord().getVersion());
+    } finally {
+      tearDownRemovalTestTable(segments);
+    }
+  }
+
+  /// Another controller adds the segment to the IdealState after the removal 
read the IdealState, e.g.
+  /// RealtimeSegmentValidationManager repairing the partition with it. The 
removal must keep its ZK metadata.
+  @Test
+  public void 
testRemoveInProgressSegmentsZKMetadataKeepsSegmentAddedToIdealStateConcurrently()
 {
+    String segmentInIdealState = "segmentInIdealState";
+    String segmentToRemove = "segmentToRemove";
+    setUpRemovalTestTable(segmentInIdealState, List.of(segmentToRemove));
+    try {
+      AtomicBoolean added = new AtomicBoolean();
+      Runnable addToIdealState = () -> {
+        if (added.compareAndSet(false, true)) {
+          IdealState idealState = 
_helixResourceManager.getTableIdealState(REMOVAL_TEST_TABLE);
+          idealState.setPartitionState(segmentToRemove, "Server_unknown_0",
+              Helix.StateModel.SegmentStateModel.CONSUMING);
+          _helixResourceManager.getHelixAdmin()
+              .setResourceIdealState(getHelixClusterName(), 
REMOVAL_TEST_TABLE, idealState);
+        }
+      };
+
+      
assertEquals(_helixResourceManager.removeInProgressSegmentsZKMetadataNotInIdealState(REMOVAL_TEST_TABLE,
+          List.of(segmentToRemove), addToIdealState), Set.of());
+      assertTrue(added.get());
+      assertNotNull(ZKMetadataProvider.getSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, segmentToRemove));
+      
assertNotNull(_helixResourceManager.getTableIdealState(REMOVAL_TEST_TABLE).getInstanceStateMap(segmentToRemove));
+    } finally {
+      tearDownRemovalTestTable(List.of(segmentToRemove));
+    }
+  }
+
+  /// The segment ZK metadata is overwritten after the removal read it, e.g. 
by an upload of a segment with the same
+  /// name. The removal must keep the new ZK metadata.
+  @Test
+  public void 
testRemoveInProgressSegmentsZKMetadataKeepsSegmentZKMetadataReplacedConcurrently()
 {
+    String segmentInIdealState = "segmentInIdealState";
+    String segmentToRemove = "segmentToRemove";
+    setUpRemovalTestTable(segmentInIdealState, List.of(segmentToRemove));
+    try {
+      AtomicBoolean replaced = new AtomicBoolean();
+      Runnable replaceSegmentZKMetadata = () -> {
+        if (replaced.compareAndSet(false, true)) {
+          SegmentZKMetadata uploadedSegmentZKMetadata = new 
SegmentZKMetadata(segmentToRemove);
+          
uploadedSegmentZKMetadata.setStatus(Segment.Realtime.Status.UPLOADED);
+          assertTrue(
+              ZKMetadataProvider.setSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, uploadedSegmentZKMetadata));
+        }
+      };
+
+      
assertEquals(_helixResourceManager.removeInProgressSegmentsZKMetadataNotInIdealState(REMOVAL_TEST_TABLE,
+          List.of(segmentToRemove), replaceSegmentZKMetadata), Set.of());
+      assertTrue(replaced.get());
+      assertEquals(ZKMetadataProvider.getSegmentZKMetadata(_propertyStore, 
REMOVAL_TEST_TABLE, segmentToRemove)
+          .getStatus(), Segment.Realtime.Status.UPLOADED);
+    } finally {
+      tearDownRemovalTestTable(List.of(segmentToRemove));
+    }
+  }
+
   @AfterClass
   public void tearDown() {
     stopFakeInstances();
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 9216dddf860..0af3d8d542c 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
@@ -1988,6 +1988,154 @@ public class PinotLLCRealtimeSegmentManagerTest {
     
assertEquals(segmentManager._segmentZKMetadataMap.get(committingSegment).getStatus(),
 Status.COMMITTING);
   }
 
+  private static void setPauseState(IdealState idealState, boolean 
tablePaused, List<Integer> pausedTopics) {
+    PauseState pauseState = new PauseState(tablePaused, 
PauseState.ReasonCode.ADMINISTRATIVE, null,
+        Long.toString(CURRENT_TIME_MS), pausedTopics);
+    
idealState.getRecord().setSimpleField(PinotLLCRealtimeSegmentManager.PAUSE_STATE,
 pauseState.toJsonString());
+  }
+
+  /// The table is paused after a failed IdealState update kept the new 
segment of the commit start. The retry
+  /// completes the commit without a new segment and removes the kept one, so 
that resuming the table starts
+  /// consumption from the offset selected by the offset criteria instead of 
adding the kept segment.
+  @Test
+  public void 
testCommitStartRetryWhileTablePausedRemovesNewSegmentOfEarlierAttempt() {
+    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));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+
+    setPauseState(segmentManager._idealState, true, List.of());
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(2);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertFalse(instanceStatesMap.containsKey(newConsumingSegment));
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()), 
Set.of(SegmentStateModel.ONLINE));
+
+    setPauseState(segmentManager._idealState, false, List.of());
+    segmentManager._exceededMaxSegmentCompletionTime = true;
+    segmentManager.ensureAllPartitionsConsuming(segmentManager._tableConfig, 
segmentManager._streamConfigs,
+        segmentManager._idealState,
+        segmentManager.getNewStreamMetadataList(segmentManager._streamConfigs, 
List.of(), segmentManager._idealState),
+        OffsetCriteria.LARGEST_OFFSET_CRITERIA);
+    String resumedSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, 
segmentManager._currentTimeMs).getSegmentName();
+    assertFalse(instanceStatesMap.containsKey(newConsumingSegment));
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(resumedSegment).values()), 
Set.of(SegmentStateModel.CONSUMING));
+    
assertEquals(segmentManager._segmentZKMetadataMap.get(resumedSegment).getStartOffset(),
+        PARTITION_OFFSET.toString());
+  }
+
+  /// Like above, with 2 kept new segments, e.g. created by attempts of the 
commit start on 2 controllers around a lead
+  /// change. The retry removes both.
+  @Test
+  public void 
testCommitStartRetryWhileTablePausedRemovesNewSegmentsOfEarlierAttempts() {
+    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 otherNewConsumingSegment =
+        new LLCSegmentName(RAW_TABLE_NAME, 0, 1, CURRENT_TIME_MS + 
TimeUnit.MINUTES.toMillis(1)).getSegmentName();
+    CommittingSegmentDescriptor committingSegmentDescriptor = 
createCommittingSegmentDescriptor(committingSegment);
+    // Another attempt of the commit start creates its own new segment, and 
its IdealState update fails as well
+    segmentManager._beforeCommitIdealStateUpdate = () -> {
+      segmentManager.persistSegmentZKMetadata(REALTIME_TABLE_NAME, new 
SegmentZKMetadata(
+          new 
ZNRecord(segmentManager._segmentZKMetadataMap.get(newConsumingSegment).toZNRecord(),
+              otherNewConsumingSegment)), -1);
+      segmentManager._beforeCommitIdealStateUpdate = () -> {
+      };
+    };
+    segmentManager._numFailedIdealStateUpdates = 1;
+    assertThrows(RuntimeException.class,
+        () -> 
segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(otherNewConsumingSegment));
+
+    setPauseState(segmentManager._idealState, true, List.of());
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(2);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(otherNewConsumingSegment));
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()), 
Set.of(SegmentStateModel.ONLINE));
+  }
+
+  /// The retry of a commit start, which completes the commit without the new 
segment kept by a failed attempt, stalls
+  /// before removing the kept segment. Meanwhile the table is resumed, and 
validation adds the kept segment to the
+  /// IdealState. The retry must then keep its ZK metadata.
+  @Test
+  public void 
testCommitStartRetryKeepsNewSegmentOfEarlierAttemptAddedByValidation() {
+    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));
+
+    setPauseState(segmentManager._idealState, true, List.of());
+    segmentManager._beforeRemovingUnusedNewSegments = () -> {
+      setPauseState(segmentManager._idealState, false, List.of());
+      segmentManager._exceededMaxSegmentCompletionTime = true;
+      segmentManager.ensureAllPartitionsConsuming();
+    };
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(2);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(newConsumingSegment).values()),
+        Set.of(SegmentStateModel.CONSUMING));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+  }
+
+  /// The topic is paused after a failed IdealState update kept the new 
segment of the commit start. The retry completes
+  /// the commit without a new segment and removes the kept one.
+  @Test
+  public void 
testCommitStartRetryWhileTopicPausedRemovesNewSegmentOfEarlierAttempt() {
+    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));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+
+    setPauseState(segmentManager._idealState, false, List.of(0));
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(2);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertFalse(instanceStatesMap.containsKey(newConsumingSegment));
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()), 
Set.of(SegmentStateModel.ONLINE));
+  }
+
+  /// The partition is removed from the stream after a failed IdealState 
update kept the new segment of the commit
+  /// start. The retry completes the commit without adding the kept segment, 
and removes it.
+  @Test
+  public void 
testCommitStartRetryAfterPartitionRemovedFromStreamRemovesNewSegmentOfEarlierAttempt()
 {
+    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));
+    
assertTrue(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+
+    segmentManager._streamMetadataList = List.of(new 
StreamMetadata(segmentManager._streamConfigs.get(0), 3,
+        IntStream.range(1, 4)
+            .mapToObj(partition -> new PartitionGroupMetadata(partition, 
PARTITION_OFFSET))
+            .collect(Collectors.toList())));
+    segmentManager._currentTimeMs += TimeUnit.MINUTES.toMillis(2);
+    segmentManager.commitSegmentMetadataToCommitting(REALTIME_TABLE_NAME, 
committingSegmentDescriptor);
+    
assertFalse(segmentManager._segmentZKMetadataMap.containsKey(newConsumingSegment));
+    Map<String, Map<String, String>> instanceStatesMap = 
segmentManager._idealState.getRecord().getMapFields();
+    assertFalse(instanceStatesMap.containsKey(newConsumingSegment));
+    assertEquals(new 
HashSet<>(instanceStatesMap.get(committingSegment).values()), 
Set.of(SegmentStateModel.ONLINE));
+  }
+
   /// 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
@@ -3290,6 +3438,9 @@ public class PinotLLCRealtimeSegmentManagerTest {
     // Runs before updating the IdealState (step 3) of a segment commit
     Runnable _beforeCommitIdealStateUpdate = () -> {
     };
+    // Runs before removing the ZK metadata of the new segments that a segment 
commit did not add to the IdealState
+    Runnable _beforeRemovingUnusedNewSegments = () -> {
+    };
     FileUploadDownloadClient _mockedFileUploadDownloadClient;
     PinotHelixResourceManager _mockResourceManager;
     // Segment name to the server endpoint re-ingestion was triggered on
@@ -3466,6 +3617,21 @@ public class PinotLLCRealtimeSegmentManagerTest {
       _beforeCommitIdealStateUpdate.run();
     }
 
+    /// Like the production removal, which is fenced against concurrent 
updates, only removes the segments that are
+    /// IN_PROGRESS and not in the IdealState when they are removed.
+    @Override
+    void removeUnusedNewSegmentsZKMetadata(String realtimeTableName, 
Set<String> segmentNames) {
+      _beforeRemovingUnusedNewSegments.run();
+      for (String segmentName : segmentNames) {
+        SegmentZKMetadata segmentZKMetadata = 
_segmentZKMetadataMap.get(segmentName);
+        if (segmentZKMetadata != null && segmentZKMetadata.getStatus() == 
Status.IN_PROGRESS
+            && 
!_idealState.getRecord().getMapFields().containsKey(segmentName)) {
+          _segmentZKMetadataMap.remove(segmentName);
+          _segmentZKMetadataVersionMap.remove(segmentName);
+        }
+      }
+    }
+
     @Override
     public IdealState getIdealState(String realtimeTableName) {
       return _idealState;


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

Reply via email to