Copilot commented on code in PR #19254:
URL: https://github.com/apache/pinot/pull/19254#discussion_r3800183823


##########
pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java:
##########
@@ -890,4 +894,149 @@ public static boolean 
isTableConfigExists(ZkHelixPropertyStore<ZNRecord> propert
     return 
propertyStore.exists(constructPropertyStorePathForResourceConfig(tableNameWithType),
         AccessOption.PERSISTENT);
   }
+
+  /// Creates a deletion marker for a table to indicate that deletion is in 
progress.
+  /// This marker is used to prevent concurrent deletions and prevent 
recreation of a table
+  /// while deletion is in progress.
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @param controllerId The ID of the controller performing the deletion
+  /// @return true if the marker was created successfully, false if a marker 
already exists
+  public static boolean 
createTableDeletionMarker(ZkHelixPropertyStore<ZNRecord> propertyStore,
+      String tableNameWithType, String controllerId) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    // create() is atomic: with concurrent callers only one succeeds, the rest 
get false.
+    return propertyStore.create(markerPath, 
buildTableDeletionMarkerRecord(tableNameWithType, controllerId),
+        AccessOption.PERSISTENT);
+  }
+
+  private static ZNRecord buildTableDeletionMarkerRecord(String 
tableNameWithType, String controllerId) {
+    ZNRecord markerRecord = new ZNRecord(tableNameWithType);
+    markerRecord.setSimpleField(DELETION_MARKER_CONTROLLER_ID_KEY, 
controllerId);
+    markerRecord.setLongField(DELETION_MARKER_START_TIME_KEY, 
System.currentTimeMillis());
+    return markerRecord;
+  }
+
+  /// Returns true when the marker record is present and has not passed 
[#DELETION_MARKER_EXPIRY_MS].
+  /// A marker with a missing or unparseable start time is treated as stale so 
that a malformed record can never
+  /// block deletion or re-creation forever.
+  private static boolean isTableDeletionMarkerValid(@Nullable ZNRecord 
markerRecord) {
+    if (markerRecord == null) {
+      return false;
+    }
+    long startTimeMs = 
markerRecord.getLongField(DELETION_MARKER_START_TIME_KEY, -1L);
+    if (startTimeMs < 0) {
+      return false;
+    }
+    return System.currentTimeMillis() - startTimeMs < 
DELETION_MARKER_EXPIRY_MS;
+  }
+
+  /// Checks if a deletion marker exists for the table and is still valid (not 
expired).
+  /// A marker is considered expired if it is older than 
DELETION_MARKER_EXPIRY_MS (24 hours).
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @return true if a valid (non-expired) deletion marker exists, false 
otherwise
+  public static boolean 
isValidTableDeletionMarkerExists(ZkHelixPropertyStore<ZNRecord> propertyStore,
+      String tableNameWithType) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    return isTableDeletionMarkerValid(propertyStore.get(markerPath, null, 
AccessOption.PERSISTENT));
+  }
+
+  /// Removes the deletion marker for a table. Call this when a deletion 
finishes, successfully or not.
+  ///
+  /// Releasing is a no-op unless `controllerId` still owns the marker. If a 
deletion stalled past the expiry
+  /// window another controller may have taken the marker over and started its 
own deletion; the stalled
+  /// controller must not then release a lock it no longer holds.
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @param controllerId The ID of the controller that expects to own the 
marker
+  public static void removeTableDeletionMarker(ZkHelixPropertyStore<ZNRecord> 
propertyStore,
+      String tableNameWithType, String controllerId) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    ZNRecord markerRecord = propertyStore.get(markerPath, null, 
AccessOption.PERSISTENT);
+    if (markerRecord == null) {
+      return;
+    }
+    String ownerId = 
markerRecord.getSimpleField(DELETION_MARKER_CONTROLLER_ID_KEY);
+    if (!Objects.equals(ownerId, controllerId)) {
+      LOGGER.warn("Not releasing the deletion marker for table: {}: it is now 
owned by controller: {}, not: {}",
+          tableNameWithType, ownerId, controllerId);
+      return;
+    }
+    propertyStore.remove(markerPath, AccessOption.PERSISTENT);

Review Comment:
   This ownership check and removal are not atomic. The old owner can read its 
marker, another controller can version-take it over, and then this 
unconditional remove deletes the new owner's marker. Also, `controllerId` is 
shared by concurrent requests on one controller, so it cannot distinguish 
successive acquisitions. Release should use an acquisition-unique token and a 
version-checked delete/transaction.



