|
53 | 53 | import org.junit.rules.TemporaryFolder; |
54 | 54 |
|
55 | 55 | import java.io.File; |
| 56 | +import java.io.IOException; |
56 | 57 | import java.lang.reflect.Constructor; |
57 | 58 | import java.lang.reflect.Field; |
58 | 59 | import java.nio.ByteBuffer; |
|
61 | 62 | import java.util.Collections; |
62 | 63 | import java.util.List; |
63 | 64 | import java.util.concurrent.BlockingQueue; |
| 65 | +import java.util.concurrent.atomic.AtomicBoolean; |
| 66 | +import java.util.concurrent.atomic.AtomicLong; |
64 | 67 |
|
65 | 68 | import static org.junit.Assert.assertEquals; |
66 | 69 | import static org.junit.Assert.assertFalse; |
@@ -332,17 +335,192 @@ public void testWalMemoryBlockRetriesWithoutRecreatingIterator() throws Exceptio |
332 | 335 | } |
333 | 336 | } |
334 | 337 |
|
| 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 | + |
335 | 508 | 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 { |
336 | 514 | final File historicalWal = |
337 | 515 | new File( |
338 | 516 | walDirectory, WALFileUtils.getLogFileName(0L, 0L, WALFileStatus.CONTAINS_SEARCH_INDEX)); |
339 | 517 | final File liveWal = |
340 | 518 | new File( |
341 | 519 | walDirectory, |
342 | | - WALFileUtils.getLogFileName(1L, REQUEST_COUNT, WALFileStatus.CONTAINS_SEARCH_INDEX)); |
| 520 | + WALFileUtils.getLogFileName(1L, requestCount, WALFileStatus.CONTAINS_SEARCH_INDEX)); |
343 | 521 |
|
344 | 522 | 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++) { |
346 | 524 | writeWalEntry(writer, searchIndex); |
347 | 525 | } |
348 | 526 | } |
|
0 commit comments