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

JackieTien97 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 c75e9a8f70b Prohibit rollback of a procedure which has passed its 
commit point (#18909)
c75e9a8f70b is described below

commit c75e9a8f70bcad4148a993c4d89dedd803da857b
Author: alpass163 <[email protected]>
AuthorDate: Tue Oct 20 16:41:34 2026 +0800

    Prohibit rollback of a procedure which has passed its commit point (#18909)
---
 .../iotdb/confignode/i18n/ProcedureMessages.java   |   7 +
 .../iotdb/confignode/i18n/ProcedureMessages.java   |   6 +
 .../iotdb/confignode/procedure/Procedure.java      |  18 ++
 .../confignode/procedure/ProcedureExecutor.java    | 122 +++++++++++++-
 .../confignode/procedure/RootProcedureStack.java   |   4 +-
 .../procedure/impl/StateMachineProcedure.java      |  44 ++++-
 .../procedure/impl/node/RemoveAINodeProcedure.java |   9 +
 .../table/AbstractSetPropertiesProcedure.java      |  17 ++
 .../impl/schema/table/AddTableColumnProcedure.java |  17 ++
 .../table/AlterTableColumnDataTypeProcedure.java   |   1 +
 .../impl/schema/table/CreateTableProcedure.java    |  10 +-
 .../schema/table/DropTableColumnProcedure.java     |   1 +
 .../impl/schema/table/DropTableProcedure.java      |  15 +-
 .../schema/table/RenameTableColumnProcedure.java   |  17 ++
 .../impl/schema/table/RenameTableProcedure.java    |  17 ++
 .../impl/schema/table/view/DropViewProcedure.java  |  18 ++
 .../procedure/impl/StateMachineProcedureTest.java  | 185 +++++++++++++++++++++
 17 files changed, 500 insertions(+), 8 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 737c1c49bb4..7154b0b4c82 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
@@ -1439,4 +1439,11 @@ public final class ProcedureMessages {
       "DataNode setSystemStatus RPC failed: %s: %s: %s; ";
   public static final String 
EXCEPTION_CONFIGNODE_TRYSETNODESTATUS_FAILED_ARG_ARG_ARG_C9BA78F4 =
       "ConfigNode trySetNodeStatus failed: %s: %s: %s; ";
+  public static final String
+      
LOG_PROCEDURE_ARG_IS_PAST_ITS_COMMIT_POINT_KEEPING_EVERY_STATE_IT_EXECUTED_CA21D7E9
 =
+          "Procedure {} is past its commit point; keeping every state it 
executed.";
+  public static final String LOG_FINISH_PROCEDURE_WITHOUT_ROLLBACK_41F7A354 =
+      "finish procedure without rollback";
+  public static final String 
LOG_DELETE_CHILD_PROCEDURES_WITHOUT_ROLLBACK_6E62D1C1 =
+      "delete child procedures without rollback";
 }
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 fe569097189..973241cd431 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
@@ -1524,4 +1524,10 @@ public final class ProcedureMessages {
       "DataNode setSystemStatus RPC 失败:%s:%s:%s; ";
   public static final String 
EXCEPTION_CONFIGNODE_TRYSETNODESTATUS_FAILED_ARG_ARG_ARG_C9BA78F4 =
       "ConfigNode trySetNodeStatus 失败:%s:%s:%s; ";
+  public static final String
+      
LOG_PROCEDURE_ARG_IS_PAST_ITS_COMMIT_POINT_KEEPING_EVERY_STATE_IT_EXECUTED_CA21D7E9
 =
+          "流程 {} 已越过提交点,保留它执行过的所有状态。";
+  public static final String LOG_FINISH_PROCEDURE_WITHOUT_ROLLBACK_41F7A354 = 
"结束流程(不回滚)";
+  public static final String 
LOG_DELETE_CHILD_PROCEDURES_WITHOUT_ROLLBACK_6E62D1C1 =
+      "删除子流程(不回滚)";
 }
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/Procedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/Procedure.java
index 10ae561c2af..dc26ced3b64 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/Procedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/Procedure.java
@@ -99,6 +99,24 @@ public abstract class Procedure<Env> implements 
Comparable<Procedure<Env>> {
   protected abstract void rollback(Env env)
       throws IOException, InterruptedException, ProcedureException;
 
+  /**
+   * Whether the states this procedure has already executed may still be 
undone. The executor asks
+   * this once, before it starts to roll back the root procedure stack. A 
procedure which returns
+   * false is finished without undoing any of its states, see {@link 
#abandonRollback()}.
+   */
+  protected boolean isRollbackSupported() {
+    return true;
+  }
+
+  /**
+   * Drops the bookkeeping of the states this procedure has executed. The 
persisted stack indexes
+   * are what a recovered rollback rebuilds its stack from, so dropping them 
is what makes the
+   * decision survive a leader switch.
+   */
+  protected synchronized void abandonRollback() {
+    stackIndexes = null;
+  }
+
   public void serialize(DataOutputStream stream) throws IOException {
     // procid
     stream.writeLong(this.procId);
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/ProcedureExecutor.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/ProcedureExecutor.java
index 82b217b93ea..0b5460486b2 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/ProcedureExecutor.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/ProcedureExecutor.java
@@ -40,8 +40,10 @@ import java.io.IOException;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Deque;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -370,7 +372,7 @@ public class ProcedureExecutor<Env> {
    *
    * @param proc procedure
    */
-  private void executeProcedure(Procedure<Env> proc) {
+  private void executeProcedure(Procedure<Env> proc) throws 
InterruptedException {
     if (proc.isFinished()) {
       LOG.debug(ProcedureMessages.IS_ALREADY_FINISHED, proc);
       return;
@@ -689,7 +691,7 @@ public class ProcedureExecutor<Env> {
    * @return lock state
    */
   private LockStateResult<Env> executeRootStackRollback(
-      Long rootProcId, RootProcedureStack<Env> procedureStack) {
+      Long rootProcId, RootProcedureStack<Env> procedureStack) throws 
InterruptedException {
     Procedure<Env> rootProcedure = procedures.get(rootProcId);
     ProcedureException exception = rootProcedure.getException();
     if (exception == null) {
@@ -700,14 +702,33 @@ public class ProcedureExecutor<Env> {
           () -> store.update(rootProcedure), "root procedure rollback");
     }
     List<Procedure<Env>> subprocStack = procedureStack.getSubproceduresStack();
+    // Decide which procedures must keep the states they have executed before 
anything is rolled
+    // back: the decision reads the state history, which the rollback itself 
shrinks. Identity is
+    // used because procedures override equals with the data of the operation 
they run.
+    final Set<Procedure<Env>> proceduresKeepingTheirStates =
+        Collections.newSetFromMap(new IdentityHashMap<>());
+    for (Procedure<Env> procedure : subprocStack) {
+      // A null slot is a procedure whose record was deleted before the 
snapshot this stack was
+      // rebuilt from, so there is nothing left to roll back for it.
+      if (procedure != null && !procedure.isSuccess() && 
!procedure.isRollbackSupported()) {
+        proceduresKeepingTheirStates.add(procedure);
+      }
+    }
     int stackTail = subprocStack.size();
     while (stackTail-- > 0) {
       Procedure<Env> procedure = subprocStack.get(stackTail);
+      if (procedure == null) {
+        continue;
+      }
       if (procedure.isSuccess()) {
         subprocStack.remove(stackTail);
         cleanupAfterRollback(procedure);
         continue;
       }
+      if (proceduresKeepingTheirStates.contains(procedure)) {
+        // Finished below, once the rest of the stack has been rolled back.
+        continue;
+      }
       ProcedureLockState lockState = acquireLock(procedure);
       if (lockState != ProcedureLockState.LOCK_ACQUIRED) {
         return new LockStateResult<>(lockState, procedure);
@@ -730,12 +751,109 @@ public class ProcedureExecutor<Env> {
       }
     }
 
+    if (!proceduresKeepingTheirStates.isEmpty()) {
+      // The children go first: releasing a child's lock writes its record 
back, which has to be
+      // covered by the root's cleanup. The root is finished last so that no 
write of a child
+      // follows the write which persists the root as finished.
+      for (Procedure<Env> procedure : proceduresKeepingTheirStates) {
+        if (procedure.hasParent()) {
+          finishWithoutRollback(procedure);
+        }
+      }
+      for (Procedure<Env> procedure : proceduresKeepingTheirStates) {
+        if (!procedure.hasParent()) {
+          finishWithoutRollback(procedure);
+        }
+      }
+      subprocStack.removeIf(proceduresKeepingTheirStates::contains);
+    }
+
     LOG.info(
         ProcedureMessages.ROLLED_BACK_TIME_DURATION_IS, rootProcedure, 
rootProcedure.elapsedTime());
     rootProcedureCleanup(rootProcedure);
     return new LockStateResult<>(ProcedureLockState.LOCK_ACQUIRED, 
rootProcedure);
   }
 
+  /**
+   * Finishes a procedure without undoing any of the states it has executed. 
The failure is still
+   * reported, as the operation the procedure was part of did not complete; 
what is dropped is only
+   * the bookkeeping of the rollback, so that neither this rollback nor the 
one a new leader
+   * recovers touches this procedure again.
+   */
+  private void finishWithoutRollback(final Procedure<Env> procedure) throws 
InterruptedException {
+    // The unlock persists the procedure through doReleaseLock(). It is done 
first, while the
+    // procedure is neither marked finished nor stripped of its state history, 
so that a snapshot
+    // taken right after this write is still a state recovery resumes. Forcing 
the release matters
+    // because the procedure is not finished yet, so releaseLock() would 
otherwise keep the lock of
+    // procedures which hold it across steps.
+    releaseLockIfHeld(procedure);
+    if (procedure.hasParent()) {
+      // Mark the procedure finished before dropping its state history, so 
that a worker which
+      // picks it up concurrently returns at the isFinished() check instead of 
rolling it back.
+      if (!procedure.isSuccess()) {
+        procedure.setState(ProcedureState.ROLLEDBACK);
+      }
+      procedure.abandonRollback();
+      try {
+        store.delete(procedure.getProcId());
+        // do not remove this procedure when exception occurred
+        procedures.remove(procedure.getProcId());
+      } catch (final Exception e) {
+        LOG.warn(ProcedureMessages.FAILED_TO_DELETE_PROCEDURE_ON_ROLLBACK, e);
+      }
+    } else {
+      // A child which never executed has no rollback step and is not visited 
above, so its record
+      // goes with the ones of the children which were.
+      // Every child record has to be gone before the root may be persisted as 
finished: a snapshot
+      // holding a finished root beside a child record makes recover() fail on 
that child. Deleting
+      // is idempotent, so it is retried until durable.
+      final long[] childProcIds = 
rollbackStack.get(procedure.getProcId()).getSubprocedureIds();
+      for (final long childProcId : childProcIds) {
+        RetryUtils.executeWithEndlessBackoffRetry(
+            () -> store.delete(childProcId),
+            
ProcedureMessages.LOG_DELETE_CHILD_PROCEDURES_WITHOUT_ROLLBACK_6E62D1C1);
+        // The retry helper also returns on interruption. Abort the entire 
rollback before
+        // marking the root finished or letting the caller clean it up.
+        if (Thread.currentThread().isInterrupted()) {
+          throw new InterruptedException();
+        }
+      }
+      if (!procedure.isSuccess()) {
+        procedure.setState(ProcedureState.ROLLEDBACK);
+      }
+      procedure.abandonRollback();
+      // The stack indexes and the states just dropped are persisted here, so 
that the rollback
+      // recovered by a new leader leaves this procedure out. Endless retry 
since this step is not
+      // idempotent.
+      RetryUtils.executeWithEndlessBackoffRetry(
+          () -> store.update(procedure),
+          ProcedureMessages.LOG_FINISH_PROCEDURE_WITHOUT_ROLLBACK_41F7A354);
+      if (Thread.currentThread().isInterrupted()) {
+        throw new InterruptedException();
+      }
+    }
+    procedure.updateMetricsOnFinish(getEnvironment(), procedure.elapsedTime(), 
false);
+    LOG.info(
+        ProcedureMessages
+            
.LOG_PROCEDURE_ARG_IS_PAST_ITS_COMMIT_POINT_KEEPING_EVERY_STATE_IT_EXECUTED_CA21D7E9,
+        procedure);
+  }
+
+  /**
+   * The rollback loop skipped acquireLock() for procedures finished without 
rollback, so a lock
+   * restored by restoreLock() on load is still recorded in lockedWhenLoading 
instead of locked.
+   * Finish that bookkeeping and release the lock here: rootProcedureCleanup() 
releases only the
+   * locks that hasLock() reports, and a procedure finished without rollback 
must not keep holding
+   * its lock.
+   */
+  private void releaseLockIfHeld(final Procedure<Env> procedure) {
+    if (procedure.hasLock() || procedure.isLockedWhenLoading()) {
+      if (acquireLock(procedure) == ProcedureLockState.LOCK_ACQUIRED) {
+        releaseLock(procedure, true);
+      }
+    }
+  }
+
   private ProcedureLockState acquireLock(Procedure<Env> proc) {
     if (proc.hasLock()) {
       return ProcedureLockState.LOCK_ACQUIRED;
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/RootProcedureStack.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/RootProcedureStack.java
index 1fa240997f7..db38c6175bb 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/RootProcedureStack.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/RootProcedureStack.java
@@ -89,7 +89,9 @@ public class RootProcedureStack<Env> {
   protected synchronized ProcedureException getException() {
     if (subprocStack != null) {
       for (Procedure<Env> proc : subprocStack) {
-        if (proc.hasException()) {
+        // Null slots come from procedures whose records were deleted before 
the stack was
+        // rebuilt, see ProcedureExecutor#executeRootStackRollback.
+        if (proc != null && proc.hasException()) {
           return proc.getException();
         }
       }
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedure.java
index f34225cede9..5f518f728e9 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedure.java
@@ -31,6 +31,7 @@ import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentLinkedDeque;
 
@@ -252,12 +253,49 @@ public abstract class StateMachineProcedure<Env, TState> 
extends Procedure<Env>
     return !states.isEmpty() && states.getLast() == EOF_STATE;
   }
 
+  @Override
+  protected synchronized void abandonRollback() {
+    super.abandonRollback();
+    states.clear();
+  }
+
   /**
-   * Used by the default implementation of abort() to know if the current 
state can be aborted and
-   * rollback can be triggered.
+   * Whether a failure in the specified state can be rolled back. A procedure 
asks this in its error
+   * handling to tell a fatal failure from a retriable one; the executor asks 
it once, through the
+   * overload without a state, to decide whether the procedure may be rolled 
back at all.
+   *
+   * <p>The executor asks with the state the procedure is at: the state which 
failed or is
+   * scheduled, or the last state executed when every state has already run. A 
procedure which
+   * returns false for the states after its commit point is therefore not 
rolled back once that
+   * point has been passed.
    */
   protected boolean isRollbackSupported(final TState state) {
-    return false;
+    return true;
+  }
+
+  /**
+   * Whether the states this procedure has executed may still be rolled back. 
The answer comes from
+   * the state the procedure is at, through the per-state overload above.
+   */
+  @Override
+  protected final boolean isRollbackSupported() {
+    return isRollbackSupported(getRollbackDecisionState());
+  }
+
+  /**
+   * The state a rollback decision is taken at. The last element of the 
history is the state which
+   * failed or is scheduled; once every state has run, the end marker is last 
and the decision falls
+   * on the state before it.
+   */
+  private TState getRollbackDecisionState() {
+    final Iterator<Integer> stateIds = states.descendingIterator();
+    while (stateIds.hasNext()) {
+      final int stateId = stateIds.next();
+      if (stateId != EOF_STATE) {
+        return getState(stateId);
+      }
+    }
+    return getInitialState();
   }
 
   private boolean noMoreState() {
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/node/RemoveAINodeProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/node/RemoveAINodeProcedure.java
index 23b041c2587..a6fdd7d6151 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/node/RemoveAINodeProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/node/RemoveAINodeProcedure.java
@@ -135,6 +135,15 @@ public class RemoveAINodeProcedure extends 
AbstractNodeProcedure<RemoveAINodeSta
     // no need to rollback
   }
 
+  /**
+   * A failure in either state is retried instead of failing the procedure, 
which has nothing to
+   * roll back.
+   */
+  @Override
+  protected boolean isRollbackSupported(final RemoveAINodeState state) {
+    return false;
+  }
+
   @Override
   protected RemoveAINodeState getState(int stateId) {
     return RemoveAINodeState.values()[stateId];
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AbstractSetPropertiesProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AbstractSetPropertiesProcedure.java
index be6f67007a3..a354f0ff3a3 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AbstractSetPropertiesProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AbstractSetPropertiesProcedure.java
@@ -162,6 +162,23 @@ public abstract class AbstractSetPropertiesProcedure
   protected abstract ConfigPhysicalPlan createSetPropertiesPlan(
       Map<String, String> properties, boolean isRollback);
 
+  /**
+   * The properties are set on the ConfigNode once SET_PROPERTIES has 
completed, and the ConfigNode
+   * schema is the source of truth the DataNodes resync from. The procedure 
therefore stays
+   * rollbackable up to SET_PROPERTIES and not after it.
+   */
+  @Override
+  protected boolean isRollbackSupported(final SetTablePropertiesState state) {
+    switch (state) {
+      case VALIDATE_TABLE:
+      case PRE_RELEASE:
+      case SET_PROPERTIES:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   @Override
   protected void rollbackState(
       final ConfigNodeProcedureEnv env, final SetTablePropertiesState state)
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AddTableColumnProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AddTableColumnProcedure.java
index 44f9e2f07f9..07bdbf44c55 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AddTableColumnProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AddTableColumnProcedure.java
@@ -154,6 +154,23 @@ public class AddTableColumnProcedure
     return "add table column";
   }
 
+  /**
+   * The column is part of the table on the ConfigNode once ADD_COLUMN has 
completed, and the
+   * ConfigNode schema is the source of truth the DataNodes resync from. The 
procedure therefore
+   * stays rollbackable up to ADD_COLUMN and not after it.
+   */
+  @Override
+  protected boolean isRollbackSupported(final AddTableColumnState state) {
+    switch (state) {
+      case COLUMN_CHECK:
+      case PRE_RELEASE:
+      case ADD_COLUMN:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   @Override
   protected void rollbackState(final ConfigNodeProcedureEnv env, final 
AddTableColumnState state)
       throws IOException, InterruptedException, ProcedureException {
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
index 12491f24a48..1cfda63fcf7 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
@@ -161,6 +161,7 @@ public class AlterTableColumnDataTypeProcedure
     setNextState(AlterTableColumnDataTypeState.COMMIT_RELEASE);
   }
 
+  /** The rollback of this procedure is a no-op, so it is never rolled back. */
   @Override
   protected boolean isRollbackSupported(final AlterTableColumnDataTypeState 
state) {
     return false;
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
index f2379ae1256..ac573463ebb 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
@@ -212,7 +212,15 @@ public class CreateTableProcedure
 
   @Override
   protected boolean isRollbackSupported(final CreateTableState state) {
-    return true;
+    switch (state) {
+      case CHECK_TABLE_EXISTENCE:
+      case PRE_CREATE:
+      case PRE_RELEASE:
+      case COMMIT_CREATE:
+        return true;
+      default:
+        return false;
+    }
   }
 
   @Override
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableColumnProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableColumnProcedure.java
index a51c636551e..75000e36ef6 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableColumnProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableColumnProcedure.java
@@ -233,6 +233,7 @@ public class DropTableColumnProcedure
     }
   }
 
+  /** The rollback of this procedure is a no-op, so it is never rolled back. */
   @Override
   protected boolean isRollbackSupported(final DropTableColumnState state) {
     return false;
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableProcedure.java
index cdb4d4b71c8..54217212a11 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/DropTableProcedure.java
@@ -386,9 +386,22 @@ public class DropTableProcedure extends 
AbstractAlterOrDropTableProcedure<DropTa
     }
   }
 
+  /**
+   * The data is deleted logically once DELETE_DATA has completed: the 
deletion is applied to every
+   * data region, and only the physical removal of the mod files continues in 
the background. The
+   * procedure is therefore not rolled back once it has moved on to 
DELETE_DEVICES. A view holds no
+   * data and goes straight to DROP_TABLE, see {@link DropViewProcedure}.
+   */
   @Override
   protected boolean isRollbackSupported(final DropTableState state) {
-    return state == DropTableState.CHECK_AND_INVALIDATE_TABLE || state == 
DropTableState.PRE_DELETE;
+    switch (state) {
+      case CHECK_AND_INVALIDATE_TABLE:
+      case PRE_DELETE:
+      case DELETE_DATA:
+        return true;
+      default:
+        return false;
+    }
   }
 
   @Override
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableColumnProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableColumnProcedure.java
index 11d10901a35..03088935ce6 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableColumnProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableColumnProcedure.java
@@ -154,6 +154,23 @@ public class RenameTableColumnProcedure
     }
   }
 
+  /**
+   * The column is renamed on the ConfigNode once RENAME_COLUMN has completed, 
and the ConfigNode
+   * schema is the source of truth the DataNodes resync from. The procedure 
therefore stays
+   * rollbackable up to RENAME_COLUMN and not after it.
+   */
+  @Override
+  protected boolean isRollbackSupported(final RenameTableColumnState state) {
+    switch (state) {
+      case COLUMN_CHECK:
+      case PRE_RELEASE:
+      case RENAME_COLUMN:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   @Override
   protected void rollbackState(final ConfigNodeProcedureEnv env, final 
RenameTableColumnState state)
       throws IOException, InterruptedException, ProcedureException {
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableProcedure.java
index e40b041d0f6..52962533049 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/RenameTableProcedure.java
@@ -144,6 +144,23 @@ public class RenameTableProcedure extends 
AbstractAlterOrDropTableProcedure<Rena
     }
   }
 
+  /**
+   * The table is renamed on the ConfigNode once RENAME_TABLE has completed, 
and the ConfigNode
+   * schema is the source of truth the DataNodes resync from. The procedure 
therefore stays
+   * rollbackable up to RENAME_TABLE and not after it.
+   */
+  @Override
+  protected boolean isRollbackSupported(final RenameTableState state) {
+    switch (state) {
+      case COLUMN_CHECK:
+      case PRE_RELEASE:
+      case RENAME_TABLE:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   @Override
   protected void rollbackState(final ConfigNodeProcedureEnv env, final 
RenameTableState state)
       throws IOException, InterruptedException, ProcedureException {
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/view/DropViewProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/view/DropViewProcedure.java
index 73a7732fc87..163150c0a09 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/view/DropViewProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/view/DropViewProcedure.java
@@ -20,6 +20,7 @@
 package org.apache.iotdb.confignode.procedure.impl.schema.table.view;
 
 import 
org.apache.iotdb.confignode.procedure.impl.schema.table.DropTableProcedure;
+import org.apache.iotdb.confignode.procedure.state.schema.DropTableState;
 import org.apache.iotdb.confignode.procedure.store.ProcedureType;
 
 import java.io.DataOutputStream;
@@ -38,6 +39,23 @@ public class DropViewProcedure extends DropTableProcedure {
     super(database, tableName, queryId, isGeneratedByPipe);
   }
 
+  /**
+   * A view holds no data, so the procedure goes straight from PRE_DELETE to 
DROP_TABLE. The view is
+   * removed from the ConfigNode once DROP_TABLE has completed; before that it 
can still be
+   * restored, so the procedure is not rolled back from COMMIT_DELETE on.
+   */
+  @Override
+  protected boolean isRollbackSupported(final DropTableState state) {
+    switch (state) {
+      case CHECK_AND_INVALIDATE_TABLE:
+      case PRE_DELETE:
+      case DROP_TABLE:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   @Override
   public void serialize(final DataOutputStream stream) throws IOException {
     stream.writeShort(
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedureTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedureTest.java
new file mode 100644
index 00000000000..340f1b88c5c
--- /dev/null
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedureTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.confignode.procedure.impl;
+
+import org.apache.iotdb.confignode.procedure.entity.SimpleSTMProcedure;
+import org.apache.iotdb.confignode.procedure.env.TestProcEnv;
+import org.apache.iotdb.confignode.procedure.exception.ProcedureException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class StateMachineProcedureTest {
+
+  private enum State {
+    PREPARE,
+    APPLY,
+    RELEASE
+  }
+
+  /**
+   * A procedure whose commit point is the completion of {@code APPLY}: it 
stays rollbackable while
+   * it is at {@code PREPARE} or {@code APPLY}, and not once it has moved on 
to {@code RELEASE}.
+   */
+  private static class CommitPointProcedure extends 
StateMachineProcedure<TestProcEnv, State> {
+
+    private boolean failAtApply;
+    private boolean failAtRelease;
+
+    @Override
+    protected Flow executeFromState(final TestProcEnv env, final State state) {
+      switch (state) {
+        case PREPARE:
+          setNextState(State.APPLY);
+          break;
+        case APPLY:
+          if (failAtApply) {
+            setFailure(new ProcedureException("apply failed"));
+          } else {
+            setNextState(State.RELEASE);
+          }
+          break;
+        case RELEASE:
+          if (failAtRelease) {
+            setFailure(new ProcedureException("release failed"));
+          }
+          return Flow.NO_MORE_STATE;
+        default:
+          throw new IllegalStateException();
+      }
+      return Flow.HAS_MORE_STATE;
+    }
+
+    @Override
+    protected void rollbackState(final TestProcEnv env, final State state) {
+      // The point of the test is that this is never called once the commit 
point has been passed.
+    }
+
+    @Override
+    protected boolean isRollbackSupported(final State state) {
+      return state != State.RELEASE;
+    }
+
+    @Override
+    protected State getState(final int stateId) {
+      return State.values()[stateId];
+    }
+
+    @Override
+    protected int getStateId(final State state) {
+      return state.ordinal();
+    }
+
+    @Override
+    protected State getInitialState() {
+      return State.PREPARE;
+    }
+
+    /** Runs one state, the way the executor does. */
+    private void runStep() throws InterruptedException {
+      execute(new TestProcEnv());
+    }
+
+    private State currentState() {
+      return getCurrentState();
+    }
+
+    private void addSlot(final int index) {
+      addStackIndex(index);
+    }
+
+    private int[] stackIndexes() {
+      return getStackIndexes();
+    }
+
+    private void abandon() {
+      abandonRollback();
+    }
+  }
+
+  @Test
+  public void rollbackIsAllowedUntilTheStateAfterTheCommitPointIsReached()
+      throws InterruptedException {
+    final CommitPointProcedure procedure = new CommitPointProcedure();
+
+    // PREPARE has completed and APPLY is scheduled.
+    procedure.runStep();
+    Assert.assertEquals(State.APPLY, procedure.currentState());
+    Assert.assertTrue(procedure.isRollbackSupported());
+
+    // APPLY has completed: the procedure is at RELEASE and must not be rolled 
back any more.
+    procedure.runStep();
+    Assert.assertEquals(State.RELEASE, procedure.currentState());
+    Assert.assertFalse(procedure.isRollbackSupported());
+
+    // Every state has run; the decision falls on the last state which ran, 
not on the end marker.
+    procedure.runStep();
+    Assert.assertFalse(procedure.isRollbackSupported());
+  }
+
+  @Test
+  public void aFailureBeforeTheCommitPointStillAllowsTheRollback() throws 
InterruptedException {
+    final CommitPointProcedure procedure = new CommitPointProcedure();
+    procedure.runStep();
+
+    procedure.failAtApply = true;
+    procedure.runStep();
+
+    Assert.assertEquals(State.APPLY, procedure.currentState());
+    Assert.assertTrue(procedure.isRollbackSupported());
+  }
+
+  @Test
+  public void aFailureAfterTheCommitPointDoesNotAllowTheRollback() throws 
InterruptedException {
+    final CommitPointProcedure procedure = new CommitPointProcedure();
+    procedure.runStep();
+    procedure.runStep();
+
+    procedure.failAtRelease = true;
+    procedure.runStep();
+
+    Assert.assertFalse(procedure.isRollbackSupported());
+  }
+
+  @Test
+  public void abandoningTheRollbackDropsTheStackSlotsAndTheStateHistory()
+      throws InterruptedException {
+    final CommitPointProcedure procedure = new CommitPointProcedure();
+    procedure.runStep();
+    procedure.runStep();
+    procedure.addSlot(0);
+    procedure.addSlot(1);
+    procedure.addSlot(2);
+    Assert.assertEquals(3, procedure.stackIndexes().length);
+    Assert.assertFalse(procedure.isRollbackSupported());
+
+    procedure.abandon();
+
+    // A recovered rollback rebuilds its stack from the persisted indexes, so 
they must be gone.
+    Assert.assertNull(procedure.stackIndexes());
+    // An empty history reports the initial state, which is how a drained 
history shows up.
+    Assert.assertEquals(State.PREPARE, procedure.currentState());
+  }
+
+  @Test
+  public void aProcedureWhichOverridesNothingIsRollbackableEverywhere() {
+    Assert.assertTrue(new SimpleSTMProcedure().isRollbackSupported());
+  }
+}

Reply via email to