##########
pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java:
##########
@@ -890,4 +894,149 @@ public static boolean 
isTableConfigExists(ZkHelixPropertyStore<ZNRecord> propert
     return 
propertyStore.exists(constructPropertyStorePathForResourceConfig(tableNameWithType),
         AccessOption.PERSISTENT);
   }
+
+  /// Creates a deletion marker for a table to indicate that deletion is in 
progress.
+  /// This marker is used to prevent concurrent deletions and prevent 
recreation of a table
+  /// while deletion is in progress.
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @param controllerId The ID of the controller performing the deletion
+  /// @return true if the marker was created successfully, false if a marker 
already exists
+  public static boolean 
createTableDeletionMarker(ZkHelixPropertyStore<ZNRecord> propertyStore,
+      String tableNameWithType, String controllerId) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    // create() is atomic: with concurrent callers only one succeeds, the rest 
get false.
+    return propertyStore.create(markerPath, 
buildTableDeletionMarkerRecord(tableNameWithType, controllerId),
+        AccessOption.PERSISTENT);
+  }
+
+  private static ZNRecord buildTableDeletionMarkerRecord(String 
tableNameWithType, String controllerId) {
+    ZNRecord markerRecord = new ZNRecord(tableNameWithType);
+    markerRecord.setSimpleField(DELETION_MARKER_CONTROLLER_ID_KEY, 
controllerId);
+    markerRecord.setLongField(DELETION_MARKER_START_TIME_KEY, 
System.currentTimeMillis());
+    return markerRecord;
+  }
+
+  /// Returns true when the marker record is present and has not passed 
[#DELETION_MARKER_EXPIRY_MS].
+  /// A marker with a missing or unparseable start time is treated as stale so 
that a malformed record can never
+  /// block deletion or re-creation forever.
+  private static boolean isTableDeletionMarkerValid(@Nullable ZNRecord 
markerRecord) {
+    if (markerRecord == null) {
+      return false;
+    }
+    long startTimeMs = 
markerRecord.getLongField(DELETION_MARKER_START_TIME_KEY, -1L);
+    if (startTimeMs < 0) {
+      return false;
+    }
+    return System.currentTimeMillis() - startTimeMs < 
DELETION_MARKER_EXPIRY_MS;
+  }
+
+  /// Checks if a deletion marker exists for the table and is still valid (not 
expired).
+  /// A marker is considered expired if it is older than 
DELETION_MARKER_EXPIRY_MS (24 hours).
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @return true if a valid (non-expired) deletion marker exists, false 
otherwise
+  public static boolean 
isValidTableDeletionMarkerExists(ZkHelixPropertyStore<ZNRecord> propertyStore,
+      String tableNameWithType) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    return isTableDeletionMarkerValid(propertyStore.get(markerPath, null, 
AccessOption.PERSISTENT));
+  }
+
+  /// Removes the deletion marker for a table. Call this when a deletion 
finishes, successfully or not.
+  ///
+  /// Releasing is a no-op unless `controllerId` still owns the marker. If a 
deletion stalled past the expiry
+  /// window another controller may have taken the marker over and started its 
own deletion; the stalled
+  /// controller must not then release a lock it no longer holds.
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @param controllerId The ID of the controller that expects to own the 
marker
+  public static void removeTableDeletionMarker(ZkHelixPropertyStore<ZNRecord> 
propertyStore,
+      String tableNameWithType, String controllerId) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    ZNRecord markerRecord = propertyStore.get(markerPath, null, 
AccessOption.PERSISTENT);
+    if (markerRecord == null) {
+      return;
+    }
+    String ownerId = 
markerRecord.getSimpleField(DELETION_MARKER_CONTROLLER_ID_KEY);
+    if (!Objects.equals(ownerId, controllerId)) {
+      LOGGER.warn("Not releasing the deletion marker for table: {}: it is now 
owned by controller: {}, not: {}",
+          tableNameWithType, ownerId, controllerId);
+      return;
+    }
+    propertyStore.remove(markerPath, AccessOption.PERSISTENT);
+  }
+
+  /// Acquires the deletion marker, taking it over if the existing one is 
stale.
+  /// This handles the case where a controller crashes during deletion, 
leaving a stale marker behind.
+  ///
+  /// @param propertyStore The Helix property store
+  /// @param tableNameWithType The table name with type suffix (e.g., 
myTable_REALTIME)
+  /// @param controllerId The ID of the controller performing the deletion
+  /// @return true if the marker was created (either new or by taking over 
expired), false if a valid marker exists
+  public static boolean 
createOrTakeoverTableDeletionMarker(ZkHelixPropertyStore<ZNRecord> 
propertyStore,
+      String tableNameWithType, String controllerId) {
+    String markerPath = 
constructPropertyStorePathForTableDeletionMarker(tableNameWithType);
+    Stat stat = new Stat();
+    ZNRecord markerRecord = propertyStore.get(markerPath, stat, 
AccessOption.PERSISTENT);
+    if (markerRecord == null) {
+      return createTableDeletionMarker(propertyStore, tableNameWithType, 
controllerId);
+    }
+    if (isTableDeletionMarkerValid(markerRecord)) {
+      return false;
+    }
+    // The marker is stale, so reclaim it. Pass the version we just observed 
so the write is rejected if another
+    // controller reclaimed it first.
+    return takeoverStaleTableDeletionMarker(propertyStore, tableNameWithType, 
controllerId, stat.getVersion());

Review Comment:
   The stale takeover does not actually preserve mutual exclusion: expiring 
this persistent marker does not revoke or fence the delete that originally 
acquired it. If that delete is paused for more than 24 hours and later resumes, 
the takeover starts a second delete while the first can continue removing 
metadata (including metadata from a later recreation). A version check only 
chooses one takeover winner; use a session-bound lock, or a renewable 
lease/fencing token that every destructive step validates before allowing 
takeover.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/SegmentDeletionManager.java:
##########
@@ -324,52 +326,72 @@ public void removeSegmentsFromStoreInBatch(String 
tableNameWithType, Collection<
       if (retentionMs <= 0) {
         filesToDelete.add(fileToDeleteURI);
       } else {
-        moveSegmentsToDeletedDir(segmentId, deletedSegmentsRetentionMs, 
rawTableName, pinotFS, fileToDeleteURI);
+        // Collect move operations for batched processing
+        String deletedFileName = deletedSegmentsRetentionMs == null ? 
URIUtils.encode(segmentId)
+            : getDeletedSegmentFileName(URIUtils.encode(segmentId), 
deletedSegmentsRetentionMs);
+        URI deletedSegmentMoveDestURI = URIUtils.getUri(_dataDir, 
DELETED_SEGMENTS, rawTableName, deletedFileName);
+        filesToMove.add(fileToDeleteURI);
+        moveDestinations.add(deletedSegmentMoveDestURI);
       }
     }
 
     try {
+      // Batch delete segments with retention <= 0
       if (!filesToDelete.isEmpty()) {
         LOGGER.info("Deleting {} segment files", filesToDelete.size());
         pinotFS.deleteBatch(filesToDelete, true);
       }
+
+      // Move the segments to the deleted directory and let the retention 
manager delete them later.
+      // PinotFS has no batch move, so this is still one move per segment; 
what this avoids relative to a
+      // per-segment helper is an exists() round trip per segment (move 
already reports a missing source) and
+      // one log line per segment, both of which dominate when dropping a 
table with many segments.
+      if (!filesToMove.isEmpty()) {
+        LOGGER.info("Moving {} segment files of table {} to the deleted 
directory", filesToMove.size(),
+            tableNameWithType);
+        int movedCount = 0;
+        List<URI> failedMoves = new ArrayList<>();
+        for (int i = 0; i < filesToMove.size(); i++) {
+          URI srcUri = filesToMove.get(i);
+          URI destUri = moveDestinations.get(i);
+          try {
+            // Overwrites the file if it already exists in the target 
directory.
+            if (pinotFS.move(srcUri, destUri, true)) {
+              // Touch is needed so that removeAgedDeletedSegments() sees a 
fresh last-modified time.
+              // Only touch destinations we actually created: touching a path 
whose move failed can
+              // materialize an empty object on object stores and leave a 
phantom deleted segment behind.
+              if (!pinotFS.touch(destUri)) {
+                LOGGER.warn("Could not touch moved segment file at {}, it may 
be aged out on the wrong schedule",
+                    destUri);
+              }
+              movedCount++;
+            } else {
+              failedMoves.add(srcUri);
+            }
+          } catch (IOException e) {
+            failedMoves.add(srcUri);
+            LOGGER.debug("Could not move segment from {} to {}", srcUri, 
destUri, e);
+          }

Review Comment:
   `touch()` can throw `IOException` after `move()` has already succeeded (the 
filesystem API and S3/GCS/HDFS implementations all allow this). The shared 
catch then records the source as a failed move and omits it from `movedCount`, 
producing an incorrect aggregate warning even though the source has moved. 
Catch touch failures separately and still count the move as successful.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java:
##########
@@ -2552,84 +2571,173 @@ public void deleteTable(String tableName, TableType 
tableType, @Nullable String
       }
     }
 
-    // Remove the table from brokerResource
-    HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+    // Acquire the deletion marker. It gives concurrent deletes mutual 
exclusion and makes addTable() refuse to
+    // re-create the table while this deletion is still tearing its metadata 
down.
+    if 
(!ZKMetadataProvider.createOrTakeoverTableDeletionMarker(_propertyStore, 
tableNameWithType, _controllerId)) {
+      // CONFLICT rather than a generic failure: a concurrent delete is an 
expected, retryable condition.
+      throw new ControllerApplicationException(LOGGER, String.format(
+          "Cannot delete table '%s': a deletion is already in progress. "
+              + "If the previous deletion failed, wait for the deletion marker 
to expire (24 hours) "
+              + "or manually clean up the deletion marker from ZK at path: 
%s/%s",
+          tableNameWithType, 
ZKMetadataProvider.getPropertyStoreTableDeletionInProgressPrefix(), 
tableNameWithType),
+          Response.Status.CONFLICT);
+    }
+    try {
+      // Remove the table from brokerResource
+      HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+
+      // Delete the table on servers
+      deleteTableOnServers(tableNameWithType);
+
+      // Remove ideal state
+      
_helixDataAccessor.removeProperty(_keyBuilder.idealStates(tableNameWithType));
+      LOGGER.info("Deleting table {}: Removed ideal state", tableNameWithType);
+
+      // Remove all stored segments for the table.
+      // Retention precedence: the request's retention parameter, then the 
table config, then the cluster default
+      // that SegmentDeletionManager applies when this is null. Reuse 
SegmentDeletionManager's own extraction
+      // helper rather than re-reading the config here: it tolerates an empty 
or malformed period, which
+      // TimeUtils.convertPeriodToMillis would otherwise throw on and make the 
table undeletable.
+      Long retentionPeriodMs = retentionPeriod != null ? 
TimeUtils.convertPeriodToMillis(retentionPeriod)
+          : 
SegmentDeletionManager.getRetentionMsFromTableConfig(getTableConfig(tableNameWithType));
+      _segmentDeletionManager.removeSegmentsFromStoreInBatch(tableNameWithType,
+          getSegmentsFromPropertyStore(tableNameWithType),
+          retentionPeriodMs);
+      LOGGER.info("Deleting table {}: Removed stored segments", 
tableNameWithType);
+
+      // Remove segment metadata
+      
ZKMetadataProvider.removeResourceSegmentsFromPropertyStore(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed segment metadata", 
tableNameWithType);
+
+      // Remove COMMITTING segment list
+      if (tableType == TableType.REALTIME) {
+        if (ZKMetadataProvider.removePauselessDebugMetadata(_propertyStore, 
tableNameWithType)) {
+          LOGGER.info("Deleting table {}: Removed pauseless debug metadata", 
tableNameWithType);
+        } else {
+          LOGGER.info("Deleting table {}: Failed to remove pauseless debug 
metadata.", tableNameWithType);
+        }
+      }
 
-    // Delete the table on servers
-    deleteTableOnServers(tableNameWithType);
+      // Remove instance partitions
+      if (tableType == TableType.OFFLINE) {
+        InstancePartitionsUtils.removeInstancePartitions(_propertyStore, 
tableNameWithType);
+      } else {
+        String rawTableName = TableNameBuilder.extractRawTableName(tableName);
+        InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
+            
InstancePartitionsType.CONSUMING.getInstancePartitionsName(rawTableName));
+        InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
+            
InstancePartitionsType.COMPLETED.getInstancePartitionsName(rawTableName));
+      }
+      LOGGER.info("Deleting table {}: Removed instance partitions", 
tableNameWithType);
 
-    // Remove ideal state
-    
_helixDataAccessor.removeProperty(_keyBuilder.idealStates(tableNameWithType));
-    LOGGER.info("Deleting table {}: Removed ideal state", tableNameWithType);
+      // Remove tier instance partitions
+      InstancePartitionsUtils.removeTierInstancePartitions(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed tier instance partitions", 
tableNameWithType);
 
-    // Remove all stored segments for the table
-    Long retentionPeriodMs = retentionPeriod != null ? 
TimeUtils.convertPeriodToMillis(retentionPeriod) : null;
-    _segmentDeletionManager.removeSegmentsFromStoreInBatch(tableNameWithType,
-        getSegmentsFromPropertyStore(tableNameWithType),
-        retentionPeriodMs);
-    LOGGER.info("Deleting table {}: Removed stored segments", 
tableNameWithType);
+      // Remove segment lineage
+      SegmentLineageAccessHelper.deleteSegmentLineage(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed segment lineage", 
tableNameWithType);
 
-    // Remove segment metadata
-    ZKMetadataProvider.removeResourceSegmentsFromPropertyStore(_propertyStore, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed segment metadata", 
tableNameWithType);
+      // Remove task related metadata
+      MinionTaskMetadataUtils.deleteTaskMetadata(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed all minion task metadata", 
tableNameWithType);
 
-    // Remove COMMITTING segment list
-    if (tableType == TableType.REALTIME) {
-      if (ZKMetadataProvider.removePauselessDebugMetadata(_propertyStore, 
tableNameWithType)) {
-        LOGGER.info("Deleting table {}: Removed pauseless debug metadata", 
tableNameWithType);
-      } else {
-        LOGGER.info("Deleting table {}: Failed to remove pauseless debug 
metadata.", tableNameWithType);
+      // Remove materialized view metadata (if any) and unregister from 
consistency manager
+      notifyMaterializedViewConsistencyManagerForTableDrop(tableNameWithType);
+      try {
+        MaterializedViewDefinitionMetadataUtils.delete(_propertyStore, 
tableNameWithType);
+        LOGGER.info("Deleting table {}: Removed MV definition metadata", 
tableNameWithType);
+      } catch (Exception e) {
+        LOGGER.debug("Deleting table {}: No MV definition metadata to remove 
or removal failed",
+            tableNameWithType, e);
+      }
+      try {
+        MaterializedViewRuntimeMetadataUtils.delete(_propertyStore, 
tableNameWithType);
+        LOGGER.info("Deleting table {}: Removed MV runtime metadata", 
tableNameWithType);
+      } catch (Exception e) {
+        LOGGER.debug("Deleting table {}: No MV runtime metadata to remove or 
removal failed",
+            tableNameWithType, e);
       }
-    }
 
-    // Remove instance partitions
-    if (tableType == TableType.OFFLINE) {
-      InstancePartitionsUtils.removeInstancePartitions(_propertyStore, 
tableNameWithType);
-    } else {
-      String rawTableName = TableNameBuilder.extractRawTableName(tableName);
-      InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
-          
InstancePartitionsType.CONSUMING.getInstancePartitionsName(rawTableName));
-      InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
-          
InstancePartitionsType.COMPLETED.getInstancePartitionsName(rawTableName));
-    }
-    LOGGER.info("Deleting table {}: Removed instance partitions", 
tableNameWithType);
+      // Remove table config
+      // NOTE: This should always be the last step for deletion to avoid race 
condition in table re-create
+      ZKMetadataProvider.removeResourceConfigFromPropertyStore(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed table config", 
tableNameWithType);
 
-    // Remove tier instance partitions
-    InstancePartitionsUtils.removeTierInstancePartitions(_propertyStore, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed tier instance partitions", 
tableNameWithType);
+      // Audit the property store for metadata that should now be gone, and 
warn about anything left behind
+      List<String> leftoverMetadata = 
findLeftoverTableMetadata(tableNameWithType, tableType);
+      if (leftoverMetadata.isEmpty()) {
+        LOGGER.info("Deleting table {}: Verified all audited metadata was 
removed", tableNameWithType);
+      } else {
+        LOGGER.warn("Deleting table {}: Finished but left {} metadata 
artifact(s) behind: {}. "
+                + "These need to be cleaned up manually before the table is 
re-created.", tableNameWithType,
+            leftoverMetadata.size(), leftoverMetadata);
+      }
 
-    // Remove segment lineage
-    SegmentLineageAccessHelper.deleteSegmentLineage(_propertyStore, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed segment lineage", 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Finish", tableNameWithType);
+    } finally {
+      // Always release the marker, even if the deletion failed, so the 
deletion can be retried or the table
+      // re-created. Releasing is a no-op if another controller has since 
taken the marker over.
+      ZKMetadataProvider.removeTableDeletionMarker(_propertyStore, 
tableNameWithType, _controllerId);
+    }
+  }
 
-    // Remove task related metadata
-    MinionTaskMetadataUtils.deleteTaskMetadata(_propertyStore, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed all minion task metadata", 
tableNameWithType);
+  /// Returns the table metadata that [#deleteTable] should have removed but 
which is still present, so the
+  /// caller can warn an operator about it. An empty list means the audited 
metadata is all gone.
+  ///
+  /// This deliberately only covers artifacts that `deleteTable` removes with 
a synchronous ZK write, so a
+  /// leftover here is a real leak rather than a timing artifact. In 
particular the ExternalView is NOT checked:
+  /// Helix removes it asynchronously via the controller pipeline after the 
IdealState is dropped, so it is
+  /// routinely still present at this point.
+  ///
+  /// The caller only warns and never throws. By the time this runs the table 
config is already gone, so the
+  /// delete has succeeded from the caller's point of view; failing the 
request would report a misleading error
+  /// for a table that no longer exists, and would not make the leftovers go 
away.
+  ///
+  /// ponytail: tier instance partitions, minion task metadata and 
materialized view metadata are not audited.

Review Comment:
   “ponytail” is unrelated to the intended caveat and makes this Javadoc 
unclear; use “Note” here.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java:
##########
@@ -2552,84 +2571,173 @@ public void deleteTable(String tableName, TableType 
tableType, @Nullable String
       }
     }
 
