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 32bff9b615a Fix procedures stuck after submission-time deserialization 
failure (#18212)
32bff9b615a is described below

commit 32bff9b615a68e1a8fd79320415591a45508ca1e
Author: Yongzao <[email protected]>
AuthorDate: Wed Jul 15 16:36:16 2026 +0800

    Fix procedures stuck after submission-time deserialization failure (#18212)
---
 .../confignode/procedure/ProcedureExecutor.java    |  28 +++++
 .../procedure/TestProcedureExecutor.java           | 127 +++++++++++++++++++++
 2 files changed, 155 insertions(+)

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 92ce0b9c2c9..2aca885c391 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
@@ -211,6 +211,14 @@ public class ProcedureExecutor<Env> {
             runnableList.add(procedure);
           }
         });
+    // A submission-time deserialization failure may be persisted before a 
rollback stack index.
+    failedList.forEach(
+        procedure -> {
+          RootProcedureStack<Env> rootStack = 
rollbackStack.get(getRootProcedureId(procedure));
+          if (rootStack != null) {
+            initializeRollbackStackForFailedProcedure(rootStack, procedure);
+          }
+        });
     restoreLocks();
 
     waitingTimeoutList.forEach(timeoutExecutor::add);
@@ -810,12 +818,32 @@ public class ProcedureExecutor<Env> {
     // Update metrics on start of a procedure
     procedure.updateMetricsOnSubmit(getEnvironment());
     RootProcedureStack<Env> stack = new RootProcedureStack<>();
+    // Persisting a newly submitted procedure may serialize and deserialize it 
through the
+    // consensus layer. If that process marks the procedure as failed before 
it is scheduled, the
+    // rollback stack still needs an entry so the executor can finish the 
failed procedure instead
+    // of leaving it in the active procedure map forever.
+    if (initializeRollbackStackForFailedProcedure(stack, procedure)) {
+      try {
+        store.update(procedure);
+      } catch (Exception e) {
+        LOG.error(ProcedureMessages.FAILED_TO_UPDATE_STORE_PROCEDURE, 
procedure, e);
+      }
+    }
     rollbackStack.put(currentProcId, stack);
     procedures.put(currentProcId, procedure);
     scheduler.addBack(procedure);
     return procedure.getProcId();
   }
 
