This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new ccbc71d60cb Fix misleading shutdown interruption warnings (#18939)
ccbc71d60cb is described below
commit ccbc71d60cb42f049601cd1eebe72ff0702b8f0b
Author: Jackie Tien <[email protected]>
AuthorDate: Wed Oct 21 10:01:29 2026 +0800
Fix misleading shutdown interruption warnings (#18939)
---
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 +
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 +
.../queryengine/execution/QueryStateMachine.java | 49 +-
.../execution/schedule/AbstractDriverThread.java | 16 +-
.../schedule/DriverTaskAbortedException.java | 2 +
.../plan/execution/ExecutionResult.java | 8 +
.../queryengine/plan/execution/QueryExecution.java | 25 +-
.../plan/scheduler/ClusterScheduler.java | 3 +-
.../plan/scheduler/FragInstanceDispatchResult.java | 12 +
.../scheduler/FragmentInstanceDispatcherImpl.java | 6 +-
.../metrics/IoTDBInternalLocalReporter.java | 156 ++++--
.../execution/QueryStateMachineTest.java | 35 ++
.../schedule/AbstractDriverThreadShutdownTest.java | 271 +++++++++
.../plan/relational/analyzer/TreeViewTest.java | 12 +-
.../metrics/IoTDBInternalLocalReporterTest.java | 623 +++++++++++++++++++++
15 files changed, 1164 insertions(+), 58 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index db0fa44dbc5..7dd677cab3f 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -1263,6 +1263,7 @@ public final class DataNodeQueryMessages {
"Interrupted when dispatching read async";
public static final String INTERRUPTED_WHEN_DISPATCHING_WRITE_ASYNC =
"Interrupted when dispatching write async";
+ public static final String MESSAGE_INTERRUPTED_ERRORS_ARG_876F1B4B =
"Interrupted errors: %s";
public static final String DESERIALIZE_CONSENSUSGROUPID_FAILED =
"Deserialize ConsensusGroupId failed. ";
public static final String CAN_T_CONNECT_TO_NODE =
@@ -3476,6 +3477,7 @@ public final class DataNodeQueryMessages {
"already being cancelled";
public static final String DRIVER_TASK_ABORTED_BY_INTERNAL_ERROR_SCHEDULED =
"internal error scheduled";
+ public static final String EXCEPTION_SCHEDULER_SHUTDOWN_323938F5 =
"scheduler shutdown";
public static final String DRIVER_TASK_ABORTED_BY_MEMORY_NOT_ENOUGH =
"Memory is not enough to execute the query task.";
public static final String ROOT_FRAGMENT_INSTANCE_PLACEMENT_ERROR_FMT =
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index f500838c1ca..203c7552ae9 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -1243,6 +1243,7 @@ public final class DataNodeQueryMessages {
"异步分发读取操作时被中断";
public static final String INTERRUPTED_WHEN_DISPATCHING_WRITE_ASYNC =
"异步分发写入操作时被中断";
+ public static final String MESSAGE_INTERRUPTED_ERRORS_ARG_876F1B4B =
"中断错误:%s";
public static final String DESERIALIZE_CONSENSUSGROUPID_FAILED =
"反序列化 ConsensusGroupId 失败。";
public static final String CAN_T_CONNECT_TO_NODE =
@@ -4127,6 +4128,7 @@ public final class DataNodeQueryMessages {
"已经在取消中";
public static final String DRIVER_TASK_ABORTED_BY_INTERNAL_ERROR_SCHEDULED =
"内部错误调度";
+ public static final String EXCEPTION_SCHEDULER_SHUTDOWN_323938F5 = "调度器关闭";
public static final String DRIVER_TASK_ABORTED_BY_MEMORY_NOT_ENOUGH =
"内存不足,无法执行查询任务。";
public static final String ROOT_FRAGMENT_INSTANCE_PLACEMENT_ERROR_FMT =
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
index 1bc3e2b0f32..2683fec32cf 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
@@ -54,7 +54,7 @@ public class QueryStateMachine {
private final StateMachine<QueryState> queryState;
private final AtomicReference<Throwable> failureException = new
AtomicReference<>();
- private final AtomicReference<TSStatus> failureStatus = new
AtomicReference<>();
+ private final AtomicReference<FailureStatus> failureStatus = new
AtomicReference<>();
public QueryStateMachine(QueryId queryId, ExecutorService executor) {
this.queryState =
@@ -90,7 +90,7 @@ public class QueryStateMachine {
}
public void transitionToPendingRetry(TSStatus failureStatus) {
- this.failureStatus.compareAndSet(null, failureStatus);
+ recordFailureStatus(failureStatus, false);
queryState.setIf(PENDING_RETRY, currentState -> currentState ==
DISPATCHING);
}
@@ -110,7 +110,7 @@ public class QueryStateMachine {
}
public boolean transitionToCanceled(Throwable throwable, TSStatus
failureStatus) {
- this.failureStatus.compareAndSet(null, failureStatus);
+ recordFailureStatus(failureStatus, false);
this.failureException.compareAndSet(null, throwable);
return transitionToDoneState(CANCELED);
}
@@ -129,10 +129,34 @@ public class QueryStateMachine {
}
public void transitionToFailed(TSStatus failureStatus) {
- this.failureStatus.compareAndSet(null, failureStatus);
+ transitionToFailed(failureStatus, false);
+ }
+
+ public void transitionToFailed(TSStatus failureStatus, boolean
failureCausedByInterruption) {
+ recordFailureStatus(failureStatus, failureCausedByInterruption);
transitionToDoneState(FAILED);
}
+ private void recordFailureStatus(TSStatus status, boolean
failureCausedByInterruption) {
+ if (status != null) {
+ // Publish the status and its origin together. A later interruption must
not reclassify
+ // the first recorded failure.
+ failureStatus.compareAndSet(null, new FailureStatus(status,
failureCausedByInterruption));
+ }
+ }
+
+ /** Only the exact recorded status carries the interruption provenance
stored with it. */
+ public boolean isFailureCausedByInterruption(TSStatus status) {
+ FailureStatus failure = failureStatus.get();
+ if (failure == null || failure.status != status ||
!failure.failureCausedByInterruption) {
+ return false;
+ }
+ // Statuses and exceptions can be recorded independently. Keep an
unrelated exception
+ // visible even if a subsequent dispatch interruption supplied the status.
+ Throwable throwable = failureException.get();
+ return throwable == null || getRootCause(throwable) instanceof
InterruptedException;
+ }
+
private boolean transitionToDoneState(QueryState doneState) {
requireNonNull(doneState,
DataNodeQueryMessages.EXCEPTION_DONESTATE_IS_NULL_D88F77E5);
checkArgument(
@@ -161,9 +185,9 @@ public class QueryStateMachine {
}
public TSStatus getFailureStatus() {
- TSStatus status = failureStatus.get();
+ FailureStatus status = failureStatus.get();
if (status != null) {
- return status;
+ return status.status;
} else {
Throwable throwable = failureException.get();
if (throwable != null) {
@@ -174,7 +198,18 @@ public class QueryStateMachine {
return RpcUtils.getStatus(((IoTDBException) t).getErrorCode(),
t.getMessage());
}
}
- return failureStatus.get();
+ status = failureStatus.get();
+ return status == null ? null : status.status;
+ }
+ }
+
+ private static class FailureStatus {
+ private final TSStatus status;
+ private final boolean failureCausedByInterruption;
+
+ private FailureStatus(TSStatus status, boolean
failureCausedByInterruption) {
+ this.status = status;
+ this.failureCausedByInterruption = failureCausedByInterruption;
}
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java
index ac34cccb437..ee5eb8623bf 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java
@@ -67,8 +67,11 @@ public abstract class AbstractDriverThread extends Thread
implements Closeable {
try {
next = queue.poll();
} catch (InterruptedException e) {
- logger.warn(
-
DataNodeQueryMessages.EXECUTOR_FAILED_TO_POLL_DRIVER_TASK_FROM_QUEUE,
this.getName());
+ if (!closed) {
+ logger.warn(
+
DataNodeQueryMessages.EXECUTOR_FAILED_TO_POLL_DRIVER_TASK_FROM_QUEUE,
+ this.getName());
+ }
Thread.currentThread().interrupt();
break;
}
@@ -86,7 +89,14 @@ public abstract class AbstractDriverThread extends Thread
implements Closeable {
try (SetThreadName driverTaskName =
new
SetThreadName(next.getDriver().getDriverTaskId().getFullId())) {
Throwable rootCause = ErrorHandlingCommonUtils.getRootCause(e);
- if (rootCause instanceof CorruptedTsFileException) {
+ if (closed && rootCause instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ // Still abort the task below so that shutdown releases its
query resources.
+ next.setAbortCause(
+ new DriverTaskAbortedException(
+ next.getDriverTaskId().getFullId(),
+ DriverTaskAbortedException.BY_SCHEDULER_SHUTDOWN));
+ } else if (rootCause instanceof CorruptedTsFileException) {
// CorruptedTsFileException no longer chains the original
IOException as its
// cause (it uses addSuppressed instead), so getRootCause
returns the exception
// itself and we can match it here.
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/DriverTaskAbortedException.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/DriverTaskAbortedException.java
index fb683db9962..0f800376d33 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/DriverTaskAbortedException.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/DriverTaskAbortedException.java
@@ -34,6 +34,8 @@ public class DriverTaskAbortedException extends Exception {
DataNodeQueryMessages.DRIVER_TASK_ABORTED_BY_ALREADY_BEING_CANCELLED;
public static final String BY_INTERNAL_ERROR_SCHEDULED =
DataNodeQueryMessages.DRIVER_TASK_ABORTED_BY_INTERNAL_ERROR_SCHEDULED;
+ public static final String BY_SCHEDULER_SHUTDOWN =
+ DataNodeQueryMessages.EXCEPTION_SCHEDULER_SHUTDOWN_323938F5;
public static final String BY_MEMORY_NOT_ENOUGH =
DataNodeQueryMessages.DRIVER_TASK_ABORTED_BY_MEMORY_NOT_ENOUGH;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
index c00df6d8c73..2f46460635a 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
@@ -26,8 +26,16 @@ public class ExecutionResult {
public final QueryId queryId;
public final TSStatus status;
+ /** Whether this failure originated from an interrupted wait rather than
another query error. */
+ public final boolean failureCausedByInterruption;
+
public ExecutionResult(QueryId queryId, TSStatus status) {
+ this(queryId, status, false);
+ }
+
+ ExecutionResult(QueryId queryId, TSStatus status, boolean
failureCausedByInterruption) {
this.queryId = queryId;
this.status = status;
+ this.failureCausedByInterruption = failureCausedByInterruption;
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
index 5a094814607..b8f753d70a0 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
@@ -644,19 +644,29 @@ public class QueryExecution implements IQueryExecution {
} catch (InterruptedException e) {
// TODO: (xingtanzjr) use more accurate error handling
Thread.currentThread().interrupt();
+ // Dispatch may already have converted an interruption to a failure
status. Preserve its
+ // recorded origin without reclassifying an unrelated failure.
+ TSStatus failureStatus = stateMachine.getFailureStatus();
+ if (failureStatus != null) {
+ return new ExecutionResult(
+ context.getQueryId(),
+ failureStatus,
+ stateMachine.isFailureCausedByInterruption(failureStatus));
+ }
return new ExecutionResult(
context.getQueryId(),
- stateMachine.getFailureStatus() == null
- ? RpcUtils.getStatus(
- TSStatusCode.INTERNAL_SERVER_ERROR,
stateMachine.getFailureMessage())
- : stateMachine.getFailureStatus());
+ RpcUtils.getStatus(TSStatusCode.INTERNAL_SERVER_ERROR,
stateMachine.getFailureMessage()),
+ // A terminal query may have failed with an exception that has no
dedicated status code.
+ !stateMachine.getState().isDone());
} catch (ExecutionException e) {
+ TSStatus failureStatus = stateMachine.getFailureStatus();
return new ExecutionResult(
context.getQueryId(),
- stateMachine.getFailureStatus() == null
+ failureStatus == null
? RpcUtils.getStatus(
TSStatusCode.INTERNAL_SERVER_ERROR,
stateMachine.getFailureMessage())
- : stateMachine.getFailureStatus());
+ : failureStatus,
+ stateMachine.isFailureCausedByInterruption(failureStatus));
}
}
@@ -727,7 +737,8 @@ public class QueryExecution implements IQueryExecution {
||
IoTDBConstant.ClientVersion.V_1_0.equals(context.getSession().getVersion())) {
planner.setRedirectInfo(analysis, CONFIG.getAddressAndPort(), tsstatus);
}
- return new ExecutionResult(context.getQueryId(), tsstatus);
+ return new ExecutionResult(
+ context.getQueryId(), tsstatus,
stateMachine.isFailureCausedByInterruption(tsstatus));
}
public DistributedQueryPlan getDistributedPlan() {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
index 5e0aa02f5c0..4c828c3808c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
@@ -125,7 +125,8 @@ public class ClusterScheduler implements IScheduler {
if (needRetry(result.getFailureStatus())) {
stateMachine.transitionToPendingRetry(result.getFailureStatus());
} else {
- stateMachine.transitionToFailed(result.getFailureStatus());
+ stateMachine.transitionToFailed(
+ result.getFailureStatus(),
result.isFailureCausedByInterruption());
}
return;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
index 6a621139455..dd9d39694e8 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
@@ -26,13 +26,21 @@ public class FragInstanceDispatchResult {
private TSStatus failureStatus;
+ private final boolean failureCausedByInterruption;
+
public FragInstanceDispatchResult(boolean successful) {
this.successful = successful;
+ this.failureCausedByInterruption = false;
}
public FragInstanceDispatchResult(TSStatus failureStatus) {
+ this(failureStatus, false);
+ }
+
+ public FragInstanceDispatchResult(TSStatus failureStatus, boolean
failureCausedByInterruption) {
this.successful = false;
this.failureStatus = failureStatus;
+ this.failureCausedByInterruption = failureCausedByInterruption;
}
public boolean isSuccessful() {
@@ -42,4 +50,8 @@ public class FragInstanceDispatchResult {
public TSStatus getFailureStatus() {
return failureStatus;
}
+
+ public boolean isFailureCausedByInterruption() {
+ return failureCausedByInterruption;
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
index 24c42a0c16f..4894a8bc106 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
@@ -330,7 +330,11 @@ public class FragmentInstanceDispatcherImpl implements
IFragInstanceDispatcher {
return immediateFuture(
new FragInstanceDispatchResult(
RpcUtils.getStatus(
- TSStatusCode.INTERNAL_SERVER_ERROR, "Interrupted errors: " +
e.getMessage())));
+ TSStatusCode.INTERNAL_SERVER_ERROR,
+ String.format(
+
DataNodeQueryMessages.MESSAGE_INTERRUPTED_ERRORS_ARG_876F1B4B,
+ e.getMessage())),
+ true));
}
if (dispatchFailures.isEmpty()) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporter.java
index 1630175738f..ad13233dbac 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporter.java
@@ -25,10 +25,12 @@ import org.apache.iotdb.commons.audit.UserEntity;
import org.apache.iotdb.commons.client.IClientManager;
import org.apache.iotdb.commons.client.exception.ClientManagerException;
import org.apache.iotdb.commons.concurrent.threadpool.ScheduledExecutorUtil;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.consensus.ConfigRegionId;
import org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.queryengine.common.SessionInfo;
import org.apache.iotdb.commons.schema.SchemaConstant;
+import org.apache.iotdb.commons.utils.ErrorHandlingCommonUtils;
import org.apache.iotdb.confignode.rpc.thrift.TDatabaseSchema;
import org.apache.iotdb.confignode.rpc.thrift.TGetDatabaseReq;
import org.apache.iotdb.confignode.rpc.thrift.TShowDatabaseResp;
@@ -72,6 +74,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -81,24 +84,27 @@ public class IoTDBInternalLocalReporter extends
IoTDBInternalReporter {
private static final Logger LOGGER =
LoggerFactory.getLogger(IoTDBInternalLocalReporter.class);
private static final SessionManager SESSION_MANAGER =
SessionManager.getInstance();
- private static final Coordinator COORDINATOR = Coordinator.getInstance();
+ private final Coordinator coordinator;
private final SessionInfo sessionInfo;
private final IPartitionFetcher partitionFetcher;
private final ISchemaFetcher schemaFetcher;
private Future<?> currentServiceFuture;
- private final ScheduledExecutorService service =
Executors.newSingleThreadScheduledExecutor();
+ private final ScheduledExecutorService service;
+ private volatile boolean stopped;
public IoTDBInternalLocalReporter() {
- partitionFetcher = ClusterPartitionFetcher.getInstance();
- schemaFetcher = ClusterSchemaFetcher.getInstance();
- sessionInfo =
+ this(
+ Executors.newSingleThreadScheduledExecutor(),
+ Coordinator.getInstance(),
new SessionInfo(
0,
new UserEntity(
AuthorityChecker.SUPER_USER_ID,
AuthorityChecker.SUPER_USER,
IoTDBDescriptor.getInstance().getConfig().getInternalAddress()),
- ZoneId.systemDefault());
+ ZoneId.systemDefault()),
+ ClusterPartitionFetcher.getInstance(),
+ ClusterSchemaFetcher.getInstance());
IClientManager<ConfigRegionId, ConfigNodeClient> configNodeClientManager =
ConfigNodeClientManager.getInstance();
@@ -125,13 +131,29 @@ public class IoTDBInternalLocalReporter extends
IoTDBInternalReporter {
}
}
+ IoTDBInternalLocalReporter(
+ ScheduledExecutorService service,
+ Coordinator coordinator,
+ SessionInfo sessionInfo,
+ IPartitionFetcher partitionFetcher,
+ ISchemaFetcher schemaFetcher) {
+ this.service = service;
+ this.coordinator = coordinator;
+ this.sessionInfo = sessionInfo;
+ this.partitionFetcher = partitionFetcher;
+ this.schemaFetcher = schemaFetcher;
+ }
+
@Override
public InternalReporterType getType() {
return InternalReporterType.IOTDB;
}
@Override
- public boolean start() {
+ public synchronized boolean start() {
+ if (isStopping()) {
+ return false;
+ }
if (currentServiceFuture != null) {
LOGGER.warn(DataNodeMiscMessages.INTERNAL_REPORTER_ALREADY_STARTED);
return false;
@@ -148,7 +170,9 @@ public class IoTDBInternalLocalReporter extends
IoTDBInternalReporter {
}
@Override
- public boolean stop() {
+ public synchronized boolean stop() {
+ // Publish this before interrupting an in-flight write, including during
reporter reload.
+ stopped = true;
if (currentServiceFuture != null) {
currentServiceFuture.cancel(true);
currentServiceFuture = null;
@@ -166,32 +190,86 @@ public class IoTDBInternalLocalReporter extends
IoTDBInternalReporter {
@Override
protected void writeMetricToIoTDB(Map<String, Object> valueMap, String
prefix, long time) {
- service.execute(
- () -> {
- try {
- TSStatus result = insertRecord(valueMap, prefix, time);
- if (result.getCode() ==
TSStatusCode.METADATA_ERROR.getStatusCode()) {
- createTimeSeries(valueMap, prefix);
- result = insertRecord(valueMap, prefix, time);
- }
- if (result.getCode() !=
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
- LOGGER.warn(DataNodeMiscMessages.FAILED_UPDATE_METRIC_VALUE,
result);
- }
- } catch (IoTDBConnectionException e1) {
- LOGGER.warn(
- DataNodeMiscMessages
-
.MISC_LOG_FAILED_TO_UPDATE_THE_VALUE_OF_METRIC_BECAUSE_OF_CONNECTION_B0FC4929,
- e1);
- } catch (IllegalPathException | QueryProcessException e2) {
- LOGGER.warn(
- DataNodeMiscMessages
-
.MISC_LOG_FAILED_TO_UPDATE_THE_VALUE_OF_METRIC_BECAUSE_OF_INTERNAL_E5C64806,
- e2);
- }
- });
+ if (isStopping()) {
+ return;
+ }
+ try {
+ service.execute(() -> writeMetric(valueMap, prefix, time));
+ } catch (RejectedExecutionException e) {
+ // stop() can shut down the executor between the check above and
submission.
+ if (!stopped) {
+ throw e;
+ }
+ }
}
- private TSStatus insertRecord(Map<String, Object> valueMap, String prefix,
long time)
+ private void writeMetric(Map<String, Object> valueMap, String prefix, long
time) {
+ if (isStopping()) {
+ return;
+ }
+ try {
+ ExecutionResult result = insertRecord(valueMap, prefix, time);
+ if (result.status.getCode() ==
TSStatusCode.METADATA_ERROR.getStatusCode() && !isStopping()) {
+ createTimeSeries(valueMap, prefix);
+ if (isStopping()) {
+ return;
+ }
+ result = insertRecord(valueMap, prefix, time);
+ }
+ if (result.status.getCode() !=
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ if (isExpectedShutdownFailure(result)) {
+ LOGGER.debug(DataNodeMiscMessages.FAILED_UPDATE_METRIC_VALUE,
result.status);
+ } else {
+ LOGGER.warn(DataNodeMiscMessages.FAILED_UPDATE_METRIC_VALUE,
result.status);
+ }
+ }
+ } catch (IoTDBConnectionException e1) {
+ if (isInterruptedDuringShutdown(e1)) {
+ LOGGER.debug(
+ DataNodeMiscMessages
+
.MISC_LOG_FAILED_TO_UPDATE_THE_VALUE_OF_METRIC_BECAUSE_OF_CONNECTION_B0FC4929,
+ e1);
+ } else {
+ LOGGER.warn(
+ DataNodeMiscMessages
+
.MISC_LOG_FAILED_TO_UPDATE_THE_VALUE_OF_METRIC_BECAUSE_OF_CONNECTION_B0FC4929,
+ e1);
+ }
+ } catch (IllegalPathException | QueryProcessException e2) {
+ if (isInterruptedDuringShutdown(e2)) {
+ LOGGER.debug(
+ DataNodeMiscMessages
+
.MISC_LOG_FAILED_TO_UPDATE_THE_VALUE_OF_METRIC_BECAUSE_OF_INTERNAL_E5C64806,
+ e2);
+ } else {
+ LOGGER.warn(
+ DataNodeMiscMessages
+
.MISC_LOG_FAILED_TO_UPDATE_THE_VALUE_OF_METRIC_BECAUSE_OF_INTERNAL_E5C64806,
+ e2);
+ }
+ }
+ }
+
+ private boolean isStopping() {
+ return stopped || CommonDescriptor.getInstance().getConfig().isStopping();
+ }
+
+ private boolean isExpectedShutdownFailure(ExecutionResult result) {
+ return isStopping()
+ && (result.failureCausedByInterruption
+ || (CommonDescriptor.getInstance().getConfig().isStopping()
+ && result.status.getCode() ==
TSStatusCode.SYSTEM_READ_ONLY.getStatusCode()));
+ }
+
+ private boolean isInterruptedDuringShutdown(Exception e) {
+ if (isStopping() && ErrorHandlingCommonUtils.getRootCause(e) instanceof
InterruptedException) {
+ Thread.currentThread().interrupt();
+ return true;
+ }
+ return false;
+ }
+
+ private ExecutionResult insertRecord(Map<String, Object> valueMap, String
prefix, long time)
throws IoTDBConnectionException, QueryProcessException,
IllegalPathException {
TSInsertRecordReq request = new TSInsertRecordReq();
List<String> measurements = new ArrayList<>();
@@ -214,10 +292,8 @@ public class IoTDBInternalLocalReporter extends
IoTDBInternalReporter {
InsertRowStatement s = StatementGenerator.createStatement(request);
final long queryId = SESSION_MANAGER.requestQueryId();
- ExecutionResult result =
- COORDINATOR.executeForTreeModel(
- s, queryId, sessionInfo, "", partitionFetcher, schemaFetcher);
- return result.status;
+ return coordinator.executeForTreeModel(
+ s, queryId, sessionInfo, "", partitionFetcher, schemaFetcher);
}
private void createTimeSeries(Map<String, Object> valueMap, String prefix)
@@ -244,10 +320,14 @@ public class IoTDBInternalLocalReporter extends
IoTDBInternalReporter {
final long queryId = SESSION_MANAGER.requestQueryId();
ExecutionResult result =
- COORDINATOR.executeForTreeModel(
+ coordinator.executeForTreeModel(
s, queryId, sessionInfo, "", partitionFetcher, schemaFetcher);
if (result.status.getCode() !=
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
- LOGGER.warn(DataNodeMiscMessages.FAILED_AUTO_CREATE_TIMESERIES, paths,
result.status);
+ if (isExpectedShutdownFailure(result)) {
+ LOGGER.debug(DataNodeMiscMessages.FAILED_AUTO_CREATE_TIMESERIES,
paths, result.status);
+ } else {
+ LOGGER.warn(DataNodeMiscMessages.FAILED_AUTO_CREATE_TIMESERIES, paths,
result.status);
+ }
}
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
index ad7ab094748..118ff7b5025 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
@@ -31,13 +31,17 @@ import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
public class QueryStateMachineTest {
@@ -158,6 +162,37 @@ public class QueryStateMachineTest {
stateMachine.getFailureStatus().code);
}
+ @Test
+ public void testInterruptionProvenanceBelongsToTheRecordedStatus() {
+ QueryStateMachine stateMachine =
+ new QueryStateMachine(genQueryId(), mock(ExecutorService.class));
+ TSStatus interruption = new
TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
+
+ stateMachine.transitionToFailed(interruption, true);
+
+ assertSame(interruption, stateMachine.getFailureStatus());
+ assertTrue(stateMachine.isFailureCausedByInterruption(interruption));
+ assertFalse(
+ stateMachine.isFailureCausedByInterruption(
+ new TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode())));
+ }
+
+ @Test
+ public void testLaterInterruptionDoesNotReclassifyExistingFailure() {
+ QueryStateMachine stateMachine =
+ new QueryStateMachine(genQueryId(), mock(ExecutorService.class));
+ TSStatus original = new
TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode());
+ TSStatus interruption = new
TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
+
+ stateMachine.transitionToFailed(original);
+ stateMachine.transitionToFailed(interruption, true);
+ stateMachine.transitionToFailed(original, true);
+
+ assertSame(original, stateMachine.getFailureStatus());
+ assertFalse(stateMachine.isFailureCausedByInterruption(original));
+ assertFalse(stateMachine.isFailureCausedByInterruption(interruption));
+ }
+
private QueryStateMachine genQueryStateMachine() {
return new QueryStateMachine(
genQueryId(),
IoTDBThreadPoolFactory.newSingleThreadExecutor("TestQueryStateMachine"));
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThreadShutdownTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThreadShutdownTest.java
new file mode 100644
index 00000000000..8f57718c665
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThreadShutdownTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.queryengine.execution.schedule;
+
+import org.apache.iotdb.calc.execution.schedule.queue.IndexedBlockingQueue;
+import org.apache.iotdb.commons.exception.QueryTimeoutException;
+import org.apache.iotdb.db.queryengine.common.FragmentInstanceId;
+import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.execution.driver.IDriver;
+import
org.apache.iotdb.db.queryengine.execution.schedule.queue.L1PriorityQueue;
+import org.apache.iotdb.db.queryengine.execution.schedule.task.DriverTask;
+import org.apache.iotdb.db.queryengine.execution.schedule.task.DriverTaskId;
+import
org.apache.iotdb.db.queryengine.execution.schedule.task.DriverTaskStatus;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+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 AbstractDriverThreadShutdownTest {
+
+ private final Logger logger = (Logger)
LoggerFactory.getLogger(AbstractDriverThread.class);
+ private final ListAppender<ILoggingEvent> appender = new ListAppender<>();
+ private final ITaskScheduler scheduler = mock(ITaskScheduler.class);
+ private final ThreadProducer producer = mock(ThreadProducer.class);
+ private final ThreadGroup threadGroup = new ThreadGroup("shutdown-test");
+ private Level originalLevel;
+
+ @Before
+ public void setUp() {
+ originalLevel = logger.getLevel();
+ logger.setLevel(Level.DEBUG);
+ appender.start();
+ logger.addAppender(appender);
+ }
+
+ @After
+ public void tearDown() {
+ logger.detachAppender(appender);
+ appender.stop();
+ logger.setLevel(originalLevel);
+ }
+
+ @Test
+ public void testCloseWhilePolling() throws Exception {
+ CountDownLatch polling = new CountDownLatch(1);
+ IndexedBlockingQueue<DriverTask> queue = pollingQueue(polling);
+ AbstractDriverThread thread =
+ new DriverTaskTimeoutSentinelThread("sentinel", threadGroup, queue,
scheduler, producer);
+
+ interruptAfterEntered(thread, polling, true);
+
+ assertFalse(hasWarning());
+ verify(scheduler, never()).toAborted(any());
+ verify(producer, never()).produce(anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testUnexpectedPollingInterruptStillWarnsAndReplacesThread()
throws Exception {
+ CountDownLatch polling = new CountDownLatch(1);
+ IndexedBlockingQueue<DriverTask> queue = pollingQueue(polling);
+ AbstractDriverThread thread =
+ new DriverTaskTimeoutSentinelThread("sentinel", threadGroup, queue,
scheduler, producer);
+
+ interruptAfterEntered(thread, polling, false);
+
+ assertTrue(hasWarning());
+ verify(producer).produce("sentinel", threadGroup, queue, producer);
+ }
+
+ @Test
+ public void testCloseWhileSentinelWaitsStillAbortsTask() throws Exception {
+ IndexedBlockingQueue<DriverTask> queue = pollingQueue(new
CountDownLatch(0));
+ DriverTask task = newTask(60_000);
+ queue.push(task);
+ CountDownLatch executing = new CountDownLatch(1);
+ AbstractDriverThread thread = sentinel(queue, executing);
+ AtomicBoolean interruptedDuringAbort = new AtomicBoolean();
+ doAnswer(
+ invocation -> {
+
interruptedDuringAbort.set(Thread.currentThread().isInterrupted());
+ return null;
+ })
+ .when(scheduler)
+ .toAborted(task);
+
+ interruptAfterEntered(thread, executing, true);
+
+ assertFalse(hasWarning());
+ assertTrue(interruptedDuringAbort.get());
+ assertAbortCause(task, DriverTaskAbortedException.BY_SCHEDULER_SHUTDOWN);
+ verify(scheduler).toAborted(task);
+ verify(scheduler, never()).enforceTimeLimit(any());
+ verify(producer, never()).produce(anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testUnexpectedSentinelInterruptStillAbortsAndWarns() throws
Exception {
+ IndexedBlockingQueue<DriverTask> queue = pollingQueue(new
CountDownLatch(0));
+ DriverTask task = newTask(60_000);
+ queue.push(task);
+ CountDownLatch executing = new CountDownLatch(1);
+ AbstractDriverThread thread = sentinel(queue, executing);
+ doAnswer(
+ invocation -> {
+ // End the test only after the running thread has handled the
unexpected interrupt.
+ thread.close();
+ return null;
+ })
+ .when(scheduler)
+ .toAborted(task);
+
+ interruptAfterEntered(thread, executing, false);
+
+ assertTrue(hasWarning());
+ assertAbortCause(task,
DriverTaskAbortedException.BY_INTERNAL_ERROR_SCHEDULED);
+ verify(scheduler).toAborted(task);
+ }
+
+ @Test
+ public void testWrappedShutdownInterruptStillAbortsTask() throws Exception {
+ verifyFailureDuringShutdown(new RuntimeException(new
InterruptedException()), false);
+ }
+
+ @Test
+ public void testNonInterruptFailureDuringShutdownStillWarns() throws
Exception {
+ verifyFailureDuringShutdown(new IllegalStateException("mock failure"),
true);
+ }
+
+ @Test
+ public void testSentinelStillEnforcesTimeout() throws Exception {
+ DriverTask task = newTask(0);
+ DriverTaskTimeoutSentinelThread thread =
+ new DriverTaskTimeoutSentinelThread(
+ "sentinel", threadGroup, pollingQueue(new CountDownLatch(0)),
scheduler, producer);
+
+ thread.execute(task);
+
+ assertTrue(task.getAbortCause().get() instanceof QueryTimeoutException);
+ verify(scheduler).toAborted(task);
+ verify(scheduler, never()).enforceTimeLimit(any());
+ }
+
+ private void verifyFailureDuringShutdown(RuntimeException failure, boolean
shouldWarn)
+ throws Exception {
+ IndexedBlockingQueue<DriverTask> queue = pollingQueue(new
CountDownLatch(0));
+ DriverTask task = newTask(60_000);
+ queue.push(task);
+ CountDownLatch executing = new CountDownLatch(1);
+ AbstractDriverThread thread =
+ new AbstractDriverThread("worker", threadGroup, queue, scheduler,
producer) {
+ @Override
+ protected void execute(DriverTask task) {
+ executing.countDown();
+ try {
+ new CountDownLatch(1).await();
+ } catch (InterruptedException e) {
+ throw failure;
+ }
+ }
+ };
+
+ interruptAfterEntered(thread, executing, true);
+
+ assertEquals(shouldWarn, hasWarning());
+ assertAbortCause(
+ task,
+ shouldWarn
+ ? DriverTaskAbortedException.BY_INTERNAL_ERROR_SCHEDULED
+ : DriverTaskAbortedException.BY_SCHEDULER_SHUTDOWN);
+ verify(scheduler).toAborted(task);
+ }
+
+ private AbstractDriverThread sentinel(
+ IndexedBlockingQueue<DriverTask> queue, CountDownLatch executing) {
+ return new DriverTaskTimeoutSentinelThread(
+ "sentinel", threadGroup, queue, scheduler, producer) {
+ @Override
+ public void execute(DriverTask task) throws InterruptedException {
+ executing.countDown();
+ super.execute(task);
+ }
+ };
+ }
+
+ private IndexedBlockingQueue<DriverTask> pollingQueue(CountDownLatch
polling) {
+ return new L1PriorityQueue<DriverTask>(
+ 10, new DriverTask.TimeoutComparator(), new DriverTask()) {
+ @Override
+ public DriverTask poll() throws InterruptedException {
+ polling.countDown();
+ return super.poll();
+ }
+ };
+ }
+
+ private DriverTask newTask(long timeoutMs) {
+ IDriver driver = mock(IDriver.class);
+ when(driver.getDriverTaskId())
+ .thenReturn(
+ new DriverTaskId(
+ new FragmentInstanceId(new PlanFragmentId(new QueryId("test"),
0), "instance"), 0));
+ return new DriverTask(driver, timeoutMs, DriverTaskStatus.READY, null, 0,
false);
+ }
+
+ private void interruptAfterEntered(
+ AbstractDriverThread thread, CountDownLatch entered, boolean close)
throws Exception {
+ thread.setDaemon(true);
+ thread.start();
+ try {
+ assertTrue(entered.await(5, TimeUnit.SECONDS));
+ if (close) {
+ thread.close();
+ }
+ thread.interrupt();
+ thread.join(10_000);
+ assertFalse("Driver thread did not exit", thread.isAlive());
+ } finally {
+ thread.close();
+ thread.interrupt();
+ thread.join(10_000);
+ }
+ }
+
+ private void assertAbortCause(DriverTask task, String reason) {
+ assertEquals(
+ new DriverTaskAbortedException(task.getDriverTaskId().getFullId(),
reason).getMessage(),
+ task.getAbortCause().get().getMessage());
+ }
+
+ private boolean hasWarning() {
+ return appender.list.stream().anyMatch(event ->
event.getLevel().isGreaterOrEqual(Level.WARN));
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
index 959bd4b9d6a..0c154850ca5 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.commons.schema.table.TsTable;
import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern;
+import org.apache.iotdb.db.schemaengine.lease.MetadataLeaseManager;
import org.apache.iotdb.db.schemaengine.table.DataNodeTableCache;
import com.google.common.collect.ImmutableList;
@@ -65,6 +66,9 @@ public class TreeViewTest {
@Before
public void setup() {
+ // This planner test has no ConfigNode heartbeats and may exceed the
default lease timeout.
+ MetadataLeaseManager.getInstance().updateFenceThresholdMs(Long.MAX_VALUE);
+ MetadataLeaseManager.getInstance().recoveryLeaseForTest(true);
TsTable tsTable = new TsTable(DEVICE_VIEW_TEST_TABLE);
tsTable.addProp(TsTable.TTL_PROPERTY, Long.MAX_VALUE + "");
tsTable.addProp(TreeViewSchema.TREE_PATH_PATTERN, "root.test" + ".**");
@@ -74,7 +78,13 @@ public class TreeViewTest {
@After
public void tearDown() {
- DataNodeTableCache.getInstance().invalid(TREE_VIEW_DB);
+ try {
+ DataNodeTableCache.getInstance().invalid(TREE_VIEW_DB);
+ } finally {
+ // Renew before restoring the timeout to avoid racing the background
lease check.
+ MetadataLeaseManager.getInstance().recoveryLeaseForTest(true);
+ MetadataLeaseManager.getInstance().updateFenceThresholdMs(20_000);
+ }
}
@Test
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
new file mode 100644
index 00000000000..a94cefc54c0
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
@@ -0,0 +1,623 @@
+/*
+ * 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.service.metrics;
+
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupId;
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
+import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.IClientManager;
+import
org.apache.iotdb.commons.client.async.AsyncDataNodeInternalServiceClient;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.execution.QueryStateMachine;
+import org.apache.iotdb.db.queryengine.plan.Coordinator;
+import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
+import org.apache.iotdb.db.queryengine.plan.analyze.IPartitionFetcher;
+import org.apache.iotdb.db.queryengine.plan.analyze.QueryType;
+import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaFetcher;
+import org.apache.iotdb.db.queryengine.plan.execution.ExecutionResult;
+import org.apache.iotdb.db.queryengine.plan.execution.QueryExecution;
+import org.apache.iotdb.db.queryengine.plan.planner.IPlanner;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.FragmentInstance;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.PlanFragment;
+import org.apache.iotdb.db.queryengine.plan.scheduler.ClusterScheduler;
+import org.apache.iotdb.db.queryengine.plan.statement.Statement;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
+import
org.apache.iotdb.db.queryengine.plan.statement.metadata.CreateMultiTimeSeriesStatement;
+import org.apache.iotdb.mpp.rpc.thrift.TSendBatchPlanNodeReq;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.stubbing.OngoingStubbing;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class IoTDBInternalLocalReporterTest {
+
+ private static int originalDataNodeId;
+
+ private final Logger logger = (Logger)
LoggerFactory.getLogger(IoTDBInternalLocalReporter.class);
+ private final ListAppender<ILoggingEvent> appender = new ListAppender<>();
+ private final Coordinator coordinator = mock(Coordinator.class);
+ private final SessionInfo sessionInfo = mock(SessionInfo.class);
+ private final IPartitionFetcher partitionFetcher =
mock(IPartitionFetcher.class);
+ private final ISchemaFetcher schemaFetcher = mock(ISchemaFetcher.class);
+ private ScheduledExecutorService service;
+ private IoTDBInternalLocalReporter reporter;
+ private Level originalLevel;
+ private boolean originalStopping;
+
+ @BeforeClass
+ public static void initializeDataNodeId() {
+ originalDataNodeId =
IoTDBDescriptor.getInstance().getConfig().getDataNodeId();
+ IoTDBDescriptor.getInstance().getConfig().setDataNodeId(0);
+ }
+
+ @AfterClass
+ public static void restoreDataNodeId() {
+
IoTDBDescriptor.getInstance().getConfig().setDataNodeId(originalDataNodeId);
+ }
+
+ @Before
+ public void setUp() {
+ originalLevel = logger.getLevel();
+ logger.setLevel(Level.DEBUG);
+ appender.start();
+ logger.addAppender(appender);
+ originalStopping = CommonDescriptor.getInstance().getConfig().isStopping();
+ CommonDescriptor.getInstance().getConfig().setStopping(false);
+ service = mock(ScheduledExecutorService.class);
+ doAnswer(
+ invocation -> {
+ invocation.<Runnable>getArgument(0).run();
+ return null;
+ })
+ .when(service)
+ .execute(any(Runnable.class));
+ reporter = newReporter(service);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Some tests intentionally restore the interrupted flag while returning a
failed status.
+ Thread.interrupted();
+ reporter.stop();
+ service.awaitTermination(5, TimeUnit.SECONDS);
+ CommonDescriptor.getInstance().getConfig().setStopping(originalStopping);
+ logger.detachAppender(appender);
+ appender.stop();
+ logger.setLevel(originalLevel);
+ }
+
+ @Test
+ public void testStoppedReporterRejectsNewWritesAndRestart() {
+ reporter.stop();
+
+ writeMetric();
+
+ assertFalse(reporter.start());
+ verify(service, never()).execute(any(Runnable.class));
+ verifyZeroInteractions(coordinator);
+ assertFalse(hasWarning());
+ }
+
+ @Test
+ public void testNodeShutdownRejectsNewWrites() {
+ CommonDescriptor.getInstance().getConfig().setStopping(true);
+
+ writeMetric();
+
+ verify(service, never()).execute(any(Runnable.class));
+ verifyZeroInteractions(coordinator);
+ assertFalse(hasWarning());
+ }
+
+ @Test
+ public void testQueuedWriteDoesNotRunAfterStop() {
+ doNothing().when(service).execute(any(Runnable.class));
+ writeMetric();
+ ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
+ verify(service).execute(task.capture());
+
+ reporter.stop();
+ task.getValue().run();
+
+ verifyZeroInteractions(coordinator);
+ assertFalse(hasWarning());
+ }
+
+ @Test
+ public void testStopBetweenCheckAndSubmission() {
+ doAnswer(
+ invocation -> {
+ reporter.stop();
+ throw new RejectedExecutionException("executor stopped");
+ })
+ .when(service)
+ .execute(any(Runnable.class));
+
+ writeMetric();
+
+ verifyZeroInteractions(coordinator);
+ assertFalse(hasWarning());
+ }
+
+ @Test(expected = RejectedExecutionException.class)
+ public void testUnexpectedSubmissionFailureIsPropagated() {
+ doThrow(new RejectedExecutionException("unexpected rejection"))
+ .when(service)
+ .execute(any(Runnable.class));
+
+ writeMetric();
+ }
+
+ @Test
+ public void testMissingTimeseriesIsCreatedAndWriteRetried() {
+ execution()
+ .thenReturn(
+ result(TSStatusCode.METADATA_ERROR),
+ result(TSStatusCode.SUCCESS_STATUS),
+ result(TSStatusCode.SUCCESS_STATUS));
+
+ writeMetric();
+
+ List<Statement> statements = executedStatements(3);
+ assertTrue(statements.get(0) instanceof InsertRowStatement);
+ assertTrue(statements.get(1) instanceof CreateMultiTimeSeriesStatement);
+ assertTrue(statements.get(2) instanceof InsertRowStatement);
+ assertFalse(hasWarning());
+ }
+
+ @Test
+ public void testStopAfterInsertDoesNotCreateTimeseries() {
+ execution()
+ .thenAnswer(
+ invocation -> {
+ reporter.stop();
+ Thread.currentThread().interrupt();
+ return result(TSStatusCode.METADATA_ERROR);
+ });
+
+ writeMetric();
+
+ executedStatements(1);
+ assertTrue(Thread.currentThread().isInterrupted());
+ assertTrue(hasWarning());
+ }
+
+ @Test
+ public void testStopInterruptsAutoCreateAndPreventsRetry() throws Exception {
+ verifyReloadInterrupt(null, null, true);
+ }
+
+ @Test
+ public void testNodeShutdownReadOnlyFailureDoesNotWarn() {
+ execution()
+ .thenAnswer(
+ invocation -> {
+ CommonDescriptor.getInstance().getConfig().setStopping(true);
+ return result(TSStatusCode.SYSTEM_READ_ONLY);
+ });
+
+ writeMetric();
+
+ executedStatements(1);
+ assertFalse(hasWarning());
+ }
+
+ @Test
+ public void testReadOnlyFailureWithoutNodeShutdownStillWarns() {
+ execution().thenReturn(result(TSStatusCode.SYSTEM_READ_ONLY));
+
+ writeMetric();
+
+ assertTrue(hasWarning());
+ }
+
+ @Test
+ public void testInterruptedWaitWithoutShutdownStillWarns() throws Exception {
+ QueryExecution queryExecution =
+ queryExecutionWithDelayedStatus(null, null, new CountDownLatch(0));
+ execution()
+ .thenAnswer(
+ invocation -> {
+ Thread.currentThread().interrupt();
+ return queryExecution.getStatus();
+ });
+
+ writeMetric();
+
+ assertTrue(hasWarning());
+ }
+
+ @Test
+ public void testInternalErrorDuringStopWithoutInterruptionStillWarns() {
+ execution()
+ .thenAnswer(
+ invocation -> {
+ reporter.stop();
+ return result(TSStatusCode.INTERNAL_SERVER_ERROR);
+ });
+
+ writeMetric();
+
+ assertTrue(hasWarning());
+ }
+
+ @Test
+ public void testAutoCreateFailureWithoutInterruptionStillWarnsDuringStop() {
+ execution()
+ .thenReturn(result(TSStatusCode.METADATA_ERROR))
+ .thenAnswer(
+ invocation -> {
+ reporter.stop();
+ return result(TSStatusCode.INTERNAL_SERVER_ERROR);
+ });
+
+ writeMetric();
+
+ executedStatements(2);
+ assertTrue(hasWarning());
+ }
+
+ @Test
+ public void testReporterReloadDoesNotHideReadOnlyFailure() {
+ execution()
+ .thenAnswer(
+ invocation -> {
+ reporter.stop();
+ return result(TSStatusCode.SYSTEM_READ_ONLY);
+ });
+
+ writeMetric();
+
+ assertTrue(hasWarning());
+ }
+
+ @Test
+ public void testReloadInterruptPreservesRecordedReadOnlyFailure() throws
Exception {
+ verifyReloadInterrupt(new
TSStatus(TSStatusCode.SYSTEM_READ_ONLY.getStatusCode()), null, false);
+ }
+
+ @Test
+ public void testReloadInterruptPreservesRecordedWriteFailure() throws
Exception {
+ verifyReloadInterrupt(
+ new TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode()), null,
false);
+ }
+
+ @Test
+ public void testReloadInterruptPreservesRecordedInternalFailure() throws
Exception {
+ verifyReloadInterrupt(
+ new TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()),
null, false);
+ }
+
+ @Test
+ public void testReloadInterruptPreservesRecordedException() throws Exception
{
+ verifyReloadInterrupt(null, new IllegalStateException("existing write
failure"), false);
+ }
+
+ @Test
+ public void testReloadInterruptPreservesRecordedAutoCreateFailure() throws
Exception {
+ verifyReloadInterrupt(
+ new TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode()), null,
true);
+ }
+
+ @Test
+ public void testReloadWithOnlyInterruptedWaitDoesNotWarn() throws Exception {
+ verifyReloadInterrupt(null, null, false);
+ }
+
+ @Test
+ public void testRemoteWriteInterruptedDuringReloadDoesNotWarn() throws
Exception {
+ verifyRemoteDispatchInterruption(false, false, null);
+ }
+
+ @Test
+ public void testRemoteAutoCreateInterruptedDuringReloadDoesNotWarn() throws
Exception {
+ verifyRemoteDispatchInterruption(true, false, null);
+ }
+
+ @Test
+ public void testRemoteInterruptionSurvivesClearedThreadFlag() throws
Exception {
+ verifyRemoteDispatchInterruption(false, true, null);
+ }
+
+ @Test
+ public void testRemoteInterruptionDoesNotReplaceExistingFailure() throws
Exception {
+ verifyRemoteDispatchInterruption(
+ false, false, new
TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode()));
+ }
+
+ @Test
+ public void testRemoteInterruptionDoesNotHideExistingException() throws
Exception {
+ verifyRemoteDispatchInterruption(
+ false, false, null, new IllegalStateException("existing write
failure"));
+ }
+
+ private void verifyRemoteDispatchInterruption(
+ boolean autoCreate, boolean clearInterrupt, TSStatus recordedFailure)
throws Exception {
+ verifyRemoteDispatchInterruption(autoCreate, clearInterrupt,
recordedFailure, null);
+ }
+
+ private void verifyRemoteDispatchInterruption(
+ boolean autoCreate,
+ boolean clearInterrupt,
+ TSStatus recordedFailure,
+ Throwable recordedException)
+ throws Exception {
+ service = Executors.newSingleThreadScheduledExecutor();
+ reporter = newReporter(service);
+ MPPQueryContext context = mock(MPPQueryContext.class);
+ when(context.getQueryId()).thenReturn(new QueryId("remote_metric"));
+ IPlanner planner = mock(IPlanner.class);
+ when(planner.analyze(context)).thenReturn(mock(IAnalysis.class));
+ ExecutorService notifications = mock(ExecutorService.class);
+ QueryExecution queryExecution = new QueryExecution(planner, context,
notifications);
+ QueryStateMachine stateMachine = stateMachine(queryExecution);
+ if (recordedFailure != null) {
+ stateMachine.transitionToFailed(recordedFailure);
+ } else if (recordedException != null) {
+ stateMachine.transitionToFailed(recordedException);
+ } else {
+ stateMachine.transitionToPlanned();
+ }
+
+ TEndPoint endpoint = new TEndPoint("192.0.2.1", 10730);
+ TDataNodeLocation remote = new
TDataNodeLocation().setInternalEndPoint(endpoint);
+ TRegionReplicaSet region =
+ new TRegionReplicaSet(
+ new TConsensusGroupId(TConsensusGroupType.DataRegion, 1),
+ Collections.singletonList(remote));
+ PlanNode node = mock(PlanNode.class);
+ when(node.serializeToByteBuffer()).thenReturn(ByteBuffer.allocate(0));
+ when(node.getChildren()).thenReturn(Collections.emptyList());
+ PlanFragment fragment = mock(PlanFragment.class);
+ when(fragment.getPlanNodeTree()).thenReturn(node);
+ FragmentInstance instance = mock(FragmentInstance.class);
+ when(instance.getHostDataNode()).thenReturn(remote);
+ when(instance.getRegionReplicaSet()).thenReturn(region);
+ when(instance.getFragment()).thenReturn(fragment);
+ DistributedQueryPlan plan = mock(DistributedQueryPlan.class);
+ when(plan.getInstances()).thenReturn(Collections.singletonList(instance));
+
+ @SuppressWarnings("unchecked")
+ IClientManager<TEndPoint, AsyncDataNodeInternalServiceClient> clients =
+ mock(IClientManager.class);
+ AsyncDataNodeInternalServiceClient client =
mock(AsyncDataNodeInternalServiceClient.class);
+ when(clients.borrowClient(endpoint)).thenReturn(client);
+ CountDownLatch dispatched = new CountDownLatch(1);
+ doAnswer(
+ invocation -> {
+ // Leave the RPC response pending so the real
AsyncPlanNodeSender waits.
+ dispatched.countDown();
+ return null;
+ })
+ .when(client)
+ .sendBatchPlanNode(any(TSendBatchPlanNodeReq.class), any());
+ ClusterScheduler scheduler =
+ new ClusterScheduler(context, stateMachine, plan, QueryType.WRITE,
null, null, clients);
+ AtomicReference<ExecutionResult> returnedResult = new AtomicReference<>();
+ OngoingStubbing<ExecutionResult> execution = execution();
+ if (autoCreate) {
+ execution = execution.thenReturn(result(TSStatusCode.METADATA_ERROR));
+ }
+ execution.thenAnswer(
+ invocation -> {
+ scheduler.start();
+ if (clearInterrupt) {
+ Thread.interrupted();
+ }
+ // Deliver the getStatus() listener immediately, also exercising its
normal return
+ // path when the interrupt flag has been consumed after dispatch.
+ doAnswer(
+ notification -> {
+ notification.<Runnable>getArgument(0).run();
+ return null;
+ })
+ .when(notifications)
+ .execute(any(Runnable.class));
+ ExecutionResult result = queryExecution.getStatus();
+ returnedResult.set(result);
+ return result;
+ });
+
+ writeMetric();
+ assertTrue(dispatched.await(5, TimeUnit.SECONDS));
+ reporter.stop();
+ assertTrue(service.awaitTermination(5, TimeUnit.SECONDS));
+
+ ExecutionResult result = returnedResult.get();
+ if (recordedFailure != null) {
+ assertSame(recordedFailure, result.status);
+ } else {
+ assertEquals(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(),
result.status.getCode());
+ }
+ boolean hasRecordedFailure = recordedFailure != null || recordedException
!= null;
+ assertEquals(hasRecordedFailure, hasWarning());
+ assertEquals(!hasRecordedFailure, result.failureCausedByInterruption);
+ executedStatements(autoCreate ? 2 : 1);
+ verify(client).sendBatchPlanNode(any(TSendBatchPlanNodeReq.class), any());
+ }
+
+ private void verifyReloadInterrupt(
+ TSStatus recordedFailure, Throwable recordedException, boolean
autoCreate) throws Exception {
+ service = Executors.newSingleThreadScheduledExecutor();
+ reporter = newReporter(service);
+ CountDownLatch waitingForStatus = new CountDownLatch(1);
+ QueryExecution queryExecution =
+ queryExecutionWithDelayedStatus(recordedFailure, recordedException,
waitingForStatus);
+ AtomicReference<ExecutionResult> returnedResult = new AtomicReference<>();
+ AtomicBoolean interrupted = new AtomicBoolean();
+ OngoingStubbing<ExecutionResult> execution = execution();
+ if (autoCreate) {
+ execution = execution.thenReturn(result(TSStatusCode.METADATA_ERROR));
+ }
+ execution.thenAnswer(
+ invocation -> {
+ ExecutionResult result = queryExecution.getStatus();
+ returnedResult.set(result);
+ interrupted.set(Thread.currentThread().isInterrupted());
+ return result;
+ });
+
+ writeMetric();
+ assertTrue(waitingForStatus.await(5, TimeUnit.SECONDS));
+ // The node remains running: stop() represents replacing just the reporter
on reload.
+ reporter.stop();
+ assertTrue(service.awaitTermination(5, TimeUnit.SECONDS));
+
+ assertTrue(interrupted.get());
+ ExecutionResult result = returnedResult.get();
+ if (recordedFailure != null) {
+ assertSame(recordedFailure, result.status);
+ } else {
+ assertEquals(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(),
result.status.getCode());
+ if (recordedException != null) {
+ assertEquals(recordedException.getMessage(),
result.status.getMessage());
+ }
+ }
+ executedStatements(autoCreate ? 2 : 1);
+ assertEquals(
+ recordedFailure == null && recordedException == null,
result.failureCausedByInterruption);
+ assertEquals(recordedFailure != null || recordedException != null,
hasWarning());
+ }
+
+ private QueryExecution queryExecutionWithDelayedStatus(
+ TSStatus recordedFailure, Throwable recordedException, CountDownLatch
waitingForStatus)
+ throws Exception {
+ MPPQueryContext context = mock(MPPQueryContext.class);
+ when(context.getQueryId()).thenReturn(new QueryId("metric_reload"));
+ IPlanner planner = mock(IPlanner.class);
+ when(planner.analyze(context)).thenReturn(mock(IAnalysis.class));
+ ExecutorService notifications = mock(ExecutorService.class);
+ QueryExecution execution = new QueryExecution(planner, context,
notifications);
+ QueryStateMachine stateMachine = stateMachine(execution);
+ if (recordedFailure != null) {
+ stateMachine.transitionToFailed(recordedFailure);
+ } else if (recordedException != null) {
+ stateMachine.transitionToFailed(recordedException);
+ }
+ // Delay listener delivery as with a busy notification executor. The real
getStatus() waits
+ // until shutdownNow() interrupts it, even when the state machine already
holds a failure.
+ doAnswer(
+ invocation -> {
+ waitingForStatus.countDown();
+ return null;
+ })
+ .when(notifications)
+ .execute(any(Runnable.class));
+ return execution;
+ }
+
+ private QueryStateMachine stateMachine(QueryExecution execution) throws
Exception {
+ Field stateMachineField =
QueryExecution.class.getDeclaredField("stateMachine");
+ stateMachineField.setAccessible(true);
+ return (QueryStateMachine) stateMachineField.get(execution);
+ }
+
+ private IoTDBInternalLocalReporter newReporter(ScheduledExecutorService
executor) {
+ return new IoTDBInternalLocalReporter(
+ executor, coordinator, sessionInfo, partitionFetcher, schemaFetcher);
+ }
+
+ private OngoingStubbing<ExecutionResult> execution() {
+ return when(
+ coordinator.executeForTreeModel(
+ any(Statement.class),
+ anyLong(),
+ same(sessionInfo),
+ eq(""),
+ same(partitionFetcher),
+ same(schemaFetcher)));
+ }
+
+ private ExecutionResult result(TSStatusCode code) {
+ return new ExecutionResult(new QueryId("metric_test"), new
TSStatus(code.getStatusCode()));
+ }
+
+ private void writeMetric() {
+ reporter.writeMetricToIoTDB(
+ Collections.singletonMap("value", 1L), "root.__system.metric.test",
1L);
+ }
+
+ private List<Statement> executedStatements(int count) {
+ ArgumentCaptor<Statement> statement =
ArgumentCaptor.forClass(Statement.class);
+ verify(coordinator, times(count))
+ .executeForTreeModel(
+ statement.capture(),
+ anyLong(),
+ same(sessionInfo),
+ eq(""),
+ same(partitionFetcher),
+ same(schemaFetcher));
+ return statement.getAllValues();
+ }
+
+ private boolean hasWarning() {
+ return appender.list.stream().anyMatch(event ->
event.getLevel().isGreaterOrEqual(Level.WARN));
+ }
+}