-    // Remove the table from brokerResource
-    HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+    // Acquire the deletion marker. It gives concurrent deletes mutual 
exclusion and makes addTable() refuse to
+    // re-create the table while this deletion is still tearing its metadata 
down.
+    if 
(!ZKMetadataProvider.createOrTakeoverTableDeletionMarker(_propertyStore, 
tableNameWithType, _controllerId)) {
+      // CONFLICT rather than a generic failure: a concurrent delete is an 
expected, retryable condition.
+      throw new ControllerApplicationException(LOGGER, String.format(
+          "Cannot delete table '%s': a deletion is already in progress. "
+              + "If the previous deletion failed, wait for the deletion marker 
to expire (24 hours) "
+              + "or manually clean up the deletion marker from ZK at path: 
%s/%s",
+          tableNameWithType, 
ZKMetadataProvider.getPropertyStoreTableDeletionInProgressPrefix(), 
tableNameWithType),
+          Response.Status.CONFLICT);
+    }
+    try {
+      // Remove the table from brokerResource
+      HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+
+      // Delete the table on servers
+      deleteTableOnServers(tableNameWithType);
+
+      // Remove ideal state
+      
_helixDataAccessor.removeProperty(_keyBuilder.idealStates(tableNameWithType));
+      LOGGER.info("Deleting table {}: Removed ideal state", tableNameWithType);
+
+      // Remove all stored segments for the table.
+      // Retention precedence: the request's retention parameter, then the 
table config, then the cluster default
+      // that SegmentDeletionManager applies when this is null. Reuse 
SegmentDeletionManager's own extraction
+      // helper rather than re-reading the config here: it tolerates an empty 
or malformed period, which
+      // TimeUtils.convertPeriodToMillis would otherwise throw on and make the 
table undeletable.
+      Long retentionPeriodMs = retentionPeriod != null ? 
TimeUtils.convertPeriodToMillis(retentionPeriod)
+          : 
SegmentDeletionManager.getRetentionMsFromTableConfig(getTableConfig(tableNameWithType));
+      _segmentDeletionManager.removeSegmentsFromStoreInBatch(tableNameWithType,
+          getSegmentsFromPropertyStore(tableNameWithType),
+          retentionPeriodMs);
+      LOGGER.info("Deleting table {}: Removed stored segments", 
tableNameWithType);
+
+      // Remove segment metadata
+      
ZKMetadataProvider.removeResourceSegmentsFromPropertyStore(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed segment metadata", 
tableNameWithType);
+
+      // Remove COMMITTING segment list
+      if (tableType == TableType.REALTIME) {
+        if (ZKMetadataProvider.removePauselessDebugMetadata(_propertyStore, 
tableNameWithType)) {
+          LOGGER.info("Deleting table {}: Removed pauseless debug metadata", 
tableNameWithType);
+        } else {
+          LOGGER.info("Deleting table {}: Failed to remove pauseless debug 
metadata.", tableNameWithType);
+        }
+      }
 
-    // Delete the table on servers
-    deleteTableOnServers(tableNameWithType);
+      // Remove instance partitions
+      if (tableType == TableType.OFFLINE) {
+        InstancePartitionsUtils.removeInstancePartitions(_propertyStore, 
tableNameWithType);
+      } else {
+        String rawTableName = TableNameBuilder.extractRawTableName(tableName);
+        InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
+            
InstancePartitionsType.CONSUMING.getInstancePartitionsName(rawTableName));
+        InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
+            
InstancePartitionsType.COMPLETED.getInstancePartitionsName(rawTableName));
+      }
+      LOGGER.info("Deleting table {}: Removed instance partitions", 
tableNameWithType);
 
