This is an automated email from the ASF dual-hosted git repository.
Caideyipi pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/dev/1.3 by this push:
new 22bc40ea6f3 [To dev/1.3] [Pipe] Fix processor worker starvation on
pipe stop (#18396) (#18443)
22bc40ea6f3 is described below
commit 22bc40ea6f3d77b43c020725649571e6d2a16dc3
Author: Caideyipi <[email protected]>
AuthorDate: Wed Aug 19 10:35:31 2026 +0800
[To dev/1.3] [Pipe] Fix processor worker starvation on pipe stop (#18396)
(#18443)
* [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
* Fix mixed line endings in dev/1.3 integration tests
---
LICENSE-binary | 2 +-
.../agent/task/connection/PipeEventCollector.java | 18 +-
.../subtask/processor/PipeProcessorSubtask.java | 81 ++++++-
.../PipeProcessorSubtaskExecutionGuard.java | 110 ++++++++++
.../processor/PipeProcessorSubtaskWorker.java | 128 ++++++++++-
.../PipeProcessorSubtaskWorkerManager.java | 15 +-
.../PipeProcessorSubtaskYieldException.java | 53 +++++
.../common/tsfile/PipeTsFileInsertionEvent.java | 103 +++++++--
.../task/PipeProcessorSubtaskExecutorTest.java | 6 +-
.../PipeProcessorSubtaskExecutionGuardTest.java | 240 +++++++++++++++++++++
.../processor/PipeProcessorSubtaskWorkerTest.java | 152 +++++++++++++
.../agent/task/execution/PipeSubtaskExecutor.java | 12 +-
.../task/subtask/PipeAbstractSinkSubtask.java | 2 +
.../pipe/agent/task/subtask/PipeSubtask.java | 17 +-
.../thrift-commons/src/main/thrift/common.thrift | 2 +-
.../src/main/thrift/confignode.thrift | 2 +-
.../src/main/thrift/datanode.thrift | 4 +-
17 files changed, 918 insertions(+), 29 deletions(-)
diff --git a/LICENSE-binary b/LICENSE-binary
index 3f412a0fc6b..6bcddb85554 100644
--- a/LICENSE-binary
+++ b/LICENSE-binary
@@ -236,7 +236,7 @@ org.eclipse.jetty:jetty-servlet:9.4.58.v20250814
org.eclipse.jetty:jetty-util:9.4.58.v20250814
com.google.code.findbugs:jsr305:3.0.2
com.librato.metrics:librato-java:2.1.0
-org.apache.thrift:libthrift:0.23.0
+org.apache.thrift:libthrift:0.23.0
io.dropwizard.metrics:metrics-core:4.2.19
io.dropwizard.metrics:metrics-jvm:3.2.2
com.librato.metrics:metrics-librato:5.1.0
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 df72ccb830d..d6710c83bd7 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
@@ -25,6 +25,8 @@ import
org.apache.iotdb.commons.pipe.datastructure.pattern.IoTDBPipePatternOpera
import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
import org.apache.iotdb.commons.pipe.event.ProgressReportEvent;
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.heartbeat.PipeHeartbeatEvent;
import
org.apache.iotdb.db.pipe.event.common.schema.PipeSchemaRegionWritePlanEvent;
import
org.apache.iotdb.db.pipe.event.common.tablet.PipeInsertNodeTabletInsertionEvent;
@@ -56,6 +58,9 @@ public class PipeEventCollector implements EventCollector {
private final boolean skipParsing;
+ private PipeProcessorSubtaskExecutionGuard processorExecutionGuard =
+ PipeProcessorSubtaskExecutionGuard.disabled();
+
private final AtomicInteger collectInvocationCount = new AtomicInteger(0);
private boolean hasNoGeneratedEvent = true;
private boolean isFailedToIncreaseReferenceCount = false;
@@ -73,6 +78,11 @@ public class PipeEventCollector implements EventCollector {
this.skipParsing = skipParsing;
}
+ public void setProcessorExecutionGuard(
+ final PipeProcessorSubtaskExecutionGuard processorExecutionGuard) {
+ this.processorExecutionGuard = processorExecutionGuard;
+ }
+
@Override
public void collect(final Event event) {
try {
@@ -91,6 +101,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) {
@@ -123,7 +135,7 @@ public class PipeEventCollector implements EventCollector {
}
private void parseAndCollectEvent(final PipeTsFileInsertionEvent
sourceEvent) throws Exception {
- if (!sourceEvent.waitForTsFileClose()) {
+ if (!sourceEvent.waitForTsFileClose(processorExecutionGuard)) {
LOGGER.warn(
"Pipe skipping temporary TsFile which shouldn't be transferred: {}",
sourceEvent.getTsFile());
@@ -140,7 +152,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 3c4b3d55019..84485bc0c04 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
@@ -34,6 +34,7 @@ import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent;
import org.apache.iotdb.db.pipe.agent.task.connection.PipeEventCollector;
import org.apache.iotdb.db.pipe.event.UserDefinedEnrichedEvent;
import org.apache.iotdb.db.pipe.event.common.heartbeat.PipeHeartbeatEvent;
+import org.apache.iotdb.db.pipe.event.common.tsfile.PipeTsFileInsertionEvent;
import org.apache.iotdb.db.pipe.metric.overview.PipeDataNodeSinglePipeMetrics;
import org.apache.iotdb.db.pipe.metric.processor.PipeProcessorMetrics;
import org.apache.iotdb.db.pipe.processor.pipeconsensus.PipeConsensusProcessor;
@@ -45,12 +46,14 @@ import
org.apache.iotdb.pipe.api.event.dml.insertion.TsFileInsertionEvent;
import org.apache.iotdb.pipe.api.exception.PipeException;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
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 {
@@ -68,6 +71,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.
@@ -88,6 +96,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
@@ -99,6 +108,7 @@ public class PipeProcessorSubtask extends
PipeReportableSubtask {
@Override
public void bindExecutors(
final ListeningExecutorService subtaskWorkerThreadPoolExecutor,
+ final ListeningScheduledExecutorService subtaskWorkerScheduledExecutor,
final ExecutorService ignored,
final PipeSubtaskScheduler subtaskScheduler) {
this.subtaskWorkerThreadPoolExecutor = subtaskWorkerThreadPoolExecutor;
@@ -109,19 +119,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
@@ -133,7 +155,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 {
// event can be supplied after the subtask is closed, so we need to
check isClosed here
if (!isClosed.get()) {
@@ -189,6 +217,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(
@@ -197,7 +228,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,
@@ -221,6 +257,8 @@ public class PipeProcessorSubtask extends
PipeReportableSubtask {
e.getMessage() != null ? " Message: " + e.getMessage() : "");
clearReferenceCountAndReleaseLastEvent(event);
}
+ } finally {
+ eventProcessingContext.compareAndSet(currentEventProcessingContext,
null);
}
return true;
@@ -233,6 +271,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;
}
@@ -262,6 +314,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 b9584d2c586..ad1548d9432 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,25 +20,50 @@
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.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")
@@ -55,7 +80,8 @@ public class PipeProcessorSubtaskWorker extends
WrappedRunnable {
subtasks.removeIf(PipeProcessorSubtask::isClosed);
}
- private boolean runSubtasks() {
+ @VisibleForTesting
+ boolean runSubtasks() {
++totalRoundInAdjustmentInterval;
boolean canSleepBeforeNextRound = true;
@@ -65,18 +91,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("subtask {} is closed, ignore exception", subtask, e);
} else {
subtask.onFailure(e);
}
+ } finally {
+ currentSubtask = null;
}
}
@@ -117,4 +149,94 @@ 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(
+ "Pipe processor worker {} has been processing the same event for {}
ms. Pipe: {}, DataRegion: {}, subtask: {}, event: {}, thread state: {}.
Stack:{}",
+ thread.getName(),
+ TimeUnit.NANOSECONDS.toMillis(currentTimeInNanos -
context.getStartTimeInNanos()),
+ subtask.getPipeName(),
+ subtask.getRegionId(),
+ subtask.getTaskID(),
+ 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 19c845d5015..ae3708fde58 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
@@ -28,6 +28,8 @@ import
org.apache.iotdb.commons.pipe.datastructure.pattern.PipePattern;
import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
import org.apache.iotdb.commons.pipe.resource.log.PipeLogger;
import
org.apache.iotdb.commons.pipe.resource.ref.PipePhantomReferenceManager.PipeEventResource;
+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.tablet.PipeRawTabletInsertionEvent;
import
org.apache.iotdb.db.pipe.event.common.tsfile.container.TsFileInsertionDataContainer;
@@ -247,6 +249,13 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
* 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;
}
@@ -260,7 +269,9 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
synchronized (isClosed) {
while (!isClosed.get()) {
+ processorExecutionGuard.check();
isClosed.wait(100);
+ processorExecutionGuard.check();
final boolean isClosedNow = resource.isClosed();
if (isClosedNow) {
@@ -527,19 +538,41 @@ public class PipeTsFileInsertionEvent extends
EnrichedEvent
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.
@@ -557,16 +590,15 @@ public class PipeTsFileInsertionEvent extends
EnrichedEvent
}
}
- 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) {
@@ -575,7 +607,7 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
Iterator<TabletInsertionEvent> iterator =
tabletInsertionEventIterator.get();
if (iterator == null) {
- if (!waitForTsFileClose()) {
+ if (!waitForTsFileClose(processorExecutionGuard)) {
LOGGER.warn(
"Pipe skipping temporary TsFile's parsing which shouldn't be
transferred: {}", tsFile);
return null;
@@ -598,12 +630,14 @@ public class PipeTsFileInsertionEvent extends
EnrichedEvent
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;
@@ -617,7 +651,7 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
}
logParserRetryOnOutOfMemory(callerName, tabletEventCount, retryCount,
e);
try {
- Thread.sleep(getParserRetryBackoffInMs(retryCount));
+ sleepForParserRetry(getParserRetryBackoffInMs(retryCount),
processorExecutionGuard);
} catch (final InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw e;
@@ -626,6 +660,24 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
}
}
+ 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());
@@ -706,6 +758,33 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
}
}
+ 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)) {
@@ -780,7 +859,7 @@ public class PipeTsFileInsertionEvent extends EnrichedEvent
}
}
- 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/PipeProcessorSubtaskExecutorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/PipeProcessorSubtaskExecutorTest.java
index 267da1356c8..bd6e00191e5 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/PipeProcessorSubtaskExecutorTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/PipeProcessorSubtaskExecutorTest.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.db.pipe.agent.task;
import
org.apache.iotdb.commons.exception.pipe.PipeRuntimeOutOfMemoryCriticalException;
import org.apache.iotdb.commons.pipe.agent.task.connection.EventSupplier;
+import org.apache.iotdb.commons.pipe.agent.task.execution.PipeSubtaskScheduler;
import org.apache.iotdb.db.pipe.agent.task.connection.PipeEventCollector;
import
org.apache.iotdb.db.pipe.agent.task.execution.PipeProcessorSubtaskExecutor;
import
org.apache.iotdb.db.pipe.agent.task.subtask.processor.PipeProcessorSubtask;
@@ -143,7 +144,10 @@ public class PipeProcessorSubtaskExecutorTest extends
PipeSubtaskExecutorTest {
}
private boolean executeOnceForTest() throws Exception {
- return executeOnce();
+ subtaskScheduler = mock(PipeSubtaskScheduler.class);
+ when(subtaskScheduler.schedule()).thenReturn(true, false);
+ allowSubmittingSelf();
+ return call();
}
}
}
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..b7c5fdc5542
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskExecutionGuardTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.PrefixPipePattern;
+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.file.metadata.PlainDeviceID;
+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 = new PlainDeviceID("root.testsg.d0");
+ resource.updateStartTime(deviceID, 0);
+ resource.updateEndTime(deviceID, 9);
+
+ return new PipeTsFileInsertionEvent(
+ resource,
+ null,
+ false,
+ false,
+ false,
+ pipeName,
+ 0,
+ null,
+ new PrefixPipePattern("root"),
+ 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..d1c34ccf471
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtaskWorkerTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.apache.commons.lang3.StringUtils;
+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(StringUtils.repeat('x', 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/execution/PipeSubtaskExecutor.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/execution/PipeSubtaskExecutor.java
index f16c6387cf5..61b821bba7f 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/execution/PipeSubtaskExecutor.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/execution/PipeSubtaskExecutor.java
@@ -26,6 +26,7 @@ import
org.apache.iotdb.commons.pipe.agent.task.subtask.PipeSubtask;
import org.apache.iotdb.commons.utils.TestOnly;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,6 +37,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
public abstract class PipeSubtaskExecutor {
@@ -50,6 +52,7 @@ public abstract class PipeSubtaskExecutor {
protected final WrappedThreadPoolExecutor underlyingThreadPool;
protected final ListeningExecutorService subtaskWorkerThreadPoolExecutor;
+ protected final ListeningScheduledExecutorService
subtaskWorkerScheduledExecutor;
private final Map<String, PipeSubtask> registeredIdSubtaskMapper;
@@ -82,6 +85,9 @@ public abstract class PipeSubtaskExecutor {
underlyingThreadPool.disableErrorLog();
}
subtaskWorkerThreadPoolExecutor =
MoreExecutors.listeningDecorator(underlyingThreadPool);
+ final ScheduledExecutorService underlyingScheduledExecutor =
+
IoTDBThreadPoolFactory.newSingleThreadScheduledExecutor(workingThreadName +
"-Scheduler");
+ subtaskWorkerScheduledExecutor =
MoreExecutors.listeningDecorator(underlyingScheduledExecutor);
subtaskCallbackListeningExecutor =
Objects.nonNull(callbackThreadName)
? IoTDBThreadPoolFactory.newSingleThreadExecutor(
@@ -104,7 +110,10 @@ public abstract class PipeSubtaskExecutor {
registeredIdSubtaskMapper.put(subtask.getTaskID(), subtask);
subtask.bindExecutors(
- subtaskWorkerThreadPoolExecutor, subtaskCallbackListeningExecutor,
schedulerSupplier(this));
+ subtaskWorkerThreadPoolExecutor,
+ subtaskWorkerScheduledExecutor,
+ subtaskCallbackListeningExecutor,
+ schedulerSupplier(this));
}
protected PipeSubtaskScheduler schedulerSupplier(final PipeSubtaskExecutor
executor) {
@@ -179,6 +188,7 @@ public abstract class PipeSubtaskExecutor {
}
subtaskWorkerThreadPoolExecutor.shutdown();
+ subtaskWorkerScheduledExecutor.shutdown();
if (subtaskCallbackListeningExecutor !=
globalSubtaskCallbackListeningExecutor) {
subtaskCallbackListeningExecutor.shutdown();
}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeAbstractSinkSubtask.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeAbstractSinkSubtask.java
index df43914494e..2b4f807caa4 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeAbstractSinkSubtask.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/subtask/PipeAbstractSinkSubtask.java
@@ -36,6 +36,7 @@ import org.apache.iotdb.pipe.api.exception.PipeException;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -71,6 +72,7 @@ public abstract class PipeAbstractSinkSubtask extends
PipeReportableSubtask {
@Override
public void bindExecutors(
final ListeningExecutorService subtaskWorkerThreadPoolExecutor,
+ final ListeningScheduledExecutorService ignoredScheduledExecutor,
final ExecutorService subtaskCallbackListeningExecutor,
final PipeSubtaskScheduler subtaskScheduler) {
this.subtaskWorkerThreadPoolExecutor = subtaskWorkerThreadPoolExecutor;
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 1b58d1d6178..b583276fa90 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
@@ -25,6 +25,7 @@ import org.apache.iotdb.pipe.api.event.Event;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,6 +66,7 @@ public abstract class PipeSubtask
public abstract void bindExecutors(
ListeningExecutorService subtaskWorkerThreadPoolExecutor,
+ ListeningScheduledExecutorService subtaskWorkerScheduledExecutor,
ExecutorService subtaskCallbackListeningExecutor,
PipeSubtaskScheduler subtaskScheduler);
@@ -128,9 +130,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}.
@@ -139,7 +146,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() {
diff --git a/iotdb-protocol/thrift-commons/src/main/thrift/common.thrift
b/iotdb-protocol/thrift-commons/src/main/thrift/common.thrift
index 5dd66be212c..b25188a4bcf 100644
--- a/iotdb-protocol/thrift-commons/src/main/thrift/common.thrift
+++ b/iotdb-protocol/thrift-commons/src/main/thrift/common.thrift
@@ -201,7 +201,7 @@ struct TPipeHeartbeatResp {
2: optional list<bool> pipeCompletedList
3: optional list<i64> pipeRemainingEventCountList
4: optional list<double> pipeRemainingTimeList
- 6: optional list<map<string, i64>> pipeRecentFailureList
+ 6: optional list<map<string, i64>> pipeRecentFailureList
}
struct TLicense {
diff --git a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift
b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift
index b04274945da..86b5c857992 100644
--- a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift
+++ b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift
@@ -729,7 +729,7 @@ struct TShowPipeInfo {
7: required string exceptionMessage
8: optional i64 remainingEventCount
9: optional double EstimatedRemainingTime
- 11: optional map<string, i64> recentFailures
+ 11: optional map<string, i64> recentFailures
}
struct TGetAllPipeInfoResp {
diff --git a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
index 0d1f60d61a8..c569b24717e 100644
--- a/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
+++ b/iotdb-protocol/thrift-datanode/src/main/thrift/datanode.thrift
@@ -307,7 +307,7 @@ struct TDataNodeHeartbeatResp {
14: optional list<bool> pipeCompletedList
15: optional list<i64> pipeRemainingEventCountList
16: optional list<double> pipeRemainingTimeList
- 19: optional list<map<string, i64>> pipeRecentFailureList
+ 19: optional list<map<string, i64>> pipeRecentFailureList
}
struct TPipeHeartbeatReq {
@@ -1077,4 +1077,4 @@ service MPPDataExchangeService {
/** Empty rpc, only for connection test */
common.TSStatus testConnectionEmptyRPC()
-}
+}