diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBSetSystemStatusTableIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBSetSystemStatusTableIT.java index efeeb888e3fe8..3699c08ab042f 100644 --- a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBSetSystemStatusTableIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBSetSystemStatusTableIT.java @@ -64,19 +64,45 @@ public void setSystemStatus() { () -> { ResultSet resultSet = statement.executeQuery("SHOW DATANODES"); int num = 0; + int manualReasonNum = 0; try { while (resultSet.next()) { String status = resultSet.getString("Status"); if (status.equals("ReadOnly")) { num++; } + // The ReadOnly requested through SQL is reported with the Manual reason. + // The StatusReason column only appears once at least one node has reported + // its reason, so a missing column means the condition is not satisfied yet. + if ("Manual".equals(resultSet.getString("StatusReason"))) { + manualReasonNum++; + } } - } catch (InconsistentDataException e) { + } catch (Exception e) { + // The StatusReason column (and the Manual reason behind it) may not be + // propagated yet, or the cluster may be transiently inconsistent while the + // status is changing: treat both as "not satisfied yet". return false; } - return num == EnvFactory.getEnv().getDataNodeWrapperList().size(); + return num == EnvFactory.getEnv().getDataNodeWrapperList().size() + && manualReasonNum == EnvFactory.getEnv().getDataNodeWrapperList().size(); }); + // The table-model NODES view keeps the status and its reason in two separate columns + // instead of merging them into "ReadOnly(Manual)". + try (ResultSet nodesResultSet = + statement.executeQuery("select * from information_schema.nodes")) { + int dataNodeNum = 0; + while (nodesResultSet.next()) { + if ("DataNode".equals(nodesResultSet.getString("node_type"))) { + dataNodeNum++; + Assert.assertEquals("ReadOnly", nodesResultSet.getString("status")); + Assert.assertEquals("Manual", nodesResultSet.getString("status_reason")); + } + } + Assert.assertEquals(EnvFactory.getEnv().getDataNodeWrapperList().size(), dataNodeNum); + } + statement.execute("SET SYSTEM TO RUNNING ON CLUSTER"); Awaitility.await() .atMost(10, TimeUnit.SECONDS) diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDatabaseIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDatabaseIT.java index 3d24915c1fed4..7db6301fbc4a3 100644 --- a/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDatabaseIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDatabaseIT.java @@ -680,6 +680,7 @@ public void testInformationSchema() throws SQLException { "node_id,INT32,TAG,", "node_type,STRING,ATTRIBUTE,", "status,STRING,ATTRIBUTE,", + "status_reason,STRING,ATTRIBUTE,", "internal_address,STRING,ATTRIBUTE,", "internal_port,INT32,ATTRIBUTE,", "version,STRING,ATTRIBUTE,", diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java index f86681f2bbec2..c9977103a053f 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/ConfigManager.java @@ -120,6 +120,7 @@ import org.apache.iotdb.confignode.manager.externalservice.ExternalServiceManager; import org.apache.iotdb.confignode.manager.load.LoadManager; import org.apache.iotdb.confignode.manager.load.cache.node.NodeHeartbeatSample; +import org.apache.iotdb.confignode.manager.load.cache.node.NodeStatistics; import org.apache.iotdb.confignode.manager.node.ClusterNodeStartUtils; import org.apache.iotdb.confignode.manager.node.NodeManager; import org.apache.iotdb.confignode.manager.node.NodeMetrics; @@ -622,26 +623,47 @@ public TShowClusterResp showCluster() { .sorted(Comparator.comparingInt(TDataNodeLocation::getDataNodeId)) .collect(Collectors.toList()); Map nodeVersionInfo = getNodeManager().getNodeVersionInfo(); - Map nodeStatus = getLoadManager().getNodeStatusWithReason(); + Map nodeStatisticsSnapshot = + getLoadManager().getNodeStatisticsSnapshot(); + Map nodeStatus = new HashMap<>(); + Map nodeStatusReason = new HashMap<>(); + nodeStatisticsSnapshot.forEach( + (nodeId, statistics) -> { + nodeStatus.put(nodeId, statistics.getStatus().getStatus()); + if (statistics.getStatusReason() != null) { + nodeStatusReason.put(nodeId, statistics.getStatusReason()); + } + }); configNodeLocations.forEach( - configNodeLocation -> - nodeStatus.putIfAbsent( - configNodeLocation.getConfigNodeId(), NodeStatus.Unknown.toString())); + configNodeLocation -> { + int configNodeId = configNodeLocation.getConfigNodeId(); + if (!nodeStatus.containsKey(configNodeId)) { + // No cache entry means the node has never reported a heartbeat. + nodeStatus.put(configNodeId, NodeStatus.Unknown.toString()); + } + }); dataNodeLocations.forEach( - dataNodeLocation -> - nodeStatus.putIfAbsent( - dataNodeLocation.getDataNodeId(), NodeStatus.Unknown.toString())); + dataNodeLocation -> { + int dataNodeId = dataNodeLocation.getDataNodeId(); + if (!nodeStatus.containsKey(dataNodeId)) { + // No cache entry means the node has never reported a heartbeat. + nodeStatus.put(dataNodeId, NodeStatus.Unknown.toString()); + } + }); List aiNodeLocations = getNodeManager().getRegisteredAINodes().stream() .map(TAINodeConfiguration::getLocation) .sorted(Comparator.comparingInt(TAINodeLocation::getAiNodeId)) .collect(Collectors.toList()); - Map nodeStatusMap = getLoadManager().getNodeStatusWithReason(); aiNodeLocations.forEach( - aiNodeLocation -> - nodeStatusMap.putIfAbsent( - aiNodeLocation.getAiNodeId(), NodeStatus.Unknown.toString())); + aiNodeLocation -> { + int aiNodeId = aiNodeLocation.getAiNodeId(); + if (!nodeStatus.containsKey(aiNodeId)) { + // No cache entry means the node has never reported a heartbeat. + nodeStatus.put(aiNodeId, NodeStatus.Unknown.toString()); + } + }); return new TShowClusterResp() .setStatus(status) @@ -649,6 +671,7 @@ public TShowClusterResp showCluster() { .setDataNodeList(dataNodeLocations) .setAiNodeList(aiNodeLocations) .setNodeStatus(nodeStatus) + .setNodeStatusReason(nodeStatusReason) .setNodeVersionInfo(nodeVersionInfo); } else { return new TShowClusterResp() @@ -657,6 +680,7 @@ public TShowClusterResp showCluster() { .setDataNodeList(Collections.emptyList()) .setAiNodeList(Collections.emptyList()) .setNodeStatus(Collections.emptyMap()) + .setNodeStatusReason(Collections.emptyMap()) .setNodeVersionInfo(Collections.emptyMap()); } } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/LoadManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/LoadManager.java index 5a424ea5a5e73..2c05870c78af2 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/LoadManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/LoadManager.java @@ -40,6 +40,7 @@ import org.apache.iotdb.confignode.manager.load.cache.LoadCache; import org.apache.iotdb.confignode.manager.load.cache.consensus.ConsensusGroupHeartbeatSample; import org.apache.iotdb.confignode.manager.load.cache.node.NodeHeartbeatSample; +import org.apache.iotdb.confignode.manager.load.cache.node.NodeStatistics; import org.apache.iotdb.confignode.manager.load.cache.region.RegionHeartbeatSample; import org.apache.iotdb.confignode.manager.load.service.EventService; import org.apache.iotdb.confignode.manager.load.service.HeartbeatService; @@ -259,22 +260,24 @@ public NodeStatus getNodeStatus(int nodeId) { } /** - * Safely get the specified Node's current status with reason. + * Safely get the specified Node's current status reason. * * @param nodeId The specified NodeId - * @return The specified Node's current status if the nodeCache contains it, Unknown otherwise + * @return The reason why the Node is in its current status, null if the node has no reason or the + * cache doesn't exist */ - public String getNodeStatusWithReason(int nodeId) { - return loadCache.getNodeStatusWithReason(nodeId); + public String getNodeStatusReason(int nodeId) { + return loadCache.getNodeStatusReason(nodeId); } /** - * Get all Node's current status with reason. + * Get all Nodes' current statistics in a single traversal of the node cache, so that each node's + * status and reason come from the same statistics snapshot. * - * @return Map + * @return Map */ - public Map getNodeStatusWithReason() { - return loadCache.getNodeStatusWithReason(); + public Map getNodeStatisticsSnapshot() { + return loadCache.getNodeStatisticsSnapshot(); } /** diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/LoadCache.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/LoadCache.java index 3416246811edc..00c766fc9eb38 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/LoadCache.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/LoadCache.java @@ -536,25 +536,29 @@ public NodeStatus getNodeStatus(int nodeId) { } /** - * Safely get the specified Node's current status with reason. + * Safely get the specified Node's current status reason. * * @param nodeId The specified NodeId - * @return The specified Node's current status if the nodeCache contains it, Unknown otherwise + * @return The reason why the Node is in its current status, null if the node has no reason or the + * cache doesn't exist */ - public String getNodeStatusWithReason(int nodeId) { - return Optional.ofNullable(nodeCacheMap.get(nodeId)) - .map(BaseNodeCache::getNodeStatusWithReason) - .orElseGet(() -> NodeStatus.Unknown.getStatus() + "(NoHeartbeat)"); + public String getNodeStatusReason(int nodeId) { + BaseNodeCache nodeCache = nodeCacheMap.get(nodeId); + return nodeCache == null ? null : nodeCache.getNodeStatusReason(); } /** - * Get all Node's current status with reason. + * Get all Nodes' current statistics in a single traversal. Each node's status and reason are read + * from the same immutable {@link NodeStatistics} snapshot, so a concurrent heartbeat update + * cannot produce a status/reason pair from different heartbeats. * - * @return Map + * @return Map */ - public Map getNodeStatusWithReason() { + public Map getNodeStatisticsSnapshot() { return nodeCacheMap.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getNodeStatusWithReason())); + .collect( + Collectors.toMap( + Map.Entry::getKey, e -> (NodeStatistics) e.getValue().getCurrentStatistics())); } /** diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/node/BaseNodeCache.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/node/BaseNodeCache.java index 4eeb96344ccd1..6b05a1906155b 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/node/BaseNodeCache.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/cache/node/BaseNodeCache.java @@ -58,12 +58,9 @@ public NodeStatus getNodeStatus() { } /** - * @return The reason why lead to current NodeStatus. + * @return The reason why lead to current NodeStatus, null if there is none. */ - public String getNodeStatusWithReason() { - NodeStatistics statistics = (NodeStatistics) this.currentStatistics.get(); - return statistics.getStatusReason() == null - ? statistics.getStatus().getStatus() - : statistics.getStatus().getStatus() + "(" + statistics.getStatusReason() + ")"; + public String getNodeStatusReason() { + return ((NodeStatistics) currentStatistics.get()).getStatusReason(); } } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java index 64d4cf741e159..86dff6eeb5005 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/node/NodeManager.java @@ -501,7 +501,7 @@ public List getRegisteredAINodeInfoList() { for (TAINodeConfiguration aiNodeConfiguration : getRegisteredAINodes()) { TAINodeInfo aiNodeInfo = new TAINodeInfo(); aiNodeInfo.setAiNodeId(aiNodeConfiguration.getLocation().getAiNodeId()); - aiNodeInfo.setStatus(getLoadManager().getNodeStatusWithReason(aiNodeInfo.getAiNodeId())); + aiNodeInfo.setStatus(getLoadManager().getNodeStatus(aiNodeInfo.getAiNodeId()).getStatus()); aiNodeInfo.setInternalAddress(aiNodeConfiguration.getLocation().getInternalEndPoint().ip); aiNodeInfo.setInternalPort(aiNodeConfiguration.getLocation().getInternalEndPoint().port); aiNodeInfoList.add(aiNodeInfo); @@ -720,7 +720,8 @@ public List getRegisteredDataNodeInfoList() { TDataNodeInfo dataNodeInfo = new TDataNodeInfo(); int dataNodeId = registeredDataNode.getLocation().getDataNodeId(); dataNodeInfo.setDataNodeId(dataNodeId); - dataNodeInfo.setStatus(getLoadManager().getNodeStatusWithReason(dataNodeId)); + dataNodeInfo.setStatus(getLoadManager().getNodeStatus(dataNodeId).getStatus()); + dataNodeInfo.setStatusReason(getLoadManager().getNodeStatusReason(dataNodeId)); dataNodeInfo.setRpcAddresss( registeredDataNode.getLocation().getClientRpcEndPoint().getIp()); dataNodeInfo.setRpcPort( @@ -856,7 +857,8 @@ public List getRegisteredConfigNodeInfoList() { TConfigNodeInfo info = new TConfigNodeInfo(); int configNodeId = configNodeLocation.getConfigNodeId(); info.setConfigNodeId(configNodeId); - info.setStatus(getLoadManager().getNodeStatusWithReason(configNodeId)); + info.setStatus(getLoadManager().getNodeStatus(configNodeId).getStatus()); + info.setStatusReason(getLoadManager().getNodeStatusReason(configNodeId)); info.setInternalAddress(configNodeLocation.getInternalEndPoint().getIp()); info.setInternalPort(configNodeLocation.getInternalEndPoint().getPort()); info.setRoleType( diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/load/cache/NodeCacheTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/load/cache/NodeCacheTest.java index a400692956621..5a83e920fcb90 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/load/cache/NodeCacheTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/manager/load/cache/NodeCacheTest.java @@ -19,13 +19,18 @@ package org.apache.iotdb.confignode.manager.load.cache; import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.cluster.NodeType; import org.apache.iotdb.confignode.manager.load.cache.node.ConfigNodeHeartbeatCache; import org.apache.iotdb.confignode.manager.load.cache.node.DataNodeHeartbeatCache; import org.apache.iotdb.confignode.manager.load.cache.node.NodeHeartbeatSample; +import org.apache.iotdb.confignode.manager.load.cache.node.NodeStatistics; +import org.apache.iotdb.mpp.rpc.thrift.TDataNodeHeartbeatResp; import org.junit.Assert; import org.junit.Test; +import java.util.Map; + public class NodeCacheTest { @Test @@ -48,4 +53,65 @@ public void updateStatisticsTest() { Assert.assertEquals(NodeStatus.Running, configNodeHeartbeatCache.getNodeStatus()); Assert.assertEquals(0, configNodeHeartbeatCache.getLoadScore()); } + + @Test + public void statusReasonPropagationTest() { + DataNodeHeartbeatCache dataNodeHeartbeatCache = new DataNodeHeartbeatCache(1); + + // A heartbeat response carrying a status reason (e.g. ReadOnly + DiskFull from the DataNode) + // publishes the reason into the node statistics. + TDataNodeHeartbeatResp heartbeatResp = + new TDataNodeHeartbeatResp() + .setHeartbeatTimestamp(System.nanoTime()) + .setStatus(NodeStatus.ReadOnly.getStatus()) + .setStatusReason(NodeStatus.DISK_FULL); + dataNodeHeartbeatCache.cacheHeartbeatSample(new NodeHeartbeatSample(heartbeatResp)); + dataNodeHeartbeatCache.updateCurrentStatistics(false); + Assert.assertEquals(NodeStatus.ReadOnly, dataNodeHeartbeatCache.getNodeStatus()); + Assert.assertEquals(NodeStatus.DISK_FULL, dataNodeHeartbeatCache.getNodeStatusReason()); + + // A heartbeat response without a status reason clears the previous reason. + heartbeatResp = + new TDataNodeHeartbeatResp() + .setHeartbeatTimestamp(System.nanoTime()) + .setStatus(NodeStatus.Running.getStatus()); + dataNodeHeartbeatCache.cacheHeartbeatSample(new NodeHeartbeatSample(heartbeatResp)); + dataNodeHeartbeatCache.updateCurrentStatistics(false); + Assert.assertEquals(NodeStatus.Running, dataNodeHeartbeatCache.getNodeStatus()); + Assert.assertNull(dataNodeHeartbeatCache.getNodeStatusReason()); + + // An Unknown decided by the failure detector (a stale heartbeat) never carries a reason. The + // stale sample is accepted because the fresh cache has an empty sliding window. + DataNodeHeartbeatCache staleCache = new DataNodeHeartbeatCache(3); + staleCache.cacheHeartbeatSample( + new NodeHeartbeatSample(System.nanoTime() - 60_000_000_000L, NodeStatus.ReadOnly)); + staleCache.updateCurrentStatistics(false); + Assert.assertEquals(NodeStatus.Unknown, staleCache.getNodeStatus()); + Assert.assertNull(staleCache.getNodeStatusReason()); + } + + @Test + public void loadCacheSnapshotKeepsStatusAndReasonFromSameStatistics() { + LoadCache loadCache = new LoadCache(); + loadCache.createNodeHeartbeatCache(NodeType.DataNode, 1); + loadCache.cacheDataNodeHeartbeatSample( + 1, + new NodeHeartbeatSample( + new TDataNodeHeartbeatResp() + .setHeartbeatTimestamp(System.nanoTime()) + .setStatus(NodeStatus.ReadOnly.getStatus()) + .setStatusReason(NodeStatus.MANUAL))); + loadCache.updateNodeStatistics(false); + + // The snapshot used by SHOW CLUSTER reads status and reason from one statistics object. + Map snapshot = loadCache.getNodeStatisticsSnapshot(); + Assert.assertEquals(1, snapshot.size()); + Assert.assertEquals(NodeStatus.ReadOnly, snapshot.get(1).getStatus()); + Assert.assertEquals(NodeStatus.MANUAL, snapshot.get(1).getStatusReason()); + Assert.assertEquals(NodeStatus.MANUAL, loadCache.getNodeStatusReason(1)); + + // A missing cache yields no reason and Unknown status, without a fake merged string. + Assert.assertNull(loadCache.getNodeStatusReason(2)); + Assert.assertEquals(NodeStatus.Unknown, loadCache.getNodeStatus(2)); + } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java index 34ef951296ce8..27b3be4878e31 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImpl.java @@ -2581,8 +2581,7 @@ void sampleDiskLoad(TLoadSample loadSample) { RamUsageEstimator.humanReadableUnits((long) totalDisk), freeDiskRatio, commonConfig.getDiskSpaceWarningThreshold()); - commonConfig.setNodeStatus(NodeStatus.ReadOnly); - commonConfig.setStatusReason(NodeStatus.DISK_FULL); + commonConfig.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); } else if (NodeStatus.ReadOnly.equals(commonConfig.getNodeStatus()) && NodeStatus.DISK_FULL.equals(commonConfig.getStatusReason())) { commonConfig.setNodeStatus(NodeStatus.Running); @@ -2761,7 +2760,12 @@ public TShowAppliedConfigurationsResp showAppliedConfigurations() throws TExcept @Override public TSStatus setSystemStatus(String status) throws TException { try { - commonConfig.setNodeStatus(NodeStatus.parse(status)); + NodeStatus nodeStatus = NodeStatus.parse(status); + if (nodeStatus == NodeStatus.ReadOnly) { + commonConfig.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + } else { + commonConfig.setNodeStatus(nodeStatus); + } } catch (Exception e) { return RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR, e.getMessage()); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/InformationSchemaContentSupplierFactory.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/InformationSchemaContentSupplierFactory.java index c0e7ca6d91a57..584ea42f0fdfa 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/InformationSchemaContentSupplierFactory.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/InformationSchemaContentSupplierFactory.java @@ -1151,6 +1151,7 @@ protected void constructLine() { location.getConfigNodeId(), NODE_TYPE_CONFIG_NODE, showClusterResp.getNodeStatus().get(location.getConfigNodeId()), + getNodeStatusReason(location.getConfigNodeId()), location.getInternalEndPoint().getIp(), location.getInternalEndPoint().getPort(), showClusterResp.getNodeVersionInfo().get(location.getConfigNodeId())); @@ -1162,6 +1163,7 @@ protected void constructLine() { location.getDataNodeId(), NODE_TYPE_DATA_NODE, showClusterResp.getNodeStatus().get(location.getDataNodeId()), + getNodeStatusReason(location.getDataNodeId()), location.getInternalEndPoint().getIp(), location.getInternalEndPoint().getPort(), showClusterResp.getNodeVersionInfo().get(location.getDataNodeId())); @@ -1173,16 +1175,26 @@ protected void constructLine() { location.getAiNodeId(), NODE_TYPE_AI_NODE, showClusterResp.getNodeStatus().get(location.getAiNodeId()), + getNodeStatusReason(location.getAiNodeId()), location.getInternalEndPoint().getIp(), location.getInternalEndPoint().getPort(), showClusterResp.getNodeVersionInfo().get(location.getAiNodeId())); } } + private String getNodeStatusReason(int nodeId) { + // The optional field is absent when the ConfigNode is an old version. + if (!showClusterResp.isSetNodeStatusReason()) { + return null; + } + return showClusterResp.getNodeStatusReason().get(nodeId); + } + private void buildNodeTsBlock( int nodeId, String nodeType, String nodeStatus, + String nodeStatusReason, String internalAddress, int internalPort, TNodeVersionInfo versionInfo) { @@ -1193,23 +1205,28 @@ private void buildNodeTsBlock( } else { columnBuilders[2].writeBinary(new Binary(nodeStatus, TSFileConfig.STRING_CHARSET)); } + if (nodeStatusReason == null) { + columnBuilders[3].appendNull(); + } else { + columnBuilders[3].writeBinary(new Binary(nodeStatusReason, TSFileConfig.STRING_CHARSET)); + } if (internalAddress == null) { - columnBuilders[3].appendNull(); + columnBuilders[4].appendNull(); } else { - columnBuilders[3].writeBinary(new Binary(internalAddress, TSFileConfig.STRING_CHARSET)); + columnBuilders[4].writeBinary(new Binary(internalAddress, TSFileConfig.STRING_CHARSET)); } - columnBuilders[4].writeInt(internalPort); + columnBuilders[5].writeInt(internalPort); if (versionInfo == null || versionInfo.getVersion() == null) { - columnBuilders[5].appendNull(); + columnBuilders[6].appendNull(); } else { - columnBuilders[5].writeBinary( + columnBuilders[6].writeBinary( new Binary(versionInfo.getVersion(), TSFileConfig.STRING_CHARSET)); } if (versionInfo == null || versionInfo.getBuildInfo() == null) { - columnBuilders[6].appendNull(); + columnBuilders[7].appendNull(); } else { - columnBuilders[6].writeBinary( + columnBuilders[7].writeBinary( new Binary(versionInfo.getBuildInfo(), TSFileConfig.STRING_CHARSET)); } resultBuilder.declarePosition(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java index 3ddd2531200dc..a0e3435dde9b4 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java @@ -1589,7 +1589,13 @@ public SettableFuture setSystemStatus(boolean onCluster, NodeS } } else { try { - CommonDescriptor.getInstance().getConfig().setNodeStatus(status); + if (status == NodeStatus.ReadOnly) { + CommonDescriptor.getInstance() + .getConfig() + .setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + } else { + CommonDescriptor.getInstance().getConfig().setNodeStatus(status); + } tsStatus = RpcUtils.getStatus(TSStatusCode.SUCCESS_STATUS); } catch (Exception e) { tsStatus = RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR, e.getMessage()); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTask.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTask.java index 17778269b7ea1..da0327908e0c9 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTask.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTask.java @@ -24,7 +24,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TNodeVersionInfo; import org.apache.iotdb.confignode.rpc.thrift.TShowClusterResp; import org.apache.iotdb.db.queryengine.common.header.DatasetHeader; -import org.apache.iotdb.db.queryengine.common.header.DatasetHeaderFactory; import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; import org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor; @@ -38,6 +37,7 @@ import org.apache.tsfile.read.common.block.TsBlockBuilder; import org.apache.tsfile.utils.Binary; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -57,6 +57,8 @@ private static void buildConfigNodesTsBlock( TsBlockBuilder builder, int nodeId, String nodeStatus, + String nodeStatusReason, + boolean hasStatusReason, String internalAddress, int internalPort, int configConsensusPort, @@ -71,35 +73,45 @@ private static void buildConfigNodesTsBlock( } else { builder.getColumnBuilder(2).writeBinary(new Binary(nodeStatus, TSFileConfig.STRING_CHARSET)); } + if (hasStatusReason) { + if (nodeStatusReason == null) { + builder.getColumnBuilder(3).appendNull(); + } else { + builder + .getColumnBuilder(3) + .writeBinary(new Binary(nodeStatusReason, TSFileConfig.STRING_CHARSET)); + } + } + int offset = hasStatusReason ? 1 : 0; if (internalAddress == null) { - builder.getColumnBuilder(3).appendNull(); + builder.getColumnBuilder(3 + offset).appendNull(); } else { builder - .getColumnBuilder(3) + .getColumnBuilder(3 + offset) .writeBinary(new Binary(internalAddress, TSFileConfig.STRING_CHARSET)); } - builder.getColumnBuilder(4).writeInt(internalPort); + builder.getColumnBuilder(4 + offset).writeInt(internalPort); builder - .getColumnBuilder(5) + .getColumnBuilder(5 + offset) .writeBinary( new Binary(Integer.toString(configConsensusPort), TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(6).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(7).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(8).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(9).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(10).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(6 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(7 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(8 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(9 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(10 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); if (versionInfo == null || versionInfo.getVersion() == null) { - builder.getColumnBuilder(11).appendNull(); + builder.getColumnBuilder(11 + offset).appendNull(); } else { builder - .getColumnBuilder(11) + .getColumnBuilder(11 + offset) .writeBinary(new Binary(versionInfo.getVersion(), TSFileConfig.STRING_CHARSET)); } if (versionInfo == null || versionInfo.getBuildInfo() == null) { - builder.getColumnBuilder(12).appendNull(); + builder.getColumnBuilder(12 + offset).appendNull(); } else { builder - .getColumnBuilder(12) + .getColumnBuilder(12 + offset) .writeBinary(new Binary(versionInfo.getBuildInfo(), TSFileConfig.STRING_CHARSET)); } builder.declarePosition(); @@ -109,6 +121,8 @@ private static void buildAINodeTsBlock( TsBlockBuilder builder, int nodeId, String nodeStatus, + String nodeStatusReason, + boolean hasStatusReason, String internalAddress, int internalPort, TNodeVersionInfo versionInfo) { @@ -123,33 +137,42 @@ private static void buildAINodeTsBlock( } else { builder.getColumnBuilder(2).writeBinary(new Binary(nodeStatus, TSFileConfig.STRING_CHARSET)); } - + if (hasStatusReason) { + if (nodeStatusReason == null) { + builder.getColumnBuilder(3).appendNull(); + } else { + builder + .getColumnBuilder(3) + .writeBinary(new Binary(nodeStatusReason, TSFileConfig.STRING_CHARSET)); + } + } + int offset = hasStatusReason ? 1 : 0; if (internalAddress == null) { - builder.getColumnBuilder(3).appendNull(); + builder.getColumnBuilder(3 + offset).appendNull(); } else { builder - .getColumnBuilder(3) + .getColumnBuilder(3 + offset) .writeBinary(new Binary(internalAddress, TSFileConfig.STRING_CHARSET)); } - builder.getColumnBuilder(4).writeInt(internalPort); - builder.getColumnBuilder(5).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(6).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(7).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(8).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(9).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); - builder.getColumnBuilder(10).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(4 + offset).writeInt(internalPort); + builder.getColumnBuilder(5 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(6 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(7 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(8 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(9 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(10 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); if (versionInfo == null || versionInfo.getVersion() == null) { - builder.getColumnBuilder(11).appendNull(); + builder.getColumnBuilder(11 + offset).appendNull(); } else { builder - .getColumnBuilder(11) + .getColumnBuilder(11 + offset) .writeBinary(new Binary(versionInfo.getVersion(), TSFileConfig.STRING_CHARSET)); } if (versionInfo == null || versionInfo.getBuildInfo() == null) { - builder.getColumnBuilder(12).appendNull(); + builder.getColumnBuilder(12 + offset).appendNull(); } else { builder - .getColumnBuilder(12) + .getColumnBuilder(12 + offset) .writeBinary(new Binary(versionInfo.getBuildInfo(), TSFileConfig.STRING_CHARSET)); } builder.declarePosition(); @@ -160,6 +183,8 @@ private static void buildDataNodesTsBlock( TsBlockBuilder builder, int nodeId, String nodeStatus, + String nodeStatusReason, + boolean hasStatusReason, String internalAddress, int internalPort, String rpcAddress, @@ -178,45 +203,57 @@ private static void buildDataNodesTsBlock( } else { builder.getColumnBuilder(2).writeBinary(new Binary(nodeStatus, TSFileConfig.STRING_CHARSET)); } + if (hasStatusReason) { + if (nodeStatusReason == null) { + builder.getColumnBuilder(3).appendNull(); + } else { + builder + .getColumnBuilder(3) + .writeBinary(new Binary(nodeStatusReason, TSFileConfig.STRING_CHARSET)); + } + } + int offset = hasStatusReason ? 1 : 0; if (internalAddress == null) { - builder.getColumnBuilder(3).appendNull(); + builder.getColumnBuilder(3 + offset).appendNull(); } else { builder - .getColumnBuilder(3) + .getColumnBuilder(3 + offset) .writeBinary(new Binary(internalAddress, TSFileConfig.STRING_CHARSET)); } - builder.getColumnBuilder(4).writeInt(internalPort); - builder.getColumnBuilder(5).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); + builder.getColumnBuilder(4 + offset).writeInt(internalPort); + builder.getColumnBuilder(5 + offset).writeBinary(new Binary("", TSFileConfig.STRING_CHARSET)); if (rpcAddress == null) { - builder.getColumnBuilder(6).appendNull(); + builder.getColumnBuilder(6 + offset).appendNull(); } else { - builder.getColumnBuilder(6).writeBinary(new Binary(rpcAddress, TSFileConfig.STRING_CHARSET)); + builder + .getColumnBuilder(6 + offset) + .writeBinary(new Binary(rpcAddress, TSFileConfig.STRING_CHARSET)); } builder - .getColumnBuilder(7) + .getColumnBuilder(7 + offset) .writeBinary(new Binary(Integer.toString(rpcPort), TSFileConfig.STRING_CHARSET)); builder - .getColumnBuilder(8) + .getColumnBuilder(8 + offset) .writeBinary(new Binary(Integer.toString(dataConsensusPort), TSFileConfig.STRING_CHARSET)); builder - .getColumnBuilder(9) + .getColumnBuilder(9 + offset) .writeBinary( new Binary(Integer.toString(schemaConsensusPort), TSFileConfig.STRING_CHARSET)); builder - .getColumnBuilder(10) + .getColumnBuilder(10 + offset) .writeBinary(new Binary(Integer.toString(mppPort), TSFileConfig.STRING_CHARSET)); if (versionInfo == null || versionInfo.getVersion() == null) { - builder.getColumnBuilder(11).appendNull(); + builder.getColumnBuilder(11 + offset).appendNull(); } else { builder - .getColumnBuilder(11) + .getColumnBuilder(11 + offset) .writeBinary(new Binary(versionInfo.getVersion(), TSFileConfig.STRING_CHARSET)); } if (versionInfo == null || versionInfo.getBuildInfo() == null) { - builder.getColumnBuilder(12).appendNull(); + builder.getColumnBuilder(12 + offset).appendNull(); } else { builder - .getColumnBuilder(12) + .getColumnBuilder(12 + offset) .writeBinary(new Binary(versionInfo.getBuildInfo(), TSFileConfig.STRING_CHARSET)); } builder.declarePosition(); @@ -224,10 +261,18 @@ private static void buildDataNodesTsBlock( public static void buildTSBlock( TShowClusterResp clusterNodeInfos, SettableFuture future) { + boolean hasStatusReason = + clusterNodeInfos.getNodeStatusReason() != null + && clusterNodeInfos.getNodeStatusReason().values().stream() + .anyMatch(reason -> reason != null && !reason.isEmpty()); + List columnHeaders = + new ArrayList<>(ColumnHeaderConstant.showClusterDetailsColumnHeaders); + if (hasStatusReason) { + // insert after the Status column + columnHeaders.add(3, new ColumnHeader(ColumnHeaderConstant.STATUS_REASON, TSDataType.TEXT)); + } List outputDataTypes = - ColumnHeaderConstant.showClusterDetailsColumnHeaders.stream() - .map(ColumnHeader::getColumnType) - .collect(Collectors.toList()); + columnHeaders.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()); TsBlockBuilder builder = new TsBlockBuilder(outputDataTypes); clusterNodeInfos @@ -238,6 +283,10 @@ public static void buildTSBlock( builder, e.getConfigNodeId(), clusterNodeInfos.getNodeStatus().get(e.getConfigNodeId()), + hasStatusReason + ? clusterNodeInfos.getNodeStatusReason().get(e.getConfigNodeId()) + : null, + hasStatusReason, e.getInternalEndPoint().getIp(), e.getInternalEndPoint().getPort(), e.getConsensusEndPoint().getPort(), @@ -251,6 +300,10 @@ public static void buildTSBlock( builder, e.getDataNodeId(), clusterNodeInfos.getNodeStatus().get(e.getDataNodeId()), + hasStatusReason + ? clusterNodeInfos.getNodeStatusReason().get(e.getDataNodeId()) + : null, + hasStatusReason, e.getInternalEndPoint().getIp(), e.getInternalEndPoint().getPort(), e.getClientRpcEndPoint().getIp(), @@ -267,11 +320,15 @@ public static void buildTSBlock( builder, e.getAiNodeId(), clusterNodeInfos.getNodeStatus().get(e.getAiNodeId()), + hasStatusReason + ? clusterNodeInfos.getNodeStatusReason().get(e.getAiNodeId()) + : null, + hasStatusReason, e.getInternalEndPoint().getIp(), e.getInternalEndPoint().getPort(), clusterNodeInfos.getNodeVersionInfo().get(e.getAiNodeId()))); - DatasetHeader datasetHeader = DatasetHeaderFactory.getShowClusterDetailsHeader(); + DatasetHeader datasetHeader = new DatasetHeader(columnHeaders, true); future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS, builder.build(), datasetHeader)); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTask.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTask.java index 8837864ab1390..d47f1666d13a8 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTask.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTask.java @@ -24,7 +24,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TNodeVersionInfo; import org.apache.iotdb.confignode.rpc.thrift.TShowClusterResp; import org.apache.iotdb.db.queryengine.common.header.DatasetHeader; -import org.apache.iotdb.db.queryengine.common.header.DatasetHeaderFactory; import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; import org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor; @@ -38,6 +37,7 @@ import org.apache.tsfile.read.common.block.TsBlockBuilder; import org.apache.tsfile.utils.Binary; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -64,6 +64,8 @@ private static void buildTsBlock( int nodeId, String nodeType, String nodeStatus, + String nodeStatusReason, + boolean hasStatusReason, String hostAddress, int port, TNodeVersionInfo versionInfo) { @@ -79,24 +81,36 @@ private static void buildTsBlock( } else { builder.getColumnBuilder(2).writeBinary(new Binary(nodeStatus, TSFileConfig.STRING_CHARSET)); } + if (hasStatusReason) { + if (nodeStatusReason == null) { + builder.getColumnBuilder(3).appendNull(); + } else { + builder + .getColumnBuilder(3) + .writeBinary(new Binary(nodeStatusReason, TSFileConfig.STRING_CHARSET)); + } + } + int offset = hasStatusReason ? 1 : 0; if (hostAddress == null) { - builder.getColumnBuilder(3).appendNull(); + builder.getColumnBuilder(3 + offset).appendNull(); } else { - builder.getColumnBuilder(3).writeBinary(new Binary(hostAddress, TSFileConfig.STRING_CHARSET)); + builder + .getColumnBuilder(3 + offset) + .writeBinary(new Binary(hostAddress, TSFileConfig.STRING_CHARSET)); } - builder.getColumnBuilder(4).writeInt(port); + builder.getColumnBuilder(4 + offset).writeInt(port); if (versionInfo == null || versionInfo.getVersion() == null) { - builder.getColumnBuilder(5).appendNull(); + builder.getColumnBuilder(5 + offset).appendNull(); } else { builder - .getColumnBuilder(5) + .getColumnBuilder(5 + offset) .writeBinary(new Binary(versionInfo.getVersion(), TSFileConfig.STRING_CHARSET)); } if (versionInfo == null || versionInfo.getBuildInfo() == null) { - builder.getColumnBuilder(6).appendNull(); + builder.getColumnBuilder(6 + offset).appendNull(); } else { builder - .getColumnBuilder(6) + .getColumnBuilder(6 + offset) .writeBinary(new Binary(versionInfo.getBuildInfo(), TSFileConfig.STRING_CHARSET)); } @@ -105,10 +119,18 @@ private static void buildTsBlock( public static void buildTsBlock( TShowClusterResp clusterNodeInfos, SettableFuture future) { + boolean hasStatusReason = + clusterNodeInfos.getNodeStatusReason() != null + && clusterNodeInfos.getNodeStatusReason().values().stream() + .anyMatch(reason -> reason != null && !reason.isEmpty()); + List columnHeaders = + new ArrayList<>(ColumnHeaderConstant.showClusterColumnHeaders); + if (hasStatusReason) { + // insert after the Status column + columnHeaders.add(3, new ColumnHeader(ColumnHeaderConstant.STATUS_REASON, TSDataType.TEXT)); + } List outputDataTypes = - ColumnHeaderConstant.showClusterColumnHeaders.stream() - .map(ColumnHeader::getColumnType) - .collect(Collectors.toList()); + columnHeaders.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()); TsBlockBuilder builder = new TsBlockBuilder(outputDataTypes); clusterNodeInfos @@ -120,6 +142,10 @@ public static void buildTsBlock( e.getConfigNodeId(), NODE_TYPE_CONFIG_NODE, clusterNodeInfos.getNodeStatus().get(e.getConfigNodeId()), + hasStatusReason + ? clusterNodeInfos.getNodeStatusReason().get(e.getConfigNodeId()) + : null, + hasStatusReason, e.getInternalEndPoint().getIp(), e.getInternalEndPoint().getPort(), clusterNodeInfos.getNodeVersionInfo().get(e.getConfigNodeId()))); @@ -133,6 +159,10 @@ public static void buildTsBlock( e.getDataNodeId(), NODE_TYPE_DATA_NODE, clusterNodeInfos.getNodeStatus().get(e.getDataNodeId()), + hasStatusReason + ? clusterNodeInfos.getNodeStatusReason().get(e.getDataNodeId()) + : null, + hasStatusReason, e.getInternalEndPoint().getIp(), e.getInternalEndPoint().getPort(), clusterNodeInfos.getNodeVersionInfo().get(e.getDataNodeId()))); @@ -147,11 +177,15 @@ public static void buildTsBlock( e.getAiNodeId(), NODE_TYPE_AI_NODE, clusterNodeInfos.getNodeStatus().get(e.getAiNodeId()), + hasStatusReason + ? clusterNodeInfos.getNodeStatusReason().get(e.getAiNodeId()) + : null, + hasStatusReason, e.getInternalEndPoint().getIp(), e.getInternalEndPoint().getPort(), clusterNodeInfos.getNodeVersionInfo().get(e.getAiNodeId()))); } - DatasetHeader datasetHeader = DatasetHeaderFactory.getShowClusterHeader(); + DatasetHeader datasetHeader = new DatasetHeader(columnHeaders, true); future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS, builder.build(), datasetHeader)); } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTask.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTask.java index a666a72d81539..53ef572c10f07 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTask.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTask.java @@ -24,7 +24,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TConfigNodeInfo; import org.apache.iotdb.confignode.rpc.thrift.TShowConfigNodesResp; import org.apache.iotdb.db.queryengine.common.header.DatasetHeader; -import org.apache.iotdb.db.queryengine.common.header.DatasetHeaderFactory; import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; import org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor; @@ -36,6 +35,7 @@ import org.apache.tsfile.read.common.block.TsBlockBuilder; import org.apache.tsfile.utils.BytesUtils; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -53,25 +53,47 @@ public ListenableFuture execute(IConfigTaskExecutor configTask public static void buildTSBlock( TShowConfigNodesResp showConfigNodesResp, SettableFuture future) { + boolean hasStatusReason = + showConfigNodesResp.getConfigNodesInfoList() != null + && showConfigNodesResp.getConfigNodesInfoList().stream() + .anyMatch( + configNodeInfo -> + configNodeInfo.getStatusReason() != null + && !configNodeInfo.getStatusReason().isEmpty()); + List columnHeaders = + new ArrayList<>(ColumnHeaderConstant.showConfigNodesColumnHeaders); + if (hasStatusReason) { + // insert after the Status column + columnHeaders.add(2, new ColumnHeader(ColumnHeaderConstant.STATUS_REASON, TSDataType.TEXT)); + } List outputDataTypes = - ColumnHeaderConstant.showConfigNodesColumnHeaders.stream() - .map(ColumnHeader::getColumnType) - .collect(Collectors.toList()); + columnHeaders.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()); TsBlockBuilder builder = new TsBlockBuilder(outputDataTypes); if (showConfigNodesResp.getConfigNodesInfoList() != null) { for (TConfigNodeInfo configNodeInfo : showConfigNodesResp.getConfigNodesInfoList()) { builder.getTimeColumnBuilder().writeLong(0L); builder.getColumnBuilder(0).writeInt(configNodeInfo.getConfigNodeId()); builder.getColumnBuilder(1).writeBinary(BytesUtils.valueOf(configNodeInfo.getStatus())); + if (hasStatusReason) { + String statusReason = configNodeInfo.getStatusReason(); + if (statusReason == null) { + builder.getColumnBuilder(2).appendNull(); + } else { + builder.getColumnBuilder(2).writeBinary(BytesUtils.valueOf(statusReason)); + } + } + int offset = hasStatusReason ? 1 : 0; builder - .getColumnBuilder(2) + .getColumnBuilder(2 + offset) .writeBinary(BytesUtils.valueOf(configNodeInfo.getInternalAddress())); - builder.getColumnBuilder(3).writeInt(configNodeInfo.getInternalPort()); - builder.getColumnBuilder(4).writeBinary(BytesUtils.valueOf(configNodeInfo.getRoleType())); + builder.getColumnBuilder(3 + offset).writeInt(configNodeInfo.getInternalPort()); + builder + .getColumnBuilder(4 + offset) + .writeBinary(BytesUtils.valueOf(configNodeInfo.getRoleType())); builder.declarePosition(); } } - DatasetHeader datasetHeader = DatasetHeaderFactory.getShowConfigNodesHeader(); + DatasetHeader datasetHeader = new DatasetHeader(columnHeaders, true); future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS, builder.build(), datasetHeader)); } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTask.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTask.java index 1adb6e68cae67..854dc33ffdbfb 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTask.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTask.java @@ -24,7 +24,6 @@ import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo; import org.apache.iotdb.confignode.rpc.thrift.TShowDataNodesResp; import org.apache.iotdb.db.queryengine.common.header.DatasetHeader; -import org.apache.iotdb.db.queryengine.common.header.DatasetHeaderFactory; import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; import org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor; @@ -37,6 +36,7 @@ import org.apache.tsfile.read.common.block.TsBlockBuilder; import org.apache.tsfile.utils.BytesUtils; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -58,10 +58,21 @@ public ListenableFuture execute(IConfigTaskExecutor configTask public static void buildTSBlock( TShowDataNodesResp showDataNodesResp, SettableFuture future) { + boolean hasStatusReason = + showDataNodesResp.getDataNodesInfoList() != null + && showDataNodesResp.getDataNodesInfoList().stream() + .anyMatch( + dataNodeInfo -> + dataNodeInfo.getStatusReason() != null + && !dataNodeInfo.getStatusReason().isEmpty()); + List columnHeaders = + new ArrayList<>(ColumnHeaderConstant.showDataNodesColumnHeaders); + if (hasStatusReason) { + // insert after the Status column + columnHeaders.add(2, new ColumnHeader(ColumnHeaderConstant.STATUS_REASON, TSDataType.TEXT)); + } List outputDataTypes = - ColumnHeaderConstant.showDataNodesColumnHeaders.stream() - .map(ColumnHeader::getColumnType) - .collect(Collectors.toList()); + columnHeaders.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()); TsBlockBuilder builder = new TsBlockBuilder(outputDataTypes); if (showDataNodesResp.getDataNodesInfoList() != null) { for (TDataNodeInfo dataNodeInfo : showDataNodesResp.getDataNodesInfoList()) { @@ -72,16 +83,25 @@ public static void buildTSBlock( .writeBinary( BytesUtils.valueOf( dataNodeInfo.getStatus() == null ? "" : dataNodeInfo.getStatus())); - - builder.getColumnBuilder(2).writeBinary(BytesUtils.valueOf(dataNodeInfo.getRpcAddresss())); - builder.getColumnBuilder(3).writeInt(dataNodeInfo.getRpcPort()); - builder.getColumnBuilder(4).writeInt(dataNodeInfo.getDataRegionNum()); - - builder.getColumnBuilder(5).writeInt(dataNodeInfo.getSchemaRegionNum()); + if (hasStatusReason) { + String statusReason = dataNodeInfo.getStatusReason(); + if (statusReason == null) { + builder.getColumnBuilder(2).appendNull(); + } else { + builder.getColumnBuilder(2).writeBinary(BytesUtils.valueOf(statusReason)); + } + } + int offset = hasStatusReason ? 1 : 0; + builder + .getColumnBuilder(2 + offset) + .writeBinary(BytesUtils.valueOf(dataNodeInfo.getRpcAddresss())); + builder.getColumnBuilder(3 + offset).writeInt(dataNodeInfo.getRpcPort()); + builder.getColumnBuilder(4 + offset).writeInt(dataNodeInfo.getDataRegionNum()); + builder.getColumnBuilder(5 + offset).writeInt(dataNodeInfo.getSchemaRegionNum()); builder.declarePosition(); } } - DatasetHeader datasetHeader = DatasetHeaderFactory.getShowDataNodesHeader(); + DatasetHeader datasetHeader = new DatasetHeader(columnHeaders, true); future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS, builder.build(), datasetHeader)); } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java index 6dd082c0efa4b..5aeec16ebe2f9 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java @@ -116,8 +116,7 @@ public void run() { ExternalRPCService.getInstance().stop(); // Reject write operations to make sure all tsfiles will be sealed - CommonDescriptor.getInstance().getConfig().setStopping(true); - CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly); + markNodeStopping(); // Wait all wal are flushed WALManager.getInstance().waitAllWALFlushed(); @@ -212,6 +211,18 @@ public void run() { watcherThread.interrupt(); } + /** + * Mark the node as stopping: reject writes and record the ReadOnly reason, so that operators can + * distinguish an orderly shutdown (Stopping) from a disk-full or error triggered ReadOnly. The + * {@code ReadOnly(Stopping)} state is transient in the real shutdown sequence; extracting the + * transition keeps it unit-testable without racing the shutdown window. + */ + static void markNodeStopping() { + CommonDescriptor.getInstance().getConfig().setStopping(true); + CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly); + CommonDescriptor.getInstance().getConfig().setStatusReason(NodeStatus.STOPPING); + } + private void triggerSnapshotForAllDataRegion() { DataRegionConsensusImpl.getInstance().getAllConsensusGroupIds().parallelStream() .forEach( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java index 6b14b43edf5ff..2250790f0ab25 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java @@ -2144,7 +2144,9 @@ protected TsFileProcessor getOrCreateTsFileProcessor(long timeRangeId, boolean s StorageEngineMessages .STORAGE_LOG_DISK_SPACE_IS_INSUFFICIENT_WHEN_CREATING_TSFILE_PROCESSOR_4032BAF0, e); - CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly); + CommonDescriptor.getInstance() + .getConfig() + .setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); throw new WriteProcessException(e.getMessage(), e.getErrorCode(), true); } catch (IOException e) { if (retryCnt < 3) { @@ -2156,7 +2158,7 @@ protected TsFileProcessor getOrCreateTsFileProcessor(long timeRangeId, boolean s .STORAGE_LOG_MEET_IOEXCEPTION_WHEN_CREATING_TSFILEPROCESSOR_CHANGE_SYSTEM_4337F729, e); DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); throw new WriteProcessException( String.format( StorageEngineMessages diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java index de1a5a09b7ddc..52c3670b3bc79 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java @@ -1869,7 +1869,7 @@ public void flushOneMemTable() { tsFileResource.getTsFile().getAbsolutePath(), e); DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); try { logger.error( StorageEngineMessages @@ -2017,7 +2017,7 @@ public void flushOneMemTable() { dataRegionName, tsFileResource.getTsFile().getAbsolutePath(), e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); break; } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java index d67cd88a4c48f..6355568d93014 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java @@ -610,7 +610,7 @@ public void run() { .STORAGE_LOG_FAIL_TO_SYNC_WAL_NODE_S_BUFFER_CHANGE_SYSTEM_MODE_TO_ERROR_8C379D57, identifier, e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); } finally { switchSyncingBufferToIdle(); } @@ -643,7 +643,7 @@ public void run() { info.rollWALFileWriterListener.fail(e); } DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); } } else if (forceFlag) { // force os cache to the storage device, avoid force twice by judging // after rolling file @@ -660,7 +660,7 @@ public void run() { for (WALFlushListener fsyncListener : info.fsyncListeners) { fsyncListener.fail(e); } - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/checkpoint/CheckpointManager.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/checkpoint/CheckpointManager.java index a1bba3bad402d..ec96f43c7d401 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/checkpoint/CheckpointManager.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/checkpoint/CheckpointManager.java @@ -222,7 +222,7 @@ public void fsyncCheckpointFile() { identifier, e); DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); } try { @@ -243,7 +243,7 @@ public void fsyncCheckpointFile() { identifier, e); DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e); - CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(); + CommonDescriptor.getInstance().getConfig().handleUnrecoverableError(e); } } finally { infoLock.unlock(); diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplDiskTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplDiskTest.java index 7dce3cc3c2de1..e1f249372851b 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplDiskTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplDiskTest.java @@ -53,11 +53,11 @@ public void setUp() { originalDataNodeId = dataNodeConfig.getDataNodeId(); dataNodeConfig.setDataNodeId(0); + // Go through Running first so that a ReadOnly with a higher-priority reason left behind by + // another test can always be overridden. commonConfig.setNodeStatus(NodeStatus.Running); - commonConfig.setStatusReason(null); commonConfig.setDiskSpaceWarningThreshold(0.05); - commonConfig.setNodeStatus(NodeStatus.ReadOnly); - commonConfig.setStatusReason(NodeStatus.DISK_FULL); + commonConfig.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); } @After @@ -95,7 +95,6 @@ public void testStorageEngineDiskAggregateStillEntersReadOnly() { when(systemMetrics.getSystemDiskTotalSpace()).thenReturn(100L); commonConfig.setNodeStatus(NodeStatus.Running); - commonConfig.setStatusReason(null); DataNodeContext dataNodeContext = mock(DataNodeContext.class); DataNodeInternalRPCServiceImpl service = new DataNodeInternalRPCServiceImpl(dataNodeContext, systemMetrics); @@ -105,4 +104,31 @@ public void testStorageEngineDiskAggregateStillEntersReadOnly() { Assert.assertEquals(NodeStatus.ReadOnly, commonConfig.getNodeStatus()); Assert.assertEquals(NodeStatus.DISK_FULL, commonConfig.getStatusReason()); } + + @Test + public void testRecoveryRequiresExactReadOnlyWithDiskFullReason() { + SystemMetrics systemMetrics = mock(SystemMetrics.class); + // The aggregate free ratio is 52%, so a ReadOnly + DiskFull state would recover. + when(systemMetrics.getSystemDiskAvailableSpace()).thenReturn(104L); + when(systemMetrics.getSystemDiskTotalSpace()).thenReturn(200L); + + DataNodeContext dataNodeContext = mock(DataNodeContext.class); + DataNodeInternalRPCServiceImpl service = + new DataNodeInternalRPCServiceImpl(dataNodeContext, systemMetrics); + + // A manually set ReadOnly must not be auto-recovered by a disk ratio that recovered. + commonConfig.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + service.sampleDiskLoad(new TLoadSample()); + Assert.assertEquals(NodeStatus.ReadOnly, commonConfig.getNodeStatus()); + Assert.assertEquals(NodeStatus.MANUAL, commonConfig.getStatusReason()); + + // Neither can a ReadOnly entered through an unrecoverable error. + // (Manual outranks UnrecoverableError, so go through Running to enter it.) + commonConfig.setNodeStatus(NodeStatus.Running); + commonConfig.setNodeStatusWithReason( + NodeStatus.ReadOnly, NodeStatus.UNRECOVERABLE_ERROR + ", 2026-09-02 10:00:00.000, broken"); + service.sampleDiskLoad(new TLoadSample()); + Assert.assertEquals(NodeStatus.ReadOnly, commonConfig.getNodeStatus()); + Assert.assertTrue(commonConfig.getStatusReason().startsWith(NodeStatus.UNRECOVERABLE_ERROR)); + } } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplSetSystemStatusTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplSetSystemStatusTest.java new file mode 100644 index 0000000000000..3cc312694d64d --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/thrift/impl/DataNodeInternalRPCServiceImplSetSystemStatusTest.java @@ -0,0 +1,91 @@ +/* + * 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.protocol.thrift.impl; + +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.conf.CommonConfig; +import org.apache.iotdb.commons.conf.CommonDescriptor; +import org.apache.iotdb.db.conf.IoTDBDescriptor; +import org.apache.iotdb.db.service.DataNode.DataNodeContext; +import org.apache.iotdb.rpc.TSStatusCode; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.mockito.Mockito.mock; + +public class DataNodeInternalRPCServiceImplSetSystemStatusTest { + + private final CommonConfig commonConfig = CommonDescriptor.getInstance().getConfig(); + private NodeStatus originalStatus; + private String originalStatusReason; + + @BeforeClass + public static void setUpClass() { + // The static initializer of DataNodeInternalRPCServiceImpl (Coordinator) requires it. + IoTDBDescriptor.getInstance().getConfig().setDataNodeId(0); + } + + @Before + public void setUp() { + originalStatus = commonConfig.getNodeStatus(); + originalStatusReason = commonConfig.getStatusReason(); + commonConfig.setNodeStatus(NodeStatus.Running); + } + + @After + public void tearDown() { + commonConfig.setNodeStatus(originalStatus); + commonConfig.setStatusReason(originalStatusReason); + } + + @Test + public void testSetSystemStatusReadOnlyCarriesManualReason() throws Exception { + final DataNodeInternalRPCServiceImpl service = + new DataNodeInternalRPCServiceImpl(mock(DataNodeContext.class)); + + // A ReadOnly requested through the RPC (i.e. by the ConfigNode on SET SYSTEM STATUS) is + // recorded with the Manual reason. + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), service.setSystemStatus("ReadOnly").getCode()); + Assert.assertEquals(NodeStatus.ReadOnly, commonConfig.getNodeStatus()); + Assert.assertEquals(NodeStatus.MANUAL, commonConfig.getStatusReason()); + + // Switching back to Running clears the reason. + Assert.assertEquals( + TSStatusCode.SUCCESS_STATUS.getStatusCode(), service.setSystemStatus("Running").getCode()); + Assert.assertEquals(NodeStatus.Running, commonConfig.getNodeStatus()); + Assert.assertNull(commonConfig.getStatusReason()); + } + + @Test + public void testSetSystemStatusInvalidStatusReturnsError() throws Exception { + final DataNodeInternalRPCServiceImpl service = + new DataNodeInternalRPCServiceImpl(mock(DataNodeContext.class)); + + Assert.assertEquals( + TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode(), + service.setSystemStatus("NotAStatus").getCode()); + Assert.assertEquals(NodeStatus.Running, commonConfig.getNodeStatus()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTaskTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTaskTest.java new file mode 100644 index 0000000000000..4aa91a04467c1 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterDetailsTaskTest.java @@ -0,0 +1,138 @@ +/* + * 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.plan.execution.config.metadata; + +import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation; +import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation; +import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.schema.column.ColumnHeader; +import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant; +import org.apache.iotdb.confignode.rpc.thrift.TNodeVersionInfo; +import org.apache.iotdb.confignode.rpc.thrift.TShowClusterResp; +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.rpc.TSStatusCode; + +import com.google.common.util.concurrent.SettableFuture; +import org.apache.tsfile.block.column.Column; +import org.apache.tsfile.enums.TSDataType; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ShowClusterDetailsTaskTest { + + private static final int CONFIG_NODE_ID = 0; + private static final int DATA_NODE_ID = 1; + + private static TShowClusterResp buildClusterResp(final Map statusReasonMap) { + final TConfigNodeLocation configNode = + new TConfigNodeLocation( + CONFIG_NODE_ID, new TEndPoint("127.0.0.1", 1000), new TEndPoint("127.0.0.1", 1001)); + final TDataNodeLocation dataNode = + new TDataNodeLocation( + DATA_NODE_ID, + new TEndPoint("127.0.0.1", 2000), + new TEndPoint("127.0.0.1", 2001), + new TEndPoint("127.0.0.1", 2002), + new TEndPoint("127.0.0.1", 2003), + new TEndPoint("127.0.0.1", 2004)); + + final Map nodeStatus = new HashMap<>(); + nodeStatus.put(CONFIG_NODE_ID, NodeStatus.Running.getStatus()); + nodeStatus.put(DATA_NODE_ID, NodeStatus.ReadOnly.getStatus()); + + final Map nodeVersionInfo = new HashMap<>(); + final TNodeVersionInfo versionInfo = new TNodeVersionInfo("2.0.11", "build"); + nodeVersionInfo.put(CONFIG_NODE_ID, versionInfo); + nodeVersionInfo.put(DATA_NODE_ID, versionInfo); + + final TShowClusterResp resp = + new TShowClusterResp( + new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()), + Collections.singletonList(configNode), + Collections.singletonList(dataNode), + Collections.emptyList(), + nodeStatus, + nodeVersionInfo); + if (statusReasonMap != null) { + resp.setNodeStatusReason(statusReasonMap); + } + return resp; + } + + private static ConfigTaskResult execute(final TShowClusterResp resp) throws Exception { + final SettableFuture future = SettableFuture.create(); + ShowClusterDetailsTask.buildTSBlock(resp, future); + return future.get(); + } + + private static List columnNames(final List headers) { + return headers.stream().map(ColumnHeader::getColumnName).collect(Collectors.toList()); + } + + @Test + public void testOldLayoutWhenNoReasonFieldPresent() throws Exception { + final ConfigTaskResult result = execute(buildClusterResp(null)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals( + columnNames(ColumnHeaderConstant.showClusterDetailsColumnHeaders), columnNames(headers)); + Assert.assertEquals( + ColumnHeaderConstant.showClusterDetailsColumnHeaders.stream() + .map(ColumnHeader::getColumnType) + .collect(Collectors.toList()), + result.getResultSetHeader().getRespDataTypes()); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + Assert.assertEquals(NodeStatus.Running.getStatus(), columns[2].getBinary(0).toString()); + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[2].getBinary(1).toString()); + } + + @Test + public void testReasonColumnInsertedWithNullForMissingReasons() throws Exception { + final Map statusReasonMap = new HashMap<>(); + statusReasonMap.put(DATA_NODE_ID, NodeStatus.DISK_FULL); + final ConfigTaskResult result = execute(buildClusterResp(statusReasonMap)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals( + ColumnHeaderConstant.showClusterDetailsColumnHeaders.size() + 1, headers.size()); + Assert.assertEquals(ColumnHeaderConstant.STATUS_REASON, headers.get(3).getColumnName()); + Assert.assertEquals(TSDataType.TEXT, headers.get(3).getColumnType()); + Assert.assertEquals( + headers.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()), + result.getResultSetHeader().getRespDataTypes()); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + Assert.assertTrue(columns[3].isNull(0)); + Assert.assertFalse(columns[3].isNull(1)); + Assert.assertEquals(NodeStatus.DISK_FULL, columns[3].getBinary(1).toString()); + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[2].getBinary(1).toString()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTaskTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTaskTest.java new file mode 100644 index 0000000000000..f61cee7d3b8ed --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowClusterTaskTest.java @@ -0,0 +1,161 @@ +/* + * 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.plan.execution.config.metadata; + +import org.apache.iotdb.common.rpc.thrift.TConfigNodeLocation; +import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation; +import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.schema.column.ColumnHeader; +import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant; +import org.apache.iotdb.confignode.rpc.thrift.TNodeVersionInfo; +import org.apache.iotdb.confignode.rpc.thrift.TShowClusterResp; +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.rpc.TSStatusCode; + +import com.google.common.util.concurrent.SettableFuture; +import org.apache.tsfile.block.column.Column; +import org.apache.tsfile.enums.TSDataType; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ShowClusterTaskTest { + + private static final int CONFIG_NODE_ID = 0; + private static final int DATA_NODE_ID = 1; + + private static TShowClusterResp buildClusterResp(final Map statusReasonMap) { + final TConfigNodeLocation configNode = + new TConfigNodeLocation( + CONFIG_NODE_ID, new TEndPoint("127.0.0.1", 1000), new TEndPoint("127.0.0.1", 1001)); + final TDataNodeLocation dataNode = + new TDataNodeLocation( + DATA_NODE_ID, + new TEndPoint("127.0.0.1", 2000), + new TEndPoint("127.0.0.1", 2001), + new TEndPoint("127.0.0.1", 2002), + new TEndPoint("127.0.0.1", 2003), + new TEndPoint("127.0.0.1", 2004)); + + final Map nodeStatus = new HashMap<>(); + nodeStatus.put(CONFIG_NODE_ID, NodeStatus.Running.getStatus()); + nodeStatus.put(DATA_NODE_ID, NodeStatus.ReadOnly.getStatus()); + + final Map nodeVersionInfo = new HashMap<>(); + final TNodeVersionInfo versionInfo = new TNodeVersionInfo("2.0.11", "build"); + nodeVersionInfo.put(CONFIG_NODE_ID, versionInfo); + nodeVersionInfo.put(DATA_NODE_ID, versionInfo); + + final TShowClusterResp resp = + new TShowClusterResp( + new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()), + Collections.singletonList(configNode), + Collections.singletonList(dataNode), + Collections.emptyList(), + nodeStatus, + nodeVersionInfo); + // An old ConfigNode response leaves the optional nodeStatusReason field unset. + if (statusReasonMap != null) { + resp.setNodeStatusReason(statusReasonMap); + } + return resp; + } + + private static ConfigTaskResult execute(final TShowClusterResp resp) throws Exception { + final SettableFuture future = SettableFuture.create(); + ShowClusterTask.buildTsBlock(resp, future); + return future.get(); + } + + private static List columnNames(final List headers) { + return headers.stream().map(ColumnHeader::getColumnName).collect(Collectors.toList()); + } + + @Test + public void testOldLayoutWhenNoReasonFieldPresent() throws Exception { + // A response from an old ConfigNode (or a cluster without any reason) keeps the legacy layout: + // no StatusReason column at all. + final ConfigTaskResult result = execute(buildClusterResp(null)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals( + columnNames(ColumnHeaderConstant.showClusterColumnHeaders), columnNames(headers)); + Assert.assertEquals( + ColumnHeaderConstant.showClusterColumnHeaders.stream() + .map(ColumnHeader::getColumnType) + .collect(Collectors.toList()), + result.getResultSetHeader().getRespDataTypes()); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + // The Status column carries the plain status, never a merged "ReadOnly(Manual)" string. + Assert.assertEquals(NodeStatus.Running.getStatus(), columns[2].getBinary(0).toString()); + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[2].getBinary(1).toString()); + } + + @Test + public void testReasonColumnInsertedWithNullForMissingReasons() throws Exception { + final Map statusReasonMap = new HashMap<>(); + statusReasonMap.put(DATA_NODE_ID, NodeStatus.MANUAL); + final ConfigTaskResult result = execute(buildClusterResp(statusReasonMap)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + // The StatusReason column is dynamically inserted right after the Status column. + Assert.assertEquals(ColumnHeaderConstant.showClusterColumnHeaders.size() + 1, headers.size()); + Assert.assertEquals(ColumnHeaderConstant.STATUS_REASON, headers.get(3).getColumnName()); + Assert.assertEquals(TSDataType.TEXT, headers.get(3).getColumnType()); + // The header, its data type list and the TsBlock column count stay consistent. + Assert.assertEquals( + headers.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()), + result.getResultSetHeader().getRespDataTypes()); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + // The ConfigNode has no reason: its StatusReason cell is NULL at the right position. + Assert.assertTrue(columns[3].isNull(0)); + // The DataNode has a reason: it shows the reason next to its plain status. + Assert.assertFalse(columns[3].isNull(1)); + Assert.assertEquals(NodeStatus.MANUAL, columns[3].getBinary(1).toString()); + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[2].getBinary(1).toString()); + } + + @Test + public void testReasonColumnAbsentWhenAllReasonsAreEmpty() throws Exception { + // An all-empty reason map behaves like a missing field: the legacy layout is kept. + final Map statusReasonMap = new HashMap<>(); + statusReasonMap.put(CONFIG_NODE_ID, ""); + statusReasonMap.put(DATA_NODE_ID, ""); + final ConfigTaskResult result = execute(buildClusterResp(statusReasonMap)); + + Assert.assertEquals( + columnNames(ColumnHeaderConstant.showClusterColumnHeaders), + columnNames(result.getResultSetHeader().getColumnHeaders())); + Assert.assertEquals( + ColumnHeaderConstant.showClusterColumnHeaders.size(), + result.getResultSet().getValueColumns().length); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTaskTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTaskTest.java new file mode 100644 index 0000000000000..c7fec58cd79a8 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowConfigNodesTaskTest.java @@ -0,0 +1,113 @@ +/* + * 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.plan.execution.config.metadata; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.schema.column.ColumnHeader; +import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant; +import org.apache.iotdb.confignode.rpc.thrift.TConfigNodeInfo; +import org.apache.iotdb.confignode.rpc.thrift.TShowConfigNodesResp; +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.rpc.TSStatusCode; + +import com.google.common.util.concurrent.SettableFuture; +import org.apache.tsfile.block.column.Column; +import org.apache.tsfile.enums.TSDataType; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class ShowConfigNodesTaskTest { + + private static final int CONFIG_NODE_WITH_REASON_ID = 1; + private static final int CONFIG_NODE_WITHOUT_REASON_ID = 2; + + private static TShowConfigNodesResp buildResp(final boolean setReasonOnFirstNode) { + final TConfigNodeInfo configNodeWithReason = + new TConfigNodeInfo( + CONFIG_NODE_WITH_REASON_ID, + NodeStatus.ReadOnly.getStatus(), + "127.0.0.1", + 1000, + "Leader"); + if (setReasonOnFirstNode) { + configNodeWithReason.setStatusReason(NodeStatus.MANUAL); + } + final TConfigNodeInfo configNodeWithoutReason = + new TConfigNodeInfo( + CONFIG_NODE_WITHOUT_REASON_ID, + NodeStatus.Running.getStatus(), + "127.0.0.1", + 1001, + "Follower"); + return new TShowConfigNodesResp(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())) + .setConfigNodesInfoList(Arrays.asList(configNodeWithReason, configNodeWithoutReason)); + } + + private static ConfigTaskResult execute(final TShowConfigNodesResp resp) throws Exception { + final SettableFuture future = SettableFuture.create(); + ShowConfigNodesTask.buildTSBlock(resp, future); + return future.get(); + } + + private static List columnNames(final List headers) { + return headers.stream().map(ColumnHeader::getColumnName).collect(Collectors.toList()); + } + + @Test + public void testOldLayoutWhenNoReasonIsSet() throws Exception { + final ConfigTaskResult result = execute(buildResp(false)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals( + columnNames(ColumnHeaderConstant.showConfigNodesColumnHeaders), columnNames(headers)); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + // The Status column carries the plain status, never a merged "ReadOnly(Manual)" string. + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[1].getBinary(0).toString()); + Assert.assertEquals(NodeStatus.Running.getStatus(), columns[1].getBinary(1).toString()); + } + + @Test + public void testReasonColumnInsertedWithNullForMissingReasons() throws Exception { + final ConfigTaskResult result = execute(buildResp(true)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals( + ColumnHeaderConstant.showConfigNodesColumnHeaders.size() + 1, headers.size()); + Assert.assertEquals(ColumnHeaderConstant.STATUS_REASON, headers.get(2).getColumnName()); + Assert.assertEquals(TSDataType.TEXT, headers.get(2).getColumnType()); + Assert.assertEquals( + headers.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()), + result.getResultSetHeader().getRespDataTypes()); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + Assert.assertFalse(columns[2].isNull(0)); + Assert.assertEquals(NodeStatus.MANUAL, columns[2].getBinary(0).toString()); + Assert.assertTrue(columns[2].isNull(1)); + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[1].getBinary(0).toString()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTaskTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTaskTest.java new file mode 100644 index 0000000000000..b088649d188c1 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowDataNodesTaskTest.java @@ -0,0 +1,106 @@ +/* + * 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.plan.execution.config.metadata; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.schema.column.ColumnHeader; +import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant; +import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo; +import org.apache.iotdb.confignode.rpc.thrift.TShowDataNodesResp; +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.rpc.TSStatusCode; + +import com.google.common.util.concurrent.SettableFuture; +import org.apache.tsfile.block.column.Column; +import org.apache.tsfile.enums.TSDataType; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class ShowDataNodesTaskTest { + + private static final int DATA_NODE_WITH_REASON_ID = 1; + private static final int DATA_NODE_WITHOUT_REASON_ID = 2; + + private static TShowDataNodesResp buildResp(final boolean setReasonOnFirstNode) { + final TDataNodeInfo dataNodeWithReason = + new TDataNodeInfo( + DATA_NODE_WITH_REASON_ID, NodeStatus.ReadOnly.getStatus(), "127.0.0.1", 6667, 1, 1); + if (setReasonOnFirstNode) { + dataNodeWithReason.setStatusReason(NodeStatus.MANUAL); + } + final TDataNodeInfo dataNodeWithoutReason = + new TDataNodeInfo( + DATA_NODE_WITHOUT_REASON_ID, NodeStatus.Running.getStatus(), "127.0.0.1", 6668, 1, 1); + return new TShowDataNodesResp(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())) + .setDataNodesInfoList(Arrays.asList(dataNodeWithReason, dataNodeWithoutReason)); + } + + private static ConfigTaskResult execute(final TShowDataNodesResp resp) throws Exception { + final SettableFuture future = SettableFuture.create(); + ShowDataNodesTask.buildTSBlock(resp, future); + return future.get(); + } + + private static List columnNames(final List headers) { + return headers.stream().map(ColumnHeader::getColumnName).collect(Collectors.toList()); + } + + @Test + public void testOldLayoutWhenNoReasonIsSet() throws Exception { + // Responses from an old ConfigNode never set the per-node statusReason field. + final ConfigTaskResult result = execute(buildResp(false)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals( + columnNames(ColumnHeaderConstant.showDataNodesColumnHeaders), columnNames(headers)); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + // The Status column carries the plain status, never a merged "ReadOnly(Manual)" string. + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[1].getBinary(0).toString()); + Assert.assertEquals(NodeStatus.Running.getStatus(), columns[1].getBinary(1).toString()); + } + + @Test + public void testReasonColumnInsertedWithNullForMissingReasons() throws Exception { + final ConfigTaskResult result = execute(buildResp(true)); + final List headers = result.getResultSetHeader().getColumnHeaders(); + + Assert.assertEquals(ColumnHeaderConstant.showDataNodesColumnHeaders.size() + 1, headers.size()); + Assert.assertEquals(ColumnHeaderConstant.STATUS_REASON, headers.get(2).getColumnName()); + Assert.assertEquals(TSDataType.TEXT, headers.get(2).getColumnType()); + Assert.assertEquals( + headers.stream().map(ColumnHeader::getColumnType).collect(Collectors.toList()), + result.getResultSetHeader().getRespDataTypes()); + final Column[] columns = result.getResultSet().getValueColumns(); + Assert.assertEquals(headers.size(), columns.length); + + // The first DataNode has the reason; the second keeps a NULL at the same position. + Assert.assertFalse(columns[2].isNull(0)); + Assert.assertEquals(NodeStatus.MANUAL, columns[2].getBinary(0).toString()); + Assert.assertTrue(columns[2].isNull(1)); + Assert.assertEquals(NodeStatus.ReadOnly.getStatus(), columns[1].getBinary(0).toString()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DataNodeShutdownHookTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DataNodeShutdownHookTest.java new file mode 100644 index 0000000000000..3acdc13ffcd47 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/DataNodeShutdownHookTest.java @@ -0,0 +1,65 @@ +/* + * 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; + +import org.apache.iotdb.commons.cluster.NodeStatus; +import org.apache.iotdb.commons.conf.CommonConfig; +import org.apache.iotdb.commons.conf.CommonDescriptor; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class DataNodeShutdownHookTest { + + private final CommonConfig commonConfig = CommonDescriptor.getInstance().getConfig(); + private NodeStatus originalStatus; + private String originalStatusReason; + private boolean originalStopping; + + @Before + public void setUp() { + originalStatus = commonConfig.getNodeStatus(); + originalStatusReason = commonConfig.getStatusReason(); + originalStopping = commonConfig.isStopping(); + commonConfig.setNodeStatus(NodeStatus.Running); + commonConfig.setStopping(false); + } + + @After + public void tearDown() { + commonConfig.setNodeStatus(originalStatus); + commonConfig.setStatusReason(originalStatusReason); + commonConfig.setStopping(originalStopping); + } + + @Test + public void testMarkNodeStoppingSetsReadOnlyWithStoppingReason() { + // The real shutdown hook performs this transition in the middle of its run() sequence; the + // extracted method is invoked directly here so the transient ReadOnly(Stopping) state is + // asserted deterministically instead of racing the shutdown window. + DataNodeShutdownHook.markNodeStopping(); + + Assert.assertTrue(commonConfig.isStopping()); + Assert.assertEquals(NodeStatus.ReadOnly, commonConfig.getNodeStatus()); + Assert.assertEquals(NodeStatus.STOPPING, commonConfig.getStatusReason()); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/disk/FolderManagerTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/disk/FolderManagerTest.java index 44d8b5816c32a..5f09d9d8cfaa7 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/disk/FolderManagerTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/disk/FolderManagerTest.java @@ -198,7 +198,6 @@ public void testFolderManagerCanSkipNodeStatusChangeWhenDiskFull() { NodeStatus originalStatus = commonConfig.getNodeStatus(); String originalStatusReason = commonConfig.getStatusReason(); commonConfig.setNodeStatus(NodeStatus.Running); - commonConfig.setStatusReason(null); try { new FolderManager(Collections.emptyList(), strategyType, false); @@ -211,4 +210,24 @@ public void testFolderManagerCanSkipNodeStatusChangeWhenDiskFull() { commonConfig.setStatusReason(originalStatusReason); } } + + @Test + public void testFolderManagerSetsReadOnlyWithDiskFullReasonWhenDiskFull() { + CommonConfig commonConfig = CommonDescriptor.getInstance().getConfig(); + NodeStatus originalStatus = commonConfig.getNodeStatus(); + String originalStatusReason = commonConfig.getStatusReason(); + commonConfig.setNodeStatus(NodeStatus.Running); + + try { + // All folders are exhausted, so the node must switch to ReadOnly with the DiskFull reason. + new FolderManager(Collections.emptyList(), strategyType, true); + fail("Expected DiskSpaceInsufficientException"); + } catch (DiskSpaceInsufficientException e) { + assertEquals(NodeStatus.ReadOnly, commonConfig.getNodeStatus()); + assertEquals(NodeStatus.DISK_FULL, commonConfig.getStatusReason()); + } finally { + commonConfig.setNodeStatus(originalStatus); + commonConfig.setStatusReason(originalStatusReason); + } + } } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/cluster/NodeStatus.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/cluster/NodeStatus.java index 518a9faaed2ec..f865ecf2cd159 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/cluster/NodeStatus.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/cluster/NodeStatus.java @@ -34,8 +34,18 @@ public enum NodeStatus { /** Only query statements are permitted */ ReadOnly("ReadOnly"); + + /** + * Reasons for entering ReadOnly. These strings cross node RPCs and are compared literally (e.g. + * the DiskFull auto-recovery in sampleDiskLoad), so they must stay locale-independent plain + * constants instead of i18n messages. + */ public static final String DISK_FULL = "DiskFull"; + public static final String MANUAL = "Manual"; + public static final String STOPPING = "Stopping"; + public static final String UNRECOVERABLE_ERROR = "UnrecoverableError"; + private final String status; NodeStatus(String status) { diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java index cb5902986b8e8..3ddac632defe5 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java @@ -27,6 +27,7 @@ import org.apache.iotdb.commons.enums.PipeRateAverage; import org.apache.iotdb.commons.i18n.ConfigMessages; import org.apache.iotdb.commons.pipe.config.PipeConfig; +import org.apache.iotdb.commons.queryengine.utils.DateTimeUtils; import org.apache.iotdb.commons.utils.FileUtils; import org.apache.iotdb.commons.utils.KillPoint.KillPoint; import org.apache.iotdb.rpc.RpcUtils; @@ -174,13 +175,20 @@ public class CommonConfig { /** Status of current system. */ private volatile NodeStatus status = NodeStatus.Running; + /** Reason for the current status, meaningful for ReadOnly and updated independently of it. */ + private volatile String statusReason = null; + + /** + * Maximum length of the error message embedded in the UnrecoverableError status reason, keeping + * the reason bounded in heartbeats and SHOW results. + */ + private static final int MAX_STATUS_REASON_LENGTH = 256; + private NodeStatus lastStatus = NodeStatus.Unknown; private String lastStatusReason = ""; private volatile boolean isStopping = false; - private volatile String statusReason = null; - private final int TTimePartitionSlotTransmitLimit = 1000; /** Disk Monitor. */ @@ -791,8 +799,30 @@ void setHandleSystemErrorStrategy(HandleSystemErrorStrategy handleSystemErrorStr this.handleSystemErrorStrategy = handleSystemErrorStrategy; } - public void handleUnrecoverableError() { - handleSystemErrorStrategy.handle(); + /** + * Handles an unrecoverable error with the given exception. The ReadOnly status reason is + * assembled here so that all call sites share one format: "UnrecoverableError, , + * ". The error message is truncated to {@link #MAX_STATUS_REASON_LENGTH} + * characters so that the reason published in heartbeats and SHOW results stays bounded. + */ + public void handleUnrecoverableError(Throwable e) { + String errorMessage = + e.getMessage() == null || e.getMessage().isEmpty() + ? e.getClass().getSimpleName() + : e.getMessage(); + if (errorMessage.length() > MAX_STATUS_REASON_LENGTH) { + errorMessage = errorMessage.substring(0, MAX_STATUS_REASON_LENGTH) + "..."; + } + handleUnrecoverableError( + NodeStatus.UNRECOVERABLE_ERROR + + ", " + + DateTimeUtils.convertLongToDate(System.currentTimeMillis(), "ms") + + ", " + + errorMessage); + } + + public void handleUnrecoverableError(String errorReason) { + handleSystemErrorStrategy.handle(errorReason); } public double getDiskSpaceWarningThreshold() { @@ -835,15 +865,53 @@ public NodeStatus getNodeStatus() { return status; } + public String getStatusReason() { + return statusReason; + } + + /** + * Sets the status reason independently of the status. The status and its reason are two separate + * values that can be updated separately, e.g. the disk-full ReadOnly and its recovery in the + * heartbeat sampler. + */ + public synchronized void setStatusReason(String statusReason) { + this.statusReason = statusReason; + } + + /** + * Sets the node status, clearing the status reason when the status changes. A write of the same + * status keeps the current reason unchanged, so an existing reason (e.g. ReadOnly + DiskFull) is + * preserved. + */ public synchronized void setNodeStatus(NodeStatus newStatus) { - if (status == newStatus) { + setNodeStatusWithReason(newStatus, null); + } + + /** + * Sets the node status and reason together. For ReadOnly the write is priority-guarded by {@link + * #setReadOnlyWithReason}. The status reason is only meaningful for ReadOnly: avoid passing a + * non-null reason together with another status (it would be displayed as e.g. Running(reason) in + * SHOW CLUSTER, and no production code does this) — the value is still written through for + * generality. + */ + public synchronized void setNodeStatusWithReason(NodeStatus newStatus, String newReason) { + if (newStatus == NodeStatus.ReadOnly) { + setReadOnlyWithReason(newReason); return; } - - logger.info(ConfigMessages.SET_SYSTEM_MODE, status, newStatus); + if (status == newStatus && Objects.equals(statusReason, newReason)) { + return; + } + logNodeStatusChange(status, newStatus); this.status = newStatus; - this.statusReason = null; + this.statusReason = newReason; + } + private void logNodeStatusChange(NodeStatus oldStatus, NodeStatus newStatus) { + if (oldStatus == newStatus) { + return; + } + logger.info(ConfigMessages.SET_SYSTEM_MODE, oldStatus, newStatus); switch (newStatus) { case ReadOnly: logger.warn(ConfigMessages.STATUS_CHANGE_TO_READ_ONLY); @@ -856,12 +924,47 @@ public synchronized void setNodeStatus(NodeStatus newStatus) { } } - public String getStatusReason() { - return statusReason; + /** + * Sets the node status to ReadOnly with the given reason, respecting reason priority: Stopping > + * Manual > UnrecoverableError > DiskFull. If a ReadOnly reason with equal or higher priority is + * already set, this call is a no-op (in particular, an UnrecoverableError keeps the first reason + * that was set). Non-ReadOnly statuses are always overridden. A null reason is treated as the + * legacy/unclassified ReadOnly with the lowest priority: it can only enter from a non-ReadOnly + * status and can never override a classified reason. + * + *

Must be called with the monitor held (only {@link #setNodeStatusWithReason} calls it). + */ + private void setReadOnlyWithReason(String reason) { + int newPriority = getReadOnlyReasonPriority(reason); + if (status == NodeStatus.ReadOnly && getReadOnlyReasonPriority(statusReason) >= newPriority) { + return; + } + logNodeStatusChange(status, NodeStatus.ReadOnly); + this.status = NodeStatus.ReadOnly; + this.statusReason = reason; } - public void setStatusReason(String statusReason) { - this.statusReason = statusReason; + /** + * Priority of ReadOnly reasons. Higher wins. Unknown reasons and null are treated as the lowest + * priority so that classified reasons can always override legacy/unknown ones. + */ + private static int getReadOnlyReasonPriority(String reason) { + if (reason == null) { + return 0; + } + if (reason.startsWith(NodeStatus.UNRECOVERABLE_ERROR)) { + return 2; + } + switch (reason) { + case NodeStatus.STOPPING: + return 4; + case NodeStatus.MANUAL: + return 3; + case NodeStatus.DISK_FULL: + return 1; + default: + return 0; + } } public int getTTimePartitionSlotTransmitLimit() { diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/FolderManager.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/FolderManager.java index 93ee02a2c15d2..bc8f159eafa0c 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/FolderManager.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/FolderManager.java @@ -142,8 +142,9 @@ private void changeToReadOnlyIfDiskFull(DiskSpaceInsufficientException e) { if (LoggerPeriodicalLogReducer.shouldLog(UtilMessages.ALL_FOLDERS_FULL_CHANGE_TO_READ_ONLY)) { logger.error(UtilMessages.ALL_FOLDERS_FULL_CHANGE_TO_READ_ONLY, e); } - CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly); - CommonDescriptor.getInstance().getConfig().setStatusReason(NodeStatus.DISK_FULL); + CommonDescriptor.getInstance() + .getConfig() + .setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); } else { logger.warn(UtilMessages.CANNOT_SELECT_FOLDER_BUT_DISK_HAS_SPACE, e); } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategy.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategy.java index 44c4a93749e99..978e7ecbbada8 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategy.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategy.java @@ -70,7 +70,9 @@ public void setFolders(List folders) throws DiskSpaceInsufficientExcepti if (!hasSpace) { if (changeSystemStatusToReadOnly) { LOGGER.error(UtilMessages.DISK_SPACE_INSUFFICIENT_READ_ONLY); - CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly); + CommonDescriptor.getInstance() + .getConfig() + .setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); } else { if (LoggerPeriodicalLogReducer.shouldLog( UtilMessages.MESSAGE_DISK_SPACE_INSUFFICIENT_DF6205B0)) { diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/enums/HandleSystemErrorStrategy.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/enums/HandleSystemErrorStrategy.java index b21dc8db89c3a..53da902872b5c 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/enums/HandleSystemErrorStrategy.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/enums/HandleSystemErrorStrategy.java @@ -33,13 +33,15 @@ public enum HandleSystemErrorStrategy { private static final Logger logger = LoggerFactory.getLogger(HandleSystemErrorStrategy.class); - public void handle() { + public void handle(String errorReason) { if (this == HandleSystemErrorStrategy.CHANGE_TO_READ_ONLY) { logger.error( CommonMessages .LOG_UNRECOVERABLE_ERROR_OCCURS_CHANGE_SYSTEM_STATUS_READ_ONLY_BECAUSE_HANDLE_05C9AD1A, new RuntimeException(CommonMessages.SYSTEM_READ_ONLY)); - CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly); + CommonDescriptor.getInstance() + .getConfig() + .setNodeStatusWithReason(NodeStatus.ReadOnly, errorReason); } else if (this == HandleSystemErrorStrategy.SHUTDOWN) { logger.error( CommonMessages diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java index d66b5a8a301eb..d23b7a0ecc2aa 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java @@ -86,6 +86,7 @@ private ColumnHeaderConstant() { public static final String NODE_ID = "NodeID"; public static final String NODE_TYPE = "NodeType"; public static final String STATUS = "Status"; + public static final String STATUS_REASON = "StatusReason"; public static final String INTERNAL_ADDRESS = "InternalAddress"; public static final String INTERNAL_PORT = "InternalPort"; public static final String CONFIG_CONSENSUS_PORT = "ConfigConsensusPort"; @@ -318,6 +319,7 @@ private ColumnHeaderConstant() { public static final String NODE_ID_TABLE_MODEL = "node_id"; public static final String NODE_TYPE_TABLE_MODEL = "node_type"; + public static final String STATUS_REASON_TABLE_MODEL = "status_reason"; public static final String INTERNAL_PORT_TABLE_MODEL = "internal_port"; public static final String BUILD_INFO_TABLE_MODEL = "build_info"; diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/InformationSchema.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/InformationSchema.java index 048ce8f8763bf..86f48a2c01581 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/InformationSchema.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/InformationSchema.java @@ -323,6 +323,9 @@ public class InformationSchema { nodesTable.addColumnSchema( new AttributeColumnSchema( ColumnHeaderConstant.STATUS.toLowerCase(Locale.ENGLISH), TSDataType.STRING)); + nodesTable.addColumnSchema( + new AttributeColumnSchema( + ColumnHeaderConstant.STATUS_REASON_TABLE_MODEL, TSDataType.STRING)); nodesTable.addColumnSchema( new AttributeColumnSchema( ColumnHeaderConstant.INTERNAL_ADDRESS_TABLE_MODEL, TSDataType.STRING)); diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/conf/CommonConfigTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/conf/CommonConfigTest.java index 2a38ad388d50e..1153d5f293e07 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/conf/CommonConfigTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/conf/CommonConfigTest.java @@ -45,12 +45,226 @@ public void testSubscriptionIsNotExposedInConfigurationTemplate() throws IOExcep @Test public void testSameNodeStatusDoesNotClearStatusReason() { CommonConfig config = new CommonConfig(); - config.setNodeStatus(NodeStatus.ReadOnly); - config.setStatusReason(NodeStatus.DISK_FULL); + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); config.setNodeStatus(NodeStatus.ReadOnly); Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); Assert.assertEquals(NodeStatus.DISK_FULL, config.getStatusReason()); } + + @Test + public void testSetReadOnlyWithReasonPriority() { + CommonConfig config = new CommonConfig(); + + // DiskFull can enter from Running. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertEquals(NodeStatus.DISK_FULL, config.getStatusReason()); + + // UnrecoverableError can override DiskFull. + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 10:00:00.000, first"); + Assert.assertEquals( + "UnrecoverableError, 2026-09-02 10:00:00.000, first", config.getStatusReason()); + + // A second UnrecoverableError keeps the first reason. + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 11:00:00.000, second"); + Assert.assertEquals( + "UnrecoverableError, 2026-09-02 10:00:00.000, first", config.getStatusReason()); + + // Manual overrides UnrecoverableError. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + Assert.assertEquals(NodeStatus.MANUAL, config.getStatusReason()); + + // DiskFull cannot override Manual. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); + Assert.assertEquals(NodeStatus.MANUAL, config.getStatusReason()); + + // UnrecoverableError cannot override Manual either. + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 12:00:00.000, third"); + Assert.assertEquals(NodeStatus.MANUAL, config.getStatusReason()); + + // Stopping overrides Manual. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.STOPPING); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + // Manual cannot override Stopping. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + // Unknown/Removing are always overridden, as in the pre-reason behavior. + config.setNodeStatus(NodeStatus.Unknown); + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertEquals(NodeStatus.DISK_FULL, config.getStatusReason()); + + config.setNodeStatus(NodeStatus.Removing); + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertEquals(NodeStatus.MANUAL, config.getStatusReason()); + } + + @Test + public void testStoppingOverridesUnrecoverableError() { + CommonConfig config = new CommonConfig(); + + // Stopping is the highest-priority reason: it overrides UnrecoverableError as well. + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 10:00:00.000, broken"); + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.STOPPING); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + // And an UnrecoverableError can not override Stopping afterwards. + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 11:00:00.000, broken again"); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + } + + @Test + public void testClassifiedReasonOverridesUnclassifiedReadOnly() { + CommonConfig config = new CommonConfig(); + + // A null-reason ReadOnly can be entered from Running... + config.setNodeStatusWithReason(NodeStatus.ReadOnly, null); + Assert.assertNull(config.getStatusReason()); + + // ...but any classified reason overrides it. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); + Assert.assertEquals(NodeStatus.DISK_FULL, config.getStatusReason()); + + config.setNodeStatusWithReason(NodeStatus.ReadOnly, null); + Assert.assertEquals(NodeStatus.DISK_FULL, config.getStatusReason()); + + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 10:00:00.000, broken"); + Assert.assertEquals( + "UnrecoverableError, 2026-09-02 10:00:00.000, broken", config.getStatusReason()); + + config.setNodeStatusWithReason(NodeStatus.ReadOnly, null); + Assert.assertEquals( + "UnrecoverableError, 2026-09-02 10:00:00.000, broken", config.getStatusReason()); + } + + @Test + public void testHandleUnrecoverableErrorBuildsStatusReason() { + // HandleSystemErrorStrategy writes the singleton config held by CommonDescriptor. + CommonConfig config = CommonDescriptor.getInstance().getConfig(); + NodeStatus originalStatus = config.getNodeStatus(); + String originalStatusReason = config.getStatusReason(); + try { + config.handleUnrecoverableError(new IOException("disk broken")); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertTrue(config.getStatusReason().startsWith(NodeStatus.UNRECOVERABLE_ERROR + ", ")); + Assert.assertTrue(config.getStatusReason().contains("disk broken")); + } finally { + config.setNodeStatus(originalStatus); + config.setStatusReason(originalStatusReason); + } + } + + @Test + public void testSetNodeStatusWithReasonPassesThroughForNonReadOnly() { + CommonConfig config = new CommonConfig(); + + // The reason is written through for non-ReadOnly statuses. + config.setNodeStatusWithReason(NodeStatus.Running, "note"); + Assert.assertEquals(NodeStatus.Running, config.getNodeStatus()); + Assert.assertEquals("note", config.getStatusReason()); + + // Writing the same snapshot is a no-op. + config.setNodeStatusWithReason(NodeStatus.Running, "note"); + Assert.assertEquals("note", config.getStatusReason()); + + // Single-arg setNodeStatus clears the reason on the same status. + config.setNodeStatus(NodeStatus.Running); + Assert.assertNull(config.getStatusReason()); + } + + @Test + public void testNullReadOnlyReasonKeepsLegacySemantics() { + CommonConfig config = new CommonConfig(); + + // A null reason can only enter from a non-ReadOnly status. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, null); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertNull(config.getStatusReason()); + + // ...and can never override a classified reason. + config.setNodeStatus(NodeStatus.Running); + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + config.setNodeStatusWithReason(NodeStatus.ReadOnly, null); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertEquals(NodeStatus.MANUAL, config.getStatusReason()); + } + + @Test + public void testHandleUnrecoverableErrorFallsBackToClassName() { + CommonConfig config = CommonDescriptor.getInstance().getConfig(); + NodeStatus originalStatus = config.getNodeStatus(); + String originalStatusReason = config.getStatusReason(); + try { + config.handleUnrecoverableError(new RuntimeException()); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertTrue(config.getStatusReason().contains("RuntimeException")); + } finally { + config.setNodeStatus(originalStatus); + config.setStatusReason(originalStatusReason); + } + } + + @Test + public void testHandleUnrecoverableErrorTruncatesLongMessage() { + CommonConfig config = CommonDescriptor.getInstance().getConfig(); + NodeStatus originalStatus = config.getNodeStatus(); + String originalStatusReason = config.getStatusReason(); + try { + StringBuilder longMessage = new StringBuilder(); + for (int i = 0; i < 300; i++) { + longMessage.append('x'); + } + config.handleUnrecoverableError(new IOException(longMessage.toString())); + String statusReason = config.getStatusReason(); + Assert.assertTrue(statusReason.endsWith("...")); + Assert.assertTrue( + statusReason.substring(statusReason.lastIndexOf(", ") + 2).length() <= 256 + 3); + } finally { + config.setNodeStatus(originalStatus); + config.setStatusReason(originalStatusReason); + } + } + + @Test + public void testStoppingEntersReadOnlyFromRunningAndBlocksAllOtherReasons() { + CommonConfig config = new CommonConfig(); + + // The shutdown hook performs exactly this transition: from Running into ReadOnly with the + // Stopping reason. The state is transient in the real shutdown sequence, so the entry is + // exercised here directly instead of racing it. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.STOPPING); + Assert.assertEquals(NodeStatus.ReadOnly, config.getNodeStatus()); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + // Stopping has the highest priority: no classified reason can override it. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.DISK_FULL); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + config.setNodeStatusWithReason(NodeStatus.ReadOnly, NodeStatus.MANUAL); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + config.setNodeStatusWithReason( + NodeStatus.ReadOnly, "UnrecoverableError, 2026-09-02 10:00:00.000, broken"); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + // ...nor can an unclassified reason. + config.setNodeStatusWithReason(NodeStatus.ReadOnly, null); + Assert.assertEquals(NodeStatus.STOPPING, config.getStatusReason()); + + // Only an explicit management change leaving ReadOnly clears the reason. + config.setNodeStatus(NodeStatus.Running); + Assert.assertEquals(NodeStatus.Running, config.getNodeStatus()); + Assert.assertNull(config.getStatusReason()); + } } diff --git a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift index 512e2e05f05fc..6be1951956eb2 100644 --- a/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift +++ b/iotdb-protocol/thrift-confignode/src/main/thrift/confignode.thrift @@ -643,6 +643,9 @@ struct TShowClusterResp { 4: required list aiNodeList 5: required map nodeStatus 6: required map nodeVersionInfo + // The reason why the node is in its current status, independent of nodeStatus. + // Absent when the ConfigNode is an old version. + 7: optional map nodeStatusReason } struct TGetClusterIdResp { @@ -685,6 +688,9 @@ struct TDataNodeInfo { 5: required i32 dataRegionNum 6: required i32 schemaRegionNum 7: optional i32 cpuCoreNum + // The reason why the DataNode is in its current status, independent of status. + // Absent when the ConfigNode is an old version. + 8: optional string statusReason } struct TAINodeInfo { @@ -723,6 +729,9 @@ struct TConfigNodeInfo { 3: required string internalAddress 4: required i32 internalPort 5: required string roleType + // The reason why the ConfigNode is in its current status, independent of status. + // Absent when the ConfigNode is an old version. + 6: optional string statusReason } struct TShowConfigNodesResp {