From 6a20e60a7d57cc1d5e741d21f20961ca8475ad7c Mon Sep 17 00:00:00 2001
From: Caideyipi <87789683+Caideyipi@users.noreply.github.com>
Date: Tue, 4 Aug 2026 19:35:54 +0800
Subject: [PATCH 1/4] Fix processor worker starvation on pipe stop
---
.../task/connection/PipeEventCollector.java | 17 +-
.../processor/PipeProcessorSubtask.java | 47 +++-
.../PipeProcessorSubtaskExecutionGuard.java | 110 ++++++++
.../processor/PipeProcessorSubtaskWorker.java | 2 +
.../PipeProcessorSubtaskYieldException.java | 53 ++++
.../tsfile/PipeTsFileInsertionEvent.java | 103 +++++++-
...ipeProcessorSubtaskExecutionGuardTest.java | 247 ++++++++++++++++++
.../pipe/agent/task/subtask/PipeSubtask.java | 15 +-
8 files changed, 577 insertions(+), 17 deletions(-)
create mode 100644 iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuard.java
create mode 100644 iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskYieldException.java
create mode 100644 iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuardTest.java
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 cc6b126384f63..ac0f6c7bf0b74 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.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 PipeEventCollector(
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 void collect(final Event event) {
} 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 @@ private void parseAndCollectEvent(final PipeRawTabletInsertionEvent sourceEvent)
}
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 @@ private void parseAndCollectEvent(final PipeTsFileInsertionEvent sourceEvent) th
}
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 a95c99ebd52e9..33465df453a24 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
@@ -56,6 +56,7 @@
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 {
@@ -73,6 +74,9 @@ 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);
// This variable is used to distinguish between old and new subtasks before and after stuck
// restart.
@@ -93,6 +97,7 @@ public PipeProcessorSubtask(
this.inputEventSupplier = inputEventSupplier;
this.pipeProcessor = pipeProcessor;
this.outputEventCollector = outputEventCollector;
+ this.outputEventCollector.setProcessorExecutionGuard(executionGuard);
this.subtaskCreationTime = System.currentTimeMillis();
// Only register dataRegions
@@ -123,12 +128,23 @@ public void bindExecutors(
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
@@ -140,7 +156,10 @@ protected boolean executeOnce() throws Exception {
return false;
}
- outputEventCollector.resetFlags();
+ executionGuard.check();
+ if (!isResumingFromYield.getAndSet(false)) {
+ outputEventCollector.resetFlags();
+ }
try {
if (event instanceof EnrichedEvent) {
((EnrichedEvent) event).throwIfNoPrivilege();
@@ -177,13 +196,16 @@ protected boolean executeOnce() throws Exception {
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(
@@ -241,6 +263,9 @@ protected boolean executeOnce() throws Exception {
.enrichWithCommitterKeyAndCommitId((EnrichedEvent) event, creationTime, regionId);
}
decreaseReferenceCountAndReleaseLastEvent(event, shouldReport);
+ } catch (final PipeProcessorSubtaskYieldException e) {
+ isResumingFromYield.set(true);
+ throw e;
} catch (final PipeRuntimeOutOfMemoryCriticalException e) {
PipeLogger.log(
LOGGER::info,
@@ -248,6 +273,10 @@ protected boolean executeOnce() throws Exception {
e.getMessage());
return false;
} catch (final Exception e) {
+ if (ExceptionUtils.getRootCause(e) instanceof PipeProcessorSubtaskYieldException) {
+ isResumingFromYield.set(true);
+ throw (PipeProcessorSubtaskYieldException) ExceptionUtils.getRootCause(e);
+ }
if (ExceptionUtils.getRootCause(e) instanceof PipeRuntimeOutOfMemoryCriticalException) {
PipeLogger.log(
LOGGER::info,
@@ -284,6 +313,20 @@ public void submitSelf() {
// 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;
}
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 0000000000000..a4eeab3482d0d
--- /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.
+ *
+ *
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 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 2bb4acc123245..e011ca5b12b41 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
@@ -72,6 +72,8 @@ private boolean runSubtasks() {
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);
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 0000000000000..3fe5242fa078c
--- /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 ba2a9d275c39d..b969ac4e61896 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.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 @@ private static String getDataRegionId(final TsFileResource resource) {
* 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 boolean waitForTsFileClose() throws InterruptedException {
synchronized (isClosed) {
while (!isClosed.get()) {
+ processorExecutionGuard.check();
isClosed.wait(100);
+ processorExecutionGuard.check();
final boolean isClosedNow = resource.isClosed();
if (isClosedNow) {
@@ -770,19 +781,41 @@ public interface TabletInsertionEventConsumer {
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 void consumeTabletInsertionEventsWithRetry(
}
}
- 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 @@ private PipeRawTabletInsertionEvent getNextTabletInsertionEventFromSavedProgress
Iterator 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 @@ private void consumeParsedTabletInsertionEventWithRetry(
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 @@ private void consumeParsedTabletInsertionEventWithRetry(
}
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 @@ private void consumeParsedTabletInsertionEventWithRetry(
}
}
+ 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 Iterable toTabletInsertionEvents(final long timeout
}
}
+ 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 @@ private void releaseTsFileParserMemoryIfReserved() {
}
}
- 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 0000000000000..3438dc62c5dc1
--- /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