-    // Remove ideal state
-    
_helixDataAccessor.removeProperty(_keyBuilder.idealStates(tableNameWithType));
-    LOGGER.info("Deleting table {}: Removed ideal state", tableNameWithType);
+      // Remove tier instance partitions
+      InstancePartitionsUtils.removeTierInstancePartitions(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed tier instance partitions", 
tableNameWithType);
 
-    // Remove all stored segments for the table
-    Long retentionPeriodMs = retentionPeriod != null ? 
TimeUtils.convertPeriodToMillis(retentionPeriod) : null;
-    _segmentDeletionManager.removeSegmentsFromStoreInBatch(tableNameWithType,
-        getSegmentsFromPropertyStore(tableNameWithType),
-        retentionPeriodMs);
-    LOGGER.info("Deleting table {}: Removed stored segments", 
tableNameWithType);
+      // Remove segment lineage
+      SegmentLineageAccessHelper.deleteSegmentLineage(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed segment lineage", 
tableNameWithType);
 
-    // Remove segment metadata
-    ZKMetadataProvider.removeResourceSegmentsFromPropertyStore(_propertyStore, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed segment metadata", 
tableNameWithType);
+      // Remove task related metadata
+      MinionTaskMetadataUtils.deleteTaskMetadata(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed all minion task metadata", 
tableNameWithType);
 
