This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 84c4bafa38d [Subscription] Reduce consensus WAL replay and ACK
contention (#18402)
84c4bafa38d is described below
commit 84c4bafa38d7aa8fc61f57fa2b9f29107c843699
Author: Caideyipi <[email protected]>
AuthorDate: Thu Aug 6 12:23:21 2026 +0800
[Subscription] Reduce consensus WAL replay and ACK contention (#18402)
* [Subscription] Reduce consensus WAL replay and ACK contention
* [Subscription] Avoid redundant consensus WAL refresh work
* [Subscription] Fast-forward WAL iterator across pending delivery
---
.../consensus/ConsensusPrefetchingQueue.java | 74 +++--
.../broker/consensus/ProgressWALIterator.java | 251 ++++++++++++---
.../consensus/ConsensusPrefetchingQueueTest.java | 340 +++++++++++++++++++++
.../broker/consensus/ProgressWALIteratorTest.java | 285 +++++++++++++++++
4 files changed, 893 insertions(+), 57 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index dd02836cac9..879346dad65 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -195,9 +195,9 @@ public class ConsensusPrefetchingQueue {
private volatile ProgressWALIterator subscriptionWALIterator;
/**
- * Seek requests must not close/reset the WAL iterator from RPC threads
because the prefetch
- * worker may be reading it concurrently. Instead, seek only records the
latest desired reset and
- * the queue's next prefetch round applies it after observing the new seek
generation.
+ * WAL cursor changes outside the iterator must not close/reset it from RPC
threads because the
+ * prefetch worker may be reading it concurrently. Instead, the latest
desired reset is recorded
+ * and applied by the next prefetch round after observing the expected seek
generation.
*/
private volatile long pendingSubscriptionWalResetSearchIndex =
Long.MIN_VALUE;
@@ -1597,12 +1597,41 @@ public class ConsensusPrefetchingQueue {
return hasLocalSearchIndex(request) && request.getSearchIndex() <
nextExpectedSearchIndex.get();
}
- private void advanceLocalCursorIfPresent(final IndexedConsensusRequest
request) {
+ private boolean advanceLocalCursorIfPresent(final IndexedConsensusRequest
request) {
if (hasLocalSearchIndex(request)) {
nextExpectedSearchIndex.set(request.getSearchIndex() + 1);
+ return true;
+ }
+ return false;
+ }
+
+ private void advanceLocalCursorFromPendingIfPresent(
+ final IndexedConsensusRequest request, final long
expectedSeekGeneration) {
+ if (advanceLocalCursorIfPresent(request)) {
+ // Pending delivery advances independently of the WAL reader. Raise its
local lower bound in
+ // place so stale local requests are filtered without rebuilding and
rescanning retained WAL.
+ final ProgressWALIterator iterator = subscriptionWALIterator;
+ if (Objects.nonNull(iterator) && seekGeneration.get() ==
expectedSeekGeneration) {
+ iterator.advanceTo(
+ nextExpectedSearchIndex.get(),
this::isWriterProgressCoveredForWalFastForward);
+ }
}
}
+ private boolean isWriterProgressCoveredForWalFastForward(
+ final long physicalTime, final int nodeId, final long localSeq) {
+ final WriterProgress candidate = new WriterProgress(physicalTime,
localSeq);
+ final WriterProgress recoveryProgress =
+ recoveryWriterProgressByWriter.get(new
WriterId(consensusGroupId.toString(), nodeId));
+ if (Objects.nonNull(recoveryProgress)
+ && compareWriterProgress(candidate, recoveryProgress) <= 0) {
+ return true;
+ }
+ final WriterProgress materializedProgress =
materializedProgressByWriter.get(nodeId);
+ return Objects.nonNull(materializedProgress)
+ && compareWriterProgress(candidate, materializedProgress) <= 0;
+ }
+
private MaterializationResult appendRealtimeRequest(
final IndexedConsensusRequest request,
final DeliveryBatchState batchState,
@@ -1678,12 +1707,12 @@ public class ConsensusPrefetchingQueue {
if (shouldSkipForRecoveryProgress(request)) {
skippedCount++;
- advanceLocalCursorIfPresent(request);
+ advanceLocalCursorFromPendingIfPresent(request,
expectedSeekGeneration);
continue;
}
if (shouldSkipForMaterializedProgress(request)) {
skippedCount++;
- advanceLocalCursorIfPresent(request);
+ advanceLocalCursorFromPendingIfPresent(request,
expectedSeekGeneration);
continue;
}
@@ -1695,7 +1724,7 @@ public class ConsensusPrefetchingQueue {
}
markMaterializedProgress(request);
processedCount++;
- advanceLocalCursorIfPresent(request);
+ advanceLocalCursorFromPendingIfPresent(request, expectedSeekGeneration);
if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
break;
}
@@ -1787,7 +1816,9 @@ public class ConsensusPrefetchingQueue {
// Use the persistent linger batch so an unexpected runtime failure cannot
orphan already
// reserved Tablets or advance replay progress past data that has become
unreachable.
final DeliveryBatchState batchState = lingerBatch;
- resetSubscriptionWALPosition(nextExpectedSearchIndex.get());
+ // Keep the iterator and its buffered next request across rounds.
Reopening it here discards the
+ // request prepared by hasNext() and repeatedly re-reads, skips, and
decompresses the same WAL
+ // segment. Pending-path cursor advances and seek operations request
explicit realignment.
final MaterializationResult materializationResult =
pumpFromSubscriptionWAL(
batchState, expectedSeekGeneration, maxWalEntries, maxTablets,
maxBatchBytes);
@@ -1820,7 +1851,6 @@ public class ConsensusPrefetchingQueue {
return MaterializationResult.SUCCESS;
}
- subscriptionWALIterator.refresh();
ensureSubscriptionWalReadable();
int entriesRead = 0;
@@ -1876,9 +1906,14 @@ public class ConsensusPrefetchingQueue {
}
private void ensureSubscriptionWalReadable() {
- if (Objects.isNull(subscriptionWALIterator)
- || subscriptionWALIterator.hasNext()
- || !(consensusReqReader instanceof WALNode)) {
+ if (Objects.isNull(subscriptionWALIterator) ||
subscriptionWALIterator.hasNext()) {
+ return;
+ }
+
+ // Listing and sorting all retained WAL files is only necessary after the
iterator is
+ // exhausted. While it still has a readable request, refreshing cannot
affect the next result.
+ subscriptionWALIterator.refresh();
+ if (subscriptionWALIterator.hasNext() || !(consensusReqReader instanceof
WALNode)) {
return;
}
@@ -1895,9 +1930,6 @@ public class ConsensusPrefetchingQueue {
currentWalIndex);
((WALNode) consensusReqReader).rollWALFile();
resetSubscriptionWALPosition(nextExpectedSearchIndex.get());
- if (Objects.nonNull(subscriptionWALIterator)) {
- subscriptionWALIterator.refresh();
- }
}
private void resetSubscriptionWALPosition(final long startSearchIndex) {
@@ -1915,6 +1947,11 @@ public class ConsensusPrefetchingQueue {
protected void onWalGapRetryScheduled() {}
private boolean hasReadableWalEntries() {
+ if (pendingSubscriptionWalResetSearchIndex != Long.MIN_VALUE) {
+ // Do not advance the stale iterator only to discard its buffered
request when the next round
+ // applies the pending realignment. Returning true keeps the worker
scheduled for that round.
+ return true;
+ }
return Objects.nonNull(subscriptionWALIterator) &&
subscriptionWALIterator.hasNext();
}
@@ -2209,7 +2246,10 @@ public class ConsensusPrefetchingQueue {
private boolean ackMissingInFlightEvent(
final SubscriptionCommitContext commitContext, final boolean silent) {
- acquireWriteLock();
+ // Late or duplicate ACKs touch the same concurrent lifecycle indexes and
commit manager as the
+ // regular in-flight ACK path. A read lock is sufficient to fence
seek/close transitions while
+ // allowing ACKs to proceed concurrently with a long-running WAL prefetch
round.
+ acquireReadLock();
try {
if (!canAcceptCommitContext(commitContext, "ack", silent)) {
return false;
@@ -2256,7 +2296,7 @@ public class ConsensusPrefetchingQueue {
}
return true;
} finally {
- releaseWriteLock();
+ releaseReadLock();
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
index 56847c8903c..56320a08a82 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
@@ -42,6 +42,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -56,6 +57,12 @@ import java.util.Set;
*/
public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensusRequest> {
+ @FunctionalInterface
+ public interface WriterProgressCoverage {
+
+ boolean isCovered(long physicalTime, int nodeId, long localSeq);
+ }
+
private static final Logger LOGGER =
LoggerFactory.getLogger(ProgressWALIterator.class);
private static final int SEARCH_INDEX_OFFSET =
@@ -65,9 +72,10 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
WALFileVersion.V2.getVersionBytes().length,
WALFileVersion.V3.getVersionBytes().length);
private final File logDirectory;
- private final long startSearchIndex;
+ private long minimumSearchIndex;
private final WALNode liveWalNode;
private File[] walFiles;
+ private long[] walFileVersionIds;
private int currentFileIndex = -1;
private ProgressWALReader currentReader;
private long currentReaderVersionId = -1L;
@@ -105,7 +113,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
private ProgressWALIterator(
final File logDirectory, final long startSearchIndex, final WALNode
liveWalNode) {
this.logDirectory = logDirectory;
- this.startSearchIndex = startSearchIndex;
+ this.minimumSearchIndex = startSearchIndex;
this.liveWalNode = liveWalNode;
refreshFileList();
}
@@ -114,23 +122,29 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
final File[] discoveredWalFiles =
WALFileUtils.listAllWALFiles(logDirectory);
if (discoveredWalFiles == null) {
walFiles = new File[0];
+ walFileVersionIds = new long[0];
return;
}
WALFileUtils.ascSortByVersionId(discoveredWalFiles);
- final List<File> filteredWalFiles = new
ArrayList<>(discoveredWalFiles.length);
+ final File[] filteredWalFiles = new File[discoveredWalFiles.length];
+ final long[] filteredWalFileVersionIds = new
long[discoveredWalFiles.length];
+ int filteredWalFileCount = 0;
for (int i = 0; i < discoveredWalFiles.length; i++) {
final File walFile = discoveredWalFiles[i];
+ final long versionId = WALFileUtils.parseVersionId(walFile.getName());
final boolean isLastWalFile = i == discoveredWalFiles.length - 1;
- if (!isLastWalFile && shouldSkipWalFile(walFile)) {
+ if (!isLastWalFile && shouldSkipWalFile(walFile, versionId)) {
continue;
}
- filteredWalFiles.add(walFile);
+ filteredWalFiles[filteredWalFileCount] = walFile;
+ filteredWalFileVersionIds[filteredWalFileCount] = versionId;
+ filteredWalFileCount++;
}
- walFiles = filteredWalFiles.toArray(new File[0]);
+ walFiles = Arrays.copyOf(filteredWalFiles, filteredWalFileCount);
+ walFileVersionIds = Arrays.copyOf(filteredWalFileVersionIds,
filteredWalFileCount);
}
- private boolean shouldSkipWalFile(final File walFile) {
- final long versionId = WALFileUtils.parseVersionId(walFile.getName());
+ private boolean shouldSkipWalFile(final File walFile, final long versionId) {
return skippedBrokenWalVersionIds.contains(versionId) ||
isHeaderOnlyWalFile(walFile);
}
@@ -139,42 +153,188 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
public void refresh() {
+ final boolean exhaustedKnownFiles = currentFileIndex >= walFiles.length;
final long currentVersionId =
(currentFileIndex >= 0 && currentFileIndex < walFiles.length)
- ? WALFileUtils.parseVersionId(walFiles[currentFileIndex].getName())
- : -1;
+ ? walFileVersionIds[currentFileIndex]
+ : exhaustedKnownFiles && walFileVersionIds.length > 0
+ ? walFileVersionIds[walFileVersionIds.length - 1]
+ : -1;
refreshFileList();
if (currentVersionId >= 0) {
+ final int refreshedIndex = Arrays.binarySearch(walFileVersionIds,
currentVersionId);
+ currentFileIndex = refreshedIndex >= 0 ? refreshedIndex :
-refreshedIndex - 2;
+ } else if (exhaustedKnownFiles) {
currentFileIndex = -1;
- for (int i = 0; i < walFiles.length; i++) {
- if (WALFileUtils.parseVersionId(walFiles[i].getName()) >=
currentVersionId) {
- currentFileIndex = i;
- break;
+ }
+ }
+
+ public boolean hasNext() {
+ while (true) {
+ if (nextReady != null) {
+ if (!shouldSkip(nextReady)) {
+ return true;
}
+ nextReady = null;
}
- if (currentFileIndex < 0) {
- currentFileIndex = walFiles.length;
+ try {
+ nextReady = advance();
+ if (nextReady != null) {
+ lastError = null;
+ }
+ } catch (IOException e) {
+ lastError = e;
+ LOGGER.warn(
+
DataNodePipeMessages.PIPE_LOG_PROGRESSWALITERATOR_ERROR_READING_WAL_2DB46D41,
e);
+ return false;
+ }
+ if (nextReady == null) {
+ return false;
}
}
}
- public boolean hasNext() {
- if (nextReady != null) {
- return true;
+ /**
+ * Advances the local search-index lower bound without rebuilding the
iterator. Whole WAL files
+ * are skipped only when every writer progress tuple in them is already
covered by queue state.
+ */
+ public void advanceTo(
+ final long targetSearchIndex, final WriterProgressCoverage
writerProgressCoverage) {
+ if (targetSearchIndex <= minimumSearchIndex) {
+ return;
}
+ minimumSearchIndex = targetSearchIndex;
+
+ if (writerProgressCoverage == null ||
!canDiscardBufferedRequests(writerProgressCoverage)) {
+ return;
+ }
+
+ refreshForNewerLiveWalFile();
+ int targetFileIndex = locateTargetFile(targetSearchIndex);
+ if (targetFileIndex < 0
+ && liveWalNode != null
+ && targetSearchIndex <= liveWalNode.getCurrentSearchIndex()) {
+ refresh();
+ targetFileIndex = locateTargetFile(targetSearchIndex);
+ }
+ if (targetFileIndex < 0 || targetFileIndex <= currentFileIndex) {
+ return;
+ }
+
+ final int firstFileToSkip = Math.max(0, currentFileIndex);
try {
- nextReady = advance();
- if (nextReady != null) {
- lastError = null;
+ for (int fileIndex = firstFileToSkip; fileIndex < targetFileIndex;
fileIndex++) {
+ if (!isWalFileCovered(fileIndex, writerProgressCoverage)) {
+ return;
+ }
}
- } catch (IOException e) {
- lastError = e;
-
LOGGER.warn(DataNodePipeMessages.PIPE_LOG_PROGRESSWALITERATOR_ERROR_READING_WAL_2DB46D41,
e);
+
+ closeCurrentReader();
+ nextReady = null;
+ pendingRequests.clear();
+ pendingSearchIndex = Long.MIN_VALUE;
+ pendingLocalSeq = Long.MIN_VALUE;
+ currentFileIndex = targetFileIndex - 1;
+ resetCurrentFileTracking();
+ } catch (final IOException ignored) {
+ // Fast-forward is opportunistic. Sequential replay remains the
correctness fallback.
+ }
+ }
+
+ private void refreshForNewerLiveWalFile() {
+ if (walFileVersionIds.length == 0
+ || (liveWalNode != null
+ && walFileVersionIds[walFileVersionIds.length - 1]
+ < liveWalNode.getCurrentWALFileVersion())) {
+ refresh();
+ }
+ }
+
+ private int locateTargetFile(final long targetSearchIndex) {
+ if (walFiles.length == 0
+ || (liveWalNode != null && targetSearchIndex >
liveWalNode.getCurrentSearchIndex())) {
+ return -1;
+ }
+ return WALFileUtils.binarySearchFileBySearchIndex(walFiles,
targetSearchIndex);
+ }
+
+ private boolean canDiscardBufferedRequests(final WriterProgressCoverage
writerProgressCoverage) {
+ return (nextReady == null || isCovered(nextReady, writerProgressCoverage))
+ && (pendingRequests.isEmpty()
+ || isCovered(
+ pendingSearchIndex,
+ pendingPhysicalTime,
+ pendingNodeId,
+ pendingLocalSeq,
+ writerProgressCoverage));
+ }
+
+ private boolean isCovered(
+ final IndexedConsensusRequest request, final WriterProgressCoverage
writerProgressCoverage) {
+ return isCovered(
+ request.getSearchIndex(),
+ request.getPhysicalTime(),
+ request.getNodeId(),
+ request.getProgressLocalSeq(),
+ writerProgressCoverage);
+ }
+
+ private boolean isCovered(
+ final long searchIndex,
+ final long physicalTime,
+ final int nodeId,
+ final long localSeq,
+ final WriterProgressCoverage writerProgressCoverage) {
+ if (searchIndex >= 0 && searchIndex < minimumSearchIndex) {
+ return true;
+ }
+ return nodeId >= 0
+ && physicalTime >= 0
+ && localSeq >= 0
+ && writerProgressCoverage.isCovered(physicalTime, nodeId, localSeq);
+ }
+
+ private boolean isWalFileCovered(
+ final int fileIndex, final WriterProgressCoverage
writerProgressCoverage) throws IOException {
+ final File walFile = walFiles[fileIndex];
+ if (WALFileVersion.getVersion(walFile) != WALFileVersion.V3) {
return false;
}
- return nextReady != null;
+
+ final WALMetaData metadata;
+ final long versionId = walFileVersionIds[fileIndex];
+ if (liveWalNode != null && versionId ==
liveWalNode.getCurrentWALFileVersion()) {
+ metadata = liveWalNode.getCurrentWALMetaDataSnapshot();
+ } else {
+ try (final ProgressWALReader reader = new ProgressWALReader(walFile)) {
+ metadata = reader.getMetaData();
+ }
+ }
+
+ final List<Integer> bufferSizes = metadata.getBuffersSize();
+ final List<Long> physicalTimes = metadata.getPhysicalTimes();
+ final List<Short> nodeIds = metadata.getNodeIds();
+ final List<Long> localSeqs = metadata.getLocalSeqs();
+ if (physicalTimes.size() != bufferSizes.size()
+ || nodeIds.size() != bufferSizes.size()
+ || localSeqs.size() != bufferSizes.size()) {
+ return false;
+ }
+
+ for (int entryIndex = 0; entryIndex < bufferSizes.size(); entryIndex++) {
+ final long physicalTime = physicalTimes.get(entryIndex);
+ final int nodeId = nodeIds.get(entryIndex);
+ final long localSeq = localSeqs.get(entryIndex);
+ if (nodeId < 0
+ || physicalTime < 0
+ || localSeq < 0
+ || !writerProgressCoverage.isCovered(physicalTime, nodeId,
localSeq)) {
+ return false;
+ }
+ }
+ return true;
}
public IndexedConsensusRequest next() {
@@ -337,7 +497,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
if (fileIndex < 0) {
return false;
}
- return openReaderAtIndex(fileIndex, consumedEntryCountInCurrentFile);
+ return openReaderAtIndex(fileIndex, consumedEntryCountInCurrentFile,
true, snapshot);
}
final int previousFileIndex =
findFileIndexByVersion(currentReaderVersionId);
@@ -351,21 +511,34 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
private boolean openReaderAtIndex(final int fileIndex, final int
skipEntries) throws IOException {
- return openReaderAtIndex(fileIndex, skipEntries, true);
+ return openReaderAtIndex(fileIndex, skipEntries, true, null);
}
private boolean openReaderAtIndex(
final int fileIndex, final int skipEntries, final boolean
allowNearLiveRetry)
throws IOException {
+ return openReaderAtIndex(fileIndex, skipEntries, allowNearLiveRetry, null);
+ }
+
+ private boolean openReaderAtIndex(
+ final int fileIndex,
+ final int skipEntries,
+ final boolean allowNearLiveRetry,
+ final WALMetaData liveMetaDataSnapshot)
+ throws IOException {
final File walFile = walFiles[fileIndex];
- final long versionId = WALFileUtils.parseVersionId(walFile.getName());
+ final long versionId = walFileVersionIds[fileIndex];
final boolean useLiveSnapshot =
liveWalNode != null && versionId ==
liveWalNode.getCurrentWALFileVersion();
try {
final ProgressWALReader reader =
useLiveSnapshot
- ? new ProgressWALReader(walFile,
liveWalNode.getCurrentWALMetaDataSnapshot())
+ ? new ProgressWALReader(
+ walFile,
+ liveMetaDataSnapshot != null
+ ? liveMetaDataSnapshot
+ : liveWalNode.getCurrentWALMetaDataSnapshot())
: new ProgressWALReader(walFile);
if (!skipEntries(reader, skipEntries)) {
reader.close();
@@ -422,18 +595,16 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
private int findFileIndexByVersion(final long versionId) {
- for (int i = 0; i < walFiles.length; i++) {
- if (WALFileUtils.parseVersionId(walFiles[i].getName()) == versionId) {
- return i;
- }
- }
- return -1;
+ final int fileIndex = Arrays.binarySearch(walFileVersionIds, versionId);
+ return fileIndex >= 0 ? fileIndex : -1;
}
private boolean openFirstReaderAfterVersion(final long versionId) throws
IOException {
- for (int i = 0; i < walFiles.length; i++) {
- if (WALFileUtils.parseVersionId(walFiles[i].getName()) > versionId
- && openReaderAtIndex(i, 0)) {
+ final int matchedFileIndex = Arrays.binarySearch(walFileVersionIds,
versionId);
+ final int firstFileIndexAfterVersion =
+ matchedFileIndex >= 0 ? matchedFileIndex + 1 : -matchedFileIndex - 1;
+ for (int i = firstFileIndexAfterVersion; i < walFiles.length; i++) {
+ if (openReaderAtIndex(i, 0)) {
return true;
}
}
@@ -495,7 +666,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
private boolean shouldSkip(final IndexedConsensusRequest request) {
- return request.getSearchIndex() >= 0 && request.getSearchIndex() <
startSearchIndex;
+ return request.getSearchIndex() >= 0 && request.getSearchIndex() <
minimumSearchIndex;
}
private void closeCurrentReader() throws IOException {
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index 2656c662e96..845a797f149 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -65,16 +65,21 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BooleanSupplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -925,6 +930,312 @@ public class ConsensusPrefetchingQueueTest {
}
}
+ @Test
+ public void testLateAckDoesNotWaitForPrefetchReadLock() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir = temporaryFolder.newFolder("late-ack-read-lock");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final DataRegionId regionId = new DataRegionId(1);
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+
+ final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
+ when(converter.convert(any()))
+ .thenReturn(Collections.singletonList(createTablet()),
Collections.emptyList());
+ when(converter.getDatabaseName()).thenReturn("db");
+
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ regionId,
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ converter,
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+
+ final IndexedConsensusRequest dataRequest =
+ new IndexedConsensusRequest(
+ 1L,
Collections.singletonList(StatementTestUtils.genInsertRowNode(1)))
+ .setPhysicalTime(1000L)
+ .setNodeId(7);
+ final IndexedConsensusRequest requestWithEmptyConversionResult =
+ new IndexedConsensusRequest(
+ 2L,
Collections.singletonList(StatementTestUtils.genInsertRowNode(2)))
+ .setPhysicalTime(1001L)
+ .setNodeId(7);
+ reader.currentSearchIndex = 2L;
+ assertTrue(pendingEntries(queue).offer(dataRequest));
+
assertTrue(pendingEntries(queue).offer(requestWithEmptyConversionResult));
+ assertNull(queue.poll("consumer"));
+ queue.drivePrefetchOnce();
+
+ final SubscriptionEvent event = queue.poll("consumer");
+ assertNotNull(event);
+ assertTrue(queue.ackSilent("consumer", event.getCommitContext()));
+
+ final ConsensusPrefetchingQueue activeQueue = queue;
+ final ReentrantReadWriteLock queueLock = queueLock(queue);
+ final CountDownLatch ackStarted = new CountDownLatch(1);
+ final CountDownLatch ackCompleted = new CountDownLatch(1);
+ final AtomicReference<Boolean> lateAckResult = new AtomicReference<>();
+ final AtomicReference<Throwable> asyncFailure = new AtomicReference<>();
+ final Thread lateAckThread =
+ new Thread(
+ () -> {
+ ackStarted.countDown();
+ try {
+ lateAckResult.set(activeQueue.ackSilent("consumer",
event.getCommitContext()));
+ } catch (final Throwable t) {
+ asyncFailure.set(t);
+ } finally {
+ ackCompleted.countDown();
+ }
+ });
+ lateAckThread.setDaemon(true);
+
+ final boolean completedWhileReadLocked;
+ queueLock.readLock().lock();
+ try {
+ lateAckThread.start();
+ assertTrue(ackStarted.await(5, TimeUnit.SECONDS));
+ completedWhileReadLocked = ackCompleted.await(5, TimeUnit.SECONDS);
+ } finally {
+ queueLock.readLock().unlock();
+ }
+ lateAckThread.join(TimeUnit.SECONDS.toMillis(5));
+
+ assertTrue(completedWhileReadLocked);
+ assertFalse(lateAckThread.isAlive());
+ if (asyncFailure.get() != null) {
+ throw new AssertionError(asyncFailure.get());
+ }
+ assertTrue(Boolean.TRUE.equals(lateAckResult.get()));
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
+ @Test
+ public void testWalCatchUpReusesIteratorAcrossRounds() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir = temporaryFolder.newFolder("reuse-wal-iterator");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ mock(ConsensusLogToTabletConverter.class),
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+
+ final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
+ when(iterator.hasNext()).thenReturn(false);
+ setSubscriptionWalIterator(queue, iterator);
+
+ final Method tryCatchUpFromWAL =
+
ConsensusPrefetchingQueue.class.getDeclaredMethod("tryCatchUpFromWAL",
long.class);
+ tryCatchUpFromWAL.setAccessible(true);
+ tryCatchUpFromWAL.invoke(queue, queue.getCurrentSeekGeneration());
+ tryCatchUpFromWAL.invoke(queue, queue.getCurrentSeekGeneration());
+
+ assertSame(iterator, subscriptionWalIterator(queue));
+ verify(iterator, times(2)).refresh();
+ verify(iterator, never()).close();
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
+ @Test
+ public void testReadableWalIteratorSkipsFileListRefresh() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir =
temporaryFolder.newFolder("skip-readable-wal-refresh");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ mock(ConsensusLogToTabletConverter.class),
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+
+ final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
+ when(iterator.hasNext()).thenReturn(true);
+ setSubscriptionWalIterator(queue, iterator);
+
+ invokeEnsureSubscriptionWalReadable(queue);
+
+ verify(iterator).hasNext();
+ verify(iterator, never()).refresh();
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
+ @Test
+ public void testWalRollDoesNotRefreshNewIteratorTwice() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir =
temporaryFolder.newFolder("skip-new-iterator-refresh");
+ final File walDirectory =
temporaryFolder.newFolder("skip-new-iterator-refresh-wal");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(walDirectory);
+ when(walNode.getCurrentSearchIndex()).thenReturn(1L);
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+
+ final ProgressWALIterator replacementIterator =
mock(ProgressWALIterator.class);
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ mock(ConsensusLogToTabletConverter.class),
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true) {
+ @Override
+ protected ProgressWALIterator createSubscriptionWALIterator(
+ final long startSearchIndex) {
+ assertEquals(1L, startSearchIndex);
+ return replacementIterator;
+ }
+ };
+
+ final ProgressWALIterator exhaustedIterator =
mock(ProgressWALIterator.class);
+ when(exhaustedIterator.hasNext()).thenReturn(false);
+ setSubscriptionWalIterator(queue, exhaustedIterator);
+
+ invokeEnsureSubscriptionWalReadable(queue);
+
+ verify(exhaustedIterator, times(2)).hasNext();
+ verify(exhaustedIterator).refresh();
+ verify(exhaustedIterator).close();
+ verify(walNode).rollWALFile();
+ verify(replacementIterator, never()).refresh();
+ assertSame(replacementIterator, subscriptionWalIterator(queue));
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
+ @Test
+ public void testPendingCursorAdvanceFastForwardsWalIteratorInPlace() throws
Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir =
temporaryFolder.newFolder("deferred-wal-realignment");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ mock(ConsensusLogToTabletConverter.class),
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+
+ final ProgressWALIterator staleIterator =
mock(ProgressWALIterator.class);
+ setSubscriptionWalIterator(queue, staleIterator);
+
+ final Method advancePendingCursor =
+ ConsensusPrefetchingQueue.class.getDeclaredMethod(
+ "advanceLocalCursorFromPendingIfPresent",
IndexedConsensusRequest.class, long.class);
+ advancePendingCursor.setAccessible(true);
+ advancePendingCursor.invoke(
+ queue,
+ new IndexedConsensusRequest(7L, Collections.emptyList()),
+ queue.getCurrentSeekGeneration());
+
+ assertEquals(8L, queue.getCurrentReadSearchIndex());
+ verify(staleIterator)
+ .advanceTo(anyLong(),
any(ProgressWALIterator.WriterProgressCoverage.class));
+ verify(staleIterator, never()).close();
+ assertSame(staleIterator, subscriptionWalIterator(queue));
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
@Test
public void testCleanupReconcilesUnindexedTabletReservation() throws
Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
@@ -1412,6 +1723,35 @@ public class ConsensusPrefetchingQueueTest {
return (BlockingQueue<IndexedConsensusRequest>) field.get(queue);
}
+ private static ReentrantReadWriteLock queueLock(final
ConsensusPrefetchingQueue queue)
+ throws Exception {
+ final Field field =
ConsensusPrefetchingQueue.class.getDeclaredField("lock");
+ field.setAccessible(true);
+ return (ReentrantReadWriteLock) field.get(queue);
+ }
+
+ private static ProgressWALIterator subscriptionWalIterator(final
ConsensusPrefetchingQueue queue)
+ throws Exception {
+ final Field field =
ConsensusPrefetchingQueue.class.getDeclaredField("subscriptionWALIterator");
+ field.setAccessible(true);
+ return (ProgressWALIterator) field.get(queue);
+ }
+
+ private static void setSubscriptionWalIterator(
+ final ConsensusPrefetchingQueue queue, final ProgressWALIterator
iterator) throws Exception {
+ final Field field =
ConsensusPrefetchingQueue.class.getDeclaredField("subscriptionWALIterator");
+ field.setAccessible(true);
+ field.set(queue, iterator);
+ }
+
+ private static void invokeEnsureSubscriptionWalReadable(final
ConsensusPrefetchingQueue queue)
+ throws Exception {
+ final Method method =
+
ConsensusPrefetchingQueue.class.getDeclaredMethod("ensureSubscriptionWalReadable");
+ method.setAccessible(true);
+ method.invoke(queue);
+ }
+
private static Tablet createTablet() {
final List<String> columnNames = Arrays.asList("device", "temperature");
final List<TSDataType> dataTypes = Arrays.asList(TSDataType.STRING,
TSDataType.DOUBLE);
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
index b37542e936d..92928663126 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
@@ -30,6 +30,7 @@ import
org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileStatus;
import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
+import org.junit.Assume;
import org.junit.Test;
import java.io.File;
@@ -41,6 +42,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ProgressWALIteratorTest {
@@ -272,6 +275,41 @@ public class ProgressWALIteratorTest {
}
}
+ @Test
+ public void testLiveWalReopenReusesMetadataSnapshot() throws Exception {
+ final Path dir =
Files.createTempDirectory("progress-wal-iterator-live-snapshot");
+ final File liveWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+
+ try {
+ try (WALWriter writer = new WALWriter(liveWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 1000L,
7, 1L));
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 1001L,
7, 2L));
+ }
+
+ final WALMetaData firstSnapshot = singleEntryMeta(19, 1L, 1L, 1000L, 7,
1L);
+ final WALMetaData secondSnapshot = firstSnapshot.copy();
+ secondSnapshot.add(19, 2L, 1L, 1001L, 7, 2L);
+
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(dir.toFile());
+ when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
+ when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(firstSnapshot,
secondSnapshot);
+
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walNode,
1L)) {
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertTrue(iterator.hasNext());
+ assertEquals(2L, iterator.next().getSearchIndex());
+ verify(walNode, times(2)).getCurrentWALMetaDataSnapshot();
+ }
+ } finally {
+ Files.deleteIfExists(liveWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
@Test
public void testIteratorMarksIncompleteScanWhenNearLiveWalCannotBeOpened()
throws Exception {
final Path dir =
Files.createTempDirectory("progress-wal-iterator-incomplete-scan");
@@ -299,6 +337,253 @@ public class ProgressWALIteratorTest {
}
}
+ @Test
+ public void testAdvanceToKeepsUncoveredFollowerRequestInCurrentFile() throws
Exception {
+ final Path dir =
Files.createTempDirectory("progress-wal-iterator-current-file-advance");
+ final File dataWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 3,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+
+ try {
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7,
1L));
+ }
+ try (WALWriter writer = new WALWriter(successorWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(-1L), singleEntryMeta(19, -1L, 1L, 200L,
8, 20L));
+ writer.write(searchableEntry(3L), singleEntryMeta(19, 3L, 1L, 300L, 7,
3L));
+ }
+
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+
+ iterator.advanceTo(3L, (physicalTime, nodeId, localSeq) -> false);
+
+ assertTrue(iterator.hasNext());
+ final IndexedConsensusRequest followerRequest = iterator.next();
+ assertEquals(-1L, followerRequest.getSearchIndex());
+ assertEquals(8, followerRequest.getNodeId());
+ assertTrue(iterator.hasNext());
+ assertEquals(3L, iterator.next().getSearchIndex());
+ }
+ } finally {
+ Files.deleteIfExists(dataWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testAdvanceToSkipsOnlyFullyCoveredWalFiles() throws Exception {
+ final Path dir =
Files.createTempDirectory("progress-wal-iterator-covered-file-advance");
+ final File localWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File followerWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 2,
WALFileStatus.CONTAINS_NONE_SEARCH_INDEX))
+ .toFile();
+ final File targetWal =
+ dir.resolve(WALFileUtils.getLogFileName(2, 2,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+
+ try {
+ try (WALWriter writer = new WALWriter(localWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7,
1L));
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 200L, 7,
2L));
+ }
+ try (WALWriter writer = new WALWriter(followerWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(-1L), singleEntryMeta(19, -1L, 1L, 300L,
8, 30L));
+ }
+ try (WALWriter writer = new WALWriter(targetWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(6L), singleEntryMeta(19, 6L, 1L, 600L, 7,
6L));
+ writer.write(searchableEntry(7L), singleEntryMeta(19, 7L, 1L, 700L, 7,
7L));
+ }
+
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+
+ iterator.advanceTo(6L, (physicalTime, nodeId, localSeq) -> nodeId ==
7);
+
+ assertTrue(iterator.hasNext());
+ final IndexedConsensusRequest uncoveredFollower = iterator.next();
+ assertEquals(-1L, uncoveredFollower.getSearchIndex());
+ assertEquals(8, uncoveredFollower.getNodeId());
+ }
+
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+
+ iterator.advanceTo(6L, (physicalTime, nodeId, localSeq) -> true);
+
+ assertTrue(iterator.hasNext());
+ assertEquals(6L, iterator.next().getSearchIndex());
+ }
+ } finally {
+ Files.deleteIfExists(localWal.toPath());
+ Files.deleteIfExists(followerWal.toPath());
+ Files.deleteIfExists(targetWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testRefreshAfterExhaustionDiscoversNextWalFile() throws
Exception {
+ final Path dir =
Files.createTempDirectory("progress-wal-iterator-refresh-after-exhaustion");
+ final File firstWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File secondWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 1,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+
+ try {
+ try (WALWriter writer = new WALWriter(firstWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7,
1L));
+ }
+
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertFalse(iterator.hasNext());
+
+ try (WALWriter writer = new WALWriter(secondWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 200L,
7, 2L));
+ }
+ iterator.refresh();
+
+ assertTrue(iterator.hasNext());
+ assertEquals(2L, iterator.next().getSearchIndex());
+ }
+ } finally {
+ Files.deleteIfExists(firstWal.toPath());
+ Files.deleteIfExists(secondWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testIteratorReusePerformance() throws Exception {
+ Assume.assumeTrue(
+ "Enable with -Diotdb.test.subscription.performance=true",
+ Boolean.getBoolean("iotdb.test.subscription.performance"));
+
+ final int entryCount =
Integer.getInteger("iotdb.test.subscription.performance.entries", 4096);
+ final int batchSize =
Integer.getInteger("iotdb.test.subscription.performance.batch-size", 64);
+ assertTrue("entry count must be positive", entryCount > 0);
+ assertTrue("batch size must be positive", batchSize > 0);
+ final Path dir =
Files.createTempDirectory("progress-wal-iterator-performance");
+ final File dataWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(
+ WALFileUtils.getLogFileName(
+ 1, entryCount + 1L, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+
+ try {
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ for (long index = 1; index <= entryCount; index++) {
+ writer.write(searchableEntry(index), singleEntryMeta(19, index, 1L,
index, 7, index));
+ }
+ }
+ try (WALWriter ignored = new WALWriter(successorWal, WALFileVersion.V3))
{
+ // Seal the data WAL so both benchmark variants read the same
historical file.
+ }
+
+ assertEquals(entryCount, consumeWithReusedIterator(dir.toFile(),
entryCount));
+
+ final long reopenStartNanos = System.nanoTime();
+ assertEquals(entryCount, consumeWithReopenedIterator(dir.toFile(),
entryCount, batchSize));
+ final long reopenNanos = System.nanoTime() - reopenStartNanos;
+
+ final long refreshStartNanos = System.nanoTime();
+ assertEquals(entryCount, consumeWithRefreshedIterator(dir.toFile(),
entryCount, batchSize));
+ final long refreshNanos = System.nanoTime() - refreshStartNanos;
+
+ final long reuseStartNanos = System.nanoTime();
+ assertEquals(entryCount, consumeWithReusedIterator(dir.toFile(),
entryCount));
+ final long reuseNanos = System.nanoTime() - reuseStartNanos;
+
+ final double reopenSpeedup = (double) reopenNanos / Math.max(1L,
reuseNanos);
+ final double refreshSpeedup = (double) refreshNanos / Math.max(1L,
reuseNanos);
+ System.out.printf(
+ "Subscription WAL iterator benchmark: entries=%d, batchSize=%d, "
+ + "reopen=%.3f ms, refreshEachBatch=%.3f ms, reuse=%.3f ms, "
+ + "reopenSpeedup=%.2fx, refreshSpeedup=%.2fx%n",
+ entryCount,
+ batchSize,
+ reopenNanos / 1_000_000.0,
+ refreshNanos / 1_000_000.0,
+ reuseNanos / 1_000_000.0,
+ reopenSpeedup,
+ refreshSpeedup);
+ assertTrue(
+ "Reusing the iterator should be faster than reopening each batch",
reopenSpeedup > 1.0);
+ } finally {
+ Files.deleteIfExists(dataWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ private static int consumeWithReopenedIterator(
+ final File walDirectory, final int entryCount, final int batchSize)
throws Exception {
+ int consumed = 0;
+ long nextSearchIndex = 1L;
+ while (consumed < entryCount) {
+ int batchCount = 0;
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(walDirectory, nextSearchIndex)) {
+ while (batchCount < batchSize && iterator.hasNext()) {
+ nextSearchIndex = iterator.next().getSearchIndex() + 1L;
+ batchCount++;
+ consumed++;
+ }
+ }
+ if (batchCount == 0) {
+ break;
+ }
+ }
+ return consumed;
+ }
+
+ private static int consumeWithReusedIterator(final File walDirectory, final
int entryCount)
+ throws Exception {
+ int consumed = 0;
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walDirectory,
1L)) {
+ while (consumed < entryCount && iterator.hasNext()) {
+ iterator.next();
+ consumed++;
+ }
+ }
+ return consumed;
+ }
+
+ private static int consumeWithRefreshedIterator(
+ final File walDirectory, final int entryCount, final int batchSize)
throws Exception {
+ int consumed = 0;
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walDirectory,
1L)) {
+ while (consumed < entryCount) {
+ iterator.refresh();
+ int batchCount = 0;
+ while (batchCount < batchSize && iterator.hasNext()) {
+ iterator.next();
+ batchCount++;
+ consumed++;
+ }
+ if (batchCount == 0) {
+ break;
+ }
+ }
+ }
+ return consumed;
+ }
+
private static ByteBuffer searchableEntry(final long bodySearchIndex) {
final ByteBuffer buffer =
ByteBuffer.allocate(WALInfoEntry.FIXED_SERIALIZED_SIZE +
PlanNodeType.BYTES + Long.BYTES);