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 9a078cc105e [Subscription] Fix WAL replay progress after realtime gaps
(#18895)
9a078cc105e is described below
commit 9a078cc105e50533004a659ab7d0a00dd8320c39
Author: Caideyipi <[email protected]>
AuthorDate: Fri Oct 16 10:56:44 2026 +0800
[Subscription] Fix WAL replay progress after realtime gaps (#18895)
* [Subscription] Fix WAL replay progress after realtime gaps
* Remove subscription WAL replay documentation
* [Subscription] Initialize WAL position inside iterator factory
---
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 2 +
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 2 +
.../consensus/ConsensusPrefetchingQueue.java | 61 ++++++++++++-
.../broker/consensus/ProgressWALIterator.java | 42 +++++++--
.../consensus/ConsensusPrefetchingQueueTest.java | 99 ++++++++++++++++++++++
...nsensusPrefetchingQueueWalBackpressureTest.java | 51 +++++++++++
.../broker/consensus/ProgressWALIteratorTest.java | 52 +++++++++++-
7 files changed, 299 insertions(+), 10 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 9d400f9d40b..d03146c60b7 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -20,6 +20,8 @@
package org.apache.iotdb.db.i18n;
public final class DataNodePipeMessages {
+ public static final String
LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B
=
+ "Subscription replay has made no cursor progress for {} ms: {}. Inspect
prefetch worker stacks and WAL I/O.";
public static final String
LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD
=
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 4891920e079..0c2282616d8 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -20,6 +20,8 @@
package org.apache.iotdb.db.i18n;
public final class DataNodePipeMessages {
+ public static final String
LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B
=
+ "订阅回放游标已连续 {} ms 未推进:{}。请检查预取工作线程栈和 WAL I/O。";
public static final String
LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD
=
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 9143e40796c..fc9f54f2901 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
@@ -321,6 +321,17 @@ public class ConsensusPrefetchingQueue {
private final SubscriptionQueueTimeTracker timeTracker = new
SubscriptionQueueTimeTracker();
+ private volatile long lastPrefetchDurationMs;
+ private volatile PrefetchRoundResult.Type lastPrefetchResult;
+ private volatile String walReplayState = "NOT_SCANNED";
+
+ // Accessed only by the serialized prefetch worker while holding the queue
read lock.
+ private boolean replayProgressObserved;
+ private long lastObservedReplayCursor;
+ private long lastReplayProgressNanos;
+ private boolean replayStallWarningLogged;
+ private long lastReplayStallWarningNanos;
+
private volatile String lastConsumerId = "";
private volatile long lastPendingAcceptedEntries = 0L;
@@ -1035,8 +1046,7 @@ public class ConsensusPrefetchingQueue {
this.nextExpectedSearchIndex.set(resolvedStart.getStartSearchIndex());
// Use the same factory as cursor resets; the default implementation still
returns null for
// readers without WAL support.
- this.subscriptionWALIterator =
- createSubscriptionWALIterator(resolvedStart.getStartSearchIndex());
+ resetSubscriptionWALPosition(resolvedStart.getStartSearchIndex());
this.prefetchInitialized = true;
timeTracker.start();
this.observedSeekGeneration = seekGeneration.get();
@@ -1621,6 +1631,8 @@ public class ConsensusPrefetchingQueue {
private static final long WAL_GAP_WAIT_LOG_INTERVAL_MS = 5_000L;
private static final long PREFETCH_STATS_LOG_INTERVAL_MS = 5_000L;
+ private static final long REPLAY_STALL_WARNING_THRESHOLD_NANOS =
TimeUnit.SECONDS.toNanos(60L);
+ private static final long REPLAY_STALL_WARNING_INTERVAL_NANOS =
TimeUnit.SECONDS.toNanos(30L);
private static final long SLOW_PREFETCH_ROUND_THRESHOLD_MS = 10_000L;
@@ -1653,6 +1665,8 @@ public class ConsensusPrefetchingQueue {
return result;
} finally {
final long durationMs = timeTracker.endPrefetch();
+ lastPrefetchDurationMs = durationMs;
+ lastPrefetchResult = Objects.nonNull(result) ? result.getType() : null;
if (debugEnabled) {
LOGGER.debug(
DataNodePipeMessages
@@ -1863,6 +1877,8 @@ public class ConsensusPrefetchingQueue {
}
return PrefetchRoundResult.rescheduleAfter(100L);
} finally {
+ final ProgressWALIterator iterator = subscriptionWALIterator;
+ walReplayState = Objects.nonNull(iterator) ? iterator.toString() :
"UNAVAILABLE";
releaseReadLock();
}
}
@@ -1873,6 +1889,8 @@ public class ConsensusPrefetchingQueue {
return;
}
+ reportReplayStallIfNecessary(System.nanoTime());
+
final long currentPendingAcceptedEntries =
pendingPathAcceptedEntries.get();
final long currentWalAcceptedEntries = walPathAcceptedEntries.get();
LOGGER.info(
@@ -1897,7 +1915,36 @@ public class ConsensusPrefetchingQueue {
lastWalAcceptedEntries = currentWalAcceptedEntries;
}
+ private void reportReplayStallIfNecessary(final long nowNanos) {
+ final long cursor = nextExpectedSearchIndex.get();
+ if (!prefetchInitialized || !isActive || isClosed || closeRequested ||
getLag() <= 0L) {
+ replayProgressObserved = false;
+ replayStallWarningLogged = false;
+ return;
+ }
+ if (!replayProgressObserved || cursor != lastObservedReplayCursor) {
+ replayProgressObserved = true;
+ lastObservedReplayCursor = cursor;
+ lastReplayProgressNanos = nowNanos;
+ replayStallWarningLogged = false;
+ return;
+ }
+ final long stalledNanos = nowNanos - lastReplayProgressNanos;
+ if (stalledNanos >= REPLAY_STALL_WARNING_THRESHOLD_NANOS
+ && (!replayStallWarningLogged
+ || nowNanos - lastReplayStallWarningNanos >=
REPLAY_STALL_WARNING_INTERVAL_NANOS)) {
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B,
+ TimeUnit.NANOSECONDS.toMillis(stalledNanos),
+ this);
+ lastReplayStallWarningNanos = nowNanos;
+ replayStallWarningLogged = true;
+ }
+ }
+
private void resetRoundStateForSeek(final long newSeekGeneration) {
+ replayProgressObserved = false;
restorePendingSubscriptionWalCursor(newSeekGeneration);
discardRealtimeEntries();
discardBatch(lingerBatch);
@@ -2477,7 +2524,12 @@ public class ConsensusPrefetchingQueue {
protected ProgressWALIterator createSubscriptionWALIterator(final long
startSearchIndex) {
if (consensusReqReader instanceof WALNode) {
- return new ProgressWALIterator((WALNode) consensusReqReader,
startSearchIndex);
+ final ProgressWALIterator iterator =
+ new ProgressWALIterator((WALNode) consensusReqReader,
startSearchIndex);
+ // The constructor sets the local filtering lower bound. Complete
initialization here by
+ // positioning past historical files only when every writer's progress
is already covered.
+ iterator.advanceTo(startSearchIndex,
this::isWriterProgressCoveredForWalFastForward);
+ return iterator;
}
return null;
}
@@ -5105,6 +5157,9 @@ public class ConsensusPrefetchingQueue {
result.put("maxDeliveryIntervalMs",
String.valueOf(getMaxDeliveryIntervalMs()));
result.put("deliveryIdleTimeMs", String.valueOf(getDeliveryIdleTimeMs()));
result.put("prefetchDurationMs", String.valueOf(getPrefetchDurationMs()));
+ result.put("lastPrefetchDurationMs",
String.valueOf(lastPrefetchDurationMs));
+ result.put("lastPrefetchResult", String.valueOf(lastPrefetchResult));
+ result.put("walReplayState", walReplayState);
result.put("maxPrefetchDurationMs",
String.valueOf(getMaxPrefetchDurationMs()));
result.put("prefetchIdleTimeMs", String.valueOf(getPrefetchIdleTimeMs()));
result.put("realtimeAdmissionBlocked",
String.valueOf(realtimeAdmissionBlocked.get()));
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 3f5186893ee..13568fd00be 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
@@ -69,12 +69,14 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
/**
* 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.
+ * cooperative: it starts with the first WAL probe, is checked between
operations, and cannot
+ * interrupt a single file read. Queue housekeeping must not consume the
scan's time slice.
*/
static final class ScanBudget {
private int remainingEntries;
- private final long startNanos;
+ private long startNanos;
+ private boolean started;
private final long maxDurationNanos;
private final LongSupplier nanoTime;
private boolean yielded;
@@ -87,11 +89,15 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
this.remainingEntries = maxEntries;
this.maxDurationNanos = maxDurationNanos;
this.nanoTime = nanoTime;
- this.startNanos = nanoTime.getAsLong();
}
private boolean shouldYield() {
- if (remainingEntries <= 0 || nanoTime.getAsLong() - startNanos >=
maxDurationNanos) {
+ final long nowNanos = nanoTime.getAsLong();
+ if (!started) {
+ startNanos = nowNanos;
+ started = true;
+ }
+ if (remainingEntries <= 0 || nowNanos - startNanos >= maxDurationNanos) {
yielded = true;
}
return yielded;
@@ -141,6 +147,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
private IndexedConsensusRequest nextReady;
private boolean scanYielded;
+ private long bodyReadAttempts;
public ProgressWALIterator(final File logDirectory) {
this(logDirectory, Long.MIN_VALUE);
@@ -279,9 +286,13 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
*/
public void advanceTo(
final long targetSearchIndex, final WriterProgressCoverage
writerProgressCoverage) {
- if (targetSearchIndex <= minimumSearchIndex) {
+ if (targetSearchIndex < minimumSearchIndex
+ || (targetSearchIndex == minimumSearchIndex && currentFileIndex >= 0))
{
return;
}
+ // A lower bound filters requests; it does not position the reader. A
fresh/rebuilt iterator
+ // already has this bound but must still be allowed to skip fully covered
historical files.
+ // Once scanning starts, equal-bound probes must not recheck the same
uncovered file footers.
minimumSearchIndex = targetSearchIndex;
if (writerProgressCoverage == null ||
!canDiscardBufferedRequests(writerProgressCoverage)) {
@@ -447,6 +458,26 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
return retryCurrentReader;
}
+ @Override
+ public String toString() {
+ final String status =
+ retryCurrentReader
+ ? "WAITING_FOR_READABLE_WAL"
+ : scanYielded
+ ? "SCAN_YIELDED"
+ : hasBufferedNext() ? "REQUEST_BUFFERED" :
"NO_BUFFERED_REQUEST";
+ return status
+ + "[fileVersion="
+ + currentReaderVersionId
+ + ", entryOffset="
+ + consumedEntryCountInCurrentFile
+ + ", bodyReadAttempts="
+ + bodyReadAttempts
+ + ", minimumSearchIndex="
+ + minimumSearchIndex
+ + "]";
+ }
+
String getSkippedBrokenWalFileDetails(final int firstFileIndex) {
final int firstAvailableIndex =
getSkippedBrokenWalFileCount() - skippedBrokenWalFileDetails.size();
@@ -515,6 +546,7 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
// hasNext() can otherwise read an arbitrary WAL prefix before
returning a request.
scanBudget.remainingEntries--;
}
+ bodyReadAttempts++;
final ByteBuffer buffer = currentReader.next();
consumedEntryCountInCurrentFile =
currentReader.getCurrentEntryIndex() + 1;
final WALEntryType type = WALEntryType.valueOf(buffer.get());
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 a0696342501..61493475115 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
@@ -36,6 +36,7 @@ import
org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueAdmission;
import org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueRegistry;
import
org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueRejectionReason;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodePipeMessages;
import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.ProgressWALReader;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileVersion;
@@ -57,6 +58,10 @@ import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseTy
import org.apache.iotdb.rpc.subscription.payload.poll.WriterId;
import org.apache.iotdb.rpc.subscription.payload.poll.WriterProgress;
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
import org.apache.tsfile.enums.ColumnCategory;
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.utils.Pair;
@@ -64,6 +69,7 @@ import org.apache.tsfile.write.record.Tablet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
+import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
@@ -113,6 +119,99 @@ public class ConsensusPrefetchingQueueTest {
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+ @Test
+ public void testReplayStallWarningIsThrottledAndResetsAfterProgress() throws
Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final Logger logger = (Logger)
LoggerFactory.getLogger(ConsensusPrefetchingQueue.class);
+ final Level originalLevel = logger.getLevel();
+ logger.setLevel(Level.WARN);
+ final ListAppender<ILoggingEvent> appender = new ListAppender<>();
+ appender.start();
+ logger.addAppender(appender);
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ reader.currentSearchIndex = 10L;
+ final IoTConsensusServerImpl server = mock(IoTConsensusServerImpl.class);
+ when(server.getConsensusReqReader()).thenReturn(reader);
+ when(server.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ server,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ mock(ConsensusLogToTabletConverter.class),
+
newCommitManager(temporaryFolder.newFolder("replay-stall-warning")),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+ final Field initialized =
+
ConsensusPrefetchingQueue.class.getDeclaredField("prefetchInitialized");
+ initialized.setAccessible(true);
+ initialized.setBoolean(queue, true);
+ final Method report =
+ ConsensusPrefetchingQueue.class.getDeclaredMethod(
+ "reportReplayStallIfNecessary", long.class);
+ report.setAccessible(true);
+ final Supplier<Long> warningCount =
+ () ->
+ appender.list.stream()
+ .filter(
+ event ->
+ event
+ .getMessage()
+ .equals(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B))
+ .count();
+ report.invoke(queue, 0L);
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(59L));
+ assertEquals(Long.valueOf(0L), warningCount.get());
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(60L));
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(89L));
+ assertEquals(Long.valueOf(1L), warningCount.get());
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(90L));
+ assertEquals(Long.valueOf(2L), warningCount.get());
+
+ final Field cursorField =
+
ConsensusPrefetchingQueue.class.getDeclaredField("nextExpectedSearchIndex");
+ cursorField.setAccessible(true);
+ ((AtomicLong) cursorField.get(queue)).incrementAndGet();
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(91L));
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(150L));
+ assertEquals(Long.valueOf(2L), warningCount.get());
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(151L));
+ assertEquals(Long.valueOf(3L), warningCount.get());
+
+ reader.currentSearchIndex = 0L;
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(250L));
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(350L));
+ reader.currentSearchIndex = 10L;
+ // A backlog appearing after an idle period starts a new observation
window.
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(450L));
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(509L));
+ assertEquals(Long.valueOf(3L), warningCount.get());
+ queue.setActive(false);
+ report.invoke(queue, TimeUnit.SECONDS.toNanos(600L));
+ assertEquals(Long.valueOf(3L), warningCount.get());
+ } finally {
+ logger.detachAppender(appender);
+ logger.setLevel(originalLevel);
+ appender.stop();
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
@Test
public void testLateOldPrefetchDeregistrationKeepsReplacementTask() {
final ConsensusSubscriptionPrefetchExecutor executor =
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 192ed7c3e9c..204a557367e 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
@@ -56,6 +56,7 @@ import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -183,6 +184,56 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
}
queue.drivePrefetchOnce();
assertNull(queue.poll("consumer"));
+ for (int round = 0; round < 10 && queue.getPendingEventCount() > 0;
round++) {
+ queue.drivePrefetchOnce();
+ }
+ assertEquals(0, queue.getPendingEventCount());
+
+ // Rebuilding at the unchanged cursor must position the reader past
covered history. The
+ // fragmented prefix is larger than a whole scan budget and must not
delay WAL takeover.
+ try (WALWriter writer = new WALWriter(liveWal, WALFileVersion.V3)) {
+ writeWalEntry(writer, 4L);
+ writeWalEntry(writer, 5L);
+ }
+ try (WALWriter ignored =
+ new WALWriter(
+ new File(
+ walDirectory,
+ WALFileUtils.getLogFileName(2L, 5L,
WALFileStatus.CONTAINS_SEARCH_INDEX)),
+ WALFileVersion.V3)) {
+ // Seal the new data file so replay can take over at index 4.
+ }
+ when(walNode.getCurrentSearchIndex()).thenReturn(5L);
+ when(walNode.getCurrentWALFileVersion()).thenReturn(2L);
+ // The factory must return an iterator that is ready for bounded replay
at the requested
+ // cursor, without requiring a separate advanceTo call from its caller.
+ try (ProgressWALIterator createdIterator =
queue.createSubscriptionWALIterator(4L)) {
+ final ProgressWALIterator.ScanBudget budget =
+ new ProgressWALIterator.ScanBudget(2, Long.MAX_VALUE);
+ assertTrue(createdIterator.hasNext(budget));
+ assertEquals(4L, createdIterator.next().getSearchIndex());
+ assertFalse(budget.hasYielded());
+ }
+ final Method reset =
+ ConsensusPrefetchingQueue.class.getDeclaredMethod(
+ "resetSubscriptionWALPosition", long.class);
+ reset.setAccessible(true);
+ reset.invoke(queue, 4L);
+ assertTrue(pendingEntries(queue).offer(createRequest(5L)));
+ queue.drivePrefetchOnce();
+ assertTrue(queue.toString(), queue.getCurrentReadSearchIndex() >= 5L);
+ for (int round = 0; round < 10 && queue.getCurrentReadSearchIndex() <
6L; round++) {
+ queue.drivePrefetchOnce();
+ }
+ assertEquals(6L, queue.getCurrentReadSearchIndex());
+ assertEquals(Arrays.asList(2L, 3L, 4L, 5L), conversions);
+ for (long expected = 4L; expected <= 5L; expected++) {
+ final SubscriptionEvent event = queue.poll("consumer");
+ assertNotNull(event);
+ assertEquals(expected,
event.getCommitContext().getWriterProgress().getLocalSeq());
+ assertTrue(queue.ack("consumer", event.getCommitContext()));
+ }
+ assertEquals(0L, queue.getWalGapSkippedEntries());
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
verify(walNode, never()).rollWALFile();
} finally {
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 cd136e4cc72..370fca53796 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
@@ -119,6 +119,34 @@ public class ProgressWALIteratorTest {
}
}
+ @Test
+ public void testScanBudgetStartsWhenWalScanningBegins() throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-delayed-scan");
+ final File walFile =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final AtomicLong clock = new AtomicLong();
+ try {
+ try (WALWriter writer = new WALWriter(walFile, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 1000L,
7, 1L));
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 2000L,
7, 2L));
+ }
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ final ProgressWALIterator.ScanBudget budget =
+ new ProgressWALIterator.ScanBudget(100, 50L, clock::get);
+ // Queue housekeeping, realtime materialization, or a JVM pause can
consume a whole
+ // time slice before the first WAL probe. That must not starve replay.
+ clock.set(1000L);
+ assertTrue(iterator.hasNext(budget));
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertFalse(budget.hasYielded());
+ }
+ } finally {
+ Files.deleteIfExists(walFile.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
@Test
public void testElapsedScanBudgetYieldsWithoutFlushingIncompleteRequest()
throws Exception {
final Path dir = Files.createTempDirectory("progress-wal-scan-time-slice");
@@ -142,8 +170,7 @@ public class ProgressWALIteratorTest {
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);
+ new ProgressWALIterator.ScanBudget(100, 50L, () ->
clock.getAndAdd(50L));
assertFalse(iterator.hasNext(expired));
assertTrue(expired.hasYielded());
assertFalse(iterator.hasBufferedNext());
@@ -974,6 +1001,27 @@ public class ProgressWALIteratorTest {
assertTrue(iterator.hasNext());
assertEquals(6L, iterator.next().getSearchIndex());
}
+
+ // A freshly rebuilt replay iterator already has this lower bound, but
has not read or
+ // skipped any files yet. It must still fast-forward covered history
before a bounded scan.
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 6L)) {
+ iterator.advanceTo(6L, (physicalTime, nodeId, localSeq) -> true);
+ final ProgressWALIterator.ScanBudget budget =
+ new ProgressWALIterator.ScanBudget(2, Long.MAX_VALUE);
+ assertTrue(iterator.hasNext(budget));
+ assertEquals(6L, iterator.next().getSearchIndex());
+ assertFalse(budget.hasYielded());
+ }
+
+ // Equal-bound realignment must retain uncovered follower entries, just
like an increasing
+ // bound. They have no local search index and cannot be skipped using
the local cursor.
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 6L)) {
+ iterator.advanceTo(6L, (physicalTime, nodeId, localSeq) -> nodeId ==
7);
+ assertTrue(iterator.hasNext());
+ assertEquals(8, iterator.next().getNodeId());
+ assertTrue(iterator.hasNext());
+ assertEquals(6L, iterator.next().getSearchIndex());
+ }
} finally {
Files.deleteIfExists(localWal.toPath());
Files.deleteIfExists(followerWal.toPath());