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

jt2594838 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 3c0c7fef006 [Pipe] Fix processor worker starvation on pipe stop 
(#18396)
3c0c7fef006 is described below

commit 3c0c7fef0068162d7b3fc582df61a82050382ac8
Author: Caideyipi <[email protected]>
AuthorDate: Tue Aug 11 14:21:28 2026 +0800

    [Pipe] Fix processor worker starvation on pipe stop (#18396)
    
    * Fix processor worker starvation on pipe stop
    
    * Add multi-pipe processor worker test
    
    * Log long-running pipe processor event stacks
    
    * refactor(pipe): reuse processor exception root cause
---
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   2 +
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   2 +
 .../agent/task/connection/PipeEventCollector.java  |  17 +-
 .../subtask/processor/PipeProcessorSubtask.java    |  85 ++++++-
 .../PipeProcessorSubtaskExecutionGuard.java        | 110 +++++++++
 .../processor/PipeProcessorSubtaskWorker.java      | 129 ++++++++++-
 .../PipeProcessorSubtaskWorkerManager.java         |  15 +-
 .../PipeProcessorSubtaskYieldException.java        |  53 +++++
 .../common/tsfile/PipeTsFileInsertionEvent.java    | 103 ++++++++-
 .../PipeProcessorSubtaskExecutionGuardTest.java    | 247 +++++++++++++++++++++
 .../processor/PipeProcessorSubtaskWorkerTest.java  | 151 +++++++++++++
 .../pipe/agent/task/subtask/PipeSubtask.java       |  15 +-
 12 files changed, 905 insertions(+), 24 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index d7c54994a6d..7da8a596579 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2588,4 +2588,6 @@ public final class DataNodePipeMessages {
       "Topic config for %s is unavailable during consensus subscription setup";
   public static final String 
LOG_FAILED_TO_RELEASE_TSFILE_PARSER_MEMORY_FOR_PIPE_ARG_CREATION_TIME_ARG_IN_DATAREGION_ARG_BECAUSE_NO_RESERVATION_EXISTS_BB8321C0
 =
       "Failed to release TsFile parser memory for Pipe {} (creation time {}) 
in DataRegion {} because no reservation exists.";
+  public static final String 
LOG_PIPE_PROCESSOR_WORKER_ARG_HAS_BEEN_PROCESSING_THE_SAME_EVENT_FOR_ARG_MS_PIPE_ARG_DATAREGION_ARG_SUBTASK_ARG_EVENT_ARG_THREAD_STATE_ARG_STACK_ARG_63B40775
 =
+      "Pipe processor worker {} has been processing the same event for {} ms. 
Pipe: {}, DataRegion: {}, subtask: {}, event: {}, thread state: {}. Stack:{}";
 }
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index beed68e1cde..35c88b9abfe 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2416,4 +2416,6 @@ public final class DataNodePipeMessages {
       "共识订阅设置期间 topic %s 的配置不可用";
   public static final String 
LOG_FAILED_TO_RELEASE_TSFILE_PARSER_MEMORY_FOR_PIPE_ARG_CREATION_TIME_ARG_IN_DATAREGION_ARG_BECAUSE_NO_RESERVATION_EXISTS_BB8321C0
 =
       "无法释放 Pipe {}(创建时间 {})在 DataRegion {} 中的 TsFile 解析器内存,因为不存在对应的预留。";
+  public static final String 
LOG_PIPE_PROCESSOR_WORKER_ARG_HAS_BEEN_PROCESSING_THE_SAME_EVENT_FOR_ARG_MS_PIPE_ARG_DATAREGION_ARG_SUBTASK_ARG_EVENT_ARG_THREAD_STATE_ARG_STACK_ARG_63B40775
 =
+      "Pipe processor worker {} 已连续处理同一 event {} 
ms。Pipe:{},DataRegion:{},subtask:{},event:{},线程状态:{}。栈:{}";
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/connection/PipeEventCollector.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/connection/PipeEventCollector.java
index cc6b126384f..ac0f6c7bf0b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/connection/PipeEventCollector.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/connection/PipeEventCollector.java
@@ -28,6 +28,8 @@ import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
 import org.apache.iotdb.commons.pipe.event.ProgressReportEvent;
 import org.apache.iotdb.db.i18n.DataNodePipeMessages;
 import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent;
+import 
org.apache.iotdb.db.pipe.agent.task.subtask.processor.PipeProcessorSubtaskExecutionGuard;
+import 
org.apache.iotdb.db.pipe.agent.task.subtask.processor.PipeProcessorSubtaskYieldException;
 import org.apache.iotdb.db.pipe.event.common.deletion.PipeDeleteDataNodeEvent;
 import org.apache.iotdb.db.pipe.event.common.heartbeat.PipeHeartbeatEvent;
 import 
org.apache.iotdb.db.pipe.event.common.tablet.PipeInsertNodeTabletInsertionEvent;
@@ -63,6 +65,8 @@ public class PipeEventCollector implements EventCollector {
   private final boolean skipParsing;
 
   private final boolean isUsedForConsensusPipe;
+  private PipeProcessorSubtaskExecutionGuard processorExecutionGuard =
+      PipeProcessorSubtaskExecutionGuard.disabled();
 
   private final AtomicInteger collectInvocationCount = new AtomicInteger(0);
   private boolean hasNoGeneratedEvent = true;
@@ -83,6 +87,11 @@ public class PipeEventCollector implements EventCollector {
     this.isUsedForConsensusPipe = isUsedInConsensusPipe;
   }
 
+  public void setProcessorExecutionGuard(
+      final PipeProcessorSubtaskExecutionGuard processorExecutionGuard) {
+    this.processorExecutionGuard = processorExecutionGuard;
+  }
+
   @Override
   public void collect(final Event event) {
     try {
@@ -97,6 +106,8 @@ public class PipeEventCollector implements EventCollector {
       } else if (!(event instanceof ProgressReportEvent)) {
         collectEvent(event);
       }
+    } catch (final PipeProcessorSubtaskYieldException e) {
+      throw e;
     } catch (final PipeException e) {
       throw e;
     } catch (final Exception e) {
@@ -131,7 +142,7 @@ public class PipeEventCollector implements EventCollector {
   }
 
   private void parseAndCollectEvent(final PipeTsFileInsertionEvent 
sourceEvent) throws Exception {
-    if (!sourceEvent.waitForTsFileClose()) {
+    if (!sourceEvent.waitForTsFileClose(processorExecutionGuard)) {
       LOGGER.warn(
           DataNodePipeMessages.PIPE_SKIPPING_TEMPORARY_TSFILE_WHICH_SHOULDN_T,
           sourceEvent.getTsFile());
@@ -148,7 +159,9 @@ public class PipeEventCollector implements EventCollector {
     }
 
     sourceEvent.consumeTabletInsertionEventsWithRetry(
-        this::collectParsedRawTableEvent, 
"PipeEventCollector::parseAndCollectEvent");
+        this::collectParsedRawTableEvent,
+        "PipeEventCollector::parseAndCollectEvent",
+        processorExecutionGuard);
     sourceEvent.close();
     if (sourceEvent.isGeneratedByHistoricalExtractor()) {
       PipeTerminateEvent.markHistoricalTsFileSplit(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
index 8c49e03cb06..0a95465b50e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
@@ -57,6 +57,7 @@ import org.slf4j.LoggerFactory;
 
 import java.util.Objects;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class PipeProcessorSubtask extends PipeReportableSubtask {
@@ -74,6 +75,11 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
   private final EventSupplier inputEventSupplier;
   private final PipeProcessor pipeProcessor;
   private final PipeEventCollector outputEventCollector;
+  private final PipeProcessorSubtaskExecutionGuard executionGuard =
+      new PipeProcessorSubtaskExecutionGuard();
+  private final AtomicBoolean isResumingFromYield = new AtomicBoolean(false);
+  private final AtomicReference<EventProcessingContext> eventProcessingContext 
=
+      new AtomicReference<>();
 
   // This variable is used to distinguish between old and new subtasks before 
and after stuck
   // restart.
@@ -94,6 +100,7 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
     this.inputEventSupplier = inputEventSupplier;
     this.pipeProcessor = pipeProcessor;
     this.outputEventCollector = outputEventCollector;
+    this.outputEventCollector.setProcessorExecutionGuard(executionGuard);
     this.subtaskCreationTime = System.currentTimeMillis();
 
     // Only register dataRegions
@@ -106,7 +113,7 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
   @Override
   public void bindExecutors(
       final ListeningExecutorService subtaskWorkerThreadPoolExecutor,
-      final ListeningScheduledExecutorService ignoredScheduledExecutor,
+      final ListeningScheduledExecutorService subtaskWorkerScheduledExecutor,
       final ExecutorService ignored,
       final PipeSubtaskScheduler subtaskScheduler) {
     this.subtaskWorkerThreadPoolExecutor = subtaskWorkerThreadPoolExecutor;
@@ -117,19 +124,31 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
       synchronized (PipeProcessorSubtaskWorkerManager.class) {
         if (subtaskWorkerManager.get() == null) {
           subtaskWorkerManager.set(
-              new 
PipeProcessorSubtaskWorkerManager(subtaskWorkerThreadPoolExecutor));
+              new PipeProcessorSubtaskWorkerManager(
+                  subtaskWorkerThreadPoolExecutor, 
subtaskWorkerScheduledExecutor));
         }
       }
     }
     subtaskWorkerManager.get().schedule(this);
   }
 
+  @Override
+  public Boolean call() throws Exception {
+    executionGuard.enter();
+    try {
+      return super.call();
+    } finally {
+      executionGuard.exit();
+    }
+  }
+
   @Override
   protected boolean executeOnce() throws Exception {
     if (isClosed.get()) {
       return false;
     }
 
+    executionGuard.check();
     final Event event =
         lastEvent != null
             ? lastEvent
@@ -141,7 +160,13 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
       return false;
     }
 
-    outputEventCollector.resetFlags();
+    executionGuard.check();
+    if (!isResumingFromYield.getAndSet(false)) {
+      outputEventCollector.resetFlags();
+    }
+    final EventProcessingContext currentEventProcessingContext =
+        new EventProcessingContext(event, System.nanoTime());
+    eventProcessingContext.set(currentEventProcessingContext);
     try {
       if (event instanceof EnrichedEvent) {
         ((EnrichedEvent) event).throwIfNoPrivilege();
@@ -178,13 +203,16 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
                 event1 -> {
                   try {
                     pipeProcessor.process(event1, outputEventCollector);
+                  } catch (PipeProcessorSubtaskYieldException e) {
+                    throw e;
                   } catch (PipeRuntimeOutOfMemoryCriticalException e) {
                     throw e;
                   } catch (Exception e) {
                     throw new PipeException(e.getMessage(), e);
                   }
                 },
-                "PipeProcessorSubtask::executeOnce");
+                "PipeProcessorSubtask::executeOnce",
+                executionGuard);
             tsFileInsertionEvent.close();
             if (tsFileInsertionEvent.isGeneratedByHistoricalExtractor()) {
               PipeTerminateEvent.markHistoricalTsFileSplit(
@@ -242,6 +270,9 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
             .enrichWithCommitterKeyAndCommitId((EnrichedEvent) event, 
creationTime, regionId);
       }
       decreaseReferenceCountAndReleaseLastEvent(event, shouldReport);
+    } catch (final PipeProcessorSubtaskYieldException e) {
+      isResumingFromYield.set(true);
+      throw e;
     } catch (final PipeRuntimeOutOfMemoryCriticalException e) {
       recordResourceFailure(event, PipeResourceFailureType.MEMORY_TIMEOUT);
       PipeLogger.log(
@@ -250,7 +281,12 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
           e.getMessage());
       return false;
     } catch (final Exception e) {
-      if (ExceptionUtils.getRootCause(e) instanceof 
PipeRuntimeOutOfMemoryCriticalException) {
+      final Throwable rootCause = ExceptionUtils.getRootCause(e);
+      if (rootCause instanceof PipeProcessorSubtaskYieldException) {
+        isResumingFromYield.set(true);
+        throw (PipeProcessorSubtaskYieldException) rootCause;
+      }
+      if (rootCause instanceof PipeRuntimeOutOfMemoryCriticalException) {
         recordResourceFailure(event, PipeResourceFailureType.MEMORY_TIMEOUT);
         PipeLogger.log(
             LOGGER::info,
@@ -275,6 +311,8 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
             e.getMessage() != null ? " Message: " + e.getMessage() : "");
         clearReferenceCountAndReleaseLastEvent(event);
       }
+    } finally {
+      eventProcessingContext.compareAndSet(currentEventProcessingContext, 
null);
     }
 
     return true;
@@ -287,6 +325,20 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
     // and the worker will be submitted to the executor
   }
 
+  @Override
+  protected void onAllowSubmittingSelf() {
+    executionGuard.start();
+  }
+
+  @Override
+  protected void onDisallowSubmittingSelf() {
+    executionGuard.stop();
+    final Event event = lastEvent;
+    if (event instanceof PipeTsFileInsertionEvent) {
+      ((PipeTsFileInsertionEvent) 
event).cancelTsFileParserMemoryReservationIfPending();
+    }
+  }
+
   public boolean isStoppedByException() {
     return lastEvent instanceof EnrichedEvent && retryCount.get() > 
MAX_RETRY_TIMES;
   }
@@ -316,6 +368,29 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
     return isClosed.get();
   }
 
+  EventProcessingContext getEventProcessingContext() {
+    return eventProcessingContext.get();
+  }
+
+  static final class EventProcessingContext {
+
+    private final Event event;
+    private final long startTimeInNanos;
+
+    EventProcessingContext(final Event event, final long startTimeInNanos) {
+      this.event = event;
+      this.startTimeInNanos = startTimeInNanos;
+    }
+
+    Event getEvent() {
+      return event;
+    }
+
+    long getStartTimeInNanos() {
+      return startTimeInNanos;
+    }
+  }
+
   @Override
   public boolean equals(final Object obj) {
     if (this == obj) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuard.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuard.java
new file mode 100644
index 00000000000..a4eeab3482d
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuard.java
@@ -0,0 +1,110 @@
+/*
+ * 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.db.pipe.agent.task.subtask.processor;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Guards one processor subtask invocation against concurrent STOP/START 
operations.
+ *
+ * <p>An invocation captures the current execution epoch. STOP invalidates 
that epoch before START
+ * can enable a new one, so an invocation started before STOP must yield even 
if the pipe is started
+ * again immediately.
+ */
+public class PipeProcessorSubtaskExecutionGuard {
+
+  private static final PipeProcessorSubtaskExecutionGuard DISABLED_GUARD =
+      new PipeProcessorSubtaskExecutionGuard(false);
+
+  private final boolean enabled;
+  private final AtomicBoolean isRunning = new AtomicBoolean(false);
+  private final AtomicLong executionEpoch = new AtomicLong(0);
+  private final ThreadLocal<Long> invocationEpoch = new ThreadLocal<>();
+
+  public PipeProcessorSubtaskExecutionGuard() {
+    this(true);
+  }
+
+  private PipeProcessorSubtaskExecutionGuard(final boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  public static PipeProcessorSubtaskExecutionGuard disabled() {
+    return DISABLED_GUARD;
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  void start() {
+    if (enabled) {
+      isRunning.set(true);
+    }
+  }
+
+  void stop() {
+    if (enabled) {
+      isRunning.set(false);
+      executionEpoch.incrementAndGet();
+    }
+  }
+
+  void enter() {
+    if (!enabled) {
+      return;
+    }
+
+    final long currentEpoch = executionEpoch.get();
+    invocationEpoch.set(currentEpoch);
+    if (!isRunning.get() || currentEpoch != executionEpoch.get()) {
+      invocationEpoch.remove();
+      throw PipeProcessorSubtaskYieldException.pauseRequested();
+    }
+  }
+
+  void exit() {
+    if (enabled) {
+      invocationEpoch.remove();
+    }
+  }
+
+  public void check() {
+    if (!isCurrentInvocationValid()) {
+      throw PipeProcessorSubtaskYieldException.pauseRequested();
+    }
+  }
+
+  public boolean isCurrentInvocationValid() {
+    if (!enabled) {
+      return true;
+    }
+
+    final Long currentInvocationEpoch = invocationEpoch.get();
+    return currentInvocationEpoch != null
+        && isRunning.get()
+        && currentInvocationEpoch == executionEpoch.get();
+  }
+
+  public void yieldIfParserNotAdmitted() {
+    throw PipeProcessorSubtaskYieldException.parserNotAdmitted();
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorker.java
index 2bb4acc1232..bc967cbc2e9 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorker.java
@@ -20,26 +20,51 @@
 package org.apache.iotdb.db.pipe.agent.task.subtask.processor;
 
 import org.apache.iotdb.commons.concurrent.WrappedRunnable;
+import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
 import org.apache.iotdb.db.i18n.DataNodePipeMessages;
+import org.apache.iotdb.pipe.api.event.Event;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 
 public class PipeProcessorSubtaskWorker extends WrappedRunnable {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(PipeProcessorSubtaskWorker.class);
 
   private static final int SLEEP_INTERVAL_ADJUSTMENT_ROUND_INTERVAL = 100;
+  private static final long LONG_RUNNING_EVENT_INITIAL_REPORT_DELAY_IN_NANOS =
+      TimeUnit.MINUTES.toNanos(10);
+  private static final long LONG_RUNNING_EVENT_REPORT_INTERVAL_IN_NANOS =
+      TimeUnit.MINUTES.toNanos(30);
+  private static final int MAX_EVENT_REPORT_LENGTH = 1024;
+  private static final int MAX_STACK_TRACE_DEPTH = 64;
+
   private int totalRoundInAdjustmentInterval = 0;
   private int workingRoundInAdjustmentInterval = 0;
   private long sleepingTimeInMilliSecond = 50;
 
-  private final Set<PipeProcessorSubtask> subtasks =
-      Collections.newSetFromMap(new ConcurrentHashMap<>());
+  private final Set<PipeProcessorSubtask> subtasks;
+
+  private volatile Thread workerThread;
+  private volatile PipeProcessorSubtask currentSubtask;
+
+  private PipeProcessorSubtask.EventProcessingContext 
lastReportedEventProcessingContext;
+  private long lastEventReportTimeInNanos = Long.MIN_VALUE;
+
+  public PipeProcessorSubtaskWorker() {
+    this(Collections.newSetFromMap(new ConcurrentHashMap<>()));
+  }
+
+  @VisibleForTesting
+  PipeProcessorSubtaskWorker(final Set<PipeProcessorSubtask> subtasks) {
+    this.subtasks = subtasks;
+  }
 
   @Override
   @SuppressWarnings("squid:S2189")
@@ -56,7 +81,8 @@ public class PipeProcessorSubtaskWorker extends 
WrappedRunnable {
     subtasks.removeIf(PipeProcessorSubtask::isClosed);
   }
 
-  private boolean runSubtasks() {
+  @VisibleForTesting
+  boolean runSubtasks() {
     ++totalRoundInAdjustmentInterval;
 
     boolean canSleepBeforeNextRound = true;
@@ -66,18 +92,24 @@ public class PipeProcessorSubtaskWorker extends 
WrappedRunnable {
         continue;
       }
 
+      workerThread = Thread.currentThread();
+      currentSubtask = subtask;
       try {
         final boolean hasAtLeastOneEventProcessed = subtask.call();
         if (hasAtLeastOneEventProcessed) {
           canSleepBeforeNextRound = false;
         }
         subtask.onSuccess(hasAtLeastOneEventProcessed);
+      } catch (final PipeProcessorSubtaskYieldException ignored) {
+        // The subtask voluntarily yields this worker without succeeding, 
failing, or retrying.
       } catch (final Exception e) {
         if (subtask.isClosed()) {
           LOGGER.warn(DataNodePipeMessages.SUBTASK_IS_CLOSED_IGNORE_EXCEPTION, 
subtask, e);
         } else {
           subtask.onFailure(e);
         }
+      } finally {
+        currentSubtask = null;
       }
     }
 
@@ -118,4 +150,95 @@ public class PipeProcessorSubtaskWorker extends 
WrappedRunnable {
   public void schedule(final PipeProcessorSubtask pipeProcessorSubtask) {
     subtasks.add(pipeProcessorSubtask);
   }
+
+  void watchLongRunningEvent() {
+    final PipeProcessorSubtask subtask = currentSubtask;
+    final Thread thread = workerThread;
+    if (subtask == null || thread == null) {
+      return;
+    }
+
+    final PipeProcessorSubtask.EventProcessingContext context = 
subtask.getEventProcessingContext();
+    final long currentTimeInNanos = System.nanoTime();
+    if (!isLongRunningEventReportDue(context, currentTimeInNanos)) {
+      return;
+    }
+
+    final StackTraceElement[] stackTrace = thread.getStackTrace();
+    // The event may finish while its stack is being captured. Do not 
attribute a later event's
+    // stack to this event.
+    if (currentSubtask != subtask || subtask.getEventProcessingContext() != 
context) {
+      return;
+    }
+
+    markLongRunningEventReported(context, currentTimeInNanos);
+    LOGGER.warn(
+        DataNodePipeMessages
+            
.LOG_PIPE_PROCESSOR_WORKER_ARG_HAS_BEEN_PROCESSING_THE_SAME_EVENT_FOR_ARG_MS_PIPE_ARG_DATAREGION_ARG_SUBTASK_ARG_EVENT_ARG_THREAD_STATE_ARG_STACK_ARG_63B40775,
+        thread.getName(),
+        TimeUnit.NANOSECONDS.toMillis(currentTimeInNanos - 
context.getStartTimeInNanos()),
+        subtask.getPipeName(),
+        subtask.getRegionId(),
+        subtask.getDisplayTaskID(),
+        getEventReport(context.getEvent()),
+        thread.getState(),
+        formatStackTrace(stackTrace));
+  }
+
+  @VisibleForTesting
+  boolean isLongRunningEventReportDue(
+      final PipeProcessorSubtask.EventProcessingContext context, final long 
currentTimeInNanos) {
+    if (context == null
+        || currentTimeInNanos - context.getStartTimeInNanos()
+            < LONG_RUNNING_EVENT_INITIAL_REPORT_DELAY_IN_NANOS) {
+      return false;
+    }
+
+    return lastReportedEventProcessingContext != context
+        || currentTimeInNanos - lastEventReportTimeInNanos
+            >= LONG_RUNNING_EVENT_REPORT_INTERVAL_IN_NANOS;
+  }
+
+  @VisibleForTesting
+  void markLongRunningEventReported(
+      final PipeProcessorSubtask.EventProcessingContext context, final long 
currentTimeInNanos) {
+    lastReportedEventProcessingContext = context;
+    lastEventReportTimeInNanos = currentTimeInNanos;
+  }
+
+  @VisibleForTesting
+  static String getEventReport(final Event event) {
+    String report = event.getClass().getName();
+    if (event instanceof EnrichedEvent) {
+      try {
+        report =
+            event.getClass().getSimpleName() + ": " + ((EnrichedEvent) 
event).coreReportMessage();
+      } catch (final RuntimeException ignored) {
+        // Keep the event class name if its diagnostic method fails.
+      }
+    }
+
+    report = report.replace('\n', ' ').replace('\r', ' ');
+    return report.length() <= MAX_EVENT_REPORT_LENGTH
+        ? report
+        : report.substring(0, MAX_EVENT_REPORT_LENGTH) + "...";
+  }
+
+  @VisibleForTesting
+  static String formatStackTrace(final StackTraceElement[] stackTrace) {
+    final StringBuilder builder = new StringBuilder();
+    final int frameCount = Math.min(stackTrace.length, MAX_STACK_TRACE_DEPTH);
+    for (int i = 0; i < frameCount; ++i) {
+      builder.append('\n').append('\t').append(stackTrace[i]);
+    }
+    if (stackTrace.length > frameCount) {
+      builder
+          .append('\n')
+          .append('\t')
+          .append("... (")
+          .append(stackTrace.length - frameCount)
+          .append(')');
+    }
+    return builder.toString();
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerManager.java
index 33d58c4b5d4..ac2dd2cd7b5 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerManager.java
@@ -19,10 +19,13 @@
 
 package org.apache.iotdb.db.pipe.agent.task.subtask.processor;
 
+import org.apache.iotdb.commons.concurrent.threadpool.ScheduledExecutorUtil;
 import org.apache.iotdb.commons.pipe.config.PipeConfig;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
 
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
 public class PipeProcessorSubtaskWorkerManager {
@@ -34,7 +37,9 @@ public class PipeProcessorSubtaskWorkerManager {
 
   private final AtomicLong scheduledTaskNumber;
 
-  public PipeProcessorSubtaskWorkerManager(ListeningExecutorService 
workerThreadPoolExecutor) {
+  public PipeProcessorSubtaskWorkerManager(
+      final ListeningExecutorService workerThreadPoolExecutor,
+      final ListeningScheduledExecutorService watcherScheduledExecutor) {
     workers = new PipeProcessorSubtaskWorker[MAX_THREAD_NUM];
     for (int i = 0; i < MAX_THREAD_NUM; i++) {
       workers[i] = new PipeProcessorSubtaskWorker();
@@ -42,10 +47,18 @@ public class PipeProcessorSubtaskWorkerManager {
     }
 
     scheduledTaskNumber = new AtomicLong(0);
+    ScheduledExecutorUtil.safelyScheduleWithFixedDelay(
+        watcherScheduledExecutor, this::watchLongRunningEvents, 1, 1, 
TimeUnit.MINUTES);
   }
 
   public void schedule(PipeProcessorSubtask pipeProcessorSubtask) {
     workers[(int) (scheduledTaskNumber.getAndIncrement() % 
MAX_THREAD_NUM)].schedule(
         pipeProcessorSubtask);
   }
+
+  private void watchLongRunningEvents() {
+    for (final PipeProcessorSubtaskWorker worker : workers) {
+      worker.watchLongRunningEvent();
+    }
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskYieldException.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskYieldException.java
new file mode 100644
index 00000000000..3fe5242fa07
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskYieldException.java
@@ -0,0 +1,53 @@
+/*
+ * 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.db.pipe.agent.task.subtask.processor;
+
+/** Internal control-flow exception that immediately yields the current 
processor worker. */
+public final class PipeProcessorSubtaskYieldException extends RuntimeException 
{
+
+  private static final PipeProcessorSubtaskYieldException 
PAUSE_REQUESTED_INSTANCE =
+      new PipeProcessorSubtaskYieldException(Reason.PAUSE_REQUESTED);
+  private static final PipeProcessorSubtaskYieldException 
PARSER_NOT_ADMITTED_INSTANCE =
+      new PipeProcessorSubtaskYieldException(Reason.PARSER_NOT_ADMITTED);
+
+  private final Reason reason;
+
+  private PipeProcessorSubtaskYieldException(final Reason reason) {
+    super(null, null, false, false);
+    this.reason = reason;
+  }
+
+  public static PipeProcessorSubtaskYieldException pauseRequested() {
+    return PAUSE_REQUESTED_INSTANCE;
+  }
+
+  public static PipeProcessorSubtaskYieldException parserNotAdmitted() {
+    return PARSER_NOT_ADMITTED_INSTANCE;
+  }
+
+  public Reason getReason() {
+    return reason;
+  }
+
+  public enum Reason {
+    PAUSE_REQUESTED,
+    PARSER_NOT_ADMITTED
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
index ba2a9d275c3..b969ac4e618 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java
@@ -38,6 +38,8 @@ import 
org.apache.iotdb.commons.pipe.resource.ref.PipePhantomReferenceManager.Pi
 import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.QualifiedObjectName;
 import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.i18n.DataNodePipeMessages;
+import 
org.apache.iotdb.db.pipe.agent.task.subtask.processor.PipeProcessorSubtaskExecutionGuard;
+import 
org.apache.iotdb.db.pipe.agent.task.subtask.processor.PipeProcessorSubtaskYieldException;
 import org.apache.iotdb.db.pipe.event.ReferenceTrackableEvent;
 import org.apache.iotdb.db.pipe.event.common.PipeInsertionEvent;
 import 
org.apache.iotdb.db.pipe.event.common.tablet.PipeRawTabletInsertionEvent;
@@ -314,6 +316,13 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
    *     otherwise.
    */
   public boolean waitForTsFileClose() throws InterruptedException {
+    return waitForTsFileClose(PipeProcessorSubtaskExecutionGuard.disabled());
+  }
+
+  public boolean waitForTsFileClose(
+      final PipeProcessorSubtaskExecutionGuard processorExecutionGuard)
+      throws InterruptedException {
+    processorExecutionGuard.check();
     if (Objects.isNull(resource)) {
       return true;
     }
@@ -327,7 +336,9 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
 
       synchronized (isClosed) {
         while (!isClosed.get()) {
+          processorExecutionGuard.check();
           isClosed.wait(100);
+          processorExecutionGuard.check();
 
           final boolean isClosedNow = resource.isClosed();
           if (isClosedNow) {
@@ -770,19 +781,41 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
 
   public void consumeTabletInsertionEventsWithRetry(
       final TabletInsertionEventConsumer consumer, final String callerName) 
throws Exception {
+    consumeTabletInsertionEventsWithRetry(
+        consumer, callerName, PipeProcessorSubtaskExecutionGuard.disabled());
+  }
+
+  public void consumeTabletInsertionEventsWithRetry(
+      final TabletInsertionEventConsumer consumer,
+      final String callerName,
+      final PipeProcessorSubtaskExecutionGuard processorExecutionGuard)
+      throws Exception {
     try {
       while (true) {
+        processorExecutionGuard.check();
         final PipeRawTabletInsertionEvent parsedEvent =
-            getNextTabletInsertionEventFromSavedProgress();
+            
getNextTabletInsertionEventFromSavedProgress(processorExecutionGuard);
         if (parsedEvent == null) {
           isTsFileParsingCompleted.set(true);
           releaseTsFileParserMemoryIfReserved();
           return;
         }
+        processorExecutionGuard.check();
         consumeParsedTabletInsertionEventWithRetry(
-            consumer, callerName, parsedTabletInsertionEventCount.get(), 
parsedEvent);
+            consumer,
+            callerName,
+            parsedTabletInsertionEventCount.get(),
+            parsedEvent,
+            processorExecutionGuard);
         pendingTabletInsertionEvent.compareAndSet(parsedEvent, null);
+        processorExecutionGuard.check();
       }
+    } catch (final PipeProcessorSubtaskYieldException e) {
+      releaseTsFileParserMemoryIfReserved();
+      if (!processorExecutionGuard.isCurrentInvocationValid()) {
+        cancelTsFileParserMemoryReservationIfPending();
+      }
+      throw e;
     } catch (final PipeRuntimeOutOfMemoryCriticalException e) {
       // Yield the active parser slot to the next pipe while retaining the 
iterator and current
       // tablet. The next retry resumes from this exact tablet instead of 
reparsing the TsFile.
@@ -800,16 +833,15 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
     }
   }
 
-  private PipeRawTabletInsertionEvent 
getNextTabletInsertionEventFromSavedProgress()
-      throws Exception {
+  private PipeRawTabletInsertionEvent 
getNextTabletInsertionEventFromSavedProgress(
+      final PipeProcessorSubtaskExecutionGuard processorExecutionGuard) throws 
Exception {
     if (isTsFileParsingCompleted.get()) {
       return null;
     }
 
-    // Reacquire parser memory after a previous failure yielded the active 
parser slot. This wait
-    // is already bounded to 20-40 seconds, while the exponential backoff 
below is only for retrying
-    // the current tablet without yielding its parser slot.
-    waitForResourceEnough4Parsing((long) ((1 + Math.random()) * 20 * 1000));
+    // Reacquire parser memory after a previous failure yielded the active 
parser slot. Processor
+    // subtasks use non-blocking admission here, while other callers retain 
the bounded wait.
+    reserveResource4Parsing(processorExecutionGuard);
 
     final PipeRawTabletInsertionEvent pendingEvent = 
pendingTabletInsertionEvent.get();
     if (pendingEvent != null) {
@@ -818,7 +850,7 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
 
     Iterator<TabletInsertionEvent> iterator = 
tabletInsertionEventIterator.get();
     if (iterator == null) {
-      if (!waitForTsFileClose()) {
+      if (!waitForTsFileClose(processorExecutionGuard)) {
         
LOGGER.warn(DataNodePipeMessages.PIPE_SKIPPING_TEMPORARY_TSFILE_S_PARSING_WHICH,
 tsFile);
         return null;
       }
@@ -840,12 +872,14 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
       final TabletInsertionEventConsumer consumer,
       final String callerName,
       final int tabletEventCount,
-      final TabletInsertionEvent parsedEvent)
+      final TabletInsertionEvent parsedEvent,
+      final PipeProcessorSubtaskExecutionGuard processorExecutionGuard)
       throws Exception {
     final PipeMemoryManager memoryManager = 
PipeDataNodeResourceManager.memory();
     long firstOutOfMemoryTimeInMs = Long.MIN_VALUE;
     int retryCount = 0;
     while (true) {
+      processorExecutionGuard.check();
       try {
         consumer.consume((PipeRawTabletInsertionEvent) parsedEvent);
         return;
@@ -859,7 +893,7 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
         }
         logParserRetryOnOutOfMemory(callerName, tabletEventCount, retryCount, 
e);
         try {
-          Thread.sleep(getParserRetryBackoffInMs(retryCount));
+          sleepForParserRetry(getParserRetryBackoffInMs(retryCount), 
processorExecutionGuard);
         } catch (final InterruptedException interruptedException) {
           Thread.currentThread().interrupt();
           throw e;
@@ -868,6 +902,24 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
     }
   }
 
+  private void sleepForParserRetry(
+      final long sleepTimeInMs, final PipeProcessorSubtaskExecutionGuard 
processorExecutionGuard)
+      throws InterruptedException {
+    if (!processorExecutionGuard.isEnabled()) {
+      Thread.sleep(sleepTimeInMs);
+      return;
+    }
+
+    final long deadlineInMs = System.currentTimeMillis() + sleepTimeInMs;
+    long remainingTimeInMs = sleepTimeInMs;
+    while (remainingTimeInMs > 0) {
+      processorExecutionGuard.check();
+      Thread.sleep(Math.min(remainingTimeInMs, 100));
+      processorExecutionGuard.check();
+      remainingTimeInMs = deadlineInMs - System.currentTimeMillis();
+    }
+  }
+
   private long getParserRetryBackoffInMs(final int retryCount) {
     final long initialBackoffInMs =
         Math.max(1, 
PipeConfig.getInstance().getPipeMemoryAllocateRetryIntervalInMs());
@@ -950,6 +1002,33 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
     }
   }
 
+  private void reserveResource4Parsing(
+      final PipeProcessorSubtaskExecutionGuard processorExecutionGuard)
+      throws InterruptedException {
+    if (!processorExecutionGuard.isEnabled()) {
+      waitForResourceEnough4Parsing((long) ((1 + Math.random()) * 20 * 1000));
+      return;
+    }
+
+    processorExecutionGuard.check();
+    final PipeMemoryManager memoryManager = 
PipeDataNodeResourceManager.memory();
+    if (tryReserveTsFileParserMemory(memoryManager)) {
+      try {
+        processorExecutionGuard.check();
+        return;
+      } catch (final PipeProcessorSubtaskYieldException e) {
+        releaseTsFileParserMemoryIfReserved();
+        throw e;
+      }
+    }
+
+    if (!processorExecutionGuard.isCurrentInvocationValid()) {
+      cancelTsFileParserMemoryReservationIfPending();
+      processorExecutionGuard.check();
+    }
+    processorExecutionGuard.yieldIfParserNotAdmitted();
+  }
+
   private void waitForResourceEnough4Parsing(final long timeoutMs) throws 
InterruptedException {
     final PipeMemoryManager memoryManager = 
PipeDataNodeResourceManager.memory();
     if (tryReserveTsFileParserMemory(memoryManager)) {
@@ -1026,7 +1105,7 @@ public class PipeTsFileInsertionEvent extends 
PipeInsertionEvent
     }
   }
 
-  private void cancelTsFileParserMemoryReservationIfPending() {
+  public void cancelTsFileParserMemoryReservationIfPending() {
     if (!isTsFileParserMemoryReserved.get()) {
       PipeDataNodeResourceManager.memory()
           .cancelTsFileParserMemoryReservation(
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuardTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuardTest.java
new file mode 100644
index 00000000000..3438dc62c5d
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuardTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.db.pipe.agent.task.subtask.processor;
+
+import org.apache.iotdb.commons.conf.CommonConfig;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.PrefixTreePattern;
+import org.apache.iotdb.commons.utils.FileUtils;
+import org.apache.iotdb.db.pipe.event.common.tsfile.PipeTsFileInsertionEvent;
+import org.apache.iotdb.db.pipe.resource.PipeDataNodeResourceManager;
+import org.apache.iotdb.db.pipe.resource.memory.PipeMemoryManager;
+import 
org.apache.iotdb.db.pipe.resource.memory.PipeMemoryManager.TsFileParserMemoryReservation;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import 
org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResourceStatus;
+
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.utils.TsFileGeneratorUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class PipeProcessorSubtaskExecutionGuardTest {
+
+  @Test
+  public void testStopAndImmediateRestartInvalidateCurrentInvocation() {
+    final PipeProcessorSubtaskExecutionGuard executionGuard =
+        new PipeProcessorSubtaskExecutionGuard();
+
+    executionGuard.start();
+    executionGuard.enter();
+    executionGuard.check();
+
+    executionGuard.stop();
+    executionGuard.start();
+    Assert.assertThrows(PipeProcessorSubtaskYieldException.class, 
executionGuard::check);
+
+    executionGuard.exit();
+    executionGuard.enter();
+    executionGuard.check();
+    executionGuard.exit();
+  }
+
+  @Test(timeout = 60000)
+  public void testParserAdmissionYieldsWithoutBlockingAndResumes() throws 
Exception {
+    final CommonConfig commonConfig = 
CommonDescriptor.getInstance().getConfig();
+    final PipeMemoryManager memoryManager = 
PipeDataNodeResourceManager.memory();
+    final long originalParserMemoryInBytes = 
commonConfig.getPipeTsFileParserMemory();
+    final int originalGlobalLimit = 
commonConfig.getPipeTsFileParserInFlightMaxNum();
+    final int originalPerPipeRegionLimit =
+        commonConfig.getPipeTsFileParserInFlightMaxNumPerPipeRegion();
+    final TsFileParserMemoryReservation blockerReservation = new 
TsFileParserMemoryReservation();
+    final TsFileParserMemoryReservation competitorReservation = new 
TsFileParserMemoryReservation();
+
+    final File tempDir = 
Files.createTempDirectory("pipeProcessorAdmissionYield").toFile();
+    PipeTsFileInsertionEvent event = null;
+    boolean isBlockerReserved = false;
+    boolean isCompetitorReserved = false;
+    try {
+      commonConfig.setPipeTsFileParserMemory(1);
+      commonConfig.setPipeTsFileParserInFlightMaxNum(1);
+      commonConfig.setPipeTsFileParserInFlightMaxNumPerPipeRegion(1);
+      isBlockerReserved =
+          memoryManager.tryReserveTsFileParserMemory("blocker", 0, "0", 
blockerReservation);
+      Assert.assertTrue(isBlockerReserved);
+
+      event = createEvent(tempDir, "admission.tsfile", "admissionPipe");
+      final PipeTsFileInsertionEvent eventToConsume = event;
+      final PipeProcessorSubtaskExecutionGuard executionGuard =
+          new PipeProcessorSubtaskExecutionGuard();
+      executionGuard.start();
+      executionGuard.enter();
+
+      final long startTimeInNanos = System.nanoTime();
+      final PipeProcessorSubtaskYieldException admissionYield =
+          Assert.assertThrows(
+              PipeProcessorSubtaskYieldException.class,
+              () ->
+                  eventToConsume.consumeTabletInsertionEventsWithRetry(
+                      parsedEvent -> 
parsedEvent.clearReferenceCount(getClass().getName()),
+                      "test",
+                      executionGuard));
+      Assert.assertEquals(
+          PipeProcessorSubtaskYieldException.Reason.PARSER_NOT_ADMITTED,
+          admissionYield.getReason());
+      Assert.assertTrue(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 
startTimeInNanos) < 1000);
+      executionGuard.exit();
+
+      executionGuard.stop();
+      event.cancelTsFileParserMemoryReservationIfPending();
+      memoryManager.releaseTsFileParserMemory("blocker", 0, "0");
+      isBlockerReserved = false;
+      isCompetitorReserved =
+          memoryManager.tryReserveTsFileParserMemory("competitor", 0, "0", 
competitorReservation);
+      Assert.assertTrue(isCompetitorReserved);
+      memoryManager.releaseTsFileParserMemory("competitor", 0, "0");
+      isCompetitorReserved = false;
+
+      final AtomicInteger consumedTabletCount = new AtomicInteger(0);
+      executionGuard.start();
+      executionGuard.enter();
+      event.consumeTabletInsertionEventsWithRetry(
+          parsedEvent -> {
+            consumedTabletCount.incrementAndGet();
+            parsedEvent.clearReferenceCount(getClass().getName());
+          },
+          "test",
+          executionGuard);
+      executionGuard.exit();
+      Assert.assertTrue(consumedTabletCount.get() > 0);
+    } finally {
+      if (event != null) {
+        event.close();
+      }
+      memoryManager.cancelTsFileParserMemoryReservation("blocker", 0, "0", 
blockerReservation);
+      memoryManager.cancelTsFileParserMemoryReservation(
+          "competitor", 0, "0", competitorReservation);
+      if (isBlockerReserved) {
+        memoryManager.releaseTsFileParserMemory("blocker", 0, "0");
+      }
+      if (isCompetitorReserved) {
+        memoryManager.releaseTsFileParserMemory("competitor", 0, "0");
+      }
+      commonConfig.setPipeTsFileParserMemory(originalParserMemoryInBytes);
+      commonConfig.setPipeTsFileParserInFlightMaxNum(originalGlobalLimit);
+      
commonConfig.setPipeTsFileParserInFlightMaxNumPerPipeRegion(originalPerPipeRegionLimit);
+      FileUtils.deleteFileOrDirectory(tempDir);
+    }
+  }
+
+  @Test(timeout = 60000)
+  public void testPauseAfterTabletResumesWithoutDuplicateConsumption() throws 
Exception {
+    final CommonConfig commonConfig = 
CommonDescriptor.getInstance().getConfig();
+    final long originalParserMemoryInBytes = 
commonConfig.getPipeTsFileParserMemory();
+    final int originalGlobalLimit = 
commonConfig.getPipeTsFileParserInFlightMaxNum();
+    final int originalPerPipeRegionLimit =
+        commonConfig.getPipeTsFileParserInFlightMaxNumPerPipeRegion();
+    final File tempDir = 
Files.createTempDirectory("pipeProcessorPauseResume").toFile();
+    final PipeTsFileInsertionEvent event = createEvent(tempDir, 
"resume.tsfile", "resumePipe");
+    final PipeProcessorSubtaskExecutionGuard executionGuard =
+        new PipeProcessorSubtaskExecutionGuard();
+    final AtomicInteger consumedTabletCount = new AtomicInteger(0);
+    final AtomicReference<Object> firstTablet = new AtomicReference<>();
+
+    try {
+      commonConfig.setPipeTsFileParserMemory(1);
+      commonConfig.setPipeTsFileParserInFlightMaxNum(1);
+      commonConfig.setPipeTsFileParserInFlightMaxNumPerPipeRegion(1);
+      executionGuard.start();
+      executionGuard.enter();
+      final PipeProcessorSubtaskYieldException pauseYield =
+          Assert.assertThrows(
+              PipeProcessorSubtaskYieldException.class,
+              () ->
+                  event.consumeTabletInsertionEventsWithRetry(
+                      parsedEvent -> {
+                        firstTablet.set(parsedEvent);
+                        consumedTabletCount.incrementAndGet();
+                        parsedEvent.clearReferenceCount(getClass().getName());
+                        executionGuard.stop();
+                      },
+                      "test",
+                      executionGuard));
+      Assert.assertEquals(
+          PipeProcessorSubtaskYieldException.Reason.PAUSE_REQUESTED, 
pauseYield.getReason());
+      executionGuard.exit();
+
+      executionGuard.start();
+      executionGuard.enter();
+      try {
+        event.consumeTabletInsertionEventsWithRetry(
+            parsedEvent -> {
+              Assert.assertNotSame(firstTablet.get(), parsedEvent);
+              consumedTabletCount.incrementAndGet();
+              parsedEvent.clearReferenceCount(getClass().getName());
+            },
+            "test",
+            executionGuard);
+      } catch (final PipeProcessorSubtaskYieldException e) {
+        Assert.fail("Unexpected yield reason: " + e.getReason());
+      }
+      executionGuard.exit();
+
+      Assert.assertTrue(consumedTabletCount.get() > 0);
+    } finally {
+      event.close();
+      commonConfig.setPipeTsFileParserMemory(originalParserMemoryInBytes);
+      commonConfig.setPipeTsFileParserInFlightMaxNum(originalGlobalLimit);
+      
commonConfig.setPipeTsFileParserInFlightMaxNumPerPipeRegion(originalPerPipeRegionLimit);
+      FileUtils.deleteFileOrDirectory(tempDir);
+    }
+  }
+
+  private PipeTsFileInsertionEvent createEvent(
+      final File tempDir, final String fileName, final String pipeName) throws 
Exception {
+    final File tsFile =
+        TsFileGeneratorUtils.generateNonAlignedTsFile(
+            new File(tempDir, fileName).getAbsolutePath(), 1, 1, 10, 0, 100, 
10, 10);
+    final TsFileResource resource = new TsFileResource(tsFile);
+    resource.setStatusForTest(TsFileResourceStatus.NORMAL);
+    final IDeviceID deviceID = 
IDeviceID.Factory.DEFAULT_FACTORY.create("root.testsg.d0");
+    resource.updateStartTime(deviceID, 0);
+    resource.updateEndTime(deviceID, 9);
+
+    return new PipeTsFileInsertionEvent(
+        false,
+        "root",
+        resource,
+        null,
+        false,
+        false,
+        false,
+        null,
+        pipeName,
+        0,
+        null,
+        new PrefixTreePattern("root"),
+        null,
+        null,
+        null,
+        null,
+        true,
+        Long.MIN_VALUE,
+        Long.MAX_VALUE);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerTest.java
new file mode 100644
index 00000000000..51a018d48d8
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.db.pipe.agent.task.subtask.processor;
+
+import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import java.util.LinkedHashSet;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PipeProcessorSubtaskWorkerTest {
+
+  @Test
+  public void testYieldingPipesDoNotBlockAnotherPipeOnSameWorker() throws 
Exception {
+    final PipeProcessorSubtaskWorker worker = new 
PipeProcessorSubtaskWorker(new LinkedHashSet<>());
+    final PipeProcessorSubtask stoppedPipe = 
createRunnableSubtask("stoppedPipe");
+    final PipeProcessorSubtask parserWaitingPipe = 
createRunnableSubtask("parserWaitingPipe");
+    final PipeProcessorSubtask runningPipe = 
createRunnableSubtask("runningPipe");
+
+    
when(stoppedPipe.call()).thenThrow(PipeProcessorSubtaskYieldException.pauseRequested());
+    when(parserWaitingPipe.call())
+        .thenThrow(PipeProcessorSubtaskYieldException.parserNotAdmitted());
+    when(runningPipe.call()).thenReturn(true);
+
+    worker.schedule(stoppedPipe);
+    worker.schedule(parserWaitingPipe);
+    worker.schedule(runningPipe);
+
+    Assert.assertFalse(worker.runSubtasks());
+
+    final InOrder inOrder = inOrder(stoppedPipe, parserWaitingPipe, 
runningPipe);
+    inOrder.verify(stoppedPipe).call();
+    inOrder.verify(parserWaitingPipe).call();
+    inOrder.verify(runningPipe).call();
+    verify(runningPipe).onSuccess(true);
+    verify(stoppedPipe, never()).onSuccess(any());
+    verify(stoppedPipe, never()).onFailure(any());
+    verify(parserWaitingPipe, never()).onSuccess(any());
+    verify(parserWaitingPipe, never()).onFailure(any());
+  }
+
+  @Test
+  public void testLongRunningEventReportIsRateLimited() {
+    final PipeProcessorSubtaskWorker worker = new 
PipeProcessorSubtaskWorker(new LinkedHashSet<>());
+    final long startTimeInNanos = 100;
+    final PipeProcessorSubtask.EventProcessingContext context =
+        new PipeProcessorSubtask.EventProcessingContext(
+            mock(EnrichedEvent.class), startTimeInNanos);
+    final long initialReportDelayInNanos = TimeUnit.MINUTES.toNanos(10);
+    final long reportIntervalInNanos = TimeUnit.MINUTES.toNanos(30);
+
+    Assert.assertFalse(
+        worker.isLongRunningEventReportDue(
+            context, startTimeInNanos + initialReportDelayInNanos - 1));
+    Assert.assertTrue(
+        worker.isLongRunningEventReportDue(context, startTimeInNanos + 
initialReportDelayInNanos));
+
+    final long firstReportTimeInNanos = startTimeInNanos + 
initialReportDelayInNanos;
+    worker.markLongRunningEventReported(context, firstReportTimeInNanos);
+    Assert.assertFalse(
+        worker.isLongRunningEventReportDue(
+            context, firstReportTimeInNanos + reportIntervalInNanos - 1));
+    Assert.assertTrue(
+        worker.isLongRunningEventReportDue(
+            context, firstReportTimeInNanos + reportIntervalInNanos));
+
+    final long nextEventStartTimeInNanos = firstReportTimeInNanos + 1;
+    final PipeProcessorSubtask.EventProcessingContext nextContext =
+        new PipeProcessorSubtask.EventProcessingContext(
+            mock(EnrichedEvent.class), nextEventStartTimeInNanos);
+    Assert.assertFalse(
+        worker.isLongRunningEventReportDue(
+            nextContext, nextEventStartTimeInNanos + initialReportDelayInNanos 
- 1));
+    Assert.assertTrue(
+        worker.isLongRunningEventReportDue(
+            nextContext, nextEventStartTimeInNanos + 
initialReportDelayInNanos));
+  }
+
+  @Test
+  public void testLongRunningEventLogPayloadIsBounded() {
+    final EnrichedEvent event = mock(EnrichedEvent.class);
+    when(event.coreReportMessage()).thenReturn("x".repeat(2048) + "\nmore");
+
+    final String eventReport = 
PipeProcessorSubtaskWorker.getEventReport(event);
+    Assert.assertEquals(1027, eventReport.length());
+    Assert.assertFalse(eventReport.contains("\n"));
+    Assert.assertTrue(eventReport.endsWith("..."));
+
+    final StackTraceElement[] stackTrace = new StackTraceElement[100];
+    for (int i = 0; i < stackTrace.length; ++i) {
+      stackTrace[i] = new StackTraceElement("Class", "method" + i, 
"File.java", i);
+    }
+    final String formattedStackTrace = 
PipeProcessorSubtaskWorker.formatStackTrace(stackTrace);
+    Assert.assertTrue(formattedStackTrace.contains("method63"));
+    Assert.assertFalse(formattedStackTrace.contains("method64"));
+    Assert.assertTrue(formattedStackTrace.contains("... (36)"));
+  }
+
+  @Test
+  @SuppressWarnings("unsafeThreadSchedule")
+  public void testWorkerManagerSchedulesWatcher() {
+    final ListeningExecutorService workerThreadPoolExecutor = 
mock(ListeningExecutorService.class);
+    final ListeningScheduledExecutorService watcherScheduledExecutor =
+        mock(ListeningScheduledExecutorService.class);
+
+    new PipeProcessorSubtaskWorkerManager(workerThreadPoolExecutor, 
watcherScheduledExecutor);
+
+    verify(workerThreadPoolExecutor, 
atLeastOnce()).submit(any(Runnable.class));
+    verify(watcherScheduledExecutor)
+        .scheduleWithFixedDelay(any(Runnable.class), eq(1L), eq(1L), 
eq(TimeUnit.MINUTES));
+  }
+
+  private PipeProcessorSubtask createRunnableSubtask(final String mockName) {
+    final PipeProcessorSubtask subtask = mock(PipeProcessorSubtask.class, 
mockName);
+    when(subtask.isClosed()).thenReturn(false);
+    when(subtask.isSubmittingSelf()).thenReturn(true);
+    when(subtask.isStoppedByException()).thenReturn(false);
+    return subtask;
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeSubtask.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeSubtask.java
index d87fe3a5f38..108f202e002 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeSubtask.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeSubtask.java
@@ -140,9 +140,14 @@ public abstract class PipeSubtask
 
   public void allowSubmittingSelf() {
     retryCount.set(0);
+    onAllowSubmittingSelf();
     shouldStopSubmittingSelf.set(false);
   }
 
+  protected void onAllowSubmittingSelf() {
+    // Do nothing by default.
+  }
+
   /**
    * Set the {@link PipeSubtask#shouldStopSubmittingSelf} state from {@code 
false} to {@code true},
    * in order to stop submitting the {@link PipeSubtask}.
@@ -151,7 +156,15 @@ public abstract class PipeSubtask
    *     {@code false} to {@code true}, {@code false} otherwise
    */
   public boolean disallowSubmittingSelf() {
-    return !shouldStopSubmittingSelf.getAndSet(true);
+    final boolean isChanged = !shouldStopSubmittingSelf.getAndSet(true);
+    if (isChanged) {
+      onDisallowSubmittingSelf();
+    }
+    return isChanged;
+  }
+
+  protected void onDisallowSubmittingSelf() {
+    // Do nothing by default.
   }
 
   public boolean isSubmittingSelf() {

Reply via email to