+  private boolean initializeRollbackStackForFailedProcedure(
+      RootProcedureStack<Env> stack, Procedure<Env> procedure) {
+    if (procedure.isFailed() && !procedure.wasExecuted()) {
+      stack.addRollbackStep(procedure);
+      return true;
+    }
+    return false;
+  }
+
   private class WorkerThread extends StoppableThread {
     private final AtomicLong startTime = new AtomicLong(Long.MAX_VALUE);
     private final AtomicReference<Procedure<Env>> activeProcedure = new 
AtomicReference<>();
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/TestProcedureExecutor.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/TestProcedureExecutor.java
index ab6ec55f4d7..95caffd4a9a 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/TestProcedureExecutor.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/TestProcedureExecutor.java
@@ -31,8 +31,11 @@ import org.junit.Assert;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class TestProcedureExecutor extends TestProcedureBase {
@@ -56,6 +59,66 @@ public class TestProcedureExecutor extends TestProcedureBase 
{
     Assert.assertEquals(acc.get(), 1);
   }
 
+  @Test
+  public void testProcedureFailedDuringSubmissionIsRolledBack() throws 
InterruptedException {
+    TestProcEnv localEnv = new TestProcEnv();
+    FailOnFirstUpdateProcedureStore localStore = new 
FailOnFirstUpdateProcedureStore();
+    ProcedureExecutor<TestProcEnv> localExecutor = new 
ProcedureExecutor<>(localEnv, localStore);
+    localEnv.setScheduler(localExecutor.getScheduler());
+    localStore.start();
+    localExecutor.init(1);
+
+    try {
+      FailedDuringSubmissionProcedure procedure = new 
FailedDuringSubmissionProcedure();
+      long procId = localExecutor.submitProcedure(procedure);
+
+      boolean rollbackStackPersisted = localStore.isRollbackStackPersisted();
+      localExecutor.startWorkers();
+      Assert.assertTrue(rollbackStackPersisted);
+      Assert.assertTrue(procedure.awaitRollback(10, TimeUnit.SECONDS));
+      ProcedureTestUtil.waitForProcedure(localExecutor, procId);
+      Assert.assertTrue(localExecutor.isFinished(procId));
+      Assert.assertEquals(ProcedureState.ROLLEDBACK, procedure.getState());
+      Assert.assertEquals(0, procedure.getExecutionCount());
+      Assert.assertEquals(1, procedure.getRollbackCount());
+    } finally {
+      localStore.stop();
+      localExecutor.stop();
+      localExecutor.join();
+      localStore.cleanup();
+    }
+  }
+
+  @Test
+  public void testFailedProcedureWithoutRollbackStackCanBeRecovered() throws 
InterruptedException {
+    FailedDuringSubmissionProcedure procedure = new 
FailedDuringSubmissionProcedure();
+    procedure.setProcId(0);
+    procedure.setProcRunnable();
+    procedure.setFailure(new ProcedureException("Failed to deserialize 
procedure"));
+
+    TestProcEnv localEnv = new TestProcEnv();
+    FailedProcedureStore localStore = new FailedProcedureStore(procedure);
+    ProcedureExecutor<TestProcEnv> localExecutor = new 
ProcedureExecutor<>(localEnv, localStore);
+    localEnv.setScheduler(localExecutor.getScheduler());
+    localStore.start();
+    localExecutor.init(1);
+    localExecutor.startWorkers();
+
+    try {
+      Assert.assertTrue(procedure.awaitRollback(10, TimeUnit.SECONDS));
+      ProcedureTestUtil.waitForProcedure(localExecutor, procedure.getProcId());
+      Assert.assertTrue(localExecutor.isFinished(procedure.getProcId()));
+      Assert.assertEquals(ProcedureState.ROLLEDBACK, procedure.getState());
+      Assert.assertEquals(0, procedure.getExecutionCount());
+      Assert.assertEquals(1, procedure.getRollbackCount());
+    } finally {
+      localStore.stop();
+      localExecutor.stop();
+      localExecutor.join();
+      localStore.cleanup();
+    }
+  }
+
   @Test
   public void testWorkerThreadStuck() throws InterruptedException {
     Semaphore latch1 = new Semaphore(2);
@@ -223,4 +286,68 @@ public class TestProcedureExecutor extends 
TestProcedureBase {
       return executionCount.get();
     }
   }
+
+  private static class FailOnFirstUpdateProcedureStore extends 
NoopProcedureStore {
+
+    private final AtomicBoolean firstUpdate = new AtomicBoolean(true);
+    private final AtomicBoolean rollbackStackPersisted = new 
AtomicBoolean(false);
+
+    @Override
+    public void update(Procedure procedure) {
+      if (firstUpdate.compareAndSet(true, false)) {
+        procedure.setFailure(new ProcedureException("Failed to deserialize 
procedure"));
+      } else if (procedure.isFailed() && procedure.wasExecuted()) {
+        rollbackStackPersisted.set(true);
+      }
+    }
+
+    private boolean isRollbackStackPersisted() {
+      return rollbackStackPersisted.get();
+    }
+  }
+
+  private static class FailedProcedureStore extends NoopProcedureStore {
+
+    private final Procedure procedure;
+
+    private FailedProcedureStore(Procedure procedure) {
+      this.procedure = procedure;
+    }
+
+    @Override
+    public List<Procedure> load() {
+      return Collections.singletonList(procedure);
+    }
+  }
+
+  private static class FailedDuringSubmissionProcedure extends 
Procedure<TestProcEnv> {
+
+    private final Semaphore rolledBack = new Semaphore(0);
+    private final AtomicInteger executionCount = new AtomicInteger();
+    private final AtomicInteger rollbackCount = new AtomicInteger();
+
+    @Override
+    protected Procedure<TestProcEnv>[] execute(TestProcEnv env) {
+      executionCount.incrementAndGet();
+      return null;
+    }
+
+    @Override
+    protected void rollback(TestProcEnv env) {
+      rollbackCount.incrementAndGet();
+      rolledBack.release();
+    }
+
+    private boolean awaitRollback(long timeout, TimeUnit unit) throws 
InterruptedException {
+      return rolledBack.tryAcquire(timeout, unit);
+    }
+
+    private int getExecutionCount() {
+      return executionCount.get();
+    }
+
+    private int getRollbackCount() {
+      return rollbackCount.get();
+    }
+  }
 }

Reply via email to