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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0a3e21231ed Fix region-group cleanup: recover-safe submission + retry 
that actually re-runs the delete (#18097)
0a3e21231ed is described below

commit 0a3e21231ed6d72aa207baa8504b709ed19297fc
Author: Yongzao <[email protected]>
AuthorDate: Thu Jul 2 21:32:45 2026 +0800

    Fix region-group cleanup: recover-safe submission + retry that actually 
re-runs the delete (#18097)
---
 .../iotdb/confignode/i18n/ProcedureMessages.java   |   2 +-
 .../iotdb/confignode/i18n/ProcedureMessages.java   |   2 +-
 .../impl/region/CreateRegionGroupsProcedure.java   |  28 +++-
 .../impl/region/RemoveRegionGroupProcedure.java    | 180 ++++++++++++++++-----
 .../impl/schema/DeleteDatabaseProcedure.java       |  27 +++-
 .../region/RemoveRegionGroupProcedureTest.java     |  28 ++++
 6 files changed, 218 insertions(+), 49 deletions(-)

diff --git 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
index 4d3cea6f8eb..87e18f5ba90 100644
--- 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
@@ -623,7 +623,7 @@ public final class ProcedureMessages {
   public static final String PID_REMOVEREGIONGROUP_STATE_FAILED =
       "[pid{}][RemoveRegionGroup] state {} failed";
   public static final String PID_REMOVEREGIONGROUP_DELETE_REPLICA_FAILED =
-      "[pid{}][RemoveRegionGroup] failed to delete a replica of region {} 
(attempt {}/{}), will retry. reason: {}";
+      "[pid{}][RemoveRegionGroup] failed to delete a replica of region {} 
(attempt {}), will keep retrying until it is deleted. reason: {}";
   public static final String PID_REMOVEREGIONGROUP_SUCCESS_PROCEDURE_TOOK =
       "[pid{}][RemoveRegionGroup] success, region group {} has been deleted. 
Procedure took {} (started at {}).";
   public static final String 
PID_MIGRATEREGION_STARTED_WILL_BE_MIGRATED_FROM_DATANODE_TO =
diff --git 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
index 9602839fae9..1cb059d6c72 100644
--- 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
@@ -621,7 +621,7 @@ public final class ProcedureMessages {
   public static final String PID_REMOVEREGIONGROUP_STATE_FAILED =
       "[pid{}][RemoveRegionGroup] 状态 {} 失败";
   public static final String PID_REMOVEREGIONGROUP_DELETE_REPLICA_FAILED =
-      "[pid{}][RemoveRegionGroup] 删除 region {} 的一个副本失败(第 {}/{} 次尝试),将重试。原因:{}";
+      "[pid{}][RemoveRegionGroup] 删除 region {} 的一个副本失败(第 {} 
次尝试),将持续重试直到删除成功。原因:{}";
   public static final String PID_REMOVEREGIONGROUP_SUCCESS_PROCEDURE_TOOK =
       "[pid{}][RemoveRegionGroup] 成功,region group {} 已删除。过程耗时 {}(开始于 {})。";
   public static final String 
PID_MIGRATEREGION_STARTED_WILL_BE_MIGRATED_FROM_DATANODE_TO =
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/CreateRegionGroupsProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/CreateRegionGroupsProcedure.java
index cb26afc97dd..276cdf432d9 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/CreateRegionGroupsProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/CreateRegionGroupsProcedure.java
@@ -108,8 +108,11 @@ public class CreateRegionGroupsProcedure
       case SHUNT_REGION_REPLICAS:
         persistPlan = new CreateRegionGroupsPlan();
         final OfferRegionMaintainTasksPlan offerPlan = new 
OfferRegionMaintainTasksPlan();
-        // RegionGroups that failed to reach a serving quorum are removed via 
a child
-        // RemoveRegionGroupProcedure, which deletes every replica that did 
get created.
+        // RegionGroups that failed to reach a serving quorum have their 
redundant (already-created)
+        // replicas removed via an independent root 
RemoveRegionGroupProcedure. Submitting them as
+        // root procedures (instead of children) keeps this procedure from 
waiting for or being
+        // failed by the cleanup: each one retries forever until those 
replicas are deleted, while
+        // this procedure proceeds to activate the region groups that did form 
a quorum.
         final List<RemoveRegionGroupProcedure> removeRegionGroupProcedures = 
new ArrayList<>();
         // Filter those RegionGroups that created successfully
         createRegionGroupsPlan
@@ -197,7 +200,26 @@ public class CreateRegionGroupsProcedure
           LOGGER.warn(
               
ConfigNodeMessages.FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE, 
e);
         }
-        removeRegionGroupProcedures.forEach(this::addChildProcedure);
+        // Submit the redundant-replica cleanups as independent root 
procedures. This is
+        // intentionally NOT guarded by isStateDeserialized(): the executor 
persists a procedure at
+        // a state BEFORE that state's body has run (it advances the state on 
the previous cycle,
+        // then may stop at the inter-state boundary on a leader switch — see
+        // ProcedureExecutor#executeProcedure), so a recovery that lands on 
SHUNT_REGION_REPLICAS
+        // means the submissions have NOT happened yet. Skipping them would 
leave the
+        // already-created
+        // replicas of sub-quorum region groups on disk with no cleanup and no 
partition-table
+        // record
+        // (the else branch above never persisted them). Re-submitting on 
recovery is safe instead:
+        // the cleanups are recomputed from the serialized 
failedRegionReplicaSets, each gets a
+        // fresh
+        // procId and performs an idempotent delete, so a duplicate is 
harmless whereas a skip
+        // leaks.
+        removeRegionGroupProcedures.forEach(
+            removeRegionGroupProcedure ->
+                env.getConfigManager()
+                    .getProcedureManager()
+                    .getExecutor()
+                    .submitProcedure(removeRegionGroupProcedure));
         setNextState(CreateRegionGroupsState.REBALANCE_DATA_PARTITION_POLICY);
         break;
       case REBALANCE_DATA_PARTITION_POLICY:
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedure.java
index 2dd2e0d6dd3..d417099e97b 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedure.java
@@ -59,11 +59,21 @@ import static 
org.apache.iotdb.rpc.TSStatusCode.SUCCESS_STATUS;
  * already-absent peer, so it works for a group of any size — including a 
sub-quorum group that
  * never finished forming. The DataNode runs the deletion asynchronously and 
this procedure polls
  * for the result, so a slow deletion is never wrongly reported as finished.
+ *
+ * <p>This procedure is submitted as an independent root procedure (not a 
child) by its callers,
+ * which only enqueue the deletion and return immediately. It therefore owns 
the deletion end to
+ * end: on any failure it retries the current replica forever (backing off 
between attempts) instead
+ * of giving up, because there is no parent left to fall back to and the 
region's peer/data must not
+ * be left on disk. Each genuine re-attempt uses a FRESH DataNode-side taskId 
(the DataNode dedups
+ * by taskId and caches a terminal result forever, so reusing one taskId would 
make every retry a
+ * no-op that never re-runs the delete); the in-flight taskId is persisted so 
a leader change
+ * re-polls the same task rather than double-submitting. It carries its own 
{@link
+ * TRegionReplicaSet} copy, so it can finish even after the caller has dropped 
the partition table,
+ * and it survives ConfigNode leader change / restart.
  */
 public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveRegionGroupState> {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(RemoveRegionGroupProcedure.class);
 
-  private static final int MAX_DELETE_REPLICA_RETRY = 3;
   private static final long DELETE_REPLICA_RETRY_INTERVAL_MS = 5_000;
 
   private TRegionReplicaSet regionReplicaSet;
@@ -75,10 +85,32 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
   // not finished deleting.
   private int currentReplicaIndex;
 
-  // Number of failed attempts on the replica at currentReplicaIndex. 
Transient: a leader change
-  // restarts the retry budget for the current replica.
+  // Number of failed attempts on the replica at currentReplicaIndex, used 
only for logging. Retries
+  // are unbounded, so this is not a budget. Transient: a leader change 
restarts the counter for the
+  // current replica.
   private transient int attemptedForCurrentReplica;
 
+  // Monotonic count of delete tasks this procedure has submitted, across all 
replicas. Persisted
+  // and
+  // only ever incremented. It is the low half of the DataNode-side taskId 
(see deleteTaskId): a
+  // fresh value per genuine re-attempt makes the DataNode re-run the delete 
instead of replaying a
+  // cached terminal result for a reused taskId (the DataNode dedups by taskId 
and never clears the
+  // cache), which is the bug this fixes. It never resets, so every taskId 
this procedure emits is
+  // distinct even across replicas and retries.
+  private long deleteTaskSeq;
+
+  // Whether a delete task for the replica at currentReplicaIndex has already 
been submitted (and
+  // thus
+  // deleteTaskSeq already identifies an in-flight task to re-poll) rather 
than needing a fresh one.
+  // Persisted so a leader change mid-attempt re-polls the SAME in-flight task 
instead of submitting
+  // a
+  // duplicate; cleared on success or when a terminal failure forces a fresh 
re-attempt.
+  private boolean deleteTaskSubmitted;
+
+  // Bit budget for deleteTaskId(): sign bit (=> negative) + PROC_ID_BITS + 
SEQ_BITS must be <= 64.
+  private static final int SEQ_BITS = 20;
+  private static final int PROC_ID_BITS = 43;
+
   public RemoveRegionGroupProcedure() {
     super();
   }
@@ -93,15 +125,27 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
     this.currentReplicaIndex = currentReplicaIndex;
   }
 
+  @TestOnly
+  void setDeleteTaskState(long deleteTaskSeq, boolean deleteTaskSubmitted) {
+    this.deleteTaskSeq = deleteTaskSeq;
+    this.deleteTaskSubmitted = deleteTaskSubmitted;
+  }
+
+  @TestOnly
+  long deleteTaskIdForTest() {
+    return deleteTaskId();
+  }
+
   @Override
   protected Flow executeFromState(ConfigNodeProcedureEnv env, 
RemoveRegionGroupState state)
       throws InterruptedException {
     final List<TDataNodeLocation> dataNodeLocations =
         regionReplicaSet == null ? null : 
regionReplicaSet.getDataNodeLocations();
     if (dataNodeLocations == null) {
-      // A null replica set means deserialization failed; fail loudly instead 
of silently reporting
-      // the group as deleted, otherwise the parent would drop the partition 
table while the region
-      // data is still on disk.
+      // A null replica set means deserialization failed. Retrying cannot 
recover the lost
+      // locations,
+      // so fail loudly instead of silently reporting the group as deleted 
(which would leave the
+      // region's peer/data on disk with no record of where it lives).
       setFailure(
           new ProcedureException(ProcedureMessages.UNSUPPORTED_STATE + 
"missing regionReplicaSet"));
       return Flow.NO_MORE_STATE;
@@ -137,26 +181,37 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
             regionId,
             simplifiedLocation(targetDataNode));
 
-        // deleteLocalPeer is idempotent (it tolerates an already-absent peer) 
and the DataNode
-        // dedups by taskId, so re-submitting after a leader change or a retry 
is safe.
+        // Start a fresh attempt (fresh taskId) unless we are resuming an 
already-submitted one
+        // after
+        // a leader change, in which case we re-poll the SAME task rather than 
submitting a
+        // duplicate.
+        if (!deleteTaskSubmitted) {
+          deleteTaskSeq++;
+          deleteTaskSubmitted = true;
+        }
+        final long deleteTaskId = deleteTaskId();
+
+        // deleteLocalPeer is idempotent (it tolerates an already-absent 
peer), and re-submitting
+        // the
+        // same taskId re-polls the same DataNode task, so resuming after a 
leader change is safe.
         final TSStatus submitStatus;
         final TRegionMigrateResult result;
         try {
           submitStatus =
-              handler.submitDeleteOldRegionPeerTask(getProcId(), 
targetDataNode, regionId);
+              handler.submitDeleteOldRegionPeerTask(deleteTaskId, 
targetDataNode, regionId);
           setKillPoint(state);
           if (submitStatus.getCode() != SUCCESS_STATUS.getStatusCode()) {
-            return retryCurrentReplicaOrFail(
+            return retryCurrentReplica(
                 String.format(
                     "submit delete task for region %s to DataNode %s failed: 
%s",
                     regionId, simplifiedLocation(targetDataNode), 
submitStatus));
           }
-          result = handler.waitTaskFinish(getProcId(), targetDataNode);
+          result = handler.waitTaskFinish(deleteTaskId, targetDataNode);
         } catch (InterruptedException e) {
           throw e;
         } catch (Exception e) {
           LOGGER.error(ProcedureMessages.PID_REMOVEREGIONGROUP_STATE_FAILED, 
getProcId(), state, e);
-          return retryCurrentReplicaOrFail(
+          return retryCurrentReplica(
               String.format(
                   "delete region %s from DataNode %s threw %s",
                   regionId, simplifiedLocation(targetDataNode), e));
@@ -164,23 +219,24 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
 
         switch (result.getTaskStatus()) {
           case SUCCESS:
-            // Advance to the next replica with a fresh retry budget.
+            // Advance to the next replica with a fresh retry counter and a 
fresh delete task.
             currentReplicaIndex++;
             attemptedForCurrentReplica = 0;
+            deleteTaskSubmitted = false;
             setNextState(RemoveRegionGroupState.DELETE_REGION_REPLICAS);
             return Flow.HAS_MORE_STATE;
           case PROCESSING:
             // waitTaskFinish() only returns PROCESSING when its polling loop 
was interrupted, i.e.
             // this ConfigNode is shutting down / losing leadership. The 
delete task is still
-            // running
-            // on the DataNode, so persist and re-poll after recovery: stay on 
this replica without
-            // advancing it and without consuming a retry attempt.
+            // running on the DataNode, so persist and re-poll after recovery: 
stay on this replica
+            // without advancing it, without consuming a retry attempt, and 
keeping deleteTaskSeq /
+            // deleteTaskSubmitted so the re-poll targets the same in-flight 
task.
             setNextState(RemoveRegionGroupState.DELETE_REGION_REPLICAS);
             return Flow.HAS_MORE_STATE;
           case TASK_NOT_EXIST:
           case FAIL:
           default:
-            return retryCurrentReplicaOrFail(
+            return retryCurrentReplica(
                 String.format(
                     "delete region %s from DataNode %s, task status is %s",
                     regionId, simplifiedLocation(targetDataNode), 
result.getTaskStatus()));
@@ -192,30 +248,57 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
   }
 
   /**
-   * Retry the replica at {@link #currentReplicaIndex} after a backoff, or 
fail the whole procedure
-   * once the per-replica retry budget is exhausted. Failing (rather than 
skipping the replica)
-   * keeps the parent from dropping the partition table while a region's 
peer/data is still on disk.
+   * Retry the replica at {@link #currentReplicaIndex} after a backoff. This 
procedure never gives
+   * up on a replica: because it is submitted as an independent root 
procedure, there is no parent
+   * to fall back to, and skipping or failing would leave the region's 
peer/data on disk. So it
+   * backs off and re-runs the same state until the replica is deleted, which 
eventually succeeds
+   * once the target DataNode is reachable: the delete is idempotent, and 
clearing {@link
+   * #deleteTaskSubmitted} here makes the next attempt use a FRESH 
DataNode-side taskId (a new
+   * {@link #deleteTaskSeq}), so the DataNode actually re-executes the delete 
instead of returning a
+   * cached terminal result for the previous taskId.
    */
-  private Flow retryCurrentReplicaOrFail(String reason) throws 
InterruptedException {
+  private Flow retryCurrentReplica(String reason) throws InterruptedException {
     attemptedForCurrentReplica++;
-    if (attemptedForCurrentReplica <= MAX_DELETE_REPLICA_RETRY) {
-      LOGGER.warn(
-          ProcedureMessages.PID_REMOVEREGIONGROUP_DELETE_REPLICA_FAILED,
-          getProcId(),
-          regionId,
-          attemptedForCurrentReplica,
-          MAX_DELETE_REPLICA_RETRY + 1,
-          reason);
-      Thread.sleep(DELETE_REPLICA_RETRY_INTERVAL_MS);
-      setNextState(RemoveRegionGroupState.DELETE_REGION_REPLICAS);
-      return Flow.HAS_MORE_STATE;
+    LOGGER.warn(
+        ProcedureMessages.PID_REMOVEREGIONGROUP_DELETE_REPLICA_FAILED,
+        getProcId(),
+        regionId,
+        attemptedForCurrentReplica,
+        reason);
+    // Force a fresh delete task on the next attempt so the DataNode re-runs 
the delete rather than
+    // replaying a cached FAIL/SUCCESS for this taskId.
+    deleteTaskSubmitted = false;
+    Thread.sleep(DELETE_REPLICA_RETRY_INTERVAL_MS);
+    setNextState(RemoveRegionGroupState.DELETE_REGION_REPLICAS);
+    return Flow.HAS_MORE_STATE;
+  }
+
+  /**
+   * The DataNode-side taskId for the current attempt, derived from this 
procedure's (globally
+   * unique, consensus-replicated) procId and its monotonic {@link 
#deleteTaskSeq}. It is packed
+   * into the NEGATIVE i64 space, which is disjoint from every real procId 
(all {@code >= 0}); other
+   * region-maintain procedures (add/remove peer) use {@code getProcId()} 
directly as the taskId
+   * against the same DataNode task map, so a negative id can never collide 
with theirs. Unlike
+   * minting from the procedure-store id allocator, this needs nothing extra 
replicated: procId is
+   * already replicated and deleteTaskSeq is persisted with this procedure, so 
the taskId is stable
+   * across a leader change and never regresses.
+   *
+   * <p>Layout: sign bit set (=> negative) | {@value PROC_ID_BITS} bits of 
procId | {@value
+   * SEQ_BITS} bits of deleteTaskSeq. The bounds are astronomically beyond any 
real cluster (a
+   * procId needs 2^43 procedures; a single group delete needs 2^20 retries), 
and are asserted
+   * rather than silently wrapped so a violation fails the procedure loudly 
instead of emitting a
+   * colliding id.
+   */
+  private long deleteTaskId() {
+    final long procId = getProcId();
+    if (procId < 0 || procId >= (1L << PROC_ID_BITS) || deleteTaskSeq >= (1L 
<< SEQ_BITS)) {
+      throw new IllegalStateException(
+          String.format(
+              "cannot derive a collision-free delete taskId: procId=%d, 
deleteTaskSeq=%d exceed the "
+                  + "%d/%d-bit budget",
+              procId, deleteTaskSeq, PROC_ID_BITS, SEQ_BITS));
     }
-    setFailure(
-        new ProcedureException(
-            String.format(
-                "[pid%d][RemoveRegionGroup] gave up after %d attempts: %s",
-                getProcId(), attemptedForCurrentReplica, reason)));
-    return Flow.NO_MORE_STATE;
+    return Long.MIN_VALUE | (procId << SEQ_BITS) | deleteTaskSeq;
   }
 
   @Override
@@ -243,6 +326,11 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
     super.serialize(stream);
     ThriftCommonsSerDeUtils.serializeTRegionReplicaSet(regionReplicaSet, 
stream);
     ReadWriteIOUtils.write(currentReplicaIndex, stream);
+    // Persist the delete-task cursor so a leader change re-derives the SAME 
in-flight taskId and
+    // re-polls it (deleteTaskSubmitted == true) instead of submitting a 
duplicate, and so the
+    // monotonic deleteTaskSeq never regresses.
+    ReadWriteIOUtils.write(deleteTaskSeq, stream);
+    ReadWriteIOUtils.write(deleteTaskSubmitted, stream);
   }
 
   @Override
@@ -252,6 +340,14 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
       regionReplicaSet = 
ThriftCommonsSerDeUtils.deserializeTRegionReplicaSet(byteBuffer);
       regionId = regionReplicaSet.getRegionId();
       currentReplicaIndex = ReadWriteIOUtils.readInt(byteBuffer);
+      // deleteTaskSeq/deleteTaskSubmitted were appended after the first 
version of this procedure.
+      // That first version only ever existed on the unreleased branch that 
added this procedure
+      // (never in a release), but a dev/CI cluster could persist a blob 
without these trailing
+      // fields; tolerate it by defaulting to "no in-flight task" instead of 
reading past the end.
+      if (byteBuffer.hasRemaining()) {
+        deleteTaskSeq = ReadWriteIOUtils.readLong(byteBuffer);
+        deleteTaskSubmitted = ReadWriteIOUtils.readBool(byteBuffer);
+      }
     } catch (ThriftSerDeException e) {
       LOGGER.error(ProcedureMessages.ERROR_IN_DESERIALIZE, this.getClass(), e);
     }
@@ -264,12 +360,14 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
     }
     RemoveRegionGroupProcedure procedure = (RemoveRegionGroupProcedure) obj;
     return this.currentReplicaIndex == procedure.currentReplicaIndex
+        && this.deleteTaskSeq == procedure.deleteTaskSeq
+        && this.deleteTaskSubmitted == procedure.deleteTaskSubmitted
         && Objects.equals(this.regionReplicaSet, procedure.regionReplicaSet);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(regionReplicaSet, currentReplicaIndex);
+    return Objects.hash(regionReplicaSet, currentReplicaIndex, deleteTaskSeq, 
deleteTaskSubmitted);
   }
 
   @Override
@@ -279,6 +377,10 @@ public class RemoveRegionGroupProcedure extends 
RegionOperationProcedure<RemoveR
         + regionReplicaSet
         + ", currentReplicaIndex="
         + currentReplicaIndex
+        + ", deleteTaskSeq="
+        + deleteTaskSeq
+        + ", deleteTaskSubmitted="
+        + deleteTaskSubmitted
         + '}';
   }
 }
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/DeleteDatabaseProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/DeleteDatabaseProcedure.java
index 4b979105c2f..e40bcfbc24c 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/DeleteDatabaseProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/DeleteDatabaseProcedure.java
@@ -103,10 +103,24 @@ public class DeleteDatabaseProcedure
               "[DeleteDatabaseProcedure] Delete DatabaseSchema: {}",
               deleteDatabaseSchema.getName());
 
-          // Delete every region group (both schema and data regions) of this 
database via a
-          // RemoveRegionGroupProcedure child. The DatabasePartitionTable 
(handled in the next
-          // state) is only removed once these children have finished, so a 
slow region deletion is
-          // always completed before the coordinator forgets about it.
+          // Enqueue deletion of every region group (both schema and data 
regions) of this database.
+          // Each is submitted as an INDEPENDENT root 
RemoveRegionGroupProcedure rather than a
+          // child:
+          // this procedure only submits the deletions and then returns, so it 
can neither wait for
+          // nor be failed/rolled-back by a slow or failing region deletion. 
Each carries its own
+          // copy of the replica set, so the deletion still completes (and 
survives leader change /
+          // restart) even after the next state drops the partition table.
+          //
+          // Submission is intentionally NOT guarded by isStateDeserialized(): 
the executor persists
+          // a procedure at a state BEFORE that state's body has run (it 
advances the state on the
+          // previous cycle, then may stop at the inter-state boundary on a 
leader switch — see
+          // ProcedureExecutor#executeProcedure). So a recovery that lands on 
this state means the
+          // submission has NOT happened yet; skipping it would drop every 
region group's cleanup
+          // while the next state still drops the partition table, orphaning 
the region peers/data
+          // on
+          // disk with no record of where they live. Re-submitting on recovery 
is safe instead:
+          // every RemoveRegionGroupProcedure gets a fresh procId and performs 
an idempotent delete,
+          // so a duplicate is harmless whereas a skip leaks data.
           final List<TRegionReplicaSet> regionReplicaSets =
               env.getAllReplicaSets(deleteDatabaseSchema.getName());
           regionReplicaSets.forEach(
@@ -115,7 +129,10 @@ public class DeleteDatabaseProcedure
                 env.getConfigManager()
                     .getLoadManager()
                     
.removeRegionGroupRelatedCache(regionReplicaSet.getRegionId());
-                addChildProcedure(new 
RemoveRegionGroupProcedure(regionReplicaSet));
+                env.getConfigManager()
+                    .getProcedureManager()
+                    .getExecutor()
+                    .submitProcedure(new 
RemoveRegionGroupProcedure(regionReplicaSet));
               });
           setNextState(DeleteDatabaseState.DELETE_DATABASE_CONFIG);
           break;
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedureTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedureTest.java
index 3ca6efaf037..e239d64c12a 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedureTest.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/region/RemoveRegionGroupProcedureTest.java
@@ -59,6 +59,9 @@ public class RemoveRegionGroupProcedureTest {
     // A non-zero cursor so the round-trip actually exercises 
currentReplicaIndex (de)serialization;
     // equals/hashCode include it, so a dropped/garbled cursor would fail the 
assertion.
     procedure.setCurrentReplicaIndex(1);
+    // Non-default delete-task cursor so the round-trip exercises 
deleteTaskSeq/deleteTaskSubmitted
+    // too; equals/hashCode include them, so a dropped/garbled value would 
fail the assertion.
+    procedure.setDeleteTaskState(42L, true);
     try (PublicBAOS byteArrayOutputStream = new PublicBAOS();
         DataOutputStream outputStream = new 
DataOutputStream(byteArrayOutputStream)) {
       procedure.serialize(outputStream);
@@ -69,4 +72,29 @@ public class RemoveRegionGroupProcedureTest {
       Assert.assertEquals(procedure, 
ProcedureFactory.getInstance().create(buffer));
     }
   }
+
+  @Test
+  public void deleteTaskIdIsNegativeAndUnique() {
+    // The DataNode taskResultMap is keyed only by taskId and is shared with 
add/remove-peer tasks,
+    // which use a procedure's (non-negative) procId directly as the taskId. 
So a delete taskId must
+    // be strictly negative (disjoint from every procId) and distinct per 
(procId, deleteTaskSeq),
+    // otherwise a later peer op could be silently deduped against a lingering 
delete-task entry.
+    final TRegionReplicaSet regionReplicaSet =
+        new TRegionReplicaSet(
+            new TConsensusGroupId(TConsensusGroupType.DataRegion, 1),
+            Arrays.asList(new TDataNodeLocation()));
+    final java.util.Set<Long> seen = new java.util.HashSet<>();
+    for (long procId : new long[] {0L, 1L, 100L, 1L << 20, (1L << 43) - 1}) {
+      for (long seq : new long[] {1L, 2L, 100L, (1L << 20) - 1}) {
+        final RemoveRegionGroupProcedure procedure =
+            new RemoveRegionGroupProcedure(regionReplicaSet);
+        procedure.setProcId(procId);
+        procedure.setDeleteTaskState(seq, true);
+        final long taskId = procedure.deleteTaskIdForTest();
+        Assert.assertTrue("taskId must be negative: " + taskId, taskId < 0);
+        Assert.assertTrue(
+            "taskId must be unique for (" + procId + "," + seq + ")", 
seen.add(taskId));
+      }
+    }
+  }
 }

Reply via email to