|
23 | 23 |
|
24 | 24 | import com.rabbitmq.client.Connection;
|
25 | 25 | import com.rabbitmq.client.ConnectionFactory;
|
| 26 | +import com.rabbitmq.stream.Constants; |
26 | 27 | import com.rabbitmq.stream.Consumer;
|
| 28 | +import com.rabbitmq.stream.ConsumerUpdateListener; |
27 | 29 | import com.rabbitmq.stream.ConsumerUpdateListener.Status;
|
28 | 30 | import com.rabbitmq.stream.Environment;
|
29 | 31 | import com.rabbitmq.stream.EnvironmentBuilder;
|
30 | 32 | import com.rabbitmq.stream.OffsetSpecification;
|
| 33 | +import com.rabbitmq.stream.StreamException; |
31 | 34 | import com.rabbitmq.stream.impl.Client.ClientParameters;
|
| 35 | +import com.rabbitmq.stream.impl.TestUtils.CallableBooleanSupplier; |
32 | 36 | import com.rabbitmq.stream.impl.TestUtils.SingleActiveConsumer;
|
33 | 37 | import com.rabbitmq.stream.impl.Utils.CompositeConsumerUpdateListener;
|
34 | 38 | import io.netty.channel.EventLoopGroup;
|
@@ -83,6 +87,30 @@ private static void publishToPartitions(
|
83 | 87 | latchAssert(publishLatch).completes();
|
84 | 88 | }
|
85 | 89 |
|
| 90 | + private static void waitUntil(CallableBooleanSupplier action) { |
| 91 | + try { |
| 92 | + waitAtMost(action); |
| 93 | + } catch (Exception e) { |
| 94 | + throw new RuntimeException(e); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private static OffsetSpecification lastStoredOffset( |
| 99 | + Consumer consumer, OffsetSpecification defaultValue) { |
| 100 | + OffsetSpecification offsetSpecification; |
| 101 | + try { |
| 102 | + long storedOffset = consumer.storedOffset(); |
| 103 | + offsetSpecification = OffsetSpecification.offset(storedOffset); |
| 104 | + } catch (StreamException e) { |
| 105 | + if (e.getCode() == Constants.RESPONSE_CODE_NO_OFFSET) { |
| 106 | + offsetSpecification = defaultValue; |
| 107 | + } else { |
| 108 | + throw e; |
| 109 | + } |
| 110 | + } |
| 111 | + return offsetSpecification; |
| 112 | + } |
| 113 | + |
86 | 114 | @BeforeEach
|
87 | 115 | void init(TestInfo info) throws Exception {
|
88 | 116 | EnvironmentBuilder environmentBuilder = Environment.builder().eventLoopGroup(eventLoopGroup);
|
@@ -521,4 +549,216 @@ void sacAutoOffsetTrackingShouldStoreOnRelanbancing() throws Exception {
|
521 | 549 | .isGreaterThanOrEqualTo(expectedMessageCountPerPartition)
|
522 | 550 | .isEqualTo(c.queryOffset(consumerName, partition).getOffset()));
|
523 | 551 | }
|
| 552 | + |
| 553 | + @Test |
| 554 | + @SingleActiveConsumer |
| 555 | + void sacManualOffsetTrackingShouldTakeOverOnRelanbancing() throws Exception { |
| 556 | + declareSuperStreamTopology(connection, superStream, partitionCount); |
| 557 | + int messageCount = 5_000; |
| 558 | + int storeEvery = messageCount / 100; |
| 559 | + AtomicInteger messageWaveCount = new AtomicInteger(); |
| 560 | + int superConsumerCount = 3; |
| 561 | + List<String> partitions = |
| 562 | + IntStream.range(0, partitionCount) |
| 563 | + .mapToObj(i -> superStream + "-" + i) |
| 564 | + .collect(Collectors.toList()); |
| 565 | + Map<String, Boolean> consumerStates = |
| 566 | + new ConcurrentHashMap<>(partitionCount * superConsumerCount); |
| 567 | + Map<String, AtomicInteger> receivedMessages = |
| 568 | + new ConcurrentHashMap<>(partitionCount * superConsumerCount); |
| 569 | + Map<String, AtomicInteger> receivedMessagesPerPartitions = |
| 570 | + new ConcurrentHashMap<>(partitionCount); |
| 571 | + Map<String, Map<String, Consumer>> consumers = new ConcurrentHashMap<>(superConsumerCount); |
| 572 | + IntStream.range(0, superConsumerCount) |
| 573 | + .mapToObj(String::valueOf) |
| 574 | + .forEach( |
| 575 | + consumer -> |
| 576 | + partitions.forEach( |
| 577 | + partition -> { |
| 578 | + consumers.put(consumer, new ConcurrentHashMap<>(partitionCount)); |
| 579 | + consumerStates.put(consumer + partition, false); |
| 580 | + receivedMessages.put(consumer + partition, new AtomicInteger(0)); |
| 581 | + })); |
| 582 | + Map<String, Long> lastReceivedOffsets = new ConcurrentHashMap<>(); |
| 583 | + partitions.forEach( |
| 584 | + partition -> { |
| 585 | + lastReceivedOffsets.put(partition, 0L); |
| 586 | + receivedMessagesPerPartitions.put(partition, new AtomicInteger(0)); |
| 587 | + }); |
| 588 | + Runnable publishOnAllPartitions = |
| 589 | + () -> { |
| 590 | + partitions.forEach( |
| 591 | + partition -> TestUtils.publishAndWaitForConfirms(cf, messageCount, partition)); |
| 592 | + messageWaveCount.incrementAndGet(); |
| 593 | + }; |
| 594 | + String consumerName = "my-app"; |
| 595 | + |
| 596 | + OffsetSpecification initialOffsetSpecification = OffsetSpecification.first(); |
| 597 | + Function<String, Consumer> consumerCreator = |
| 598 | + consumer -> { |
| 599 | + AtomicInteger received = new AtomicInteger(); |
| 600 | + ConsumerUpdateListener consumerUpdateListener = |
| 601 | + context -> { |
| 602 | + consumers.get(consumer).putIfAbsent(context.stream(), context.consumer()); |
| 603 | + consumerStates.put(consumer + context.stream(), context.status() == Status.ACTIVE); |
| 604 | + OffsetSpecification offsetSpecification = null; |
| 605 | + if (context.status() == Status.ACTIVE) { |
| 606 | + try { |
| 607 | + long storedOffset = context.consumer().storedOffset() + 1; |
| 608 | + offsetSpecification = OffsetSpecification.offset(storedOffset); |
| 609 | + } catch (StreamException e) { |
| 610 | + if (e.getCode() == Constants.RESPONSE_CODE_NO_OFFSET) { |
| 611 | + offsetSpecification = initialOffsetSpecification; |
| 612 | + } else { |
| 613 | + throw e; |
| 614 | + } |
| 615 | + } |
| 616 | + } else if (context.previousStatus() == Status.ACTIVE |
| 617 | + && context.status() == Status.PASSIVE) { |
| 618 | + long lastReceivedOffset = lastReceivedOffsets.get(context.stream()); |
| 619 | + context.consumer().store(lastReceivedOffset); |
| 620 | + waitUntil(() -> context.consumer().storedOffset() == lastReceivedOffset); |
| 621 | + } |
| 622 | + return offsetSpecification; |
| 623 | + }; |
| 624 | + return environment |
| 625 | + .consumerBuilder() |
| 626 | + .singleActiveConsumer() |
| 627 | + .superStream(superStream) |
| 628 | + .offset(initialOffsetSpecification) |
| 629 | + .name(consumerName) |
| 630 | + .autoTrackingStrategy() |
| 631 | + .builder() |
| 632 | + .consumerUpdateListener(consumerUpdateListener) |
| 633 | + .messageHandler( |
| 634 | + (context, message) -> { |
| 635 | + if (received.incrementAndGet() % storeEvery == 0) { |
| 636 | + context.storeOffset(); |
| 637 | + } |
| 638 | + lastReceivedOffsets.put(context.stream(), context.offset()); |
| 639 | + receivedMessagesPerPartitions.get(context.stream()).incrementAndGet(); |
| 640 | + receivedMessages.get(consumer + context.stream()).incrementAndGet(); |
| 641 | + }) |
| 642 | + .build(); |
| 643 | + }; |
| 644 | + |
| 645 | + Consumer consumer0 = consumerCreator.apply("0"); |
| 646 | + waitAtMost( |
| 647 | + () -> |
| 648 | + consumerStates.get("0" + partitions.get(0)) |
| 649 | + && consumerStates.get("0" + partitions.get(1)) |
| 650 | + && consumerStates.get("0" + partitions.get(2))); |
| 651 | + |
| 652 | + publishOnAllPartitions.run(); |
| 653 | + |
| 654 | + waitAtMost( |
| 655 | + () -> |
| 656 | + receivedMessages.get("0" + partitions.get(0)).get() == messageCount |
| 657 | + && receivedMessages.get("0" + partitions.get(1)).get() == messageCount |
| 658 | + && receivedMessages.get("0" + partitions.get(2)).get() == messageCount); |
| 659 | + |
| 660 | + Consumer consumer1 = consumerCreator.apply("1"); |
| 661 | + |
| 662 | + waitAtMost( |
| 663 | + () -> |
| 664 | + consumerStates.get("0" + partitions.get(0)) |
| 665 | + && consumerStates.get("1" + partitions.get(1)) |
| 666 | + && consumerStates.get("0" + partitions.get(2))); |
| 667 | + |
| 668 | + publishOnAllPartitions.run(); |
| 669 | + |
| 670 | + waitAtMost( |
| 671 | + () -> |
| 672 | + receivedMessages.get("0" + partitions.get(0)).get() == messageCount * 2 |
| 673 | + && receivedMessages.get("1" + partitions.get(1)).get() == messageCount |
| 674 | + && receivedMessages.get("0" + partitions.get(2)).get() == messageCount * 2); |
| 675 | + |
| 676 | + Consumer consumer2 = consumerCreator.apply("2"); |
| 677 | + |
| 678 | + waitAtMost( |
| 679 | + () -> |
| 680 | + consumerStates.get("0" + partitions.get(0)) |
| 681 | + && consumerStates.get("1" + partitions.get(1)) |
| 682 | + && consumerStates.get("2" + partitions.get(2))); |
| 683 | + |
| 684 | + publishOnAllPartitions.run(); |
| 685 | + |
| 686 | + waitAtMost( |
| 687 | + () -> |
| 688 | + receivedMessages.get("0" + partitions.get(0)).get() == messageCount * 3 |
| 689 | + && receivedMessages.get("1" + partitions.get(1)).get() == messageCount * 2 |
| 690 | + && receivedMessages.get("2" + partitions.get(2)).get() == messageCount); |
| 691 | + |
| 692 | + java.util.function.Consumer<String> storeLastProcessedOffsets = |
| 693 | + consumerIndex -> { |
| 694 | + consumers.get(consumerIndex).entrySet().stream() |
| 695 | + .filter( |
| 696 | + partitionToInnerConsumer -> { |
| 697 | + // we take only the inner consumers that are active |
| 698 | + return consumerStates.get(consumerIndex + partitionToInnerConsumer.getKey()); |
| 699 | + }) |
| 700 | + .forEach( |
| 701 | + entry -> { |
| 702 | + String partition = entry.getKey(); |
| 703 | + Consumer consumer = entry.getValue(); |
| 704 | + long offset = lastReceivedOffsets.get(partition); |
| 705 | + consumer.store(offset); |
| 706 | + waitUntil(() -> consumer.storedOffset() == offset); |
| 707 | + }); |
| 708 | + }; |
| 709 | + storeLastProcessedOffsets.accept("0"); |
| 710 | + consumer0.close(); |
| 711 | + |
| 712 | + waitAtMost( |
| 713 | + () -> |
| 714 | + consumerStates.get("1" + partitions.get(0)) |
| 715 | + && consumerStates.get("2" + partitions.get(1)) |
| 716 | + && consumerStates.get("1" + partitions.get(2))); |
| 717 | + |
| 718 | + publishOnAllPartitions.run(); |
| 719 | + |
| 720 | + waitAtMost( |
| 721 | + () -> |
| 722 | + receivedMessages.get("1" + partitions.get(0)).get() == messageCount |
| 723 | + && receivedMessages.get("2" + partitions.get(1)).get() == messageCount |
| 724 | + && receivedMessages.get("1" + partitions.get(2)).get() == messageCount); |
| 725 | + |
| 726 | + storeLastProcessedOffsets.accept("1"); |
| 727 | + consumer1.close(); |
| 728 | + |
| 729 | + waitAtMost( |
| 730 | + () -> |
| 731 | + consumerStates.get("2" + partitions.get(0)) |
| 732 | + && consumerStates.get("2" + partitions.get(1)) |
| 733 | + && consumerStates.get("2" + partitions.get(2))); |
| 734 | + |
| 735 | + publishOnAllPartitions.run(); |
| 736 | + |
| 737 | + waitAtMost( |
| 738 | + () -> |
| 739 | + receivedMessages.get("2" + partitions.get(0)).get() == messageCount |
| 740 | + && receivedMessages.get("2" + partitions.get(1)).get() == messageCount * 2 |
| 741 | + && receivedMessages.get("2" + partitions.get(2)).get() == messageCount * 2); |
| 742 | + |
| 743 | + storeLastProcessedOffsets.accept("2"); |
| 744 | + consumer2.close(); |
| 745 | + |
| 746 | + assertThat(messageWaveCount).hasValue(5); |
| 747 | + assertThat( |
| 748 | + receivedMessages.values().stream() |
| 749 | + .map(AtomicInteger::get) |
| 750 | + .mapToInt(Integer::intValue) |
| 751 | + .sum()) |
| 752 | + .isEqualTo(messageCount * partitionCount * 5); |
| 753 | + int expectedMessageCountPerPartition = messageCount * messageWaveCount.get(); |
| 754 | + receivedMessagesPerPartitions |
| 755 | + .values() |
| 756 | + .forEach(v -> assertThat(v).hasValue(expectedMessageCountPerPartition)); |
| 757 | + Client c = cf.get(); |
| 758 | + partitions.forEach( |
| 759 | + partition -> |
| 760 | + assertThat(lastReceivedOffsets.get(partition)) |
| 761 | + .isGreaterThanOrEqualTo(expectedMessageCountPerPartition) |
| 762 | + .isEqualTo(c.queryOffset(consumerName, partition).getOffset())); |
| 763 | + } |
524 | 764 | }
|
0 commit comments