Skip to content

Commit fc8525d

Browse files
authored
[To dev/1.3] Fence IoTConsensus writes before storage shutdown (#18841, #18893) (#18896)
* Fence IoTConsensus applies before storage shutdown (#18841) * Fix queued IoTConsensus writes during shutdown * Test queued IoTConsensus writes across storage shutdown * Reproduce WAL checkpoint loss from writes after shutdown * Fence IoTConsensus applies before storage shutdown (cherry picked from commit 532d6c1) * Reuse DataRegion insert lock for IoTConsensus shutdown fencing (#18893) (cherry picked from commit d2b97bc)
1 parent 08a7c89 commit fc8525d

10 files changed

Lines changed: 1170 additions & 60 deletions

File tree

‎integration-test/src/test/java/org/apache/iotdb/pipe/it/autocreate/IoTDBPipeClusterIT.java‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -442,31 +442,32 @@ private void flushTreeDataRegionReplicasAfterReplicationComplete(
442442

443443
private void waitForTreeDataRegionReplicationComplete(
444444
final BaseEnv env, final List<String> databases) {
445+
// IoTConsensus accepts writes on any replica, including a previous leader during failover.
446+
// Check every replica's outgoing replication before flushing or restarting the cluster.
445447
await()
446448
.pollInterval(500, TimeUnit.MILLISECONDS)
447449
.atMost(2, TimeUnit.MINUTES)
448450
.untilAsserted(
449451
() -> {
450452
try (final SyncConfigNodeIServiceClient client =
451453
(SyncConfigNodeIServiceClient) env.getLeaderConfigNodeConnection()) {
452-
final List<TRegionInfo> leaderRegionInfoList =
453-
showTreeDataRegionLeaders(databases, client);
454+
final List<TRegionInfo> regionInfoList = showTreeDataRegions(databases, client);
454455
Assert.assertFalse(
455-
"No tree DataRegion leader found for databases " + databases,
456-
leaderRegionInfoList.isEmpty());
456+
"No tree DataRegion found for databases " + databases,
457+
regionInfoList.isEmpty());
457458

458-
for (final TRegionInfo regionInfo : leaderRegionInfoList) {
459-
final DataNodeWrapper leaderNode =
459+
for (final TRegionInfo regionInfo : regionInfoList) {
460+
final DataNodeWrapper replicaNode =
460461
findDataNodeWrapperByPort(env, regionInfo.getClientRpcPort());
461462
final String metricsUrl =
462463
"http://"
463-
+ leaderNode.getIp()
464+
+ replicaNode.getIp()
464465
+ ":"
465-
+ leaderNode.getMetricPort()
466+
+ replicaNode.getMetricPort()
466467
+ "/metrics";
467468
final String metricsContent = env.getUrlContent(metricsUrl, null);
468469
Assert.assertNotNull(
469-
"Failed to fetch metrics from leader DataNode at " + metricsUrl,
470+
"Failed to fetch metrics from replica DataNode at " + metricsUrl,
470471
metricsContent);
471472
assertSyncLagIsZero(metricsContent, buildDataRegionTag(regionInfo), metricsUrl);
472473
}
@@ -476,20 +477,25 @@ private void waitForTreeDataRegionReplicationComplete(
476477

477478
private List<TRegionInfo> showTreeDataRegionLeaders(
478479
final List<String> databases, final SyncConfigNodeIServiceClient client) throws TException {
480+
final List<TRegionInfo> result = new ArrayList<>();
481+
for (final TRegionInfo regionInfo : showTreeDataRegions(databases, client)) {
482+
if (RegionRoleType.Leader.getRoleType().equals(regionInfo.getRoleType())) {
483+
result.add(regionInfo);
484+
}
485+
}
486+
return result;
487+
}
488+
489+
private List<TRegionInfo> showTreeDataRegions(
490+
final List<String> databases, final SyncConfigNodeIServiceClient client) throws TException {
479491
final TShowRegionResp showRegionResp =
480492
client.showRegion(
481493
new TShowRegionReq()
482494
.setConsensusGroupType(TConsensusGroupType.DataRegion)
483495
.setDatabases(databases));
484496
Assert.assertEquals(
485497
TSStatusCode.SUCCESS_STATUS.getStatusCode(), showRegionResp.getStatus().getCode());
486-
final List<TRegionInfo> result = new ArrayList<>();
487-
for (final TRegionInfo regionInfo : showRegionResp.getRegionInfoList()) {
488-
if (RegionRoleType.Leader.getRoleType().equals(regionInfo.getRoleType())) {
489-
result.add(regionInfo);
490-
}
491-
}
492-
return result;
498+
return showRegionResp.getRegionInfoList();
493499
}
494500

495501
private DataNodeWrapper findDataNodeWrapperByPort(final BaseEnv env, final int port) {

‎iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.iotdb.commons.utils.CommonDateTimeUtils;
3232
import org.apache.iotdb.commons.utils.KillPoint.DataNodeKillPoints;
3333
import org.apache.iotdb.commons.utils.KillPoint.KillPoint;
34+
import org.apache.iotdb.commons.utils.StatusUtils;
3435
import org.apache.iotdb.consensus.IStateMachine;
3536
import org.apache.iotdb.consensus.common.DataSet;
3637
import org.apache.iotdb.consensus.common.Peer;
@@ -1010,7 +1011,13 @@ private TSStatus cacheAndInsertLatestNode(DeserializedBatchIndexedConsensusReque
10101011
List<TSStatus> subStatus = new LinkedList<>();
10111012
for (IConsensusRequest insertNode : request.getInsertNodes()) {
10121013
insertNode.markAsGeneratedByRemoteConsensusLeader();
1013-
subStatus.add(stateMachine.write(insertNode));
1014+
// Reject requests that became read-only while queued. This is an early rejection only:
1015+
// the state machine must also fence the full apply against storage shutdown, since the
1016+
// replica can become read-only after this check.
1017+
subStatus.add(
1018+
isReadOnly()
1019+
? StatusUtils.getStatus(TSStatusCode.SYSTEM_READ_ONLY)
1020+
: stateMachine.write(insertNode));
10141021
}
10151022
long applyTime = System.nanoTime();
10161023
ioTConsensusServerMetrics.recordApplyCost(applyTime - sortTime);
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.consensus.iot;
21+
22+
import org.apache.iotdb.common.rpc.thrift.TEndPoint;
23+
import org.apache.iotdb.common.rpc.thrift.TSStatus;
24+
import org.apache.iotdb.commons.consensus.DataRegionId;
25+
import org.apache.iotdb.commons.utils.StatusUtils;
26+
import org.apache.iotdb.consensus.common.Peer;
27+
import org.apache.iotdb.consensus.common.request.DeserializedBatchIndexedConsensusRequest;
28+
import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
29+
import org.apache.iotdb.consensus.config.IoTConsensusConfig;
30+
import org.apache.iotdb.consensus.iot.util.TestEntry;
31+
import org.apache.iotdb.consensus.iot.util.TestStateMachine;
32+
import org.apache.iotdb.rpc.TSStatusCode;
33+
34+
import org.junit.Rule;
35+
import org.junit.Test;
36+
import org.junit.rules.TemporaryFolder;
37+
38+
import java.util.Collections;
39+
import java.util.TreeSet;
40+
import java.util.concurrent.Executors;
41+
import java.util.concurrent.ScheduledExecutorService;
42+
import java.util.concurrent.TimeUnit;
43+
import java.util.concurrent.atomic.AtomicBoolean;
44+
import java.util.concurrent.atomic.AtomicReference;
45+
46+
import static org.junit.Assert.assertEquals;
47+
import static org.junit.Assert.assertFalse;
48+
import static org.junit.Assert.assertTrue;
49+
50+
public class IoTConsensusServerImplTest {
51+
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
52+
53+
@Test
54+
public void testSyncLogBecomesReadOnlyWhileWaiting() throws Exception {
55+
final ScheduledExecutorService backgroundTaskService =
56+
Executors.newSingleThreadScheduledExecutor();
57+
final AtomicBoolean readOnly = new AtomicBoolean();
58+
final TestStateMachine stateMachine =
59+
new TestStateMachine() {
60+
@Override
61+
public boolean isReadOnly() {
62+
return readOnly.get();
63+
}
64+
};
65+
final Peer thisNode = new Peer(new DataRegionId(1), 0, new TEndPoint("127.0.0.1", 6667));
66+
final IoTConsensusServerImpl server =
67+
new IoTConsensusServerImpl(
68+
temporaryFolder.getRoot().getAbsolutePath(),
69+
thisNode,
70+
new TreeSet<>(Collections.singletonList(thisNode)),
71+
stateMachine,
72+
backgroundTaskService,
73+
null,
74+
null,
75+
IoTConsensusConfig.newBuilder()
76+
.setReplication(
77+
IoTConsensusConfig.Replication.newBuilder()
78+
.setMaxWaitingTimeForWaitBatchInMs(100)
79+
.build())
80+
.build());
81+
final DeserializedBatchIndexedConsensusRequest request =
82+
new DeserializedBatchIndexedConsensusRequest(1, 2, 2);
83+
for (int index = 1; index <= 2; index++) {
84+
request.add(
85+
new IndexedConsensusRequest(
86+
index, index, Collections.singletonList(new TestEntry(index, thisNode))));
87+
}
88+
final AtomicReference<TSStatus> status = new AtomicReference<>();
89+
final Thread syncLogThread = new Thread(() -> status.set(server.syncLog(1, request)));
90+
try {
91+
assertFalse(server.isReadOnly());
92+
syncLogThread.start();
93+
final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
94+
while (syncLogThread.getState() != Thread.State.TIMED_WAITING
95+
&& System.nanoTime() < deadline) {
96+
Thread.sleep(10);
97+
}
98+
assertEquals(Thread.State.TIMED_WAITING, syncLogThread.getState());
99+
readOnly.set(true);
100+
syncLogThread.join(TimeUnit.SECONDS.toMillis(15));
101+
assertFalse(syncLogThread.isAlive());
102+
assertEquals(2, status.get().getSubStatusSize());
103+
assertEquals(
104+
TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(),
105+
status.get().getSubStatus().get(0).getCode());
106+
assertEquals(
107+
StatusUtils.getStatus(TSStatusCode.SYSTEM_READ_ONLY).getMessage(),
108+
status.get().getSubStatus().get(0).getMessage());
109+
assertEquals(
110+
TSStatusCode.SYSTEM_READ_ONLY.getStatusCode(),
111+
status.get().getSubStatus().get(1).getCode());
112+
assertTrue(stateMachine.getRequestSet().isEmpty());
113+
114+
// The sender must be able to retry the complete batch after the replica becomes writable.
115+
readOnly.set(false);
116+
server.reloadConsensusConfig(
117+
IoTConsensusConfig.newBuilder()
118+
.setReplication(
119+
IoTConsensusConfig.Replication.newBuilder()
120+
.setMaxPendingBatchesNum(1)
121+
.setMaxWaitingTimeForWaitBatchInMs(100)
122+
.build())
123+
.build());
124+
final TSStatus retryStatus = server.syncLog(1, request);
125+
assertTrue(
126+
retryStatus.getSubStatus().stream()
127+
.allMatch(s -> s.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()));
128+
assertEquals(2, stateMachine.getRequestSet().size());
129+
} finally {
130+
syncLogThread.interrupt();
131+
syncLogThread.join(TimeUnit.SECONDS.toMillis(5));
132+
backgroundTaskService.shutdownNow();
133+
}
134+
}
135+
}

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/statemachine/dataregion/DataExecutionVisitor.java‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public TSStatus visitPlan(PlanNode node, DataRegion context) {
6060
public TSStatus visitInsertRow(InsertRowNode node, DataRegion dataRegion) {
6161
try {
6262
dataRegion.insert(node);
63-
dataRegion.insertSeparatorToWAL();
6463
return StatusUtils.OK;
6564
} catch (OutOfTTLException e) {
6665
LOGGER.warn("Error in executing plan node: {}, caused by {}", node, e.getMessage());
@@ -78,7 +77,6 @@ public TSStatus visitInsertRow(InsertRowNode node, DataRegion dataRegion) {
7877
public TSStatus visitInsertTablet(InsertTabletNode node, DataRegion dataRegion) {
7978
try {
8079
dataRegion.insertTablet(node);
81-
dataRegion.insertSeparatorToWAL();
8280
return StatusUtils.OK;
8381
} catch (OutOfTTLException e) {
8482
LOGGER.warn("Error in executing plan node: {}, caused by {}", node, e.getMessage());
@@ -115,7 +113,6 @@ public TSStatus visitInsertTablet(InsertTabletNode node, DataRegion dataRegion)
115113
public TSStatus visitInsertRows(InsertRowsNode node, DataRegion dataRegion) {
116114
try {
117115
dataRegion.insert(node);
118-
dataRegion.insertSeparatorToWAL();
119116
return StatusUtils.OK;
120117
} catch (WriteProcessRejectException e) {
121118
LOGGER.warn("Reject in executing plan node: {}, caused by {}", node, e.getMessage());
@@ -149,8 +146,9 @@ public TSStatus visitInsertRows(InsertRowsNode node, DataRegion dataRegion) {
149146
public TSStatus visitInsertMultiTablets(InsertMultiTabletsNode node, DataRegion dataRegion) {
150147
try {
151148
dataRegion.insertTablets(node);
152-
dataRegion.insertSeparatorToWAL();
153149
return StatusUtils.OK;
150+
} catch (WriteProcessRejectException e) {
151+
return RpcUtils.getStatus(e.getErrorCode(), e.getMessage());
154152
} catch (BatchProcessException e) {
155153
LOGGER.warn("Batch failure in executing a InsertMultiTabletsNode.");
156154
TSStatus firstStatus = null;
@@ -181,7 +179,6 @@ public TSStatus visitInsertRowsOfOneDevice(
181179
InsertRowsOfOneDeviceNode node, DataRegion dataRegion) {
182180
try {
183181
dataRegion.insert(node);
184-
dataRegion.insertSeparatorToWAL();
185182
return StatusUtils.OK;
186183
} catch (WriteProcessRejectException e) {
187184
LOGGER.warn("Reject in executing plan node: {}, caused by {}", node, e.getMessage());
@@ -244,6 +241,8 @@ public TSStatus visitDeleteData(DeleteDataNode node, DataRegion dataRegion) {
244241
dataRegion.insertSeparatorToWAL();
245242
PipeInsertionDataNodeListener.getInstance().listenToDeleteData(node);
246243
return StatusUtils.OK;
244+
} catch (WriteProcessRejectException e) {
245+
return RpcUtils.getStatus(e.getErrorCode(), e.getMessage());
247246
} catch (IOException | IllegalPathException e) {
248247
LOGGER.error("Error in executing plan node: {}", node, e);
249248
return new TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode());

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/DataNodeShutdownHook.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public void run() {
9595
// Reject write operations to make sure all tsfiles will be sealed
9696
CommonDescriptor.getInstance().getConfig().setStopping(true);
9797
CommonDescriptor.getInstance().getConfig().setNodeStatus(NodeStatus.ReadOnly);
98+
// A pre-apply read-only check can race with this transition. Drain admitted IoTConsensus
99+
// applies, including their WAL separators, and permanently reject new ones before flushing.
100+
StorageEngine.getInstance().stopIoTConsensusWrites();
98101
// Wait all wal are flushed
99102
WALManager.getInstance().waitAllWALFlushed();
100103

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ private void asyncRecoverTsFileResource() {
398398

399399
@Override
400400
public void stop() {
401+
stopIoTConsensusWrites();
401402
loadTsFileManager.stop();
402403
for (DataRegion dataRegion : dataRegionMap.values()) {
403404
if (dataRegion != null) {
@@ -417,6 +418,7 @@ public void stop() {
417418

418419
@Override
419420
public void shutdown(long milliseconds) throws ShutdownException {
421+
stopIoTConsensusWrites();
420422
loadTsFileManager.stop();
421423
try {
422424
for (DataRegion dataRegion : dataRegionMap.values()) {
@@ -484,6 +486,17 @@ public synchronized void reset() {
484486
dataRegionMap.clear();
485487
}
486488

489+
public void stopIoTConsensusWrites() {
490+
if (!CONFIG.getDataRegionConsensusProtocolClass().equals(ConsensusFactory.IOT_CONSENSUS)) {
491+
return;
492+
}
493+
for (DataRegion dataRegion : dataRegionMap.values()) {
494+
if (dataRegion != null) {
495+
dataRegion.stopIoTConsensusWrites();
496+
}
497+
}
498+
}
499+
487500
/** flush command Sync asyncCloseOneProcessor all file node processors. */
488501
public void syncCloseAllProcessor() {
489502
LOGGER.info("Start closing all database processor");

0 commit comments

Comments
 (0)