GH-1309: Fix NPE when no syncCommitTimeout set
Resolves https://github.com/spring-projects/spring-kafka/issues/1309 When no commit timeout is provided we update the properties in the local container properties. The parent container's properties must also be updated because the parent is passed into the error handler (e.g. for stopping the entire container when an error occurs). Also, fix a problem where the child container instead of the parent was passed into the batch error handler.
This commit is contained in:
committed by
Artem Bilan
parent
e4e59f9324
commit
c91d491d10
@@ -132,7 +132,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
private static final boolean MICROMETER_PRESENT = ClassUtils.isPresent(
|
||||
"io.micrometer.core.instrument.MeterRegistry", KafkaMessageListenerContainer.class.getClassLoader());
|
||||
|
||||
private final AbstractMessageListenerContainer<K, V> container;
|
||||
private final AbstractMessageListenerContainer<K, V> thisOrParentContainer;
|
||||
|
||||
private final TopicPartitionOffset[] topicPartitions;
|
||||
|
||||
@@ -205,7 +205,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
|
||||
super(consumerFactory, containerProperties);
|
||||
Assert.notNull(consumerFactory, "A ConsumerFactory must be provided");
|
||||
this.container = container == null ? this : container;
|
||||
this.thisOrParentContainer = container == null ? this : container;
|
||||
if (topicPartitions != null) {
|
||||
this.topicPartitions = Arrays.stream(topicPartitions)
|
||||
.map(org.springframework.kafka.support.TopicPartitionInitialOffset::toTPO)
|
||||
@@ -230,7 +230,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
|
||||
super(consumerFactory, containerProperties);
|
||||
Assert.notNull(consumerFactory, "A ConsumerFactory must be provided");
|
||||
this.container = container == null ? this : container;
|
||||
this.thisOrParentContainer = container == null ? this : container;
|
||||
if (topicPartitions != null) {
|
||||
this.topicPartitions = Arrays.copyOf(topicPartitions, topicPartitions.length);
|
||||
}
|
||||
@@ -379,28 +379,28 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
private void publishIdleContainerEvent(long idleTime, Consumer<?, ?> consumer, boolean paused) {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ListenerContainerIdleEvent(this,
|
||||
this.container, idleTime, getBeanName(), getAssignedPartitions(), consumer, paused));
|
||||
this.thisOrParentContainer, idleTime, getBeanName(), getAssignedPartitions(), consumer, paused));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishNonResponsiveConsumerEvent(long timeSinceLastPoll, Consumer<?, ?> consumer) {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(
|
||||
new NonResponsiveConsumerEvent(this, this.container, timeSinceLastPoll,
|
||||
new NonResponsiveConsumerEvent(this, this.thisOrParentContainer, timeSinceLastPoll,
|
||||
getBeanName(), getAssignedPartitions(), consumer));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishConsumerPausedEvent(Collection<TopicPartition> partitions) {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerPausedEvent(this, this.container,
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerPausedEvent(this, this.thisOrParentContainer,
|
||||
Collections.unmodifiableCollection(partitions)));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishConsumerResumedEvent(Collection<TopicPartition> partitions) {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerResumedEvent(this, this.container,
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerResumedEvent(this, this.thisOrParentContainer,
|
||||
Collections.unmodifiableCollection(partitions)));
|
||||
}
|
||||
}
|
||||
@@ -409,7 +409,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
try {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(
|
||||
new ConsumerStoppingEvent(this, this.container, consumer, getAssignedPartitions()));
|
||||
new ConsumerStoppingEvent(this, this.thisOrParentContainer, consumer, getAssignedPartitions()));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -419,32 +419,32 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
|
||||
private void publishConsumerStoppedEvent() {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerStoppedEvent(this, this.container));
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerStoppedEvent(this, this.thisOrParentContainer));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishConsumerStartingEvent() {
|
||||
this.startLatch.countDown();
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerStartingEvent(this, this.container));
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerStartingEvent(this, this.thisOrParentContainer));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishConsumerStartedEvent() {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerStartedEvent(this, this.container));
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerStartedEvent(this, this.thisOrParentContainer));
|
||||
}
|
||||
}
|
||||
|
||||
private void publishConsumerFailedToStart() {
|
||||
if (getApplicationEventPublisher() != null) {
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerFailedToStartEvent(this, this.container));
|
||||
getApplicationEventPublisher().publishEvent(new ConsumerFailedToStartEvent(this, this.thisOrParentContainer));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractMessageListenerContainer<?, ?> parentOrThis() {
|
||||
return this.container;
|
||||
return this.thisOrParentContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -659,6 +659,11 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
if (this.containerProperties.getSyncCommitTimeout() == null) {
|
||||
// update the property so we can use it directly from code elsewhere
|
||||
this.containerProperties.setSyncCommitTimeout(this.syncCommitTimeout);
|
||||
if (KafkaMessageListenerContainer.this.thisOrParentContainer != null) {
|
||||
KafkaMessageListenerContainer.this.thisOrParentContainer
|
||||
.getContainerProperties()
|
||||
.setSyncCommitTimeout(this.syncCommitTimeout);
|
||||
}
|
||||
}
|
||||
this.maxPollInterval = obtainMaxPollInterval(consumerProperties);
|
||||
this.micrometerHolder = obtainMicrometerHolder();
|
||||
@@ -1108,11 +1113,11 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
try {
|
||||
if (!this.isBatchListener && this.errorHandler != null) {
|
||||
this.errorHandler.handle(e, Collections.emptyList(), this.consumer,
|
||||
KafkaMessageListenerContainer.this.container);
|
||||
KafkaMessageListenerContainer.this.thisOrParentContainer);
|
||||
}
|
||||
else if (this.isBatchListener && this.batchErrorHandler != null) {
|
||||
this.batchErrorHandler.handle(e, new ConsumerRecords<K, V>(Collections.emptyMap()), this.consumer,
|
||||
KafkaMessageListenerContainer.this);
|
||||
KafkaMessageListenerContainer.this.thisOrParentContainer);
|
||||
}
|
||||
else {
|
||||
this.logger.error(e, "Consumer exception");
|
||||
@@ -1431,7 +1436,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
private void invokeBatchErrorHandler(final ConsumerRecords<K, V> records, RuntimeException e) {
|
||||
if (this.batchErrorHandler instanceof ContainerAwareBatchErrorHandler) {
|
||||
this.batchErrorHandler.handle(decorateException(e), records, this.consumer,
|
||||
KafkaMessageListenerContainer.this.container);
|
||||
KafkaMessageListenerContainer.this.thisOrParentContainer);
|
||||
}
|
||||
else {
|
||||
this.batchErrorHandler.handle(decorateException(e), records, this.consumer);
|
||||
@@ -1672,7 +1677,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
records.add(iterator.next());
|
||||
}
|
||||
this.errorHandler.handle(decorateException(e), records, this.consumer,
|
||||
KafkaMessageListenerContainer.this.container);
|
||||
KafkaMessageListenerContainer.this.thisOrParentContainer);
|
||||
}
|
||||
else {
|
||||
this.errorHandler.handle(decorateException(e), record, this.consumer);
|
||||
|
||||
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -341,8 +342,16 @@ public class EnableKafkaIntegrationTests {
|
||||
MessageListenerContainer listenerContainer = registry.getListenerContainer("manualStart");
|
||||
assertThat(listenerContainer).isNotNull();
|
||||
assertThat(listenerContainer.isRunning()).isFalse();
|
||||
assertThat(listenerContainer.getContainerProperties().getSyncCommitTimeout()).isNull();
|
||||
this.registry.start();
|
||||
assertThat(listenerContainer.isRunning()).isTrue();
|
||||
assertThat(((ConcurrentMessageListenerContainer<?, ?>) listenerContainer)
|
||||
.getContainers()
|
||||
.get(0)
|
||||
.getContainerProperties().getSyncCommitTimeout())
|
||||
.isEqualTo(Duration.ofSeconds(60));
|
||||
assertThat(listenerContainer.getContainerProperties().getSyncCommitTimeout())
|
||||
.isEqualTo(Duration.ofSeconds(60));
|
||||
listenerContainer.stop();
|
||||
assertThat(KafkaTestUtils.getPropertyValue(listenerContainer, "containerProperties.syncCommits", Boolean.class))
|
||||
.isFalse();
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.springframework.kafka.listener;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
@@ -36,6 +38,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
|
||||
@@ -95,7 +98,7 @@ public class SeekToCurrentBatchErrorHandlerTests {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void discardRemainingRecordsFromPollAndSeek() throws Exception {
|
||||
void discardRemainingRecordsFromPollAndSeek() throws Exception {
|
||||
assertThat(this.config.deliveryLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.pollLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
this.registry.stop();
|
||||
@@ -121,7 +124,7 @@ public class SeekToCurrentBatchErrorHandlerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackOff() {
|
||||
void testBackOff() {
|
||||
SeekToCurrentBatchErrorHandler eh = new SeekToCurrentBatchErrorHandler();
|
||||
eh.setBackOff(new FixedBackOff(10L, 3));
|
||||
@SuppressWarnings("rawtypes")
|
||||
@@ -136,6 +139,46 @@ public class SeekToCurrentBatchErrorHandlerTests {
|
||||
assertThat(System.currentTimeMillis() - t1).isGreaterThanOrEqualTo(100L);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void verifyCorrectContainer() throws InterruptedException {
|
||||
ConsumerFactory consumerFactory = mock(ConsumerFactory.class);
|
||||
final Consumer consumer = mock(Consumer.class);
|
||||
AtomicBoolean first = new AtomicBoolean(true);
|
||||
willAnswer(invocation -> {
|
||||
if (first.getAndSet(false)) {
|
||||
throw new IllegalStateException("intentional");
|
||||
}
|
||||
Thread.sleep(50);
|
||||
return new ConsumerRecords(Collections.emptyMap());
|
||||
}).given(consumer).poll(any());
|
||||
given(consumerFactory.createConsumer(anyString(), anyString(), anyString(),
|
||||
eq(KafkaTestUtils.defaultPropertyOverrides())))
|
||||
.willReturn(consumer);
|
||||
ContainerProperties containerProperties = new ContainerProperties("foo");
|
||||
containerProperties.setGroupId("grp");
|
||||
containerProperties.setMessageListener((BatchMessageListener) record -> { });
|
||||
containerProperties.setMissingTopicsFatal(false);
|
||||
ConcurrentMessageListenerContainer container = new ConcurrentMessageListenerContainer<>(consumerFactory,
|
||||
containerProperties);
|
||||
AtomicReference<MessageListenerContainer> parent = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
container.setBatchErrorHandler(new ContainerAwareBatchErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, ConsumerRecords<?, ?> data, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
|
||||
parent.set(container);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
container.start();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
container.stop();
|
||||
assertThat(parent.get()).isSameAs(container);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public static class Config {
|
||||
@@ -233,6 +276,7 @@ public class SeekToCurrentBatchErrorHandlerTests {
|
||||
});
|
||||
factory.setBatchListener(true);
|
||||
factory.getContainerProperties().setTransactionManager(tm());
|
||||
factory.setMissingTopicsFatal(false);
|
||||
return factory;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user