|
32 | 32 | import java.util.Iterator;
|
33 | 33 | import java.util.List;
|
34 | 34 | import java.util.Map;
|
| 35 | +import java.util.Set; |
| 36 | +import java.util.concurrent.ConcurrentHashMap; |
35 | 37 | import java.util.concurrent.CountDownLatch;
|
36 | 38 | import java.util.concurrent.TimeUnit;
|
37 | 39 | import java.util.concurrent.atomic.AtomicBoolean;
|
|
76 | 78 | import org.springframework.kafka.core.KafkaTemplate;
|
77 | 79 | import org.springframework.kafka.core.ProducerFactory;
|
78 | 80 | import org.springframework.kafka.event.ListenerContainerIdleEvent;
|
| 81 | +import org.springframework.kafka.listener.AbstractConsumerSeekAware; |
79 | 82 | import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
|
80 | 83 | import org.springframework.kafka.listener.ConsumerAwareErrorHandler;
|
81 | 84 | import org.springframework.kafka.listener.ConsumerAwareListenerErrorHandler;
|
@@ -152,7 +155,7 @@ public class EnableKafkaIntegrationTests {
|
152 | 155 | "annotated22reply", "annotated23", "annotated23reply", "annotated24", "annotated24reply",
|
153 | 156 | "annotated25", "annotated25reply1", "annotated25reply2", "annotated26", "annotated27", "annotated28",
|
154 | 157 | "annotated29", "annotated30", "annotated30reply", "annotated31", "annotated32", "annotated33",
|
155 |
| - "annotated34", "annotated35", "annotated36", "annotated37", "foo", "manualStart"); |
| 158 | + "annotated34", "annotated35", "annotated36", "annotated37", "foo", "manualStart", "seekOnIdle"); |
156 | 159 |
|
157 | 160 | private static EmbeddedKafkaBroker embeddedKafka = embeddedKafkaRule.getEmbeddedKafka();
|
158 | 161 |
|
@@ -201,6 +204,9 @@ public class EnableKafkaIntegrationTests {
|
201 | 204 | @Autowired
|
202 | 205 | private ConcurrentKafkaListenerContainerFactory<Integer, String> transactionalFactory;
|
203 | 206 |
|
| 207 | + @Autowired |
| 208 | + private SeekToLastOnIdleListener seekOnIdleListener; |
| 209 | + |
204 | 210 | @Test
|
205 | 211 | public void testAnonymous() {
|
206 | 212 | MessageListenerContainer container = this.registry
|
@@ -757,6 +763,24 @@ public void testProjection() throws InterruptedException {
|
757 | 763 | assertThat(this.listener.username).isEqualTo("SomeUsername");
|
758 | 764 | }
|
759 | 765 |
|
| 766 | + @SuppressWarnings("unchecked") |
| 767 | + @Test |
| 768 | + public void testSeekToLastOnIdle() throws InterruptedException { |
| 769 | + this.registry.getListenerContainer("seekOnIdle").start(); |
| 770 | + this.seekOnIdleListener.waitForBalancedAssignment(); |
| 771 | + this.template.send("seekOnIdle", 0, 0, "foo"); |
| 772 | + this.template.send("seekOnIdle", 1, 1, "bar"); |
| 773 | + assertThat(this.seekOnIdleListener.latch1.await(10, TimeUnit.SECONDS)).isTrue(); |
| 774 | + assertThat(this.seekOnIdleListener.latch2.getCount()).isEqualTo(2L); |
| 775 | + this.seekOnIdleListener.rewindAllOneRecord(); |
| 776 | + assertThat(this.seekOnIdleListener.latch2.await(10, TimeUnit.SECONDS)).isTrue(); |
| 777 | + assertThat(this.seekOnIdleListener.latch3.getCount()).isEqualTo(1L); |
| 778 | + this.seekOnIdleListener.rewindOnePartitionOneRecord("seekOnIdle", 1); |
| 779 | + assertThat(this.seekOnIdleListener.latch3.await(10, TimeUnit.SECONDS)).isTrue(); |
| 780 | + this.registry.getListenerContainer("seekOnIdle").stop(); |
| 781 | + assertThat(KafkaTestUtils.getPropertyValue(this.seekOnIdleListener, "callbacks", Map.class)).hasSize(0); |
| 782 | + } |
| 783 | + |
760 | 784 | @Configuration
|
761 | 785 | @EnableKafka
|
762 | 786 | @EnableTransactionManagement(proxyTargetClass = true)
|
@@ -933,6 +957,7 @@ public KafkaListenerContainerFactory<?> batchSpyFactory() {
|
933 | 957 | factory.setRecordFilterStrategy(recordFilter());
|
934 | 958 | // always send to the same partition so the replies are in order for the test
|
935 | 959 | factory.setReplyTemplate(partitionZeroReplyingTemplate());
|
| 960 | + factory.setMissingTopicsFatal(false); |
936 | 961 | return factory;
|
937 | 962 | }
|
938 | 963 |
|
@@ -968,6 +993,7 @@ public KafkaListenerContainerFactory<?> batchManualFactory2() {
|
968 | 993 | ContainerProperties props = factory.getContainerProperties();
|
969 | 994 | props.setAckMode(AckMode.MANUAL_IMMEDIATE);
|
970 | 995 | props.setIdleEventInterval(100L);
|
| 996 | + props.setPollTimeout(50L); |
971 | 997 | factory.setRecordFilterStrategy(manualFilter());
|
972 | 998 | factory.setAckDiscarded(true);
|
973 | 999 | factory.setRetryTemplate(new RetryTemplate());
|
@@ -1050,6 +1076,11 @@ public Listener listener() {
|
1050 | 1076 | return new Listener();
|
1051 | 1077 | }
|
1052 | 1078 |
|
| 1079 | + @Bean |
| 1080 | + public SeekToLastOnIdleListener seekOnIdle() { |
| 1081 | + return new SeekToLastOnIdleListener(); |
| 1082 | + } |
| 1083 | + |
1053 | 1084 | @Bean
|
1054 | 1085 | public IfaceListener<String> ifaceListener() {
|
1055 | 1086 | return new IfaceListenerImpl();
|
@@ -1698,6 +1729,68 @@ public void registerSeekCallback(ConsumerSeekCallback callback) {
|
1698 | 1729 |
|
1699 | 1730 | }
|
1700 | 1731 |
|
| 1732 | + public static class SeekToLastOnIdleListener extends AbstractConsumerSeekAware { |
| 1733 | + |
| 1734 | + private final CountDownLatch latch1 = new CountDownLatch(10); |
| 1735 | + |
| 1736 | + private final CountDownLatch latch2 = new CountDownLatch(12); |
| 1737 | + |
| 1738 | + private final CountDownLatch latch3 = new CountDownLatch(13); |
| 1739 | + |
| 1740 | + private final Set<Thread> consumerThreads = ConcurrentHashMap.newKeySet(); |
| 1741 | + |
| 1742 | + @KafkaListener(id = "seekOnIdle", topics = "seekOnIdle", autoStartup = "false", concurrency = "2", |
| 1743 | + clientIdPrefix = "seekOnIdle", containerFactory = "kafkaManualAckListenerContainerFactory") |
| 1744 | + public void listen(@SuppressWarnings("unused") String in, Acknowledgment ack) { |
| 1745 | + this.latch1.countDown(); |
| 1746 | + this.latch2.countDown(); |
| 1747 | + this.latch3.countDown(); |
| 1748 | + ack.acknowledge(); |
| 1749 | + } |
| 1750 | + |
| 1751 | + @Override |
| 1752 | + public void onIdleContainer(Map<org.apache.kafka.common.TopicPartition, Long> assignments, |
| 1753 | + ConsumerSeekCallback callback) { |
| 1754 | + |
| 1755 | + if (this.latch1.getCount() > 0) { |
| 1756 | + assignments.keySet().forEach(tp -> callback.seekRelative(tp.topic(), tp.partition(), -1, true)); |
| 1757 | + } |
| 1758 | + } |
| 1759 | + |
| 1760 | + public void rewindAllOneRecord() { |
| 1761 | + getSeekCallbacks() |
| 1762 | + .forEach((tp, callback) -> |
| 1763 | + callback.seekRelative(tp.topic(), tp.partition(), -1, true)); |
| 1764 | + } |
| 1765 | + |
| 1766 | + public void rewindOnePartitionOneRecord(String topic, int partition) { |
| 1767 | + getSeekCallbackFor(new org.apache.kafka.common.TopicPartition(topic, partition)) |
| 1768 | + .seekRelative(topic, partition, -1, true); |
| 1769 | + } |
| 1770 | + |
| 1771 | + @Override |
| 1772 | + public synchronized void onPartitionsAssigned(Map<org.apache.kafka.common.TopicPartition, Long> assignments, |
| 1773 | + ConsumerSeekCallback callback) { |
| 1774 | + |
| 1775 | + super.onPartitionsAssigned(assignments, callback); |
| 1776 | + if (assignments.size() > 0) { |
| 1777 | + this.consumerThreads.add(Thread.currentThread()); |
| 1778 | + notifyAll(); |
| 1779 | + } |
| 1780 | + } |
| 1781 | + |
| 1782 | + public synchronized void waitForBalancedAssignment() throws InterruptedException { |
| 1783 | + int n = 0; |
| 1784 | + while (this.consumerThreads.size() < 2) { |
| 1785 | + wait(1000); |
| 1786 | + if (n++ > 20) { |
| 1787 | + throw new IllegalStateException("Balanced distribution did not occur"); |
| 1788 | + } |
| 1789 | + } |
| 1790 | + } |
| 1791 | + |
| 1792 | + } |
| 1793 | + |
1701 | 1794 | interface IfaceListener<T> {
|
1702 | 1795 |
|
1703 | 1796 | void listen(T foo);
|
|
0 commit comments