This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 312c205c8b5 Fix schema region NPE during DataNode shutdown (#18821)
312c205c8b5 is described below
commit 312c205c8b5dc3701dd51ccd676f1c6f26796dc6
Author: Jiang Tian <[email protected]>
AuthorDate: Mon Oct 12 13:56:16 2026 +0800
Fix schema region NPE during DataNode shutdown (#18821)
* Fix schema region NPE during DataNode shutdown
* Handle missing schema regions in schema writes
* Handle cleared MTree roots during lookup
* Return retryable status while SchemaEngine clears regions
* Return retryable status for alter during schema teardown
---
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 +
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 +
.../execution/executor/RegionWriteExecutor.java | 36 ++-
.../apache/iotdb/db/schemaengine/SchemaEngine.java | 9 +
.../mtree/impl/mem/MTreeBelowSGMemoryImpl.java | 6 +
.../mtree/impl/pbtree/MTreeBelowSGCachedImpl.java | 7 +-
.../executor/RegionWriteExecutorTest.java | 268 +++++++++++++++++++++
.../mtree/impl/mem/MTreeBelowSGMemoryImplTest.java | 14 ++
.../impl/pbtree/MTreeBelowSGCachedImplTest.java | 18 +-
9 files changed, 358 insertions(+), 4 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 1672c9024f5..73b27b06223 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
@@ -20,6 +20,8 @@
package org.apache.iotdb.db.i18n;
public final class DataNodeQueryMessages {
+ public static final String
MESSAGE_SCHEMA_REGION_IS_UNAVAILABLE_PLEASE_RETRY_LATER_D642510A =
+ "Schema region is unavailable. Please retry later.";
public static final String
EXCEPTION_FRAGMENT_INSTANCE_ARG_IS_ALREADY_ARG_B44984B4 =
"Fragment instance %s is already %s";
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 114fc1240ff..ed1fefe479a 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
@@ -20,6 +20,8 @@
package org.apache.iotdb.db.i18n;
public final class DataNodeQueryMessages {
+ public static final String
MESSAGE_SCHEMA_REGION_IS_UNAVAILABLE_PLEASE_RETRY_LATER_D642510A =
+ "SchemaRegion 不可用,请稍后重试。";
public static final String
EXCEPTION_FRAGMENT_INSTANCE_ARG_IS_ALREADY_ARG_B44984B4 =
"Fragment instance %s 已处于 %s 状态";
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java
index 6fc521a7f72..05328211947 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java
@@ -176,6 +176,12 @@ public class RegionWriteExecutor {
return RegionExecutionResult.create(
false, errorMsg,
RpcUtils.getStatus(TSStatusCode.NO_AVAILABLE_REGION_GROUP));
}
+ // SchemaEngine clears regions before DataNodeRegionManager removes
their validation locks.
+ // Failures from an in-flight schema write during that window must
remain retryable.
+ if (groupId instanceof SchemaRegionId && schemaEngine.isClearing()) {
+ return RegionExecutionResult.create(
+ false, e.getMessage(),
RpcUtils.getStatus(TSStatusCode.NO_AVAILABLE_REGION_GROUP));
+ }
LOGGER.warn(e.getMessage(), e);
return RegionExecutionResult.create(
false,
@@ -842,9 +848,9 @@ public class RegionWriteExecutor {
}
/**
- * Check the quota before creating time series.
+ * Check region availability, data types and quota before creating time
series.
*
- * @return null if the quota is not exceeded, otherwise return the
execution result.
+ * @return null if all checks pass, otherwise return the execution result.
*/
private RegionExecutionResult checkQuotaAndTypeBeforeCreatingTimeSeries(
final ISchemaRegion schemaRegion,
@@ -852,6 +858,11 @@ public class RegionWriteExecutor {
final int size,
final List<String> measurements,
final List<TSDataType> dataTypes) {
+ // Shutdown may clear SchemaEngine while the region's validation lock
still exists.
+ // Reject the request as retryable before dereferencing the missing
region.
+ if (schemaRegion == null) {
+ return createSchemaRegionUnavailableResult();
+ }
for (int i = 0; i < measurements.size(); ++i) {
if (dataTypes.get(i) == TSDataType.OBJECT) {
final String errorStr =
@@ -933,6 +944,9 @@ public class RegionWriteExecutor {
AlterTimeSeriesNode node, WritePlanNodeExecutionContext context,
boolean receivedFromPipe) {
ISchemaRegion schemaRegion =
schemaEngine.getSchemaRegion((SchemaRegionId) context.getRegionId());
+ if (schemaRegion == null) {
+ return createSchemaRegionUnavailableResult();
+ }
try {
MeasurementPath measurementPath =
schemaRegion.fetchMeasurementPath(node.getPath());
if (node.isAlterView() &&
!measurementPath.getMeasurementSchema().isLogicalView()) {
@@ -957,6 +971,9 @@ public class RegionWriteExecutor {
new PipeEnrichedWritePlanNode(node), context)
: PlanVisitor.super.visitAlterTimeSeries(node, context);
} catch (MetadataException e) {
+ if (schemaEngine.isClearing()) {
+ return createSchemaRegionUnavailableResult();
+ }
return RegionExecutionResult.create(
true, e.getMessage(), RpcUtils.getStatus(e.getErrorCode(),
e.getMessage()));
}
@@ -1123,6 +1140,9 @@ public class RegionWriteExecutor {
final ISchemaRegion schemaRegion =
schemaEngine.getSchemaRegion((SchemaRegionId) context.getRegionId());
if
(CONFIG.getSchemaRegionConsensusProtocolClass().equals(ConsensusFactory.RATIS_CONSENSUS))
{
+ if (schemaRegion == null) {
+ return createSchemaRegionUnavailableResult();
+ }
context.getRegionWriteValidationRWLock().writeLock().lock();
try {
// step 1. make sure all target paths do NOT exist.
@@ -1180,6 +1200,9 @@ public class RegionWriteExecutor {
final boolean receivedFromPipe) {
final ISchemaRegion schemaRegion =
schemaEngine.getSchemaRegion((SchemaRegionId) context.getRegionId());
+ if (schemaRegion == null) {
+ return createSchemaRegionUnavailableResult();
+ }
try {
schemaRegion.checkSchemaQuota(node.getTableName(),
node.getDeviceIdList());
} catch (final SchemaQuotaExceededException e) {
@@ -1192,6 +1215,15 @@ public class RegionWriteExecutor {
: PlanVisitor.super.visitCreateOrUpdateTableDevice(node, context);
}
+ private RegionExecutionResult createSchemaRegionUnavailableResult() {
+ // A validation lock can outlive its region during shutdown, so all
local schema checks
+ // must report a missing region as retryable even when the lock is still
registered.
+ final String message =
+
DataNodeQueryMessages.MESSAGE_SCHEMA_REGION_IS_UNAVAILABLE_PLEASE_RETRY_LATER_D642510A;
+ return RegionExecutionResult.create(
+ false, message,
RpcUtils.getStatus(TSStatusCode.NO_AVAILABLE_REGION_GROUP, message));
+ }
+
@Override
public RegionExecutionResult visitPipeEnrichedWritePlanNode(
final PipeEnrichedWritePlanNode node, final
WritePlanNodeExecutionContext context) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/SchemaEngine.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/SchemaEngine.java
index 58a8c96a9dd..ce8ba551a40 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/SchemaEngine.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/SchemaEngine.java
@@ -91,6 +91,9 @@ public class SchemaEngine {
private SchemaMetricManager schemaMetricManager;
+ // Published so write handlers can identify teardown failures before region
locks are removed.
+ private volatile boolean clearing;
+
private final DataNodeSchemaQuotaManager schemaQuotaManager =
DataNodeSchemaQuotaManager.getInstance();
@@ -110,6 +113,7 @@ public class SchemaEngine {
}
public void init() {
+ clearing = false;
logger.info(
DataNodeSchemaMessages.USED_SCHEMA_ENGINE_MODE,
CommonDescriptor.getInstance().getConfig().getSchemaEngineMode());
@@ -244,6 +248,7 @@ public class SchemaEngine {
}
public synchronized void clear() {
+ clearing = true;
schemaRegionLoader.clear();
// clearSchemaResource will shut down release and flush task in PBTree
mode, which must be
@@ -268,6 +273,10 @@ public class SchemaEngine {
ClusterTemplateManager.getInstance().clear();
}
+ public boolean isClearing() {
+ return clearing;
+ }
+
public ISchemaRegion getSchemaRegion(final SchemaRegionId regionId) {
return schemaRegionMap.get(regionId);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
index c29f4ec7d39..f920c6d0efe 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImpl.java
@@ -1041,6 +1041,9 @@ public class MTreeBelowSGMemoryImpl {
public IMemMNode getNodeByPath(final PartialPath path) throws
PathNotExistException {
final String[] nodes = path.getNodes();
IMemMNode cur = databaseMNode;
+ if (cur == null) {
+ throw new PathNotExistException(path.getFullPath(), true);
+ }
IMemMNode next;
for (int i = levelOfDB + 1; i < nodes.length; i++) {
next = cur.getChild(nodes[i]);
@@ -1924,6 +1927,9 @@ public class MTreeBelowSGMemoryImpl {
// region table device management
public int getTableDeviceNotExistNum(final String tableName, final
List<Object[]> deviceIdList) {
+ if (databaseMNode == null) {
+ return deviceIdList.size();
+ }
final IMemMNode tableNode = databaseMNode.getChild(tableName);
int notExistNum = deviceIdList.size();
if (tableNode == null) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
index 188620ef6a5..e6ee1e36ffa 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImpl.java
@@ -1074,6 +1074,9 @@ public class MTreeBelowSGCachedImpl {
ICachedMNode cur = databaseMNode;
ICachedMNode next;
try {
+ if (cur == null) {
+ throw new PathNotExistException(path.getFullPath(), true);
+ }
for (int i = levelOfDB + 1; i < nodes.length; i++) {
next = store.getChild(cur, nodes[i]);
if (next == null) {
@@ -1090,7 +1093,9 @@ public class MTreeBelowSGCachedImpl {
pinMNode(cur);
return cur;
} finally {
- unPinPath(cur);
+ if (cur != null) {
+ unPinPath(cur);
+ }
}
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutorTest.java
index ac3215d7027..22521458c17 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutorTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutorTest.java
@@ -22,14 +22,30 @@ package org.apache.iotdb.db.queryengine.execution.executor;
import org.apache.iotdb.common.rpc.thrift.TSStatus;
import org.apache.iotdb.commons.consensus.ConsensusGroupId;
import org.apache.iotdb.commons.consensus.DataRegionId;
+import org.apache.iotdb.commons.consensus.SchemaRegionId;
import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.path.MeasurementPath;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import
org.apache.iotdb.commons.schema.view.viewExpression.leaf.TimeSeriesViewOperand;
+import org.apache.iotdb.consensus.ConsensusFactory;
import org.apache.iotdb.consensus.IConsensus;
import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.exception.metadata.PathNotExistException;
import org.apache.iotdb.db.protocol.thrift.impl.DataNodeRegionManager;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.WritePlanNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.metadata.write.AlterTimeSeriesNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.metadata.write.InternalCreateMultiTimeSeriesNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.metadata.write.InternalCreateTimeSeriesNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.metadata.write.MeasurementGroup;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.metadata.write.view.CreateLogicalViewNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.pipe.PipeEnrichedWritePlanNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
+import
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.CreateOrUpdateTableDeviceNode;
+import
org.apache.iotdb.db.queryengine.plan.statement.metadata.AlterTimeSeriesStatement.AlterType;
import org.apache.iotdb.db.schemaengine.SchemaEngine;
+import org.apache.iotdb.db.schemaengine.schemaregion.ISchemaRegion;
import org.apache.iotdb.db.schemaengine.template.ClusterTemplateManager;
import org.apache.iotdb.db.trigger.executor.TriggerFireResult;
import org.apache.iotdb.db.trigger.executor.TriggerFireVisitor;
@@ -37,17 +53,269 @@ import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.iotdb.trigger.api.enums.TriggerEvent;
import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.utils.Pair;
import org.junit.Test;
import org.mockito.Mockito;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class RegionWriteExecutorTest {
+ @Test
+ public void testAlterTimeSeriesRegionAvailability() throws Exception {
+ // Alter validation fetches the measurement before consensus; a missing
region must be
+ // retryable.
+ checkSchemaWriteWithRegionAvailability(
+ new AlterTimeSeriesNode(
+ new PlanNodeId("alter"),
+ new MeasurementPath("root.sg.d.s"),
+ AlterType.ADD_TAGS,
+ Collections.singletonMap("tag", "value"),
+ null,
+ null,
+ null,
+ false));
+ }
+
+ @Test
+ public void testCreateLogicalViewRegionAvailability() throws Exception {
+ // Ratis checks target existence locally. SimpleConsensus must retain its
direct write path.
+ checkSchemaWriteWithRegionAvailability(
+ new CreateLogicalViewNode(
+ new PlanNodeId("view"),
+ Collections.singletonMap(
+ new MeasurementPath("root.sg.d.v"), new
TimeSeriesViewOperand("root.sg.d.s"))));
+ }
+
+ @Test
+ public void testCreateOrUpdateTableDeviceRegionAvailability() throws
Exception {
+ // Table-device quota validation has the same missing-region window as
tree auto-creation.
+ checkSchemaWriteWithRegionAvailability(
+ new CreateOrUpdateTableDeviceNode(
+ new PlanNodeId("table"),
+ "db",
+ "table1",
+ Collections.singletonList(new Object[] {"device1"}),
+ Collections.emptyList(),
+ Collections.singletonList(new Object[0])));
+ }
+
+ private void checkSchemaWriteWithRegionAvailability(final WritePlanNode
node) throws Exception {
+ final String originalProtocol =
+
IoTDBDescriptor.getInstance().getConfig().getSchemaRegionConsensusProtocolClass();
+ try {
+ for (String protocol :
+ new String[] {ConsensusFactory.RATIS_CONSENSUS,
ConsensusFactory.SIMPLE_CONSENSUS}) {
+
IoTDBDescriptor.getInstance().getConfig().setSchemaRegionConsensusProtocolClass(protocol);
+ for (boolean available : new boolean[] {false, true}) {
+ for (boolean fromPipe : new boolean[] {false, true}) {
+ final IConsensus consensus = Mockito.mock(IConsensus.class);
+ final DataNodeRegionManager regionManager =
Mockito.mock(DataNodeRegionManager.class);
+ final SchemaEngine schemaEngine = Mockito.mock(SchemaEngine.class);
+ final SchemaRegionId regionId = new SchemaRegionId(1);
+ final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ // Retain the lock even when the region has already been cleared
during shutdown.
+
Mockito.when(regionManager.getRegionLock(regionId)).thenReturn(lock);
+ final ISchemaRegion region = Mockito.mock(ISchemaRegion.class);
+ Mockito.when(schemaEngine.getSchemaRegion(regionId))
+ .thenReturn(available ? region : null);
+ if (node instanceof AlterTimeSeriesNode alterNode) {
+ Mockito.when(region.fetchMeasurementPath(alterNode.getPath()))
+ .thenReturn(new MeasurementPath("root.sg.d.s",
TSDataType.INT64));
+ }
+ final RegionWriteExecutor executor =
+ new RegionWriteExecutor(
+ Mockito.mock(IConsensus.class),
+ consensus,
+ regionManager,
+ schemaEngine,
+ Mockito.mock(ClusterTemplateManager.class),
+ Mockito.mock(TriggerFireVisitor.class));
+ final WritePlanNode request = fromPipe ? new
PipeEnrichedWritePlanNode(node) : node;
+ Mockito.when(consensus.write(regionId, request))
+ .thenReturn(new
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+
+ final boolean validatesRegion =
+ !(node instanceof CreateLogicalViewNode)
+ || ConsensusFactory.RATIS_CONSENSUS.equals(protocol);
+ final boolean accepted = available || !validatesRegion;
+ final RegionExecutionResult result = executor.execute(regionId,
request);
+ assertEquals(accepted, result.isAccepted());
+ assertEquals(
+ (accepted ? TSStatusCode.SUCCESS_STATUS :
TSStatusCode.NO_AVAILABLE_REGION_GROUP)
+ .getStatusCode(),
+ result.getStatus().getCode());
+ assertFalse(lock.isWriteLocked());
+ assertEquals(0, lock.getReadLockCount());
+ if (accepted) {
+ Mockito.verify(consensus).write(regionId, request);
+ if (node instanceof AlterTimeSeriesNode alterNode) {
+
Mockito.verify(region).fetchMeasurementPath(alterNode.getPath());
+ } else if (node instanceof CreateOrUpdateTableDeviceNode
tableNode) {
+ Mockito.verify(region)
+ .checkSchemaQuota(tableNode.getTableName(),
tableNode.getDeviceIdList());
+ } else if (validatesRegion) {
+ Mockito.verify(region)
+ .checkMeasurementExistence(
+ new PartialPath("root.sg.d"),
Collections.singletonList("v"), null);
+ }
+ } else {
+ Mockito.verifyZeroInteractions(consensus);
+ assertEquals(result.getMessage(),
result.getStatus().getMessage());
+ }
+ }
+ }
+ }
+ } finally {
+ IoTDBDescriptor.getInstance()
+ .getConfig()
+ .setSchemaRegionConsensusProtocolClass(originalProtocol);
+ }
+ }
+
+ @Test
+ public void testInternalCreateTimeSeriesAfterSchemaRegionCleared() throws
Exception {
+ // A shutdown can clear the region while its validation lock remains
registered.
+ checkInternalCreateWithRegionAvailability(false, false);
+ }
+
+ @Test
+ public void testInternalCreateMultiTimeSeriesAfterSchemaRegionCleared()
throws Exception {
+ // Batch and Pipe auto-creation must return a retryable status for the
same shutdown window.
+ checkInternalCreateWithRegionAvailability(true, false);
+ }
+
+ @Test
+ public void testInternalCreateWithAvailableSchemaRegion() throws Exception {
+ // The guard must preserve quota checks and consensus writes for both
live-region requests.
+ checkInternalCreateWithRegionAvailability(false, true);
+ checkInternalCreateWithRegionAvailability(true, true);
+ }
+
+ @Test
+ public void testSchemaWriteWhenRegionIsClearedAfterLookup() throws Exception
{
+ final IConsensus consensus = Mockito.mock(IConsensus.class);
+ final DataNodeRegionManager regionManager =
Mockito.mock(DataNodeRegionManager.class);
+ final SchemaEngine schemaEngine = Mockito.mock(SchemaEngine.class);
+ final SchemaRegionId regionId = new SchemaRegionId(1);
+ final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ final ISchemaRegion region = Mockito.mock(ISchemaRegion.class);
+ final AtomicBoolean clearing = new AtomicBoolean(false);
+ Mockito.when(regionManager.getRegionLock(regionId)).thenReturn(lock);
+ Mockito.when(schemaEngine.getSchemaRegion(regionId)).thenReturn(region);
+ Mockito.when(schemaEngine.isClearing()).thenAnswer(invocation ->
clearing.get());
+ Mockito.doAnswer(
+ invocation -> {
+ // The region was obtained before shutdown cleared its MTree.
+ clearing.set(true);
+ throw new PathNotExistException("root.sg.d.s", true);
+ })
+ .when(region)
+ .fetchMeasurementPath(Mockito.any());
+
+ final RegionWriteExecutor executor =
+ new RegionWriteExecutor(
+ Mockito.mock(IConsensus.class),
+ consensus,
+ regionManager,
+ schemaEngine,
+ Mockito.mock(ClusterTemplateManager.class),
+ Mockito.mock(TriggerFireVisitor.class));
+ final AlterTimeSeriesNode request =
+ new AlterTimeSeriesNode(
+ new PlanNodeId("alter"),
+ new MeasurementPath("root.sg.d.s"),
+ AlterType.ADD_TAGS,
+ Collections.singletonMap("tag", "value"),
+ null,
+ null,
+ null,
+ false);
+
+ final RegionExecutionResult result = executor.execute(regionId, request);
+
+ assertFalse(result.isAccepted());
+ assertEquals(
+ TSStatusCode.NO_AVAILABLE_REGION_GROUP.getStatusCode(),
result.getStatus().getCode());
+ assertFalse(lock.isWriteLocked());
+ assertEquals(0, lock.getReadLockCount());
+ Mockito.verifyZeroInteractions(consensus);
+ }
+
+ private void checkInternalCreateWithRegionAvailability(boolean batch,
boolean available)
+ throws Exception {
+ final String originalProtocol =
+
IoTDBDescriptor.getInstance().getConfig().getSchemaRegionConsensusProtocolClass();
+ try {
+ for (String protocol :
+ new String[] {ConsensusFactory.SIMPLE_CONSENSUS,
ConsensusFactory.RATIS_CONSENSUS}) {
+
IoTDBDescriptor.getInstance().getConfig().setSchemaRegionConsensusProtocolClass(protocol);
+ for (boolean fromPipe : new boolean[] {false, true}) {
+ final IConsensus consensus = Mockito.mock(IConsensus.class);
+ final DataNodeRegionManager regionManager =
Mockito.mock(DataNodeRegionManager.class);
+ final SchemaEngine schemaEngine = Mockito.mock(SchemaEngine.class);
+ final SchemaRegionId regionId = new SchemaRegionId(1);
+ final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ Mockito.when(regionManager.getRegionLock(regionId)).thenReturn(lock);
+ final ISchemaRegion region = Mockito.mock(ISchemaRegion.class);
+ Mockito.when(schemaEngine.getSchemaRegion(regionId))
+ .thenReturn(available ? region : null);
+ final RegionWriteExecutor executor =
+ new RegionWriteExecutor(
+ Mockito.mock(IConsensus.class),
+ consensus,
+ regionManager,
+ schemaEngine,
+ Mockito.mock(ClusterTemplateManager.class),
+ Mockito.mock(TriggerFireVisitor.class));
+ final PartialPath device = new PartialPath("root.sg.d");
+ final MeasurementGroup measurements = new MeasurementGroup();
+ measurements.addMeasurement(
+ "s", TSDataType.INT64, TSEncoding.PLAIN, CompressionType.SNAPPY);
+ final WritePlanNode node =
+ batch
+ ? new InternalCreateMultiTimeSeriesNode(
+ new PlanNodeId("test"),
+ Collections.singletonMap(device, new Pair<>(false,
measurements)))
+ : new InternalCreateTimeSeriesNode(
+ new PlanNodeId("test"), device, measurements, false);
+ final WritePlanNode request = fromPipe ? new
PipeEnrichedWritePlanNode(node) : node;
+ Mockito.when(consensus.write(regionId, request))
+ .thenReturn(new
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+
+ final RegionExecutionResult result = executor.execute(regionId,
request);
+ assertEquals(available, result.isAccepted());
+ assertEquals(
+ (available ? TSStatusCode.SUCCESS_STATUS :
TSStatusCode.NO_AVAILABLE_REGION_GROUP)
+ .getStatusCode(),
+ result.getStatus().getCode());
+ assertFalse(lock.isWriteLocked());
+ assertEquals(0, lock.getReadLockCount());
+ if (available) {
+ Mockito.verify(region).checkSchemaQuota(device, 1);
+ Mockito.verify(consensus).write(regionId, request);
+ } else {
+ Mockito.verifyZeroInteractions(consensus);
+ assertEquals(1, measurements.size());
+ }
+ }
+ }
+ } finally {
+ IoTDBDescriptor.getInstance()
+ .getConfig()
+ .setSchemaRegionConsensusProtocolClass(originalProtocol);
+ }
+ }
+
@Test
public void testInsertRowNode() throws ConsensusException {
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
index 1dd3fa456b7..2b811f1e945 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/mem/MTreeBelowSGMemoryImplTest.java
@@ -43,6 +43,7 @@ import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -50,6 +51,19 @@ import java.util.concurrent.atomic.AtomicInteger;
public class MTreeBelowSGMemoryImplTest {
+ @Test
+ public void testPathAndTableDeviceChecksAfterClear() throws Exception {
+ final MTreeBelowSGMemoryImpl mtree = newMTree();
+ mtree.clear();
+
+ Assert.assertFalse(mtree.checkDeviceNodeExists(new
PartialPath("root.sg.d")));
+ assertPathNotExist(mtree, new PartialPath("root.sg.d"));
+ Assert.assertEquals(
+ 2,
+ mtree.getTableDeviceNotExistNum(
+ "table", Arrays.asList(new Object[] {"a"}, new Object[] {"b"})));
+ }
+
@Test
public void
testDeviceDescendantFlagIsMaintainedAcrossCreateDeleteAndRebuild() throws
Exception {
final MTreeBelowSGMemoryImpl mtree = newMTree();
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
index 81936640b49..5c0ebf82d40 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/schemaregion/mtree/impl/pbtree/MTreeBelowSGCachedImplTest.java
@@ -178,6 +178,17 @@ public class MTreeBelowSGCachedImplTest {
new SchemaRegionCachedMetric(regionStatistics, "root.sg"));
}
+ @Test
+ public void testPathLookupAfterClear() throws Exception {
+ mtree = newMTree();
+ mtree.clear();
+ final MTreeBelowSGCachedImpl clearedMTree = mtree;
+ mtree = null;
+
+ Assert.assertFalse(clearedMTree.checkDeviceNodeExists(new
PartialPath("root.sg.d")));
+ assertPathNotExist(clearedMTree, new PartialPath("root.sg.d"));
+ }
+
private void createBooleanTimeSeries(final String path) throws
MetadataException {
mtree.createTimeSeries(
new MeasurementPath(path),
@@ -207,8 +218,13 @@ public class MTreeBelowSGCachedImplTest {
}
private void assertPathNotExist(final PartialPath path) throws
MetadataException {
+ assertPathNotExist(mtree, path);
+ }
+
+ private void assertPathNotExist(final MTreeBelowSGCachedImpl targetMTree,
final PartialPath path)
+ throws MetadataException {
try {
- mtree.getNodeByPath(path);
+ targetMTree.getNodeByPath(path);
Assert.fail("Expected path not exist: " + path.getFullPath());
} catch (final PathNotExistException ignored) {
// expected