GH-99 Idle Container Events

Resolves #99

Also change `ContainerProperties` to use accessors

Also fix `stop(Runnable callback` logic for the container registry and
concurrent container.

* Polishing according PR comments and some typos fixes
This commit is contained in:
Gary Russell
2016-06-01 11:51:08 -04:00
committed by Artem Bilan
parent 31541fe58e
commit eff22d0922
11 changed files with 430 additions and 101 deletions

View File

@@ -18,6 +18,8 @@ package org.springframework.kafka.config;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.listener.AbstractMessageListenerContainer;
import org.springframework.kafka.listener.adapter.DeDuplicationStrategy;
@@ -32,11 +34,12 @@ import org.springframework.kafka.support.converter.MessageConverter;
* @param <V> the value type.
*
* @author Stephane Nicoll
* @author Gary Russell
*
* @see AbstractMessageListenerContainer
*/
public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMessageListenerContainer<K, V>, K, V>
implements KafkaListenerContainerFactory<C> {
implements KafkaListenerContainerFactory<C>, ApplicationEventPublisherAware {
private final ContainerProperties containerProperties = new ContainerProperties("propertiesFactory");
@@ -50,6 +53,8 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
private DeDuplicationStrategy<K, V> deDuplicationStrategy;
private ApplicationEventPublisher applicationEventPublisher;
/**
* Specify a {@link ConsumerFactory} to use.
* @param consumerFactory The consumer factory.
@@ -96,6 +101,11 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
this.deDuplicationStrategy = deDuplicationStrategy;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Obtain the properties template for this factory - set properties as needed
* and they will be copied to a final properties instance for the endpoint.
@@ -116,6 +126,9 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
if (this.phase != null) {
instance.setPhase(this.phase);
}
if (this.applicationEventPublisher != null) {
instance.setApplicationEventPublisher(this.applicationEventPublisher);
}
if (endpoint.getId() != null) {
instance.setBeanName(endpoint.getId());
}

View File

@@ -249,7 +249,12 @@ public class KafkaListenerEndpointRegistry implements DisposableBean, SmartLifec
Collection<MessageListenerContainer> listenerContainers = getListenerContainers();
AggregatingCallback aggregatingCallback = new AggregatingCallback(listenerContainers.size(), callback);
for (MessageListenerContainer listenerContainer : listenerContainers) {
listenerContainer.stop(aggregatingCallback);
if (listenerContainer.isRunning()) {
listenerContainer.stop(aggregatingCallback);
}
else {
aggregatingCallback.run();
}
}
}
@@ -297,7 +302,7 @@ public class KafkaListenerEndpointRegistry implements DisposableBean, SmartLifec
@Override
public void run() {
if (this.count.decrementAndGet() == 0) {
if (this.count.decrementAndGet() <= 0) {
this.finishCallback.run();
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.kafka.event;
import org.springframework.context.ApplicationEvent;
/**
* Base class for events.
*
* @author Gary Russell
*
*/
@SuppressWarnings("serial")
public abstract class KafkaEvent extends ApplicationEvent {
public KafkaEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.kafka.event;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.kafka.common.TopicPartition;
/**
* An event that is emitted when a container is idle if the container
* is configured to do so.
*
* @author Gary Russell
*
*/
@SuppressWarnings("serial")
public class ListenerContainerIdleEvent extends KafkaEvent {
private final long idleTime;
private final String listenerId;
private final List<TopicPartition> topicPartitions;
public ListenerContainerIdleEvent(Object source, long idleTime, String id,
Collection<TopicPartition> topicPartitions) {
super(source);
this.idleTime = idleTime;
this.listenerId = id;
this.topicPartitions = new ArrayList<>(topicPartitions);
}
/**
* How long the container has been idle.
* @return the time in milliseconds.
*/
public long getIdleTime() {
return this.idleTime;
}
/**
* The TopicPartitions the container is listening to.
* @return the TopicPartition list.
*/
public Collection<TopicPartition> getTopicPartitions() {
return Collections.unmodifiableList(this.topicPartitions);
}
/**
* The id of the listener (if {@code @RabbitListener}) or the container bean name.
* @return the id.
*/
public String getListenerId() {
return this.listenerId;
}
@Override
public String toString() {
return "ListenerContainerIdleEvent [idleTime="
+ ((float) this.idleTime / 1000) + "s, listenerId=" + this.listenerId
+ ", container=" + getSource()
+ ", topicPartitions=" + this.topicPartitions + "]";
}
}

View File

@@ -0,0 +1,4 @@
/**
* Application Events.
*/
package org.springframework.kafka.event;

View File

@@ -28,6 +28,8 @@ import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.TopicPartition;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.kafka.listener.config.ContainerProperties;
import org.springframework.retry.RecoveryCallback;
@@ -44,7 +46,7 @@ import org.springframework.util.Assert;
* @author Marius Bogoevici
*/
public abstract class AbstractMessageListenerContainer<K, V>
implements MessageListenerContainer, BeanNameAware, SmartLifecycle {
implements MessageListenerContainer, BeanNameAware, ApplicationEventPublisherAware, SmartLifecycle {
protected final Log logger = LogFactory.getLog(this.getClass()); // NOSONAR
@@ -107,6 +109,8 @@ public abstract class AbstractMessageListenerContainer<K, V>
private String beanName;
private ApplicationEventPublisher applicationEventPublisher;
private boolean autoStartup = true;
private int phase = 0;
@@ -116,8 +120,8 @@ public abstract class AbstractMessageListenerContainer<K, V>
protected AbstractMessageListenerContainer(ContainerProperties containerProperties) {
Assert.notNull(containerProperties, "'containerProperties' cannot be null");
this.containerProperties = containerProperties;
if (containerProperties.consumerRebalanceListener == null) {
containerProperties.consumerRebalanceListener = createConsumerRebalanceListener();
if (containerProperties.getConsumerRebalanceListener() == null) {
containerProperties.setConsumerRebalanceListener(createConsumerRebalanceListener());
}
}
@@ -130,6 +134,15 @@ public abstract class AbstractMessageListenerContainer<K, V>
return this.beanName;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public ApplicationEventPublisher getApplicationEventPublisher() {
return this.applicationEventPublisher;
}
@Override
public boolean isAutoStartup() {
return this.autoStartup;
@@ -163,28 +176,28 @@ public abstract class AbstractMessageListenerContainer<K, V>
@Override
public void setupMessageListener(Object messageListener) {
this.containerProperties.messageListener = messageListener;
this.containerProperties.setMessageListener(messageListener);
}
@Override
public final void start() {
synchronized (this.lifecycleMonitor) {
Assert.isTrue(
this.containerProperties.messageListener instanceof MessageListener
|| this.containerProperties.messageListener instanceof AcknowledgingMessageListener,
this.containerProperties.getMessageListener() instanceof MessageListener
|| this.containerProperties.getMessageListener() instanceof AcknowledgingMessageListener,
"Either a " + MessageListener.class.getName() + " or a "
+ AcknowledgingMessageListener.class.getName() + " must be provided");
if (this.containerProperties.recoveryCallback == null) {
this.containerProperties.recoveryCallback = new RecoveryCallback<Void>() {
if (this.containerProperties.getRecoveryCallback() == null) {
this.containerProperties.setRecoveryCallback(new RecoveryCallback<Void>() {
@Override
public Void recover(RetryContext context) throws Exception {
@SuppressWarnings("unchecked")
ConsumerRecord<K, V> record = (ConsumerRecord<K, V>) context.getAttribute("record");
Throwable lastThrowable = context.getLastThrowable();
if (AbstractMessageListenerContainer.this.containerProperties.errorHandler != null
if (AbstractMessageListenerContainer.this.containerProperties.getErrorHandler() != null
&& lastThrowable instanceof Exception) {
AbstractMessageListenerContainer.this.containerProperties.errorHandler
AbstractMessageListenerContainer.this.containerProperties.getErrorHandler()
.handle((Exception) lastThrowable, record);
}
else {
@@ -194,7 +207,7 @@ public abstract class AbstractMessageListenerContainer<K, V>
return null;
}
};
});
}
doStart();
}
@@ -212,7 +225,7 @@ public abstract class AbstractMessageListenerContainer<K, V>
}
});
try {
latch.await(this.containerProperties.shutdownTimeout, TimeUnit.MILLISECONDS);
latch.await(this.containerProperties.getShutdownTimeout(), TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
}

View File

@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.kafka.common.TopicPartition;
@@ -96,18 +97,19 @@ public class ConcurrentMessageListenerContainer<K, V> extends AbstractMessageLis
protected void doStart() {
if (!isRunning()) {
ContainerProperties containerProperties = getContainerProperties();
if (containerProperties.topicPartitions != null
&& this.concurrency > containerProperties.topicPartitions.length) {
TopicPartition[] topicPartitions = containerProperties.getTopicPartitions();
if (topicPartitions != null
&& this.concurrency > topicPartitions.length) {
this.logger.warn("When specific partitions are provided, the concurrency must be less than or "
+ "equal to the number of partitions; reduced from " + this.concurrency + " to "
+ containerProperties.topicPartitions.length);
this.concurrency = containerProperties.topicPartitions.length;
+ topicPartitions.length);
this.concurrency = topicPartitions.length;
}
setRunning(true);
for (int i = 0; i < this.concurrency; i++) {
KafkaMessageListenerContainer<K, V> container;
if (containerProperties.topicPartitions == null) {
if (topicPartitions == null) {
container = new KafkaMessageListenerContainer<>(this.consumerFactory, containerProperties);
}
else {
@@ -117,6 +119,9 @@ public class ConcurrentMessageListenerContainer<K, V> extends AbstractMessageLis
if (getBeanName() != null) {
container.setBeanName(getBeanName() + "-" + i);
}
if (getApplicationEventPublisher() != null) {
container.setApplicationEventPublisher(getApplicationEventPublisher());
}
container.start();
this.containers.add(container);
}
@@ -124,24 +129,23 @@ public class ConcurrentMessageListenerContainer<K, V> extends AbstractMessageLis
}
private TopicPartition[] partitionSubset(ContainerProperties containerProperties, int i) {
TopicPartition[] topicPartitions = containerProperties.getTopicPartitions();
if (this.concurrency == 1) {
return containerProperties.topicPartitions;
return topicPartitions;
}
else {
int numPartitions = containerProperties.topicPartitions.length;
int numPartitions = topicPartitions.length;
if (numPartitions == this.concurrency) {
return new TopicPartition[] { containerProperties.topicPartitions[i] };
return new TopicPartition[] { topicPartitions[i] };
}
else {
int perContainer = numPartitions / this.concurrency;
TopicPartition[] subset;
if (i == this.concurrency - 1) {
subset = Arrays.copyOfRange(containerProperties.topicPartitions, i * perContainer,
containerProperties.topicPartitions.length);
subset = Arrays.copyOfRange(topicPartitions, i * perContainer, topicPartitions.length);
}
else {
subset = Arrays.copyOfRange(containerProperties.topicPartitions, i * perContainer,
(i + 1) * perContainer);
subset = Arrays.copyOfRange(topicPartitions, i * perContainer, (i + 1) * perContainer);
}
return subset;
}
@@ -152,14 +156,37 @@ public class ConcurrentMessageListenerContainer<K, V> extends AbstractMessageLis
* Under lifecycle lock.
*/
@Override
protected void doStop(Runnable callback) {
protected void doStop(final Runnable callback) {
final AtomicInteger count = new AtomicInteger();
if (isRunning()) {
setRunning(false);
for (KafkaMessageListenerContainer<K, V> container : this.containers) {
container.stop(callback);
if (container.isRunning()) {
count.incrementAndGet();
}
}
for (KafkaMessageListenerContainer<K, V> container : this.containers) {
if (container.isRunning()) {
container.stop(new Runnable() {
@Override
public void run() {
if (count.decrementAndGet() <= 0) {
callback.run();
}
}
});
}
}
this.containers.clear();
}
}
@Override
public String toString() {
return "ConcurrentMessageListenerContainer [concurrency=" + this.concurrency + ", beanName="
+ this.getBeanName() + ", running=" + this.isRunning() + "]";
}
}

View File

@@ -44,9 +44,11 @@ import org.apache.kafka.clients.consumer.OffsetCommitCallback;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.WakeupException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.kafka.KafkaException;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.event.ListenerContainerIdleEvent;
import org.springframework.kafka.listener.config.ContainerProperties;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.retry.RetryCallback;
@@ -111,7 +113,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
this.topicPartitions = Arrays.asList(topicPartitions).toArray(new TopicPartition[topicPartitions.length]);
}
else {
this.topicPartitions = containerProperties.topicPartitions;
this.topicPartitions = containerProperties.getTopicPartitions();
}
}
@@ -140,7 +142,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
return;
}
setRunning(true);
Object messageListener = getContainerProperties().messageListener;
Object messageListener = getContainerProperties().getMessageListener();
Assert.state(messageListener != null, "A MessageListener is required");
if (messageListener instanceof AcknowledgingMessageListener) {
this.acknowledgingMessageListener = (AcknowledgingMessageListener<K, V>) messageListener;
@@ -152,19 +154,19 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
throw new IllegalStateException("messageListener must be 'MessageListener' "
+ "or 'AcknowledgingMessageListener', not " + messageListener.getClass().getName());
}
if (getContainerProperties().consumerTaskExecutor == null) {
if (getContainerProperties().getConsumerTaskExecutor() == null) {
SimpleAsyncTaskExecutor consumerExecutor = new SimpleAsyncTaskExecutor(
(getBeanName() == null ? "" : getBeanName()) + "-kafka-consumer-");
getContainerProperties().consumerTaskExecutor = consumerExecutor;
getContainerProperties().setConsumerTaskExecutor(consumerExecutor);
}
if (getContainerProperties().listenerTaskExecutor == null) {
if (getContainerProperties().getListenerTaskExecutor() == null) {
SimpleAsyncTaskExecutor listenerExecutor = new SimpleAsyncTaskExecutor(
(getBeanName() == null ? "" : getBeanName()) + "-kafka-listener-");
getContainerProperties().listenerTaskExecutor = listenerExecutor;
getContainerProperties().setListenerTaskExecutor(listenerExecutor);
}
this.listenerConsumer = new ListenerConsumer(this.listener, this.acknowledgingMessageListener,
getContainerProperties().recentOffset);
this.listenerConsumerFuture = getContainerProperties().consumerTaskExecutor
getContainerProperties().getRecentOffset());
this.listenerConsumerFuture = getContainerProperties().getConsumerTaskExecutor()
.submitListenable(this.listenerConsumer);
}
@@ -182,6 +184,10 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
@Override
public void onSuccess(Object result) {
if (KafkaMessageListenerContainer.this.logger.isDebugEnabled()) {
KafkaMessageListenerContainer.this.logger
.debug(KafkaMessageListenerContainer.this + " stopped normally");
}
if (callback != null) {
callback.run();
}
@@ -192,14 +198,22 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
}
@Override
public String toString() {
return "KafkaMessageListenerContainer [id=" + getBeanName() + ", topicPartitions=" + getAssignedPartitions()
+ "]";
}
private final class ListenerConsumer implements SchedulingAwareRunnable {
private final Log logger = LogFactory.getLog(ListenerConsumer.class);
private final ContainerProperties containerProperties = getContainerProperties();
private final OffsetCommitCallback commitCallback = this.containerProperties.commitCallback != null
? this.containerProperties.commitCallback
private final OffsetCommitCallback commitCallback = this.containerProperties.getCommitCallback() != null
? this.containerProperties.getCommitCallback()
: new LoggingCommitCallback();
private final Consumer<K, V> consumer;
@@ -216,20 +230,23 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
private final boolean autoCommit = KafkaMessageListenerContainer.this.consumerFactory.isAutoCommit();
private final boolean isManualAck = this.containerProperties.ackMode.equals(AckMode.MANUAL);
private final boolean isManualAck = this.containerProperties.getAckMode().equals(AckMode.MANUAL);
private final boolean isManualImmediateAck = this.containerProperties.ackMode.equals(AckMode.MANUAL_IMMEDIATE)
|| this.containerProperties.ackMode.equals(AckMode.MANUAL_IMMEDIATE_SYNC);
private final boolean isManualImmediateAck =
this.containerProperties.getAckMode().equals(AckMode.MANUAL_IMMEDIATE)
|| this.containerProperties.getAckMode().equals(AckMode.MANUAL_IMMEDIATE_SYNC);
private final boolean isAnyManualAck = this.isManualAck || this.isManualImmediateAck;
private final boolean isRecordAck = this.containerProperties.ackMode.equals(AckMode.RECORD);
private final boolean isRecordAck = this.containerProperties.getAckMode().equals(AckMode.RECORD);
private final BlockingQueue<ConsumerRecords<K, V>> recordsToProcess = new LinkedBlockingQueue<>(
this.containerProperties.queueDepth);
private final BlockingQueue<ConsumerRecords<K, V>> recordsToProcess =
new LinkedBlockingQueue<>(this.containerProperties.getQueueDepth());
private final BlockingQueue<ConsumerRecord<K, V>> acks = new LinkedBlockingQueue<>();
private final ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher();
private volatile Collection<TopicPartition> definedPartitions;
private volatile Collection<TopicPartition> assignedPartitions;
@@ -252,7 +269,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
private ListenerConsumer(MessageListener<K, V> listener, AcknowledgingMessageListener<K, V> ackListener,
long recentOffset) {
Assert.state(!this.isAnyManualAck || !this.autoCommit,
"Consumer cannot be configured for auto commit for ackMode " + this.containerProperties.ackMode);
"Consumer cannot be configured for auto commit for ackMode " + this.containerProperties.getAckMode());
Consumer<K, V> consumer = KafkaMessageListenerContainer.this.consumerFactory.createConsumer();
ConsumerRebalanceListener rebalanceListener = new ConsumerRebalanceListener() {
@@ -284,7 +301,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
+ "autocommit mode, so transition will be handled by the consumer");
}
}
getContainerProperties().consumerRebalanceListener.onPartitionsRevoked(partitions);
getContainerProperties().getConsumerRebalanceListener().onPartitionsRevoked(partitions);
}
@Override
@@ -299,17 +316,17 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
&& !CollectionUtils.isEmpty(partitions)) {
startInvoker();
}
getContainerProperties().consumerRebalanceListener.onPartitionsAssigned(partitions);
getContainerProperties().getConsumerRebalanceListener().onPartitionsAssigned(partitions);
}
};
if (KafkaMessageListenerContainer.this.topicPartitions == null) {
if (this.containerProperties.topicPattern != null) {
consumer.subscribe(this.containerProperties.topicPattern, rebalanceListener);
if (this.containerProperties.getTopicPattern() != null) {
consumer.subscribe(this.containerProperties.getTopicPattern(), rebalanceListener);
}
else {
consumer.subscribe(Arrays.asList(this.containerProperties.topics), rebalanceListener);
consumer.subscribe(Arrays.asList(this.containerProperties.getTopics()), rebalanceListener);
}
}
else {
@@ -326,7 +343,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
private void startInvoker() {
ListenerConsumer.this.invoker = new ListenerInvoker();
ListenerConsumer.this.listenerInvokerFuture = this.containerProperties.listenerTaskExecutor
ListenerConsumer.this.listenerInvokerFuture = this.containerProperties.getListenerTaskExecutor()
.submit(ListenerConsumer.this.invoker);
}
@@ -349,16 +366,21 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
}
ConsumerRecords<K, V> unsent = null;
long lastReceive = System.currentTimeMillis();
long lastAlertAt = lastReceive;
while (isRunning()) {
try {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Polling (paused=" + this.paused + ")...");
}
ConsumerRecords<K, V> records = this.consumer.poll(this.containerProperties.pollTimeout);
ConsumerRecords<K, V> records = this.consumer.poll(this.containerProperties.getPollTimeout());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Received: " + records.count() + " records");
}
if (records != null && records.count() > 0) {
if (this.containerProperties.getIdleEventInterval() != null) {
lastReceive = System.currentTimeMillis();
}
handleManualAcks();
// if the container is set to auto-commit, then execute in the
// same thread
@@ -379,6 +401,16 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
}
}
else {
if (this.containerProperties.getIdleEventInterval() != null) {
long now = System.currentTimeMillis();
if (now > lastReceive + this.containerProperties.getIdleEventInterval()
&& now > lastAlertAt + this.containerProperties.getIdleEventInterval()) {
publishIdleContainerEvent(now - lastReceive);
lastAlertAt = now;
}
}
}
unsent = checkPause(unsent);
if (!this.paused && !this.autoCommit) {
processCommits();
@@ -388,8 +420,8 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
unsent = checkPause(unsent);
}
catch (Exception e) {
if (this.containerProperties.errorHandler != null) {
this.containerProperties.errorHandler.handle(e, null);
if (this.containerProperties.getErrorHandler() != null) {
this.containerProperties.getErrorHandler().handle(e, null);
}
else {
this.logger.error("Container exception", e);
@@ -411,10 +443,17 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
}
private void publishIdleContainerEvent(long idleTime) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new ListenerContainerIdleEvent(
KafkaMessageListenerContainer.this, idleTime, getBeanName(), getAssignedPartitions()));
}
}
private void stopInvokerAndCommitManualAcks() {
this.invoker.stop();
try {
this.listenerInvokerFuture.get(this.containerProperties.shutdownTimeout, TimeUnit.MILLISECONDS);
this.listenerInvokerFuture.get(this.containerProperties.getShutdownTimeout(), TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -439,7 +478,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
private ConsumerRecords<K, V> checkPause(ConsumerRecords<K, V> unsent) {
if (this.paused && this.recordsToProcess.size() < this.containerProperties.queueDepth) {
if (this.paused && this.recordsToProcess.size() < this.containerProperties.getQueueDepth()) {
// Listener has caught up.
this.consumer.resume(
this.assignedPartitions.toArray(new TopicPartition[this.assignedPartitions.size()]));
@@ -459,8 +498,8 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
private boolean sendToListener(final ConsumerRecords<K, V> records) throws InterruptedException {
if (this.containerProperties.pauseEnabled && CollectionUtils.isEmpty(this.definedPartitions)) {
return !this.recordsToProcess.offer(records, this.containerProperties.pauseAfter,
if (this.containerProperties.isPauseEnabled() && CollectionUtils.isEmpty(this.definedPartitions)) {
return !this.recordsToProcess.offer(records, this.containerProperties.getPauseAfter(),
TimeUnit.MILLISECONDS);
}
else {
@@ -503,7 +542,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
if (ListenerConsumer.this.logger.isDebugEnabled()) {
ListenerConsumer.this.logger.debug("Committing: " + commits);
}
if (ListenerConsumer.this.containerProperties.ackMode.equals(AckMode.MANUAL_IMMEDIATE)) {
if (ListenerConsumer.this.containerProperties.getAckMode().equals(AckMode.MANUAL_IMMEDIATE)) {
ListenerConsumer.this.consumer.commitAsync(commits,
ListenerConsumer.this.commitCallback);
}
@@ -521,8 +560,9 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
try {
if (this.acknowledgingMessageListener != null) {
if (this.containerProperties.retryTemplate != null) {
this.containerProperties.retryTemplate.execute(new RetryCallback<Void, KafkaException>() {
if (this.containerProperties.getRetryTemplate() != null) {
this.containerProperties.getRetryTemplate().execute(
new RetryCallback<Void, KafkaException>() {
@Override
public Void doWithRetry(RetryContext context) throws KafkaException {
@@ -532,15 +572,16 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
return null;
}
}, this.containerProperties.recoveryCallback);
}, this.containerProperties.getRecoveryCallback());
}
else {
this.acknowledgingMessageListener.onMessage(record, new ConsumerAcknowledgment(record));
}
}
else {
if (this.containerProperties.retryTemplate != null) {
this.containerProperties.retryTemplate.execute(new RetryCallback<Void, KafkaException>() {
if (this.containerProperties.getRetryTemplate() != null) {
this.containerProperties.getRetryTemplate().execute(
new RetryCallback<Void, KafkaException>() {
@Override
public Void doWithRetry(RetryContext context) throws KafkaException {
@@ -549,7 +590,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
return null;
}
}, this.containerProperties.recoveryCallback);
}, this.containerProperties.getRecoveryCallback());
}
else {
this.listener.onMessage(record);
@@ -561,8 +602,8 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
}
catch (Exception e) {
if (this.containerProperties.errorHandler != null) {
this.containerProperties.errorHandler.handle(e, record);
if (this.containerProperties.getErrorHandler() != null) {
this.containerProperties.getErrorHandler().handle(e, record);
}
else {
this.logger.error("Listener threw an exception and no error handler for " + record, e);
@@ -574,19 +615,19 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
private void processCommits() {
this.count += this.acks.size();
long now;
AckMode ackMode = this.containerProperties.ackMode;
AckMode ackMode = this.containerProperties.getAckMode();
if (!this.isManualImmediateAck) {
if (!this.isManualAck) {
updatePendingOffsets();
}
boolean countExceeded = this.count >= this.containerProperties.ackCount;
boolean countExceeded = this.count >= this.containerProperties.getAckCount();
if (ackMode.equals(AckMode.BATCH) || ackMode.equals(AckMode.COUNT) && countExceeded) {
commitIfNecessary();
this.count = 0;
}
else {
now = System.currentTimeMillis();
boolean elapsed = now - this.last > this.containerProperties.ackTime;
boolean elapsed = now - this.last > this.containerProperties.getAckTime();
if (ackMode.equals(AckMode.TIME) && elapsed) {
commitIfNecessary();
this.last = now;
@@ -669,7 +710,7 @@ public class KafkaMessageListenerContainer<K, V> extends AbstractMessageListener
}
if (!commits.isEmpty()) {
try {
if (this.containerProperties.syncCommits) {
if (this.containerProperties.isSyncCommits()) {
this.consumer.commitSync(commits);
}
else {

View File

@@ -51,17 +51,17 @@ public class ContainerProperties {
/**
* Topic names.
*/
public final String[] topics;
private final String[] topics;
/**
* Topic pattern.
*/
public final Pattern topicPattern;
private final Pattern topicPattern;
/**
* Topics/partitions.
*/
public final TopicPartition[] topicPartitions;
private final TopicPartition[] topicPartitions;
/**
* The ack mode to use when auto ack (in the configuration properties) is false.
@@ -76,102 +76,102 @@ public class ContainerProperties {
* {@link AcknowledgingMessageListener}.
* </ul>
*/
public AbstractMessageListenerContainer.AckMode ackMode = AckMode.BATCH;
private AbstractMessageListenerContainer.AckMode ackMode = AckMode.BATCH;
/**
* The number of outstanding record count after which offsets should be
* committed when {@link AckMode#COUNT} or {@link AckMode#COUNT_TIME} is being
* used.
*/
public int ackCount;
private int ackCount;
/**
* The time (ms) after which outstanding offsets should be committed when
* {@link AckMode#TIME} or {@link AckMode#COUNT_TIME} is being used. Should be
* larger than
*/
public long ackTime;
private long ackTime;
/**
* The message listener; must be a {@link MessageListener} or
* {@link AcknowledgingMessageListener}.
*/
public Object messageListener;
private Object messageListener;
/**
* The max time to block in the consumer waiting for records.
*/
public volatile long pollTimeout = 1000;
private volatile long pollTimeout = 1000;
/**
* The executor for threads that poll the consumer.
*/
public AsyncListenableTaskExecutor consumerTaskExecutor;
private AsyncListenableTaskExecutor consumerTaskExecutor;
/**
* The executor for threads that invoke the listener.
*/
public AsyncListenableTaskExecutor listenerTaskExecutor;
private AsyncListenableTaskExecutor listenerTaskExecutor;
/**
* The error handler to call when the listener throws an exception.
*/
public ErrorHandler errorHandler = new LoggingErrorHandler();
private ErrorHandler errorHandler = new LoggingErrorHandler();
/**
* When using Kafka group management and {@link #setPauseEnabled(boolean)} is
* true, the delay after which the consumer should be paused. Default 10000.
*/
public long pauseAfter = DEFAULT_PAUSE_AFTER;
private long pauseAfter = DEFAULT_PAUSE_AFTER;
/**
* When true, avoids rebalancing when this consumer is slow or throws a
* qualifying exception - pauses the consumer. Default: true.
* @see #pauseAfter
*/
public boolean pauseEnabled = true;
private boolean pauseEnabled = true;
/**
* A retry template to retry deliveries.
*/
public RetryTemplate retryTemplate;
private RetryTemplate retryTemplate;
/**
* A recovery callback to be invoked when retries are exhausted. By default
* the error handler is invoked.
*/
public RecoveryCallback<Void> recoveryCallback;
private RecoveryCallback<Void> recoveryCallback;
/**
* Set the queue depth for handoffs from the consumer thread to the listener
* thread. Default 1 (up to 2 in process).
*/
public int queueDepth = DEFAULT_QUEUE_DEPTH;
private int queueDepth = DEFAULT_QUEUE_DEPTH;
/**
* The timeout for shutting down the container. This is the maximum amount of
* time that the invocation to {@code #stop(Runnable)} will block for, before
* returning.
*/
public long shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
private long shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
/**
* The offset to this number of records back from the latest when starting.
* Overrides any consumer properties (earliest, latest). Only applies when
* explicit topic/partition assignment is provided.
*/
public long recentOffset;
private long recentOffset;
/**
* A user defined {@link ConsumerRebalanceListener} implementation.
*/
public ConsumerRebalanceListener consumerRebalanceListener;
private ConsumerRebalanceListener consumerRebalanceListener;
/**
* The commit callback; by default a simple logging callback is used to log
* success at DEBUG level and failures at ERROR level.
*/
public OffsetCommitCallback commitCallback;
private OffsetCommitCallback commitCallback;
/**
* Whether or not to call consumer.commitSync() or commitAsync() when the
@@ -179,7 +179,9 @@ public class ContainerProperties {
* https://github.com/spring-projects/spring-kafka/issues/62 At the time of
* writing, async commits are not entirely reliable.
*/
public boolean syncCommits = true;
private boolean syncCommits = true;
private Long idleEventInterval;
public ContainerProperties(String... topics) {
Assert.notEmpty(topics, "An array of topicPartitions must be provided");
@@ -374,10 +376,14 @@ public class ContainerProperties {
this.syncCommits = syncCommits;
}
/*
* Although we generally use field access, we need the getters so we can copy
* the properties from the factory when creating an annotated listener.
/**
* Set the idle event interval; when set, an event is emitted if a poll returns
* no records and this interval has elapsed since a record was returned.
* @param idleEventInterval the interval.
*/
public void setIdleEventInterval(Long idleEventInterval) {
this.idleEventInterval = idleEventInterval;
}
public String[] getTopics() {
return this.topics;
@@ -463,4 +469,8 @@ public class ContainerProperties {
return this.syncCommits;
}
public Long getIdleEventInterval() {
return this.idleEventInterval;
}
}

View File

@@ -36,6 +36,7 @@ import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
@@ -45,6 +46,7 @@ import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.event.ListenerContainerIdleEvent;
import org.springframework.kafka.listener.AbstractMessageListenerContainer.AckMode;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.kafka.listener.MessageListenerContainer;
@@ -124,6 +126,8 @@ public class EnableKafkaIntegrationTests {
assertThat(this.listener.latch4.await(20, TimeUnit.SECONDS)).isTrue();
assertThat(this.listener.record.value()).isEqualTo("foo");
assertThat(this.listener.ack).isNotNull();
assertThat(this.listener.eventLatch.await(20, TimeUnit.SECONDS)).isTrue();
assertThat(this.listener.event.getListenerId().startsWith("qux-"));
template.send("annotated5", 0, 0, "foo");
template.send("annotated5", 1, 0, "bar");
@@ -236,6 +240,7 @@ public class EnableKafkaIntegrationTests {
factory.setConsumerFactory(manualConsumerFactory());
ContainerProperties props = factory.getContainerProperties();
props.setAckMode(AckMode.MANUAL_IMMEDIATE);
props.setIdleEventInterval(100L);
return factory;
}
@@ -354,6 +359,8 @@ public class EnableKafkaIntegrationTests {
private final CountDownLatch latch7 = new CountDownLatch(1);
private final CountDownLatch eventLatch = new CountDownLatch(1);
private volatile Integer partition;
private volatile ConsumerRecord<?, ?> record;
@@ -366,6 +373,8 @@ public class EnableKafkaIntegrationTests {
private Foo foo;
private volatile ListenerContainerIdleEvent event;
@KafkaListener(id = "manualStart", topics = "manualStart",
containerFactory = "kafkaAutoStartFalseListenerContainerFactory")
public void manualStart(String foo) {
@@ -401,6 +410,12 @@ public class EnableKafkaIntegrationTests {
this.latch4.countDown();
}
@EventListener(condition = "event.listenerId.startsWith('qux')")
public void eventHandler(ListenerContainerIdleEvent event) {
this.event = event;
eventLatch.countDown();
}
@KafkaListener(id = "fiz", topicPartitions = {
@TopicPartition(topic = "annotated5", partitions = { "#{'${foo:0,1}'.split(',')}" }),
@TopicPartition(topic = "annotated6", partitions = { "0", "1" })
@@ -442,7 +457,7 @@ public class EnableKafkaIntegrationTests {
latch1.countDown();
}
@KafkaListener(topics = "annotated9")
@KafkaListener(id = "ifctx", topics = "annotated9")
@Transactional
public void listenTx(String foo) {
latch2.countDown();
@@ -457,7 +472,7 @@ public class EnableKafkaIntegrationTests {
}
}
@KafkaListener(topics = "annotated8")
@KafkaListener(id = "multi", topics = "annotated8")
static class MultiListenerBean {
private final CountDownLatch latch1 = new CountDownLatch(1);

View File

@@ -149,7 +149,7 @@ public KafkaMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,
ContainerProperties containerProperties)
public KafkaMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,
ContainerProperties containerProperties, TopicPartition... topicPartitions) {
ContainerProperties containerProperties, TopicPartition... topicPartitions)
----
@@ -228,13 +228,13 @@ NOTE: `MANUAL`, `MANUAL_IMMEDIATE`, and `MANUAL_IMMEDIATE_SYNC` require the list
----
public interface AcknowledgingMessageListener<K, V> {
void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment);
void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment);
}
public interface Acknowledgment {
void acknowledge();
void acknowledge();
}
----
@@ -409,3 +409,87 @@ public void jsonListener(Foo foo) {
...
}
----
[[idle-containers]]
===== Detecting Idle Asynchronous Consumers
While efficient, one problem with asynchronous consumers is detecting when they are idle - users might want to take
some action if no messages arrive for some period of time.
You can configure the listener container to publish a `ListenerContainerIdleEvent` when some time passes with no message delivery.
While the container is idle, an event will be published every `idleEventInterval` milliseconds.
To configure this feature, set the `idleEventInterval` on the container:
[source, java]
----
@Bean
public KafKaMessageListenerContainer(ConnectionFactory connectionFactory) {
ContainerProperties containerProps = new ContainerProperties("topic1", "topic2");
...
containerProps.setIdleEventInterval(60000L);
...
KafKaMessageListenerContainer<String, String> container = new KafKaMessageListenerContainer<>(...);
return container;
}
----
Or, for a `@KafkaListener`...
[source, java]
----
@Bean
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
...
factory.getContainerProperties().setIdleEventInterval(60000L);
...
return factory;
}
----
In each of these cases, an event will be published once per minute while the container is idle.
====== Event Consumption
You can capture these events by implementing `ApplicationListener` - either a general listener, or one narrowed to only receive this specific event.
You can also use `@EventListener`, introduced in Spring Framework 4.2.
The following example combines the `@KafkaListener` and `@EventListener` into a single class.
It's important to understand that the application listener will get events for all containers so you may need to
check the listener id if you want to take specific action based on which container is idle.
You can also use the `@EventListener` `condition` for this purpose.
The events have 4 properties:
- `source` - the listener container instance
- `id` - the listener id (or container bean name)
- `idleTime` - the time the container had been idle when the event was published
- `topicPartitions` - the topics/partitions that the container was assigned at the time the event was generated
[source, xml]
----
public class Listener {
@KafkaListener(id = "qux", topics = "annotated")
public void listen4(@Payload String foo, Acknowledgment ack) {
...
}
@EventListener(condition = "event.listenerId.startsWith('qux-')")
public void eventHandler(ListenerContainerIdleEvent event) {
this.event = event;
eventLatch.countDown();
}
}
----
IMPORTANT: Event listeners will see events for all containers; so, in the example above, we narrow the events received based on the listener ID.
Since containers created for the `@KafkaListener` support concurrency, the actual containers are named `id-n` where the `n` is a unique value for each instance to support the concurrency.
Hence we use `startsWith` in the condition.
CAUTION: If you wish to use the idle event to stop the lister container, you should not call `container.stop()` on the thread that calls the listener - it will cause delays and unnecessary log messages.
Instead, you should hand off the event to a different thread that can then stop the container.
Also, you should not `stop()` the container instance in the event if it is a child container, you should stop the concurrent container instead.