-    // Remove COMMITTING segment list
-    if (tableType == TableType.REALTIME) {
-      if (ZKMetadataProvider.removePauselessDebugMetadata(_propertyStore, 
tableNameWithType)) {
-        LOGGER.info("Deleting table {}: Removed pauseless debug metadata", 
tableNameWithType);
-      } else {
-        LOGGER.info("Deleting table {}: Failed to remove pauseless debug 
metadata.", tableNameWithType);
+      // Remove materialized view metadata (if any) and unregister from 
consistency manager
+      notifyMaterializedViewConsistencyManagerForTableDrop(tableNameWithType);
+      try {
+        MaterializedViewDefinitionMetadataUtils.delete(_propertyStore, 
tableNameWithType);
+        LOGGER.info("Deleting table {}: Removed MV definition metadata", 
tableNameWithType);
+      } catch (Exception e) {
+        LOGGER.debug("Deleting table {}: No MV definition metadata to remove 
or removal failed",
+            tableNameWithType, e);
+      }
+      try {
+        MaterializedViewRuntimeMetadataUtils.delete(_propertyStore, 
tableNameWithType);
+        LOGGER.info("Deleting table {}: Removed MV runtime metadata", 
tableNameWithType);
+      } catch (Exception e) {
+        LOGGER.debug("Deleting table {}: No MV runtime metadata to remove or 
removal failed",
+            tableNameWithType, e);
       }
-    }
 
-    // Remove instance partitions
-    if (tableType == TableType.OFFLINE) {
-      InstancePartitionsUtils.removeInstancePartitions(_propertyStore, 
tableNameWithType);
-    } else {
-      String rawTableName = TableNameBuilder.extractRawTableName(tableName);
-      InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
-          
InstancePartitionsType.CONSUMING.getInstancePartitionsName(rawTableName));
-      InstancePartitionsUtils.removeInstancePartitions(_propertyStore,
-          
InstancePartitionsType.COMPLETED.getInstancePartitionsName(rawTableName));
-    }
-    LOGGER.info("Deleting table {}: Removed instance partitions", 
tableNameWithType);
+      // Remove table config
+      // NOTE: This should always be the last step for deletion to avoid race 
condition in table re-create
+      ZKMetadataProvider.removeResourceConfigFromPropertyStore(_propertyStore, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed table config", 
tableNameWithType);
 
-    // Remove tier instance partitions
-    InstancePartitionsUtils.removeTierInstancePartitions(_propertyStore, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed tier instance partitions", 
tableNameWithType);
+      // Audit the property store for metadata that should now be gone, and 
warn about anything left behind
+      List<String> leftoverMetadata = 
findLeftoverTableMetadata(tableNameWithType, tableType);
+      if (leftoverMetadata.isEmpty()) {
+        LOGGER.info("Deleting table {}: Verified all audited metadata was 
removed", tableNameWithType);
+      } else {
+        LOGGER.warn("Deleting table {}: Finished but left {} metadata 
artifact(s) behind: {}. "
+                + "These need to be cleaned up manually before the table is 
re-created.", tableNameWithType,
+            leftoverMetadata.size(), leftoverMetadata);
+      }

Review Comment:
   The audit is not actually warn-only: any transient Helix/property-store 
exception from `findLeftoverTableMetadata` escapes after the table config has 
been removed, so the endpoint returns a misleading 500 despite the stated 
contract. Catch audit failures here and log them without changing the completed 
deletion result.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java:
##########
@@ -2552,84 +2571,173 @@ public void deleteTable(String tableName, TableType 
tableType, @Nullable String
       }
     }
 
-    // Remove the table from brokerResource
-    HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+    // Acquire the deletion marker. It gives concurrent deletes mutual 
exclusion and makes addTable() refuse to
+    // re-create the table while this deletion is still tearing its metadata 
down.
+    if 
(!ZKMetadataProvider.createOrTakeoverTableDeletionMarker(_propertyStore, 
tableNameWithType, _controllerId)) {
+      // CONFLICT rather than a generic failure: a concurrent delete is an 
expected, retryable condition.
+      throw new ControllerApplicationException(LOGGER, String.format(
+          "Cannot delete table '%s': a deletion is already in progress. "
+              + "If the previous deletion failed, wait for the deletion marker 
to expire (24 hours) "
+              + "or manually clean up the deletion marker from ZK at path: 
%s/%s",
+          tableNameWithType, 
ZKMetadataProvider.getPropertyStoreTableDeletionInProgressPrefix(), 
tableNameWithType),
+          Response.Status.CONFLICT);
+    }
+    try {
+      // Remove the table from brokerResource
+      HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+
+      // Delete the table on servers
+      deleteTableOnServers(tableNameWithType);
+
+      // Remove ideal state
+      
_helixDataAccessor.removeProperty(_keyBuilder.idealStates(tableNameWithType));
+      LOGGER.info("Deleting table {}: Removed ideal state", tableNameWithType);
+
+      // Remove all stored segments for the table.
+      // Retention precedence: the request's retention parameter, then the 
table config, then the cluster default
+      // that SegmentDeletionManager applies when this is null. Reuse 
SegmentDeletionManager's own extraction
+      // helper rather than re-reading the config here: it tolerates an empty 
or malformed period, which
+      // TimeUtils.convertPeriodToMillis would otherwise throw on and make the 
table undeletable.
+      Long retentionPeriodMs = retentionPeriod != null ? 
TimeUtils.convertPeriodToMillis(retentionPeriod)
+          : 
SegmentDeletionManager.getRetentionMsFromTableConfig(getTableConfig(tableNameWithType));

Review Comment:
   This changes the default retention precedence, but the public DELETE-table 
`@ApiParam` and `DeleteTableCommand` help still say an omitted value goes 
directly to the cluster setting and then `7d`. Update both user-facing 
descriptions to include the table config first, as the segment-deletion API 
already does.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java:
##########
@@ -2552,84 +2571,173 @@ public void deleteTable(String tableName, TableType 
tableType, @Nullable String
       }
     }
 
-    // Remove the table from brokerResource
-    HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
-    LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+    // Acquire the deletion marker. It gives concurrent deletes mutual 
exclusion and makes addTable() refuse to
+    // re-create the table while this deletion is still tearing its metadata 
down.
+    if 
(!ZKMetadataProvider.createOrTakeoverTableDeletionMarker(_propertyStore, 
tableNameWithType, _controllerId)) {
+      // CONFLICT rather than a generic failure: a concurrent delete is an 
expected, retryable condition.
+      throw new ControllerApplicationException(LOGGER, String.format(
+          "Cannot delete table '%s': a deletion is already in progress. "
+              + "If the previous deletion failed, wait for the deletion marker 
to expire (24 hours) "
+              + "or manually clean up the deletion marker from ZK at path: 
%s/%s",
+          tableNameWithType, 
ZKMetadataProvider.getPropertyStoreTableDeletionInProgressPrefix(), 
tableNameWithType),
+          Response.Status.CONFLICT);
+    }
+    try {
+      // Remove the table from brokerResource
+      HelixHelper.removeResourceFromBrokerIdealState(_helixZkManager, 
tableNameWithType);
+      LOGGER.info("Deleting table {}: Removed from broker resource", 
tableNameWithType);
+
+      // Delete the table on servers
+      deleteTableOnServers(tableNameWithType);
+
+      // Remove ideal state
+      
_helixDataAccessor.removeProperty(_keyBuilder.idealStates(tableNameWithType));
+      LOGGER.info("Deleting table {}: Removed ideal state", tableNameWithType);
+
+      // Remove all stored segments for the table.
+      // Retention precedence: the request's retention parameter, then the 
table config, then the cluster default
+      // that SegmentDeletionManager applies when this is null. Reuse 
SegmentDeletionManager's own extraction
+      // helper rather than re-reading the config here: it tolerates an empty 
or malformed period, which
+      // TimeUtils.convertPeriodToMillis would otherwise throw on and make the 
table undeletable.
+      Long retentionPeriodMs = retentionPeriod != null ? 
TimeUtils.convertPeriodToMillis(retentionPeriod)
+          : 
SegmentDeletionManager.getRetentionMsFromTableConfig(getTableConfig(tableNameWithType));

Review Comment:
   The new table-config retention fallback has no regression test in the 
controller tests. Add a deletion test with no request retention and a 
distinctive `deletedSegmentsRetentionPeriod`, then verify that value reaches 
the segment move naming/manager; also cover that an explicit request value 
still takes precedence.



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to