Skip to content

Commit 3d57d25

Browse files
authored
[Subscription] Preserve WAL replay progress across pending gaps (#18831)
1 parent 510fc7c commit 3d57d25

2 files changed

Lines changed: 191 additions & 4 deletions

File tree

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,7 +2046,7 @@ private MaterializationResult accumulateFromPending(
20462046
} finally {
20472047
// Advance once for the consumed prefix, including when a gap or memory block stops the
20482048
// batch. Per-entry fast-forward can repeatedly inspect the same uncovered WAL file footer.
2049-
// Gap recovery creates its own iterator at the current cursor before reading WAL.
2049+
// Gap recovery realigns the same iterator at the current cursor before reading WAL.
20502050
final ProgressWALIterator iterator = subscriptionWALIterator;
20512051
if (nextExpectedSearchIndex.get() > startSearchIndex
20522052
&& Objects.nonNull(iterator)
@@ -2077,10 +2077,16 @@ private MaterializationResult fillGapFromWAL(
20772077
final int maxTablets,
20782078
final long maxBatchBytes) {
20792079
pendingWalGapRetryRequested = false;
2080-
resetSubscriptionWALPosition(fromIndex);
20812080
if (seekGeneration.get() != expectedSeekGeneration || isClosed) {
20822081
return MaterializationResult.STALE;
20832082
}
2083+
// Preserve the reader position and any memory-blocked request across pending-queue gaps.
2084+
// Reopening here scans retained WAL prefixes again on every bounded replay round, and drops
2085+
// the request cached for a memory retry. advanceTo skips covered work without losing uncovered
2086+
// writer progress, including requests without a local search index.
2087+
if (Objects.nonNull(subscriptionWALIterator)) {
2088+
subscriptionWALIterator.advanceTo(fromIndex, this::isWriterProgressCoveredForWalFastForward);
2089+
}
20842090
// A pending-queue jump can span millions of WAL entries when real-time delivery has overflowed.
20852091
// Keep gap recovery within the normal per-round budget so sparse topic filtering cannot hold
20862092
// the
@@ -2223,6 +2229,9 @@ private MaterializationResult pumpFromSubscriptionWAL(
22232229
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_ERROR_READING_SUBSCRIPTION_WAL_A3888AC5,
22242230
this,
22252231
e);
2232+
// next() may have consumed a request whose materialization failed. Reusing this reader
2233+
// would lose that request, so recover from the unchanged cursor on the next round.
2234+
requestSubscriptionWalReset(nextExpectedSearchIndex.get(), expectedSeekGeneration);
22262235
break;
22272236
}
22282237
}

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

Lines changed: 180 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.junit.rules.TemporaryFolder;
5454

5555
import java.io.File;
56+
import java.io.IOException;
5657
import java.lang.reflect.Constructor;
5758
import java.lang.reflect.Field;
5859
import java.nio.ByteBuffer;
@@ -61,6 +62,8 @@
6162
import java.util.Collections;
6263
import java.util.List;
6364
import java.util.concurrent.BlockingQueue;
65+
import java.util.concurrent.atomic.AtomicBoolean;
66+
import java.util.concurrent.atomic.AtomicLong;
6467

6568
import static org.junit.Assert.assertEquals;
6669
import static org.junit.Assert.assertFalse;
@@ -332,17 +335,192 @@ public void testWalMemoryBlockRetriesWithoutRecreatingIterator() throws Exceptio
332335
}
333336
}
334337

