Skip to content

Commit 1540092

Browse files
garyrussellartembilan
authored andcommitted
SIKGH-198: Add pause/resume to Listener Containers
See: spring-attic/spring-integration-kafka#198
1 parent 6a41127 commit 1540092

File tree

7 files changed

+129
-0
lines changed

7 files changed

+129
-0
lines changed

spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ public enum AckMode {
118118

119119
private volatile boolean running = false;
120120

121+
private volatile boolean paused;
122+
121123
protected AbstractMessageListenerContainer(ContainerProperties containerProperties) {
122124
Assert.notNull(containerProperties, "'containerProperties' cannot be null");
123125

@@ -187,6 +189,10 @@ public boolean isRunning() {
187189
return this.running;
188190
}
189191

192+
protected boolean isPaused() {
193+
return this.paused;
194+
}
195+
190196
public void setPhase(int phase) {
191197
this.phase = phase;
192198
}
@@ -241,6 +247,16 @@ public void run() {
241247
}
242248
}
243249

250+
@Override
251+
public void pause() {
252+
this.paused = true;
253+
}
254+
255+
@Override
256+
public void resume() {
257+
this.paused = false;
258+
}
259+
244260
@Override
245261
public void stop(Runnable callback) {
246262
synchronized (this.lifecycleMonitor) {

spring-kafka/src/main/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainer.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ public void run() {
213213
}
214214
}
215215

216+
@Override
217+
public void pause() {
218+
super.pause();
219+
this.containers.forEach(c -> c.pause());
220+
}
221+
222+
@Override
223+
public void resume() {
224+
super.resume();
225+
this.containers.forEach(c -> c.resume());
226+
}
227+
216228
@Override
217229
public String toString() {
218230
return "ConcurrentMessageListenerContainer [concurrency=" + this.concurrency + ", beanName="

spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,8 @@ private final class ListenerConsumer implements SchedulingAwareRunnable, Consume
391391

392392
private boolean taskSchedulerExplicitlySet;
393393

394+
private boolean consumerPaused;
395+
394396
@SuppressWarnings("unchecked")
395397
ListenerConsumer(GenericMessageListener<?> listener, ListenerType listenerType) {
396398
Assert.state(!this.isAnyManualAck || !this.autoCommit,
@@ -655,7 +657,21 @@ public void run() {
655657
processCommits();
656658
}
657659
processSeeks();
660+
if (!this.consumerPaused && isPaused()) {
661+
this.consumer.pause(this.consumer.assignment());
662+
this.consumerPaused = true;
663+
if (this.logger.isDebugEnabled()) {
664+
this.logger.debug("Paused consumption from: " + this.consumer.paused());
665+
}
666+
}
658667
ConsumerRecords<K, V> records = this.consumer.poll(this.containerProperties.getPollTimeout());
668+
if (this.consumerPaused && !isPaused()) {
669+
if (this.logger.isDebugEnabled()) {
670+
this.logger.debug("Resuming consumption from: " + this.consumer.paused());
671+
}
672+
this.consumer.resume(this.consumer.paused());
673+
this.consumerPaused = false;
674+
}
659675
if (records != null && this.logger.isDebugEnabled()) {
660676
this.logger.debug("Received: " + records.count() + " records");
661677
if (records.count() > 0 && this.logger.isTraceEnabled()) {

spring-kafka/src/main/java/org/springframework/kafka/listener/MessageListenerContainer.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,20 @@ default Collection<TopicPartition> getAssignedPartitions() {
6969
throw new UnsupportedOperationException("This container doesn't support retrieving its assigned partitions");
7070
}
7171

72+
/**
73+
* Pause this container before the next poll().
74+
* @since 2.1.3
75+
*/
76+
default void pause() {
77+
throw new UnsupportedOperationException("This container doesn't support pause");
78+
}
79+
80+
/**
81+
* Resume this container, if paused, after the next poll().
82+
* @since 2.1.3
83+
*/
84+
default void resume() {
85+
throw new UnsupportedOperationException("This container doesn't support resume");
86+
}
87+
7288
}

spring-kafka/src/test/java/org/springframework/kafka/listener/KafkaMessageListenerContainerTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Arrays;
3434
import java.util.BitSet;
3535
import java.util.Collection;
36+
import java.util.Collections;
3637
import java.util.HashMap;
3738
import java.util.List;
3839
import java.util.Map;
@@ -1621,6 +1622,59 @@ public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
16211622
logger.info("Stop rebalance after failed record");
16221623
}
16231624

1625+
@SuppressWarnings({ "unchecked", "rawtypes" })
1626+
@Test
1627+
public void testPauseResume() throws Exception {
1628+
ConsumerFactory<Integer, String> cf = mock(ConsumerFactory.class);
1629+
Consumer<Integer, String> consumer = mock(Consumer.class);
1630+
given(cf.createConsumer(isNull(), eq("clientId"), isNull())).willReturn(consumer);
1631+
final Map<TopicPartition, List<ConsumerRecord<Integer, String>>> records = new HashMap<>();
1632+
records.put(new TopicPartition("foo", 0), Arrays.asList(
1633+
new ConsumerRecord<>("foo", 0, 0L, 1, "foo"),
1634+
new ConsumerRecord<>("foo", 0, 1L, 1, "bar")));
1635+
ConsumerRecords<Integer, String> consumerRecords = new ConsumerRecords<>(records);
1636+
ConsumerRecords<Integer, String> emptyRecords = new ConsumerRecords<>(Collections.emptyMap());
1637+
AtomicBoolean first = new AtomicBoolean(true);
1638+
given(consumer.poll(anyLong())).willAnswer(i -> {
1639+
Thread.sleep(50);
1640+
return first.getAndSet(false) ? consumerRecords : emptyRecords;
1641+
});
1642+
final CountDownLatch commitLatch = new CountDownLatch(2);
1643+
willAnswer(i -> {
1644+
commitLatch.countDown();
1645+
return null;
1646+
}).given(consumer).commitSync(any(Map.class));
1647+
given(consumer.assignment()).willReturn(records.keySet());
1648+
final CountDownLatch pauseLatch = new CountDownLatch(1);
1649+
willAnswer(i -> {
1650+
pauseLatch.countDown();
1651+
return null;
1652+
}).given(consumer).pause(records.keySet());
1653+
given(consumer.paused()).willReturn(records.keySet());
1654+
final CountDownLatch resumeLatch = new CountDownLatch(1);
1655+
willAnswer(i -> {
1656+
resumeLatch.countDown();
1657+
return null;
1658+
}).given(consumer).resume(records.keySet());
1659+
TopicPartitionInitialOffset[] topicPartition = new TopicPartitionInitialOffset[] {
1660+
new TopicPartitionInitialOffset("foo", 0) };
1661+
ContainerProperties containerProps = new ContainerProperties(topicPartition);
1662+
containerProps.setAckMode(AckMode.RECORD);
1663+
containerProps.setClientId("clientId");
1664+
containerProps.setIdleEventInterval(100L);
1665+
containerProps.setMessageListener((MessageListener) r -> { });
1666+
KafkaMessageListenerContainer<Integer, String> container =
1667+
new KafkaMessageListenerContainer<>(cf, containerProps);
1668+
container.start();
1669+
assertThat(commitLatch.await(10, TimeUnit.SECONDS)).isTrue();
1670+
verify(consumer, times(2)).commitSync(any(Map.class));
1671+
container.pause();
1672+
assertThat(pauseLatch.await(10, TimeUnit.SECONDS)).isTrue();
1673+
container.resume();
1674+
assertThat(resumeLatch.await(10, TimeUnit.SECONDS)).isTrue();
1675+
container.stop();
1676+
}
1677+
16241678
private Consumer<?, ?> spyOnConsumer(KafkaMessageListenerContainer<Integer, String> container) {
16251679
Consumer<?, ?> consumer = spy(
16261680
KafkaTestUtils.getPropertyValue(container, "listenerConsumer.consumer", Consumer.class));

src/reference/asciidoc/kafka.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,16 @@ You can also perform seek operations from `onIdleContainer()` when an idle conta
13691369

13701370
To arbitrarily seek at runtime, use the callback reference from the `registerSeekCallback` for the appropriate thread.
13711371

1372+
[[pause-resume]]
1373+
==== Pausing/Resuming Listener Containers
1374+
1375+
_Version 2.1.3_ added `pause()` and `resume()` methods to listener containers.
1376+
Previously, you could pause a consumer within a `ConsumerAwareMessageListener` and resume it by listening for `ListenerContainerIdleEvent` s, which provide access to the `Consumer` object.
1377+
While you could pause a consumer in an idle container via an event listener, in some cases this was not thread-safe since there is no guarantee that the event listener is invoked on the consumer thread.
1378+
To safely pause/resume consumers, you should use the methods on the listener containers.
1379+
`pause()` takes effect just before the next `poll()`; `resume` takes effect, just after the current `poll()` returns.
1380+
When a container is paused, it continues to `poll()` the consumer, avoiding a rebalance if group management is being used, but will not retrieve any records; refer to the Kafka documentation for more information.
1381+
13721382
[[serdes]]
13731383
==== Serialization/Deserialization and Message Conversion
13741384

src/reference/asciidoc/whats-new.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ See <<serdes>> for more information.
1515
Container Error handlers are now provided for both record and batch listeners that treat any exceptions thrown by the listener as fatal; they stop the container.
1616
See <<annotation-error-handling>> for more information.
1717

18+
==== Pausing/Resuming Containers
19+
20+
The listener containers now have `pause()` and `resume()` methods (since _version 2.1.3_).
21+
See <<pause-resume>> for more information.
22+
1823
==== Stateful Retry
1924

2025
Starting with _version 2.1.3_, stateful retry can be configured; see <<stateful-retry>> for more information.

0 commit comments

Comments
 (0)