Skip to content

Commit 83f6428

Browse files
authored
Fix subscription WAL replay cursor boundaries (#18810)
* Fix subscription AT_END replay cursor boundary * Fix beginning seek counting the empty WAL sentinel as a gap
1 parent 99a3e06 commit 83f6428

3 files changed

Lines changed: 304 additions & 8 deletions

File tree

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,11 +1103,17 @@ && compareWriterProgress(requestProgress, storedWriterProgress) == 0) {
11031103
"resolved first uncovered replayable WAL record");
11041104
}
11051105
return ReplayLocateDecision.atEnd(
1106-
consensusReqReader.getCurrentSearchIndex(),
1106+
nextSearchIndexAfterCurrent(),
11071107
effectiveRecoveryRegionProgress,
11081108
"all locally replayable WAL records are already covered");
11091109
}
11101110

1111+
private long nextSearchIndexAfterCurrent() {
1112+
// The reader reports the last local WAL index, while the subscription cursor is the next
1113+
// index to read. Starting at the last index would replay it and report one spurious WAL gap.
1114+
return consensusReqReader.getCurrentSearchIndex() + 1L;
1115+
}
1116+
11111117
protected ReplayLocateDecision locateReplayStartForRegionProgress(
11121118
final RegionProgress regionProgress, final boolean seekAfter) {
11131119
if (!(consensusReqReader instanceof WALNode)) {
@@ -3068,19 +3074,19 @@ public void cleanUp() {
30683074
// ======================== Seek ========================
30693075

30703076
/**
3071-
* Seeks to the earliest available WAL position. The actual position depends on WAL retention: if
3072-
* old files have been reclaimed, the earliest available position may be later than 0.
3077+
* Seeks to the earliest available WAL position. Local consensus search indexes start at 1; 0
3078+
* denotes an empty WAL. If old files have been reclaimed, replay may start at a later index.
30733079
*/
30743080
public void seekToBeginning() {
3075-
seekToResolvedPosition(0L, new RegionProgress(Collections.emptyMap()), "beginning");
3081+
seekToResolvedPosition(
3082+
FIRST_CONSENSUS_SEARCH_INDEX, new RegionProgress(Collections.emptyMap()), "beginning");
30763083
}
30773084

30783085
/**
30793086
* Seeks to the current WAL write position. After this, only newly written data will be consumed.
30803087
*/
30813088
public void seekToEnd() {
3082-
seekToResolvedPosition(
3083-
consensusReqReader.getCurrentSearchIndex(), computeTailRegionProgress(), "end");
3089+
seekToResolvedPosition(nextSearchIndexAfterCurrent(), computeTailRegionProgress(), "end");
30843090
}
30853091

30863092
public void seekToRegionProgress(final RegionProgress regionProgress) {
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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.db.subscription.broker.consensus;
21+
22+
import org.apache.iotdb.commons.conf.CommonConfig;
23+
import org.apache.iotdb.commons.conf.CommonDescriptor;
24+
import org.apache.iotdb.commons.consensus.DataRegionId;
25+
import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
26+
import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl;
27+
import org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy;
28+
import org.apache.iotdb.consensus.iot.WriterSafeFrontierTracker;
29+
import org.apache.iotdb.db.conf.IoTDBDescriptor;
30+
import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
31+
import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
32+
import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
33+
import org.apache.iotdb.db.subscription.event.SubscriptionEvent;
34+
import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
35+
import org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutor;
36+
import org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutorManager;
37+
import org.apache.iotdb.rpc.subscription.config.TopicConstant;
38+
import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
39+
import org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType;
40+
import org.apache.iotdb.rpc.subscription.payload.poll.TabletsPayload;
41+
42+
import org.apache.tsfile.enums.ColumnCategory;
43+
import org.apache.tsfile.enums.TSDataType;
44+
import org.apache.tsfile.write.record.Tablet;
45+
import org.junit.Rule;
46+
import org.junit.Test;
47+
import org.junit.rules.TemporaryFolder;
48+
import org.junit.runner.RunWith;
49+
import org.powermock.api.mockito.PowerMockito;
50+
import org.powermock.core.classloader.annotations.PowerMockIgnore;
51+
import org.powermock.core.classloader.annotations.PrepareForTest;
52+
import org.powermock.modules.junit4.PowerMockRunner;
53+
54+
import java.io.File;
55+
import java.util.ArrayList;
56+
import java.util.Arrays;
57+
import java.util.Collections;
58+
import java.util.Iterator;
59+
import java.util.List;
60+
import java.util.concurrent.TimeUnit;
61+
62+
import static org.junit.Assert.assertEquals;
63+
import static org.mockito.ArgumentMatchers.any;
64+
import static org.mockito.Mockito.mock;
65+
import static org.mockito.Mockito.times;
66+
import static org.mockito.Mockito.verify;
67+
import static org.mockito.Mockito.when;
68+
69+
@RunWith(PowerMockRunner.class)
70+
@PrepareForTest(ConsensusSubscriptionPrefetchExecutorManager.class)
71+
@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"})
72+
public class ConsensusPrefetchingQueueSeekTest {
73+
74+
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
75+
76+
@Test(timeout = 10_000L)
77+
public void testSeekToBeginningReplaysFirstLocalEntryWithoutWalGap() throws Exception {
78+
assertBeginningReplay(Collections.singletonList(createRequest(1L, 1L, 7)), 2L, 0L);
79+
}
80+
81+
@Test(timeout = 10_000L)
82+
public void testSeekToBeginningPreservesFollowerEntryBeforeFirstLocalEntry() throws Exception {
83+
assertBeginningReplay(
84+
Arrays.asList(createRequest(-1L, 10L, 8), createRequest(1L, 1L, 7)), 2L, 0L);
85+
}
86+
87+
@Test(timeout = 10_000L)
88+
public void testSeekToBeginningCountsOnlyMissingValidLocalIndexes() throws Exception {
89+
// Local indexes 1 and 2 are absent. Index 0 is the empty-WAL sentinel, not a missing entry.
90+
assertBeginningReplay(
91+
Arrays.asList(createRequest(3L, 3L, 7), createRequest(4L, 4L, 7)), 5L, 2L);
92+
}
93+
94+
private void assertBeginningReplay(
95+
final List<IndexedConsensusRequest> requests,
96+
final long expectedNextIndex,
97+
final long expectedSkippedEntries)
98+
throws Exception {
99+
final CommonConfig config = CommonDescriptor.getInstance().getConfig();
100+
final int originalBatchMaxDelay = config.getSubscriptionConsensusBatchMaxDelayInMs();
101+
final int originalPrefetchThreads =
102+
config.getSubscriptionConsensusPrefetchExecutorMaxThreadNum();
103+
final String originalSystemDir = IoTDBDescriptor.getInstance().getConfig().getSystemDir();
104+
final File systemDir = temporaryFolder.newFolder();
105+
ConsensusSubscriptionPrefetchExecutor executor = null;
106+
ConsensusPrefetchingQueue queue = null;
107+
try {
108+
config.setSubscriptionConsensusBatchMaxDelayInMs(0);
109+
config.setSubscriptionConsensusPrefetchExecutorMaxThreadNum(1);
110+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(systemDir.getAbsolutePath());
111+
112+
// Supply an isolated runtime even in builds where subscriptions are disabled. Seek reset
113+
// and replay still run through the real serial prefetch worker and the public queue API.
114+
executor = new ConsensusSubscriptionPrefetchExecutor();
115+
final ConsensusSubscriptionPrefetchExecutorManager manager =
116+
mock(ConsensusSubscriptionPrefetchExecutorManager.class);
117+
PowerMockito.mockStatic(ConsensusSubscriptionPrefetchExecutorManager.class);
118+
PowerMockito.when(ConsensusSubscriptionPrefetchExecutorManager.getInstance())
119+
.thenReturn(manager);
120+
when(manager.getExecutor()).thenReturn(executor);
121+
122+
final WALNode walNode = mock(WALNode.class);
123+
when(walNode.getCurrentSearchIndex()).thenReturn(expectedNextIndex - 1L);
124+
when(walNode.getLogDirectory()).thenReturn(systemDir);
125+
final IoTConsensusServerImpl server = mock(IoTConsensusServerImpl.class);
126+
when(server.getConsensusReqReader()).thenReturn(walNode);
127+
when(server.getWriterSafeFrontierTracker()).thenReturn(new WriterSafeFrontierTracker());
128+
final ConsensusLogToTabletConverter converter = mock(ConsensusLogToTabletConverter.class);
129+
when(converter.getDatabaseName()).thenReturn("db");
130+
when(converter.isTableModel()).thenReturn(true);
131+
when(converter.convert(any()))
132+
.thenAnswer(
133+
invocation ->
134+
Collections.singletonList(
135+
createTablet(((InsertRowNode) invocation.getArgument(0)).getTime())));
136+
137+
queue =
138+
new ConsensusPrefetchingQueue(
139+
"consumerGroup",
140+
"topic",
141+
TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
142+
new DataRegionId(1),
143+
server,
144+
new SubscriptionWalRetentionPolicy(
145+
"topic",
146+
SubscriptionWalRetentionPolicy.UNBOUNDED,
147+
SubscriptionWalRetentionPolicy.UNBOUNDED),
148+
converter,
149+
new ConsensusSubscriptionCommitManager(
150+
(consumerGroupId, topicName, regionId) ->
151+
ConsensusSubscriptionCommitManager.ConfigNodeProgressQueryResult.absent()),
152+
new RegionProgress(Collections.emptyMap()),
153+
expectedNextIndex,
154+
1L,
155+
true) {
156+
@Override
157+
protected ProgressWALIterator createSubscriptionWALIterator(
158+
final long startSearchIndex) {
159+
final Iterator<IndexedConsensusRequest> retainedEntries =
160+
requests.stream()
161+
.filter(
162+
request ->
163+
request.getSearchIndex() < 0
164+
|| request.getSearchIndex() >= startSearchIndex)
165+
.iterator();
166+
final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
167+
when(iterator.hasNext()).thenAnswer(ignored -> retainedEntries.hasNext());
168+
when(iterator.next()).thenAnswer(ignored -> retainedEntries.next());
169+
return iterator;
170+
}
171+
};
172+
queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 1024 * 1024));
173+
174+
queue.seekToBeginning();
175+
176+
final List<Long> actualTimestamps = new ArrayList<>();
177+
final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
178+
while (actualTimestamps.size() < requests.size() && System.nanoTime() < deadline) {
179+
final SubscriptionEvent event = queue.poll("consumer");
180+
if (event == null) {
181+
TimeUnit.MILLISECONDS.sleep(10L);
182+
continue;
183+
}
184+
assertEquals(
185+
SubscriptionPollResponseType.TABLETS.getType(),
186+
event.getCurrentResponse().getResponseType());
187+
for (final Tablet tablet :
188+
((TabletsPayload) event.getCurrentResponse().getPayload()).getTablets()) {
189+
assertEquals(1, tablet.getRowSize());
190+
actualTimestamps.add(tablet.getTimestamps()[0]);
191+
}
192+
}
193+
final List<Long> expectedTimestamps = new ArrayList<>();
194+
for (final IndexedConsensusRequest request : requests) {
195+
expectedTimestamps.add(((InsertRowNode) request.getRequests().get(0)).getTime());
196+
}
197+
Collections.sort(expectedTimestamps);
198+
Collections.sort(actualTimestamps);
199+
assertEquals(expectedTimestamps, actualTimestamps);
200+
assertEquals(expectedNextIndex, queue.getCurrentReadSearchIndex());
201+
assertEquals(expectedSkippedEntries, queue.getWalGapSkippedEntries());
202+
verify(converter, times(requests.size())).convert(any());
203+
} finally {
204+
if (queue != null) {
205+
queue.close();
206+
}
207+
if (executor != null) {
208+
executor.shutdown();
209+
}
210+
config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
211+
config.setSubscriptionConsensusPrefetchExecutorMaxThreadNum(originalPrefetchThreads);
212+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
213+
}
214+
}
215+
216+
private static IndexedConsensusRequest createRequest(
217+
final long searchIndex, final long localSeq, final int writerNodeId) {
218+
return new IndexedConsensusRequest(
219+
searchIndex,
220+
localSeq,
221+
Collections.singletonList(
222+
StatementTestUtils.genInsertRowNode(Math.toIntExact(localSeq))))
223+
.setPhysicalTime(1000L + localSeq)
224+
.setNodeId(writerNodeId);
225+
}
226+
227+
private static Tablet createTablet(final long timestamp) {
228+
final Tablet tablet =
229+
new Tablet(
230+
"sensors",
231+
Arrays.asList("device", "temperature"),
232+
Arrays.asList(TSDataType.STRING, TSDataType.DOUBLE),
233+
Arrays.asList(ColumnCategory.TAG, ColumnCategory.FIELD),
234+
1);
235+
tablet.addTimestamp(0, timestamp);
236+
tablet.addValue(0, 0, "d1");
237+
tablet.addValue(0, 1, 36.5);
238+
tablet.setRowSize(1);
239+
return tablet;
240+
}
241+
}

‎iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,12 +612,13 @@ public void testReplayStartPreservesUncoveredFollowerEntries() throws Exception
612612
reader.currentSearchIndex = 5L;
613613
final ConsensusPrefetchingQueue.ReplayLocateDecision tailDecision =
614614
queue.scanReplayStartForRequests(
615-
Collections.singletonList(createRequest(-1L, 11L, 101L, 8)).iterator(),
615+
Arrays.asList(createRequest(5L, 10L, 100L, 8), createRequest(-1L, 11L, 101L, 8))
616+
.iterator(),
616617
regionProgress,
617618
true);
618619

619620
assertEquals(ConsensusPrefetchingQueue.ReplayLocateStatus.AT_END, tailDecision.getStatus());
620-
assertEquals(5L, tailDecision.getStartSearchIndex());
621+
assertEquals(6L, tailDecision.getStartSearchIndex());
621622
assertEquals(
622623
committedProgress,
623624
tailDecision.getRecoveryRegionProgress().getWriterPositions().get(formerLeader));
@@ -1129,6 +1130,7 @@ public void testAtEndReplayLookupPreservesRequestedWriterFrontier() throws Excep
11291130
queue.scanReplayStartForRequests(Collections.emptyIterator(), requestedProgress, true);
11301131

11311132
assertEquals(ConsensusPrefetchingQueue.ReplayLocateStatus.AT_END, decision.getStatus());
1133+
assertEquals(6L, decision.getStartSearchIndex());
11321134
assertEquals(requestedProgress, decision.getRecoveryRegionProgress());
11331135
} finally {
11341136
if (queue != null) {
@@ -1138,6 +1140,53 @@ public void testAtEndReplayLookupPreservesRequestedWriterFrontier() throws Excep
11381140
}
11391141
}
11401142

1143+
@Test
1144+
public void testAtEndInitializationHasNoTailGap() throws Exception {
1145+
final String originalSystemDir = IoTDBDescriptor.getInstance().getConfig().getSystemDir();
1146+
final File systemDir = temporaryFolder.newFolder("atEndNoTailGap");
1147+
final File walDirectory = temporaryFolder.newFolder("atEndNoTailGapWal");
1148+
ConsensusPrefetchingQueue queue = null;
1149+
try {
1150+
final DataRegionId regionId = new DataRegionId(7);
1151+
final WALNode walNode = mock(WALNode.class);
1152+
when(walNode.getLogDirectory()).thenReturn(walDirectory);
1153+
when(walNode.getCurrentSearchIndex()).thenReturn(5L);
1154+
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
1155+
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
1156+
when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new WriterSafeFrontierTracker());
1157+
final RegionProgress committedProgress =
1158+
new RegionProgress(
1159+
Collections.singletonMap(
1160+
new WriterId(regionId.toString(), 7), new WriterProgress(1000L, 5L)));
1161+
queue =
1162+
new ConsensusPrefetchingQueue(
1163+
"consumerGroup",
1164+
"topic",
1165+
TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
1166+
regionId,
1167+
serverImpl,
1168+
new SubscriptionWalRetentionPolicy(
1169+
"topic",
1170+
SubscriptionWalRetentionPolicy.UNBOUNDED,
1171+
SubscriptionWalRetentionPolicy.UNBOUNDED),
1172+
mock(ConsensusLogToTabletConverter.class),
1173+
newCommitManager(systemDir),
1174+
committedProgress,
1175+
6L,
1176+
1L,
1177+
true);
1178+
1179+
assertNull(queue.poll("consumer"));
1180+
assertEquals(6L, queue.getCurrentReadSearchIndex());
1181+
assertEquals(0L, queue.getRawWalGap());
1182+
} finally {
1183+
if (queue != null) {
1184+
queue.close();
1185+
}
1186+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
1187+
}
1188+
}
1189+
11411190
@Test
11421191
public void testAtEndInitializationPreservesPendingQueueRegistrationBoundary() throws Exception {
11431192
final String originalSystemDir = IoTDBDescriptor.getInstance().getConfig().getSystemDir();

0 commit comments

Comments
 (0)