338+
@Test
339+
public void testPendingGapReplayDoesNotRescanPrefixAcrossBoundedRounds() throws Exception {
340+
assertPendingGapReplay(false, false);
341+
}
342+
343+
@Test
344+
public void testPendingGapReplayKeepsMemoryBlockedRequestAcrossRounds() throws Exception {
345+
assertPendingGapReplay(true, false);
346+
}
347+
348+
@Test
349+
public void testPendingGapReplayRetriesRequestAfterConversionFailure() throws Exception {
350+
assertPendingGapReplay(false, true);
351+
}
352+
353+
private void assertPendingGapReplay(
354+
final boolean memoryBackpressure, final boolean materializationFailure) throws Exception {
355+
final String originalSystemDir = IoTDBDescriptor.getInstance().getConfig().getSystemDir();
356+
final CommonConfig config = CommonDescriptor.getInstance().getConfig();
357+
final int originalBatchMaxWalEntries = config.getSubscriptionConsensusBatchMaxWalEntries();
358+
final int originalBatchMaxTabletCount = config.getSubscriptionConsensusBatchMaxTabletCount();
359+
final long originalBatchMaxSize = config.getSubscriptionConsensusBatchMaxSizeInBytes();
360+
final int originalBatchMaxDelay = config.getSubscriptionConsensusBatchMaxDelayInMs();
361+
final File systemDir = temporaryFolder.newFolder("system-pending-gap-replay");
362+
final File walDirectory = temporaryFolder.newFolder("wal-pending-gap-replay");
363+
ConsensusPrefetchingQueue queue = null;
364+
try {
365+
final long oneTabletBytes = createTablet().ramBytesUsed();
366+
config.setSubscriptionConsensusBatchMaxWalEntries(2);
367+
config.setSubscriptionConsensusBatchMaxTabletCount(1);
368+
config.setSubscriptionConsensusBatchMaxSizeInBytes(oneTabletBytes * 16L);
369+
config.setSubscriptionConsensusBatchMaxDelayInMs(60_000);
370+
371+
final int replayedRequests = materializationFailure ? 7 : 6;
372+
final int rounds = memoryBackpressure ? replayedRequests : (replayedRequests + 1) / 2;
373+
final int requestCount = replayedRequests + 2 * rounds;
374+
writeSealedWal(walDirectory, requestCount);
375+
376+
final WALNode walNode = mock(WALNode.class);
377+
when(walNode.getLogDirectory()).thenReturn(walDirectory);
378+
when(walNode.getCurrentSearchIndex()).thenReturn((long) requestCount);
379+
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
380+
when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
381+
382+
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
383+
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
384+
when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new WriterSafeFrontierTracker());
385+
386+
final ConsensusLogToTabletConverter converter = mock(ConsensusLogToTabletConverter.class);
387+
final AtomicBoolean failureInjected = new AtomicBoolean();
388+
when(converter.convert(any()))
389+
.thenAnswer(
390+
invocation -> {
391+
final InsertNode insertNode = (InsertNode) invocation.getArgument(0);
392+
if (materializationFailure
393+
&& insertNode.getSearchIndex() == 2L
394+
&& failureInjected.compareAndSet(false, true)) {
395+
throw new IllegalStateException("transient conversion failure");
396+
}
397+
return Collections.singletonList(createTablet());
398+
});
399+
when(converter.getDatabaseName()).thenReturn("db");
400+
401+
final AtomicLong closedIteratorBodyReads = new AtomicLong();
402+
final DataRegionId regionId = new DataRegionId(1);
403+
queue =
404+
new ConsensusPrefetchingQueue(
405+
"consumerGroup",
406+
"topic",
407+
TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
408+
regionId,
409+
serverImpl,
410+
new SubscriptionWalRetentionPolicy(
411+
"topic",
412+
SubscriptionWalRetentionPolicy.UNBOUNDED,
413+
SubscriptionWalRetentionPolicy.UNBOUNDED),
414+
converter,
415+
newCommitManager(systemDir),
416+
new RegionProgress(Collections.emptyMap()),
417+
1L,
418+
1L,
419+
true) {
420+
@Override
421+
protected ProgressWALIterator createSubscriptionWALIterator(
422+
final long startSearchIndex) {
423+
return new ProgressWALIterator(walNode, startSearchIndex) {
424+
@Override
425+
public void close() throws IOException {
426+
closedIteratorBodyReads.addAndGet(walBodyReadCount(this));
427+
super.close();
428+
}
429+
};
430+
}
431+
};
432+
final TrackingSubscriptionMemoryManager memoryManager =
433+
new TrackingSubscriptionMemoryManager(
434+
memoryBackpressure
435+
? oneTabletBytes + Math.max(1L, oneTabletBytes / 2L)
436+
: oneTabletBytes * 16L);
437+
queue.setSubscriptionMemoryManager(memoryManager);
438+
439+
assertNull(queue.poll("consumer"));
440+
// Admission failures leave a hole before newer pending requests. Keep a suffix queued so
441+
// every bounded round enters fillGapFromWAL, including after ACK releases Tablet memory.
442+
for (long index = replayedRequests + 1L; index <= requestCount; index++) {
443+
assertTrue(pendingEntries(queue).offer(createRequest(index)));
444+
}
445+
446+
long deliveredLocalSeq = 0L;
447+
for (int round = 0; round < rounds; round++) {
448+
queue.drivePrefetchOnce();
449+
final long expectedLocalSeq =
450+
materializationFailure
451+
? round * 2L + 1L
452+
: memoryBackpressure ? round + 1L : (round + 1L) * 2L;
453+
assertEquals(expectedLocalSeq + 1L, queue.getCurrentReadSearchIndex());
454+
assertMemoryBounded(queue, memoryManager);
455+
if (memoryBackpressure) {
456+
assertTrue(subscriptionWalIterator(queue).hasBufferedNext());
457+
}
458+
while (deliveredLocalSeq < expectedLocalSeq) {
459+
final SubscriptionEvent event = queue.poll("consumer");
460+
assertNotNull(event);
461+
deliveredLocalSeq++;
462+
assertEquals(
463+
deliveredLocalSeq, event.getCommitContext().getWriterProgress().getLocalSeq());
464+
assertEquals(
465+
1000L + deliveredLocalSeq,
466+
event.getCommitContext().getWriterProgress().getPhysicalTime());
467+
assertTrue(queue.ack("consumer", event.getCommitContext()));
468+
}
469+
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
470+
}
471+
472+
assertEquals(replayedRequests, deliveredLocalSeq);
473+
assertEquals(replayedRequests, queue.getWalPathAcceptedEntries());
474+
assertEquals(0L, queue.getPendingPathAcceptedEntries());
475+
assertEquals(0L, queue.getWalGapSkippedEntries());
476+
assertEquals(0L, queue.getSubscriptionUncommittedEventCount());
477+
assertEquals(0L, queue.getRetainedTabletBytes());
478+
// Grouping reads one request ahead; a blocked request can require one further lookahead.
479+
// Count actual body reads, including discarded iterators, rather than timing disk caches.
480+
// An exceptional conversion failure needs one reset, rereading its three consumed bodies.
481+
assertEquals(
482+
replayedRequests + (memoryBackpressure ? 2L : 1L) + (materializationFailure ? 3L : 0L),
483+
closedIteratorBodyReads.get() + walBodyReadCount(subscriptionWalIterator(queue)));
484+
} finally {
485+
if (queue != null) {
486+
queue.close();
487+
}
488+
config.setSubscriptionConsensusBatchMaxWalEntries(originalBatchMaxWalEntries);
489+
config.setSubscriptionConsensusBatchMaxTabletCount(originalBatchMaxTabletCount);
490+
config.setSubscriptionConsensusBatchMaxSizeInBytes(originalBatchMaxSize);
491+
config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
492+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
493+
}
494+
}
495+
496+
private static int walBodyReadCount(final ProgressWALIterator iterator) {
497+
try {
498+
// All requests stay in one historical file, so this offset is the number of body reads.
499+
final Field field =
500+
ProgressWALIterator.class.getDeclaredField("consumedEntryCountInCurrentFile");
501+
field.setAccessible(true);
502+
return field.getInt(iterator);
503+
} catch (final ReflectiveOperationException e) {
504+
throw new AssertionError(e);
505+
}
506+
}
507+
335508
private static void writeSealedWal(final File walDirectory) throws Exception {
509+
writeSealedWal(walDirectory, REQUEST_COUNT);
510+
}
511+
512+
private static void writeSealedWal(final File walDirectory, final int requestCount)
513+
throws Exception {
336514
final File historicalWal =
337515
new File(
338516
walDirectory, WALFileUtils.getLogFileName(0L, 0L, WALFileStatus.CONTAINS_SEARCH_INDEX));
339517
final File liveWal =
340518
new File(
341519
walDirectory,
342-
WALFileUtils.getLogFileName(1L, REQUEST_COUNT, WALFileStatus.CONTAINS_SEARCH_INDEX));
520+
WALFileUtils.getLogFileName(1L, requestCount, WALFileStatus.CONTAINS_SEARCH_INDEX));
343521

344522
try (WALWriter writer = new WALWriter(historicalWal, WALFileVersion.V3)) {
345-
for (long searchIndex = 1L; searchIndex <= REQUEST_COUNT; searchIndex++) {
523+
for (long searchIndex = 1L; searchIndex <= requestCount; searchIndex++) {
346524
writeWalEntry(writer, searchIndex);
347525
}
348526
}

0 commit comments

Comments
 (0)