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 0c0116afe22 Bound consensus subscription WAL scans per prefetch round
(#18849)
0c0116afe22 is described below
commit 0c0116afe22532f32312a3cb30352a818fba4e38
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 14 16:10:57 2026 +0800
Bound consensus subscription WAL scans per prefetch round (#18849)
---
.../consensus/ConsensusPrefetchingQueue.java | 100 ++++++++++-----
.../broker/consensus/ProgressWALIterator.java | 78 +++++++++++-
...onsensusPrefetchingQueueInitializationTest.java | 5 +-
.../ConsensusPrefetchingQueueSeekTest.java | 3 +-
.../consensus/ConsensusPrefetchingQueueTest.java | 35 ++++--
...nsensusPrefetchingQueueWalBackpressureTest.java | 140 +++++++++++++++++++--
.../broker/consensus/ProgressWALIteratorTest.java | 110 ++++++++++++++++
7 files changed, 414 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 63c06f06960..a678646a735 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
@@ -386,6 +386,7 @@ public class ConsensusPrefetchingQueue {
SUCCESS,
MEMORY_BLOCKED,
WAL_GAP,
+ WAL_SCAN_YIELDED,
STALE
}
@@ -1546,6 +1547,8 @@ public class ConsensusPrefetchingQueue {
private static final long WAL_GAP_RETRY_SLEEP_MS = 10L;
private static final long MEMORY_RETRY_SLEEP_MS = 100L;
+ // Yield long WAL scans to other regions without closing readers or
splitting writer requests.
+ private static final long WAL_SCAN_TIME_SLICE_NANOS =
TimeUnit.MILLISECONDS.toNanos(50L);
private static final long WAL_GAP_WAIT_LOG_INTERVAL_MS = 5_000L;
private static final long PREFETCH_STATS_LOG_INTERVAL_MS = 5_000L;
@@ -1619,6 +1622,7 @@ public class ConsensusPrefetchingQueue {
acquireReadLock();
try {
+ final ProgressWALIterator.ScanBudget walScanBudget = newWalScanBudget();
if (isClosed || closeRequested || !prefetchInitialized) {
return PrefetchRoundResult.dormant();
}
@@ -1642,10 +1646,10 @@ public class ConsensusPrefetchingQueue {
if (!isActive) {
blockRealtimeAdmission(SubscriptionQueueRejectionReason.INACTIVE_OR_CLOSED);
- return computeIdleRoundResult();
+ return computeIdleRoundResult(walScanBudget);
}
if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
- return computeIdleRoundResult();
+ return computeIdleRoundResult(walScanBudget);
}
final SubscriptionConfig config = SubscriptionConfig.getInstance();
@@ -1666,7 +1670,7 @@ public class ConsensusPrefetchingQueue {
if (!realtimeEntriesByWriter.isEmpty()) {
blockRealtimeAdmission(SubscriptionQueueRejectionReason.WRITER_BACKLOG);
}
- return computeIdleRoundResult();
+ return computeIdleRoundResult(walScanBudget);
}
if (shouldWaitForSubscriptionMemory()) {
blockRealtimeAdmission(memoryBlockReason);
@@ -1699,11 +1703,18 @@ public class ConsensusPrefetchingQueue {
observedSeekGeneration,
maxWalEntries,
maxTablets,
- maxBatchBytes);
+ maxBatchBytes,
+ walScanBudget);
} finally {
pendingEntries.release(batch);
}
if (batchResult != MaterializationResult.SUCCESS) {
+ if (batchResult == MaterializationResult.WAL_SCAN_YIELDED) {
+ if (!lingerBatch.isEmpty() && !flushBatch(lingerBatch,
observedSeekGeneration)) {
+ resetRoundStateForSeek(seekGeneration.get());
+ }
+ return PrefetchRoundResult.rescheduleNow();
+ }
if (batchResult == MaterializationResult.WAL_GAP) {
if (!lingerBatch.isEmpty() && !flushBatch(lingerBatch,
observedSeekGeneration)) {
resetRoundStateForSeek(seekGeneration.get());
@@ -1727,7 +1738,11 @@ public class ConsensusPrefetchingQueue {
}
if (batch.isEmpty() && lingerBatch.isEmpty()) {
- final MaterializationResult walResult =
tryCatchUpFromWAL(observedSeekGeneration);
+ final MaterializationResult walResult =
+ tryCatchUpFromWAL(observedSeekGeneration, walScanBudget);
+ if (walResult == MaterializationResult.WAL_SCAN_YIELDED) {
+ return PrefetchRoundResult.rescheduleNow();
+ }
if (walResult == MaterializationResult.MEMORY_BLOCKED) {
blockRealtimeAdmission(memoryBlockReason);
return PrefetchRoundResult.rescheduleAfter(MEMORY_RETRY_SLEEP_MS);
@@ -1763,7 +1778,7 @@ public class ConsensusPrefetchingQueue {
}
maybeInjectWatermark();
- return computeIdleRoundResult();
+ return computeIdleRoundResult(walScanBudget);
} catch (final Throwable fatal) {
LOGGER.error(
DataNodePipeMessages
@@ -1832,14 +1847,21 @@ public class ConsensusPrefetchingQueue {
return batch;
}
- private PrefetchRoundResult computeIdleRoundResult() {
+ private static ProgressWALIterator.ScanBudget newWalScanBudget() {
+ return new ProgressWALIterator.ScanBudget(
+
SubscriptionConfig.getInstance().getSubscriptionConsensusBatchMaxWalEntries(),
+ WAL_SCAN_TIME_SLICE_NANOS);
+ }
+
+ private PrefetchRoundResult computeIdleRoundResult(
+ final ProgressWALIterator.ScanBudget walScanBudget) {
if (isClosed || !prefetchInitialized || !isActive) {
return PrefetchRoundResult.dormant();
}
if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
return PrefetchRoundResult.dormant();
}
- if (hasImmediatePrefetchableWork()) {
+ if (hasImmediatePrefetchableWork(walScanBudget)) {
return PrefetchRoundResult.rescheduleNow();
}
long delayMs = Long.MAX_VALUE;
@@ -1887,9 +1909,13 @@ public class ConsensusPrefetchingQueue {
}
private boolean hasImmediatePrefetchableWork() {
+ return hasImmediatePrefetchableWork(newWalScanBudget());
+ }
+
+ private boolean hasImmediatePrefetchableWork(final
ProgressWALIterator.ScanBudget walScanBudget) {
return !pendingEntries.isEmpty()
|| !realtimeEntriesByWriter.isEmpty()
- || hasReadableWalEntries();
+ || hasReadableWalEntries(walScanBudget);
}
private boolean hasHistoricalWalLag() {
@@ -1967,7 +1993,8 @@ public class ConsensusPrefetchingQueue {
final long expectedSeekGeneration,
final int maxWalEntries,
final int maxTablets,
- final long maxBatchBytes) {
+ final long maxBatchBytes,
+ final ProgressWALIterator.ScanBudget walScanBudget) {
int processedCount = 0;
int skippedCount = 0;
@@ -1996,7 +2023,8 @@ public class ConsensusPrefetchingQueue {
expectedSeekGeneration,
maxWalEntries,
maxTablets,
- maxBatchBytes);
+ maxBatchBytes,
+ walScanBudget);
if (gapFillResult != MaterializationResult.SUCCESS) {
return gapFillResult;
}
@@ -2075,7 +2103,8 @@ public class ConsensusPrefetchingQueue {
final long expectedSeekGeneration,
final int maxWalEntries,
final int maxTablets,
- final long maxBatchBytes) {
+ final long maxBatchBytes,
+ final ProgressWALIterator.ScanBudget walScanBudget) {
pendingWalGapRetryRequested = false;
if (seekGeneration.get() != expectedSeekGeneration || isClosed) {
return MaterializationResult.STALE;
@@ -2093,7 +2122,12 @@ public class ConsensusPrefetchingQueue {
// queue read lock indefinitely and block unsubscribe from acquiring the
close write lock.
final MaterializationResult pumpResult =
pumpFromSubscriptionWAL(
- batchState, expectedSeekGeneration, maxWalEntries, maxTablets,
maxBatchBytes);
+ batchState,
+ expectedSeekGeneration,
+ maxWalEntries,
+ maxTablets,
+ maxBatchBytes,
+ walScanBudget);
if (pumpResult != MaterializationResult.SUCCESS) {
return pumpResult;
}
@@ -2132,7 +2166,8 @@ public class ConsensusPrefetchingQueue {
* Try catch-up from WAL when the pending queue was empty. This handles
cold-start or scenarios
* where the subscription started after data was already written.
*/
- private MaterializationResult tryCatchUpFromWAL(final long
expectedSeekGeneration) {
+ private MaterializationResult tryCatchUpFromWAL(
+ final long expectedSeekGeneration, final ProgressWALIterator.ScanBudget
walScanBudget) {
final SubscriptionConfig config = SubscriptionConfig.getInstance();
final int maxTablets =
config.getSubscriptionConsensusBatchMaxTabletCount();
final long maxBatchBytes =
config.getSubscriptionConsensusBatchMaxSizeInBytes();
@@ -2146,8 +2181,14 @@ public class ConsensusPrefetchingQueue {
// segment. Pending-path cursor advances and seek operations request
explicit realignment.
final MaterializationResult materializationResult =
pumpFromSubscriptionWAL(
- batchState, expectedSeekGeneration, maxWalEntries, maxTablets,
maxBatchBytes);
- if (materializationResult != MaterializationResult.SUCCESS) {
+ batchState,
+ expectedSeekGeneration,
+ maxWalEntries,
+ maxTablets,
+ maxBatchBytes,
+ walScanBudget);
+ if (materializationResult != MaterializationResult.SUCCESS
+ && materializationResult != MaterializationResult.WAL_SCAN_YIELDED) {
if (materializationResult == MaterializationResult.MEMORY_BLOCKED &&
!batchState.isEmpty()) {
if (!flushBatch(batchState, expectedSeekGeneration)) {
discardBatch(batchState);
@@ -2163,7 +2204,7 @@ public class ConsensusPrefetchingQueue {
discardBatch(batchState);
return MaterializationResult.STALE;
}
- return MaterializationResult.SUCCESS;
+ return materializationResult;
}
private MaterializationResult pumpFromSubscriptionWAL(
@@ -2171,7 +2212,8 @@ public class ConsensusPrefetchingQueue {
final long expectedSeekGeneration,
final int maxWalEntries,
final int maxTablets,
- final long maxBatchBytes) {
+ final long maxBatchBytes,
+ final ProgressWALIterator.ScanBudget walScanBudget) {
if (Objects.isNull(subscriptionWALIterator)) {
return MaterializationResult.SUCCESS;
}
@@ -2185,13 +2227,13 @@ public class ConsensusPrefetchingQueue {
maxWalEntries,
prefetchingQueue.size());
}
- ensureSubscriptionWalReadable();
+ ensureSubscriptionWalReadable(walScanBudget);
reportUnreadableWalReplayIfNecessary();
int entriesRead = 0;
while (entriesRead < maxWalEntries
- && subscriptionWALIterator.hasNext()
- && prefetchingQueue.size() < MAX_PREFETCHING_QUEUE_SIZE) {
+ && prefetchingQueue.size() < MAX_PREFETCHING_QUEUE_SIZE
+ && subscriptionWALIterator.hasNext(walScanBudget)) {
reportUnreadableWalReplayIfNecessary();
try {
final IndexedConsensusRequest walEntry =
subscriptionWALIterator.next();
@@ -2245,7 +2287,9 @@ public class ConsensusPrefetchingQueue {
entriesRead,
nextExpectedSearchIndex.get());
}
- return MaterializationResult.SUCCESS;
+ return walScanBudget.hasYielded()
+ ? MaterializationResult.WAL_SCAN_YIELDED
+ : MaterializationResult.SUCCESS;
}
private void reportUnreadableWalReplayIfNecessary() {
@@ -2298,11 +2342,11 @@ public class ConsensusPrefetchingQueue {
nextExpectedSearchIndex.set(actualSearchIndex + 1);
}
- private void ensureSubscriptionWalReadable() {
+ private void ensureSubscriptionWalReadable(final
ProgressWALIterator.ScanBudget walScanBudget) {
if (Objects.isNull(subscriptionWALIterator)) {
return;
}
- if (subscriptionWALIterator.hasNext()) {
+ if (subscriptionWALIterator.hasNext(walScanBudget) ||
walScanBudget.hasYielded()) {
reportUnreadableWalReplayIfNecessary();
return;
}
@@ -2311,7 +2355,7 @@ public class ConsensusPrefetchingQueue {
// 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()) {
+ if (subscriptionWALIterator.hasNext(walScanBudget) ||
walScanBudget.hasYielded()) {
reportUnreadableWalReplayIfNecessary();
return;
}
@@ -2355,7 +2399,7 @@ public class ConsensusPrefetchingQueue {
protected void onWalGapRetryScheduled() {}
- private boolean hasReadableWalEntries() {
+ private boolean hasReadableWalEntries(final ProgressWALIterator.ScanBudget
walScanBudget) {
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.
@@ -2364,9 +2408,9 @@ public class ConsensusPrefetchingQueue {
if (Objects.isNull(subscriptionWALIterator)) {
return false;
}
- final boolean readable = subscriptionWALIterator.hasNext();
+ final boolean readable = subscriptionWALIterator.hasNext(walScanBudget);
reportUnreadableWalReplayIfNecessary();
- return readable;
+ return readable || walScanBudget.hasYielded();
}
private void requestSubscriptionWalReset(
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 3636829274f..3f5186893ee 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
@@ -51,6 +51,7 @@ import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
+import java.util.function.LongSupplier;
/**
* Writer-based WAL iterator for the new subscription progress model.
@@ -66,6 +67,41 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
boolean isCovered(long physicalTime, int nodeId, long localSeq);
}
+ /**
+ * One shared budget for all WAL probes and reads in a prefetch round. The
time limit is
+ * cooperative: it is checked between operations and cannot interrupt a
single file read.
+ */
+ static final class ScanBudget {
+
+ private int remainingEntries;
+ private final long startNanos;
+ private final long maxDurationNanos;
+ private final LongSupplier nanoTime;
+ private boolean yielded;
+
+ ScanBudget(final int maxEntries, final long maxDurationNanos) {
+ this(maxEntries, maxDurationNanos, System::nanoTime);
+ }
+
+ ScanBudget(final int maxEntries, final long maxDurationNanos, final
LongSupplier nanoTime) {
+ this.remainingEntries = maxEntries;
+ this.maxDurationNanos = maxDurationNanos;
+ this.nanoTime = nanoTime;
+ this.startNanos = nanoTime.getAsLong();
+ }
+
+ private boolean shouldYield() {
+ if (remainingEntries <= 0 || nanoTime.getAsLong() - startNanos >=
maxDurationNanos) {
+ yielded = true;
+ }
+ return yielded;
+ }
+
+ boolean hasYielded() {
+ return yielded;
+ }
+ }
+
private static final Logger LOGGER =
LoggerFactory.getLogger(ProgressWALIterator.class);
private static final int MAX_SKIPPED_FILE_DETAILS = 8;
@@ -104,6 +140,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
private final List<IConsensusRequest> pendingRequests = new ArrayList<>();
private IndexedConsensusRequest nextReady;
+ private boolean scanYielded;
public ProgressWALIterator(final File logDirectory) {
this(logDirectory, Long.MIN_VALUE);
@@ -183,6 +220,15 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
public boolean hasNext() {
+ return hasNext(null);
+ }
+
+ /**
+ * A false result can also mean that the round's scan budget was consumed.
In that case the reader
+ * position and incomplete request fragments are preserved for the next
round.
+ */
+ boolean hasNext(final ScanBudget scanBudget) {
+ scanYielded = false;
while (true) {
if (nextReady != null) {
if (!shouldSkip(nextReady)) {
@@ -191,7 +237,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
nextReady = null;
}
try {
- nextReady = advance();
+ nextReady = advance(scanBudget);
if (nextReady != null) {
lastError = null;
}
@@ -214,6 +260,10 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
return nextReady != null && !shouldSkip(nextReady);
}
+ boolean hasYieldedScan() {
+ return scanYielded;
+ }
+
/**
* Keeps a request returned by {@link #next()} available for a retry without
reopening the WAL.
* The caller uses this when materialization is temporarily blocked and
replay progress has not
@@ -433,11 +483,16 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
lastError = null;
incompleteScan = false;
incompleteScanDetail = null;
+ scanYielded = false;
resetCurrentFileTracking();
}
- private IndexedConsensusRequest advance() throws IOException {
+ private IndexedConsensusRequest advance(final ScanBudget scanBudget) throws
IOException {
while (true) {
+ if (scanBudget != null && scanBudget.shouldYield()) {
+ scanYielded = true;
+ return null;
+ }
if (retryCurrentReader) {
refresh();
final int retryFileIndex =
findFileIndexByVersion(currentReaderVersionId);
@@ -455,6 +510,11 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
if (currentReader != null && currentReader.hasNext()) {
try {
+ if (scanBudget != null) {
+ // Count physical entries, including skipped requests and request
fragments. A single
+ // hasNext() can otherwise read an arbitrary WAL prefix before
returning a request.
+ scanBudget.remainingEntries--;
+ }
final ByteBuffer buffer = currentReader.next();
consumedEntryCountInCurrentFile =
currentReader.getCurrentEntryIndex() + 1;
final WALEntryType type = WALEntryType.valueOf(buffer.get());
@@ -529,8 +589,8 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
continue;
}
- if (!openNextReader()) {
- if (retryCurrentReader) {
+ if (!openNextReader(scanBudget)) {
+ if (scanYielded || retryCurrentReader) {
return null;
}
final IndexedConsensusRequest flushed = flushPending();
@@ -542,8 +602,13 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
}
- private boolean openNextReader() throws IOException {
- while (++currentFileIndex < walFiles.length) {
+ private boolean openNextReader(final ScanBudget scanBudget) throws
IOException {
+ while (currentFileIndex + 1 < walFiles.length) {
+ if (scanBudget != null && scanBudget.shouldYield()) {
+ scanYielded = true;
+ return false;
+ }
+ currentFileIndex++;
if (openReaderAtIndex(currentFileIndex, 0)) {
return true;
}
@@ -551,6 +616,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
return false;
}
}
+ currentFileIndex = walFiles.length;
return false;
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
index 93b805c3032..a663437e360 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
@@ -210,7 +210,10 @@ public class ConsensusPrefetchingQueueInitializationTest {
assertNull(queue.poll("consumer"));
assertEquals(1L, queue.getInitializedStatus());
assertEquals(2L, queue.getCurrentReadSearchIndex());
- queue.drivePrefetchOnce();
+ // WAL replay may yield when the round's time slice expires before
reaching the tail.
+ for (int round = 0; round < 10 && queue.getCurrentReadSearchIndex() < 4L;
round++) {
+ queue.drivePrefetchOnce();
+ }
final ArgumentCaptor<InsertNode> requests =
ArgumentCaptor.forClass(InsertNode.class);
verify(converter, times(2)).convert(requests.capture());
assertEquals(
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueSeekTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueSeekTest.java
index 54fcb704f00..ab5aff9b0d7 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueSeekTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueSeekTest.java
@@ -164,7 +164,8 @@ public class ConsensusPrefetchingQueueSeekTest {
|| request.getSearchIndex() >=
startSearchIndex)
.iterator();
final ProgressWALIterator iterator =
mock(ProgressWALIterator.class);
- when(iterator.hasNext()).thenAnswer(ignored ->
retainedEntries.hasNext());
+ when(iterator.hasNext(any(ProgressWALIterator.ScanBudget.class)))
+ .thenAnswer(ignored -> retainedEntries.hasNext());
when(iterator.next()).thenAnswer(ignored ->
retainedEntries.next());
return iterator;
}
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 0b9e12eda48..7f9c4139040 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
@@ -1758,7 +1758,8 @@ public class ConsensusPrefetchingQueueTest {
createRequest(5L))
.iterator();
final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
- when(iterator.hasNext()).thenAnswer(ignored -> walEntries.hasNext());
+ when(iterator.hasNext(any(ProgressWALIterator.ScanBudget.class)))
+ .thenAnswer(ignored -> walEntries.hasNext());
when(iterator.next()).thenAnswer(ignored -> walEntries.next());
queue =
@@ -1916,7 +1917,8 @@ public class ConsensusPrefetchingQueueTest {
createRequest(5L))
.iterator();
final ProgressWALIterator iterator =
mock(ProgressWALIterator.class);
- when(iterator.hasNext()).thenAnswer(ignored ->
walEntries.hasNext());
+ when(iterator.hasNext(any(ProgressWALIterator.ScanBudget.class)))
+ .thenAnswer(ignored -> walEntries.hasNext());
when(iterator.next()).thenAnswer(ignored -> walEntries.next());
return iterator;
}
@@ -2264,14 +2266,21 @@ public class ConsensusPrefetchingQueueTest {
true);
final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
- when(iterator.hasNext()).thenReturn(false);
+
when(iterator.hasNext(any(ProgressWALIterator.ScanBudget.class))).thenReturn(false);
setSubscriptionWalIterator(queue, iterator);
final Method tryCatchUpFromWAL =
-
ConsensusPrefetchingQueue.class.getDeclaredMethod("tryCatchUpFromWAL",
long.class);
+ ConsensusPrefetchingQueue.class.getDeclaredMethod(
+ "tryCatchUpFromWAL", long.class,
ProgressWALIterator.ScanBudget.class);
tryCatchUpFromWAL.setAccessible(true);
- tryCatchUpFromWAL.invoke(queue, queue.getCurrentSeekGeneration());
- tryCatchUpFromWAL.invoke(queue, queue.getCurrentSeekGeneration());
+ tryCatchUpFromWAL.invoke(
+ queue,
+ queue.getCurrentSeekGeneration(),
+ new ProgressWALIterator.ScanBudget(128, Long.MAX_VALUE));
+ tryCatchUpFromWAL.invoke(
+ queue,
+ queue.getCurrentSeekGeneration(),
+ new ProgressWALIterator.ScanBudget(128, Long.MAX_VALUE));
assertSame(iterator, subscriptionWalIterator(queue));
verify(iterator, times(2)).refresh();
@@ -2313,12 +2322,12 @@ public class ConsensusPrefetchingQueueTest {
true);
final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
- when(iterator.hasNext()).thenReturn(true);
+
when(iterator.hasNext(any(ProgressWALIterator.ScanBudget.class))).thenReturn(true);
setSubscriptionWalIterator(queue, iterator);
invokeEnsureSubscriptionWalReadable(queue);
- verify(iterator).hasNext();
+ verify(iterator).hasNext(any(ProgressWALIterator.ScanBudget.class));
verify(iterator, never()).refresh();
} finally {
if (queue != null) {
@@ -2369,12 +2378,12 @@ public class ConsensusPrefetchingQueueTest {
};
final ProgressWALIterator exhaustedIterator =
mock(ProgressWALIterator.class);
- when(exhaustedIterator.hasNext()).thenReturn(false);
+
when(exhaustedIterator.hasNext(any(ProgressWALIterator.ScanBudget.class))).thenReturn(false);
setSubscriptionWalIterator(queue, exhaustedIterator);
invokeEnsureSubscriptionWalReadable(queue);
- verify(exhaustedIterator, times(2)).hasNext();
+ verify(exhaustedIterator,
times(2)).hasNext(any(ProgressWALIterator.ScanBudget.class));
verify(exhaustedIterator).refresh();
verify(exhaustedIterator).close();
verify(walNode).rollWALFile();
@@ -2428,6 +2437,7 @@ public class ConsensusPrefetchingQueueTest {
verify(iterator).hasBufferedNext();
verify(iterator, never()).hasNext();
+ verify(iterator,
never()).hasNext(any(ProgressWALIterator.ScanBudget.class));
verify(iterator, never()).next();
verify(iterator, never()).refresh();
assertEquals(1L, queue.getCurrentReadSearchIndex());
@@ -3203,9 +3213,10 @@ public class ConsensusPrefetchingQueueTest {
private static void invokeEnsureSubscriptionWalReadable(final
ConsensusPrefetchingQueue queue)
throws Exception {
final Method method =
-
ConsensusPrefetchingQueue.class.getDeclaredMethod("ensureSubscriptionWalReadable");
+ ConsensusPrefetchingQueue.class.getDeclaredMethod(
+ "ensureSubscriptionWalReadable",
ProgressWALIterator.ScanBudget.class);
method.setAccessible(true);
- method.invoke(queue);
+ method.invoke(queue, new ProgressWALIterator.ScanBudget(128,
Long.MAX_VALUE));
}
private static void invokeReportUnreadableWalReplayIfNecessary(
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
index bb7b2e59371..377f20801c4 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
@@ -73,6 +73,8 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ConsensusPrefetchingQueueWalBackpressureTest {
@@ -82,6 +84,118 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+ @Test
+ public void testYieldedGapScanKeepsIteratorAndDeliversEachRequestOnce()
throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final CommonConfig config = CommonDescriptor.getInstance().getConfig();
+ final int originalBatchMaxWalEntries =
config.getSubscriptionConsensusBatchMaxWalEntries();
+ final int originalBatchMaxTabletCount =
config.getSubscriptionConsensusBatchMaxTabletCount();
+ final int originalBatchMaxDelay =
config.getSubscriptionConsensusBatchMaxDelayInMs();
+ final File systemDir = temporaryFolder.newFolder("system-yielded-wal-gap");
+ final File walDirectory = temporaryFolder.newFolder("yielded-wal-gap");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ config.setSubscriptionConsensusBatchMaxWalEntries(2);
+ config.setSubscriptionConsensusBatchMaxTabletCount(1);
+ config.setSubscriptionConsensusBatchMaxDelayInMs(0);
+ final File dataWal =
+ new File(
+ walDirectory,
+ WALFileUtils.getLogFileName(0L, 0L,
WALFileStatus.CONTAINS_SEARCH_INDEX));
+ final File liveWal =
+ new File(
+ walDirectory,
+ WALFileUtils.getLogFileName(1L, 3L,
WALFileStatus.CONTAINS_SEARCH_INDEX));
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ // The replay cursor is 2, but its WAL file has a fragmented prefix
for local index 1.
+ for (int fragment = 0; fragment < 9; fragment++) {
+ writeWalEntry(writer, 1L);
+ }
+ writeWalEntry(writer, 2L);
+ writeWalEntry(writer, 3L);
+ }
+ try (WALWriter ignored = new WALWriter(liveWal, WALFileVersion.V3)) {
+ // Keep a sealed successor so the fragmented prefix can be read.
+ }
+
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(walDirectory);
+ when(walNode.getCurrentSearchIndex()).thenReturn(3L);
+ when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
+ when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new
WALMetaData());
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ final List<Long> conversions = new ArrayList<>();
+ final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
+ when(converter.convert(any()))
+ .thenAnswer(
+ invocation -> {
+ conversions.add(((InsertNode)
invocation.getArgument(0)).getSearchIndex());
+ return Collections.singletonList(createTablet());
+ });
+ when(converter.getDatabaseName()).thenReturn("db");
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ converter,
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 2L,
+ 2L,
+ true);
+ final SubscriptionMemoryManager memoryManager =
+ new SubscriptionMemoryManager(createTablet().ramBytesUsed() * 8L);
+ queue.setSubscriptionMemoryManager(memoryManager);
+ assertNull(queue.poll("consumer"));
+ assertTrue(pendingEntries(queue).offer(createRequest(3L)));
+
+ assertEquals(PrefetchRoundResult.Type.RESCHEDULE_NOW,
queue.drivePrefetchOnce().getType());
+ final ProgressWALIterator iterator = subscriptionWalIterator(queue);
+ assertTrue(iterator.hasYieldedScan());
+ assertTrue(conversions.isEmpty());
+ assertEquals(2L, queue.getCurrentReadSearchIndex());
+ verify(walNode, never()).rollWALFile();
+
+ // Repeated realtime jumps must continue the yielded scan rather than
reset it to index 2.
+ for (int round = 0; round < 20 && queue.getCurrentReadSearchIndex() <
4L; round++) {
+ if (queue.getCurrentReadSearchIndex() < 3L) {
+ assertTrue(pendingEntries(queue).offer(createRequest(3L)));
+ }
+ queue.drivePrefetchOnce();
+ assertSame(iterator, subscriptionWalIterator(queue));
+ }
+ assertEquals(4L, queue.getCurrentReadSearchIndex());
+ assertEquals(Arrays.asList(2L, 3L), conversions);
+ for (long expected = 2L; expected <= 3L; expected++) {
+ final SubscriptionEvent event = queue.poll("consumer");
+ assertNotNull(event);
+ assertEquals(expected,
event.getCommitContext().getWriterProgress().getLocalSeq());
+ assertTrue(queue.ack("consumer", event.getCommitContext()));
+ }
+ queue.drivePrefetchOnce();
+ assertNull(queue.poll("consumer"));
+ assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+ verify(walNode, never()).rollWALFile();
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
config.setSubscriptionConsensusBatchMaxWalEntries(originalBatchMaxWalEntries);
+
config.setSubscriptionConsensusBatchMaxTabletCount(originalBatchMaxTabletCount);
+ config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
@Test
public void testAckRecoversDrainedSuffixFromWalWithoutReoffer() throws
Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
@@ -369,8 +483,8 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
config.setSubscriptionConsensusBatchMaxDelayInMs(60_000);
final int replayedRequests = materializationFailure ? 7 : 6;
- final int rounds = memoryBackpressure ? replayedRequests :
(replayedRequests + 1) / 2;
- final int requestCount = replayedRequests + 2 * rounds;
+ final int maxRounds = 20;
+ final int requestCount = replayedRequests + 2 * maxRounds;
writeSealedWal(walDirectory, requestCount);
final WALNode walNode = mock(WALNode.class);
@@ -444,15 +558,20 @@ public class ConsensusPrefetchingQueueWalBackpressureTest
{
}
long deliveredLocalSeq = 0L;
- for (int round = 0; round < rounds; round++) {
+ for (int round = 0; round < maxRounds && deliveredLocalSeq <
replayedRequests; round++) {
+ // Physical scan limits include lookahead and skipped prefixes; a
deadline can also end a
+ // round early. Bound the final logical batch so replay stops at the
asserted frontier.
+ final int maxWalEntries =
+ memoryBackpressure ? 2 : (int) Math.min(2L, replayedRequests -
deliveredLocalSeq);
+ config.setSubscriptionConsensusBatchMaxWalEntries(maxWalEntries);
+ final long memoryRejections =
queue.getSubscriptionMemoryRejectionCount();
queue.drivePrefetchOnce();
- final long expectedLocalSeq =
- materializationFailure
- ? round * 2L + 1L
- : memoryBackpressure ? round + 1L : (round + 1L) * 2L;
- assertEquals(expectedLocalSeq + 1L, queue.getCurrentReadSearchIndex());
+ final long expectedLocalSeq = queue.getCurrentReadSearchIndex() - 1L;
+ assertTrue(expectedLocalSeq >= deliveredLocalSeq);
+ assertTrue(expectedLocalSeq <= replayedRequests);
+ assertTrue(expectedLocalSeq - deliveredLocalSeq <= maxWalEntries);
assertMemoryBounded(queue, memoryManager);
- if (memoryBackpressure) {
+ if (queue.getSubscriptionMemoryRejectionCount() > memoryRejections) {
assertTrue(subscriptionWalIterator(queue).hasBufferedNext());
}
while (deliveredLocalSeq < expectedLocalSeq) {
@@ -475,6 +594,9 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
assertEquals(0L, queue.getWalGapSkippedEntries());
assertEquals(0L, queue.getSubscriptionUncommittedEventCount());
assertEquals(0L, queue.getRetainedTabletBytes());
+ if (memoryBackpressure) {
+ assertTrue(queue.getSubscriptionMemoryRejectionCount() > 0L);
+ }
// Grouping reads one request ahead; a blocked request can require one
further lookahead.
// Count actual body reads, including discarded iterators, rather than
timing disk caches.
// An exceptional conversion failure needs one reset, rereading its
three consumed bodies.
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 d6159e6c692..cd136e4cc72 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
@@ -47,6 +47,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -58,6 +59,115 @@ import static org.mockito.Mockito.when;
public class ProgressWALIteratorTest {
+ @Test
+ public void
testBoundedScanKeepsSkippedPrefixAndPendingFragmentsAcrossRounds() throws
Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-bounded-scan");
+ final File dataWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 2,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ for (int fragment = 0; fragment < 5; fragment++) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 1000L,
7, 1L));
+ }
+ for (int fragment = 0; fragment < 5; fragment++) {
+ writer.write(
+ searchableEntry(SearchNode.NO_CONSENSUS_INDEX),
+ singleEntryMeta(19, SearchNode.NO_CONSENSUS_INDEX, 1L, 2000L, 8,
50L));
+ }
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 3000L,
7, 2L));
+ }
+ try (WALWriter ignored = new WALWriter(successorWal, WALFileVersion.V3))
{
+ // Seal a successor so replay reads the historical file.
+ }
+
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 2L)) {
+ for (int round = 0; round < 5; round++) {
+ final ProgressWALIterator.ScanBudget budget =
+ new ProgressWALIterator.ScanBudget(2, Long.MAX_VALUE);
+ assertFalse(iterator.hasNext(budget));
+ assertTrue(budget.hasYielded());
+ assertTrue(iterator.hasYieldedScan());
+ assertFalse(iterator.hasBufferedNext());
+ assertFalse(iterator.hasIncompleteScan());
+ // Neither a skipped prefix nor an unfinished follower request is a
completed delivery.
+ assertFalse(iterator.hasNext(budget));
+ }
+
+ final ProgressWALIterator.ScanBudget budget =
+ new ProgressWALIterator.ScanBudget(2, Long.MAX_VALUE);
+ assertTrue(iterator.hasNext(budget));
+ final IndexedConsensusRequest follower = iterator.next();
+ assertEquals(SearchNode.NO_CONSENSUS_INDEX, follower.getSearchIndex());
+ assertEquals(8, follower.getNodeId());
+ assertEquals(50L, follower.getProgressLocalSeq());
+ assertEquals(5, follower.getRequests().size());
+ // Repeated probes share the same budget and retain the final pending
local request.
+ assertTrue(iterator.hasNext(budget));
+ assertEquals(2L, iterator.next().getSearchIndex());
+ assertFalse(iterator.hasNext(budget));
+ assertFalse(budget.hasYielded());
+ assertFalse(iterator.hasIncompleteScan());
+ }
+ } finally {
+ Files.deleteIfExists(dataWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testElapsedScanBudgetYieldsWithoutFlushingIncompleteRequest()
throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-scan-time-slice");
+ final File dataWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 1,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final AtomicLong clock = new AtomicLong();
+ try {
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 1000L,
7, 1L));
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 1000L,
7, 1L));
+ }
+ try (WALWriter ignored = new WALWriter(successorWal, WALFileVersion.V3))
{
+ // Seal a successor so replay reads the historical file.
+ }
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ final ProgressWALIterator.ScanBudget first =
+ new ProgressWALIterator.ScanBudget(1, Long.MAX_VALUE);
+ assertFalse(iterator.hasNext(first));
+ final ProgressWALIterator.ScanBudget expired =
+ new ProgressWALIterator.ScanBudget(100, 50L, clock::get);
+ clock.set(50L);
+ assertFalse(iterator.hasNext(expired));
+ assertTrue(expired.hasYielded());
+ assertFalse(iterator.hasBufferedNext());
+ assertFalse(iterator.hasIncompleteScan());
+
+ final ProgressWALIterator.ScanBudget resumed =
+ new ProgressWALIterator.ScanBudget(100, 50L, clock::get);
+ assertTrue(iterator.hasNext(resumed));
+ final IndexedConsensusRequest request = iterator.next();
+ assertEquals(1L, request.getSearchIndex());
+ assertEquals(2, request.getRequests().size());
+ iterator.retry(request);
+ // Already buffered requests remain available even when no further I/O
is permitted.
+ assertTrue(iterator.hasNext(expired));
+ assertEquals(request, iterator.next());
+ assertFalse(iterator.hasNext(resumed));
+ }
+ } finally {
+ Files.deleteIfExists(dataWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
@Test
public void testIteratorGroupsByLocalSeqAndCarriesWriterMetadata() throws
Exception {
final Path dir = Files.createTempDirectory("progress-wal-iterator");