GH-2089: Remove 2.x Deprecations
Resolves https://github.com/spring-projects/spring-kafka/issues/2089 - remove deprecations - remove implementations of legacy error handlers - deprecate legacy error handler interfaces * Fix deprecation warnings. * Fix more deprecation warnings.
This commit is contained in:
@@ -6,9 +6,9 @@ For changes in earlier version, see <<history>>.
|
||||
[[x28-kafka-client]]
|
||||
==== Kafka Client Version
|
||||
|
||||
This version requires the 3.0.0 `kafka-clients`
|
||||
This version requires the 3.1.0 `kafka-clients`
|
||||
|
||||
IMPORTANT: When using transactions, `kafka-clients` 3.0.0 and later no longer support `EOSMode.V2` (aka `BETA`) (and automatic fallback to `V1` - aka `ALPHA`) with brokers earlier than 2.5; you must therefore override the default `EOSMode` (`V2`) with `V1` if your brokers are older (or upgrade your brokers).
|
||||
IMPORTANT: When using transactions, the minimum broker version is 2.5.
|
||||
|
||||
See <<exactly-once>> and https://cwiki.apache.org/confluence/display/KAFKA/KIP-447%3A+Producer+scalability+for+exactly+once+semantics[KIP-447] for more information.
|
||||
|
||||
|
||||
@@ -35,12 +35,9 @@ import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.listener.AbstractMessageListenerContainer;
|
||||
import org.springframework.kafka.listener.AfterRollbackProcessor;
|
||||
import org.springframework.kafka.listener.BatchErrorHandler;
|
||||
import org.springframework.kafka.listener.BatchInterceptor;
|
||||
import org.springframework.kafka.listener.CommonErrorHandler;
|
||||
import org.springframework.kafka.listener.ContainerProperties;
|
||||
import org.springframework.kafka.listener.ErrorHandler;
|
||||
import org.springframework.kafka.listener.GenericErrorHandler;
|
||||
import org.springframework.kafka.listener.RecordInterceptor;
|
||||
import org.springframework.kafka.listener.adapter.BatchToRecordAdapter;
|
||||
import org.springframework.kafka.listener.adapter.RecordFilterStrategy;
|
||||
@@ -49,7 +46,6 @@ import org.springframework.kafka.requestreply.ReplyingKafkaOperations;
|
||||
import org.springframework.kafka.support.JavaUtils;
|
||||
import org.springframework.kafka.support.TopicPartitionOffset;
|
||||
import org.springframework.kafka.support.converter.MessageConverter;
|
||||
import org.springframework.retry.RecoveryCallback;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -74,7 +70,8 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
|
||||
private final ContainerProperties containerProperties = new ContainerProperties((Pattern) null); // NOSONAR
|
||||
|
||||
private GenericErrorHandler<?> errorHandler;
|
||||
@SuppressWarnings("deprecation")
|
||||
private org.springframework.kafka.listener.GenericErrorHandler<?> errorHandler;
|
||||
|
||||
private CommonErrorHandler commonErrorHandler;
|
||||
|
||||
@@ -90,10 +87,6 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
|
||||
private Boolean ackDiscarded;
|
||||
|
||||
private RetryTemplate retryTemplate;
|
||||
|
||||
private RecoveryCallback<? extends Object> recoveryCallback;
|
||||
|
||||
private Boolean statefulRetry;
|
||||
|
||||
private Boolean batchListener;
|
||||
@@ -177,25 +170,6 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
this.ackDiscarded = ackDiscarded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a retryTemplate.
|
||||
* @param retryTemplate the template.
|
||||
* @deprecated since 2.8 - use a suitably configured error handler instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setRetryTemplate(RetryTemplate retryTemplate) {
|
||||
this.retryTemplate = retryTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback to be used with the {@link #setRetryTemplate(RetryTemplate)
|
||||
* retryTemplate}.
|
||||
* @param recoveryCallback the callback.
|
||||
*/
|
||||
public void setRecoveryCallback(RecoveryCallback<? extends Object> recoveryCallback) {
|
||||
this.recoveryCallback = recoveryCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* When using a {@link RetryTemplate} Set to true to enable stateful retry. Use in
|
||||
* conjunction with a
|
||||
@@ -257,7 +231,7 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
* @see #setCommonErrorHandler(CommonErrorHandler)
|
||||
*/
|
||||
@Deprecated
|
||||
public void setErrorHandler(ErrorHandler errorHandler) {
|
||||
public void setErrorHandler(org.springframework.kafka.listener.ErrorHandler errorHandler) {
|
||||
this.errorHandler = errorHandler;
|
||||
}
|
||||
|
||||
@@ -269,13 +243,14 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
* @see #setCommonErrorHandler(CommonErrorHandler)
|
||||
*/
|
||||
@Deprecated
|
||||
public void setBatchErrorHandler(BatchErrorHandler errorHandler) {
|
||||
public void setBatchErrorHandler(org.springframework.kafka.listener.BatchErrorHandler errorHandler) {
|
||||
this.errorHandler = errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link CommonErrorHandler} which can handle errors for both record
|
||||
* and batch listeners. Replaces the use of {@link GenericErrorHandler}s.
|
||||
* Set the {@link CommonErrorHandler} which can handle errors for both record and
|
||||
* batch listeners. Replaces the use of
|
||||
* {@link org.springframework.kafka.listener.GenericErrorHandler}s.
|
||||
* @param commonErrorHandler the handler.
|
||||
* @since 2.8
|
||||
*/
|
||||
@@ -361,16 +336,17 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
this.containerCustomizer = containerCustomizer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
if (this.commonErrorHandler == null && this.errorHandler != null) {
|
||||
if (Boolean.TRUE.equals(this.batchListener)) {
|
||||
Assert.state(this.errorHandler instanceof BatchErrorHandler,
|
||||
Assert.state(this.errorHandler instanceof org.springframework.kafka.listener.BatchErrorHandler,
|
||||
() -> "The error handler must be a BatchErrorHandler, not " +
|
||||
this.errorHandler.getClass().getName());
|
||||
}
|
||||
else {
|
||||
Assert.state(this.errorHandler instanceof ErrorHandler,
|
||||
Assert.state(this.errorHandler instanceof org.springframework.kafka.listener.ErrorHandler,
|
||||
() -> "The error handler must be an ErrorHandler, not " +
|
||||
this.errorHandler.getClass().getName());
|
||||
}
|
||||
@@ -398,8 +374,6 @@ public abstract class AbstractKafkaListenerContainerFactory<C extends AbstractMe
|
||||
JavaUtils.INSTANCE
|
||||
.acceptIfNotNull(this.recordFilterStrategy, aklEndpoint::setRecordFilterStrategy)
|
||||
.acceptIfNotNull(this.ackDiscarded, aklEndpoint::setAckDiscarded)
|
||||
.acceptIfNotNull(this.retryTemplate, aklEndpoint::setRetryTemplate)
|
||||
.acceptIfNotNull(this.recoveryCallback, aklEndpoint::setRecoveryCallback)
|
||||
.acceptIfNotNull(this.statefulRetry, aklEndpoint::setStatefulRetry)
|
||||
.acceptIfNotNull(this.replyTemplate, aklEndpoint::setReplyTemplate)
|
||||
.acceptIfNotNull(this.replyHeadersConfigurer, aklEndpoint::setReplyHeadersConfigurer)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2021 the original author or authors.
|
||||
* Copyright 2014-2022 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.
|
||||
@@ -94,8 +94,6 @@ public abstract class AbstractKafkaListenerEndpoint<K, V>
|
||||
|
||||
private boolean ackDiscarded;
|
||||
|
||||
private RetryTemplate retryTemplate;
|
||||
|
||||
private RecoveryCallback<? extends Object> recoveryCallback;
|
||||
|
||||
private boolean statefulRetry;
|
||||
@@ -326,34 +324,6 @@ public abstract class AbstractKafkaListenerEndpoint<K, V>
|
||||
this.ackDiscarded = ackDiscarded;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected RetryTemplate getRetryTemplate() {
|
||||
return this.retryTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a retryTemplate.
|
||||
* @param retryTemplate the template.
|
||||
* @deprecated since 2.8 - use a suitably configured error handler instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setRetryTemplate(RetryTemplate retryTemplate) {
|
||||
this.retryTemplate = retryTemplate;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected RecoveryCallback<?> getRecoveryCallback() {
|
||||
return this.recoveryCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback to be used with the {@link #setRetryTemplate(RetryTemplate)}.
|
||||
* @param recoveryCallback the callback.
|
||||
*/
|
||||
public void setRecoveryCallback(RecoveryCallback<? extends Object> recoveryCallback) {
|
||||
this.recoveryCallback = recoveryCallback;
|
||||
}
|
||||
|
||||
protected boolean isStatefulRetry() {
|
||||
return this.statefulRetry;
|
||||
}
|
||||
@@ -510,7 +480,7 @@ public abstract class AbstractKafkaListenerEndpoint<K, V>
|
||||
protected abstract MessagingMessageListenerAdapter<K, V> createMessageListener(MessageListenerContainer container,
|
||||
@Nullable MessageConverter messageConverter);
|
||||
|
||||
@SuppressWarnings({ "unchecked", "deprecation" })
|
||||
@SuppressWarnings("unchecked")
|
||||
private void setupMessageListener(MessageListenerContainer container,
|
||||
@Nullable MessageConverter messageConverter) {
|
||||
|
||||
@@ -523,14 +493,6 @@ public abstract class AbstractKafkaListenerEndpoint<K, V>
|
||||
boolean isBatchListener = isBatchListener();
|
||||
Assert.state(messageListener != null,
|
||||
() -> "Endpoint [" + this + "] must provide a non null message listener");
|
||||
Assert.state(this.retryTemplate == null || !isBatchListener,
|
||||
"A 'RetryTemplate' is not supported with a batch listener; consider configuring the container "
|
||||
+ "with a suitably configured 'SeekToCurrentBatchErrorHandler' instead");
|
||||
if (this.retryTemplate != null) {
|
||||
messageListener = new org.springframework.kafka.listener.adapter.RetryingMessageListenerAdapter<>(
|
||||
(MessageListener<K, V>) messageListener,
|
||||
this.retryTemplate, this.recoveryCallback, this.statefulRetry);
|
||||
}
|
||||
if (this.recordFilterStrategy != null) {
|
||||
if (isBatchListener) {
|
||||
if (((MessagingMessageListenerAdapter<K, V>) messageListener).isConsumerRecords()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -89,8 +89,6 @@ public class StreamsBuilderFactoryBean extends AbstractFactoryBean<StreamsBuilde
|
||||
|
||||
private StateRestoreListener stateRestoreListener;
|
||||
|
||||
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
|
||||
|
||||
private StreamsUncaughtExceptionHandler streamsUncaughtExceptionHandler;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
@@ -191,17 +189,6 @@ public class StreamsBuilderFactoryBean extends AbstractFactoryBean<StreamsBuilde
|
||||
this.stateListener = stateListener; // NOSONAR (sync)
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsolete.
|
||||
* @param exceptionHandler the handler.
|
||||
* @deprecated in favor of
|
||||
* {@link #setStreamsUncaughtExceptionHandler(StreamsUncaughtExceptionHandler)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler exceptionHandler) {
|
||||
this.uncaughtExceptionHandler = exceptionHandler; // NOSONAR (sync)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link StreamsUncaughtExceptionHandler}. Supercedes
|
||||
* {@link #setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)}.
|
||||
@@ -340,9 +327,6 @@ public class StreamsBuilderFactoryBean extends AbstractFactoryBean<StreamsBuilde
|
||||
if (this.streamsUncaughtExceptionHandler != null) {
|
||||
this.kafkaStreams.setUncaughtExceptionHandler(this.streamsUncaughtExceptionHandler);
|
||||
}
|
||||
else {
|
||||
this.kafkaStreams.setUncaughtExceptionHandler(this.uncaughtExceptionHandler);
|
||||
}
|
||||
if (this.kafkaStreamsCustomizer != null) {
|
||||
this.kafkaStreamsCustomizer.customize(this.kafkaStreams);
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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
|
||||
*
|
||||
* https://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.config;
|
||||
|
||||
/**
|
||||
* A customizer for the {@link StreamsBuilderFactoryBean} that is implicitly created by
|
||||
* {@link org.springframework.kafka.annotation.EnableKafkaStreams}. If exactly one
|
||||
* implementation of this interface is found in the application context (or one is marked
|
||||
* as {@link org.springframework.context.annotation.Primary}, it will be invoked after the
|
||||
* factory bean has been created and before it is started.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.3
|
||||
* @deprecated in favor of {@code StreamsBuilderFactoryBeanConfigurer} due to a name
|
||||
* clash with a similar class in Spring Boot.
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface StreamsBuilderFactoryBeanCustomizer {
|
||||
|
||||
/**
|
||||
* Configure the factory bean.
|
||||
* @param factoryBean the factory bean.
|
||||
*/
|
||||
void configure(StreamsBuilderFactoryBean factoryBean);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2021 the original author or authors.
|
||||
* Copyright 2015-2022 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.
|
||||
@@ -198,35 +198,6 @@ public interface KafkaOperations<K, V> {
|
||||
*/
|
||||
void flush();
|
||||
|
||||
/**
|
||||
* When running in a transaction, send the consumer offset(s) to the transaction. The
|
||||
* group id is obtained from
|
||||
* {@link org.springframework.kafka.support.KafkaUtils#getConsumerGroupId()}. It is
|
||||
* not necessary to call this method if the operations are invoked on a listener
|
||||
* container thread (and the listener container is configured with a
|
||||
* {@link org.springframework.kafka.transaction.KafkaAwareTransactionManager}) since
|
||||
* the container will take care of sending the offsets to the transaction.
|
||||
* @param offsets The offsets.
|
||||
* @since 1.3
|
||||
* @deprecated in the 3.0.0 KafkaProducer.
|
||||
*/
|
||||
@Deprecated
|
||||
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets);
|
||||
|
||||
/**
|
||||
* When running in a transaction, send the consumer offset(s) to the transaction. It
|
||||
* is not necessary to call this method if the operations are invoked on a listener
|
||||
* container thread (and the listener container is configured with a
|
||||
* {@link org.springframework.kafka.transaction.KafkaAwareTransactionManager}) since
|
||||
* the container will take care of sending the offsets to the transaction.
|
||||
* @param offsets The offsets.
|
||||
* @param consumerGroupId the consumer's group.id.
|
||||
* @since 1.3
|
||||
* @deprecated in the 3.0.0 KafkaProducer.
|
||||
*/
|
||||
@Deprecated
|
||||
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId);
|
||||
|
||||
/**
|
||||
* When running in a transaction, send the consumer offset(s) to the transaction. It
|
||||
* is not necessary to call this method if the operations are invoked on a listener
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2021 the original author or authors.
|
||||
* Copyright 2015-2022 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.
|
||||
@@ -546,20 +546,6 @@ public class KafkaTemplate<K, V> implements KafkaOperations<K, V>, ApplicationCo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets) {
|
||||
sendOffsetsToTransaction(offsets, KafkaUtils.getConsumerGroupId());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
@Deprecated
|
||||
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) {
|
||||
producerForOffsets().sendOffsetsToTransaction(offsets, consumerGroupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
|
||||
ConsumerGroupMetadata groupMetadata) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 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.
|
||||
@@ -102,18 +102,6 @@ public class RoutingKafkaTemplate extends KafkaTemplate<Object, Object> {
|
||||
throw new UnsupportedOperationException(THIS_METHOD_IS_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) {
|
||||
throw new UnsupportedOperationException(THIS_METHOD_IS_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets) {
|
||||
throw new UnsupportedOperationException(THIS_METHOD_IS_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
|
||||
ConsumerGroupMetadata groupMetadata) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2021 the original author or authors.
|
||||
* Copyright 2019-2022 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.
|
||||
@@ -121,23 +121,6 @@ public class ReactiveKafkaProducerTemplate<K, V> implements AutoCloseable, Dispo
|
||||
return this.sender.send(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the producer.
|
||||
* @return {@link Mono#empty()}.
|
||||
* @deprecated - flush does not make sense in the context of a reactive flow since,
|
||||
* the send completion signal is a send result, which implies that a flush is
|
||||
* redundant. If you use this method with reactor-kafka 1.3 or later, it must be
|
||||
* scheduled to avoid a deadlock; see
|
||||
* https://issues.apache.org/jira/browse/KAFKA-10790 (since 2.7).
|
||||
*/
|
||||
@Deprecated
|
||||
public Mono<?> flush() {
|
||||
return doOnProducer(producer -> {
|
||||
producer.flush();
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
public Flux<PartitionInfo> partitionsFromProducerFor(String topic) {
|
||||
Mono<List<PartitionInfo>> partitionsInfo = doOnProducer(producer -> producer.partitionsFor(topic));
|
||||
return partitionsInfo.flatMapIterable(Function.identity());
|
||||
|
||||
@@ -86,6 +86,7 @@ public abstract class AbstractMessageListenerContainer<K, V>
|
||||
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private GenericErrorHandler<?> errorHandler;
|
||||
|
||||
private CommonErrorHandler commonErrorHandler;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2020 the original author or authors.
|
||||
* Copyright 2015-2022 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.
|
||||
@@ -29,7 +29,9 @@ import org.springframework.lang.Nullable;
|
||||
* @author Gary Russell
|
||||
*
|
||||
* @since 1.1
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface BatchErrorHandler extends GenericErrorHandler<ConsumerRecords<?, ?>> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Simple handler that logs each record.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 1.1
|
||||
* @deprecated - use the {@link CommonLoggingErrorHandler} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class BatchLoggingErrorHandler implements BatchErrorHandler {
|
||||
|
||||
private static final LogAccessor LOGGER =
|
||||
new LogAccessor(LogFactory.getLog(BatchLoggingErrorHandler.class));
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> data) {
|
||||
StringBuilder message = new StringBuilder("Error while processing:\n");
|
||||
if (data == null) {
|
||||
message.append("null ");
|
||||
}
|
||||
else {
|
||||
for (ConsumerRecord<?, ?> record : data) {
|
||||
message.append(ListenerUtils.recordToString(record)).append('\n');
|
||||
}
|
||||
}
|
||||
LOGGER.error(thrownException, () -> message.substring(0, message.length() - 1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import org.springframework.util.Assert;
|
||||
* @since 2.3
|
||||
*
|
||||
*/
|
||||
public class CompositeRecordInterceptor<K, V> implements ConsumerAwareRecordInterceptor<K, V> {
|
||||
public class CompositeRecordInterceptor<K, V> implements RecordInterceptor<K, V> {
|
||||
|
||||
private final Collection<RecordInterceptor<K, V>> delegates = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An error handler that delegates to different error handlers, depending on the exception
|
||||
* type.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.7.4
|
||||
* @deprecated in favor of {@link CommonDelegatingErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class ConditionalDelegatingBatchErrorHandler implements ListenerInvokingBatchErrorHandler {
|
||||
|
||||
private final ContainerAwareBatchErrorHandler defaultErrorHandler;
|
||||
|
||||
private final Map<Class<? extends Throwable>, ContainerAwareBatchErrorHandler> delegates = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Construct an instance with a default error handler that will be invoked if the
|
||||
* exception has no matches.
|
||||
* @param defaultErrorHandler the default error handler.
|
||||
*/
|
||||
public ConditionalDelegatingBatchErrorHandler(ContainerAwareBatchErrorHandler defaultErrorHandler) {
|
||||
Assert.notNull(defaultErrorHandler, "'defaultErrorHandler' cannot be null");
|
||||
this.defaultErrorHandler = defaultErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the delegate error handlers; a {@link LinkedHashMap} argument is recommended so
|
||||
* that the delegates are searched in a known order.
|
||||
* @param delegates the delegates.
|
||||
*/
|
||||
public void setErrorHandlers(Map<Class<? extends Throwable>, ContainerAwareBatchErrorHandler> delegates) {
|
||||
this.delegates.clear();
|
||||
this.delegates.putAll(delegates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delegate to the end of the current collection.
|
||||
* @param throwable the throwable for this handler.
|
||||
* @param handler the handler.
|
||||
*/
|
||||
public void addDelegate(Class<? extends Throwable> throwable, ContainerAwareBatchErrorHandler handler) {
|
||||
this.delegates.put(throwable, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> records, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
|
||||
// Never called but, just in case
|
||||
doHandle(thrownException, records, consumer, container, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> records, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container, Runnable invokeListener) {
|
||||
|
||||
doHandle(thrownException, records, consumer, container, invokeListener);
|
||||
}
|
||||
|
||||
protected void doHandle(Exception thrownException, @Nullable ConsumerRecords<?, ?> records, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container, @Nullable Runnable invokeListener) {
|
||||
|
||||
Throwable cause = thrownException;
|
||||
if (cause instanceof ListenerExecutionFailedException) {
|
||||
cause = thrownException.getCause();
|
||||
}
|
||||
if (cause != null) {
|
||||
Class<? extends Throwable> causeClass = cause.getClass();
|
||||
for (Entry<Class<? extends Throwable>, ContainerAwareBatchErrorHandler> entry : this.delegates.entrySet()) {
|
||||
if (entry.getKey().isAssignableFrom(causeClass)) {
|
||||
entry.getValue().handle(thrownException, records, consumer, container, invokeListener);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.defaultErrorHandler.handle(thrownException, records, consumer, container, invokeListener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An error handler that delegates to different error handlers, depending on the exception
|
||||
* type.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.7.4
|
||||
* @deprecated in favor of {@link CommonDelegatingErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class ConditionalDelegatingErrorHandler implements ContainerAwareErrorHandler {
|
||||
|
||||
private final ContainerAwareErrorHandler defaultErrorHandler;
|
||||
|
||||
private final Map<Class<? extends Throwable>, ContainerAwareErrorHandler> delegates = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Construct an instance with a default error handler that will be invoked if the
|
||||
* exception has no matches.
|
||||
* @param defaultErrorHandler the default error handler.
|
||||
*/
|
||||
public ConditionalDelegatingErrorHandler(ContainerAwareErrorHandler defaultErrorHandler) {
|
||||
Assert.notNull(defaultErrorHandler, "'defaultErrorHandler' cannot be null");
|
||||
this.defaultErrorHandler = defaultErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the delegate error handlers; a {@link LinkedHashMap} argument is recommended so
|
||||
* that the delegates are searched in a known order.
|
||||
* @param delegates the delegates.
|
||||
*/
|
||||
public void setErrorHandlers(Map<Class<? extends Throwable>, ContainerAwareErrorHandler> delegates) {
|
||||
this.delegates.clear();
|
||||
this.delegates.putAll(delegates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delegate to the end of the current collection.
|
||||
* @param throwable the throwable for this handler.
|
||||
* @param handler the handler.
|
||||
*/
|
||||
public void addDelegate(Class<? extends Throwable> throwable, ContainerAwareErrorHandler handler) {
|
||||
this.delegates.put(throwable, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable List<ConsumerRecord<?, ?>> records, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
|
||||
Throwable cause = thrownException;
|
||||
if (cause instanceof ListenerExecutionFailedException) {
|
||||
cause = thrownException.getCause();
|
||||
}
|
||||
if (cause != null) {
|
||||
Class<? extends Throwable> causeClass = cause.getClass();
|
||||
for (Entry<Class<? extends Throwable>, ContainerAwareErrorHandler> entry : this.delegates.entrySet()) {
|
||||
if (entry.getKey().isAssignableFrom(causeClass)) {
|
||||
entry.getValue().handle(thrownException, records, consumer, container);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.defaultErrorHandler.handle(thrownException, records, consumer, container);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -27,8 +27,10 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.0
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface ConsumerAwareBatchErrorHandler extends BatchErrorHandler {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -29,8 +29,10 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.0
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface ConsumerAwareErrorHandler extends ErrorHandler {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -17,9 +17,6 @@
|
||||
package org.springframework.kafka.listener;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link RecordInterceptor} that has access to the {@link Consumer}.
|
||||
@@ -29,20 +26,11 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.7
|
||||
* @deprecated - use {@link RecordInterceptor}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface ConsumerAwareRecordInterceptor<K, V> extends RecordInterceptor<K, V> {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
@Nullable
|
||||
default ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record) {
|
||||
throw new UnsupportedOperationException("Container should never call this");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record, Consumer<K, V> consumer);
|
||||
|
||||
}
|
||||
|
||||
@@ -352,35 +352,6 @@ public class ConsumerProperties {
|
||||
this.kafkaConsumerProperties = kafkaConsumerProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication/authorization retry interval.
|
||||
* @return the interval.
|
||||
* @deprecated in favor of {@link #getAuthExceptionRetryInterval()}.
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public Duration getAuthorizationExceptionRetryInterval() {
|
||||
return this.authExceptionRetryInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the interval between retries after and
|
||||
* {@link org.apache.kafka.common.errors.AuthenticationException} or
|
||||
* {@code org.apache.kafka.common.errors.AuthorizationException} is thrown by
|
||||
* {@code KafkaConsumer}. By default the field is null and retries are disabled. In
|
||||
* such case the container will be stopped.
|
||||
*
|
||||
* The interval must be less than {@code max.poll.interval.ms} consumer property.
|
||||
*
|
||||
* @param authorizationExceptionRetryInterval the duration between retries
|
||||
* @since 2.3.5
|
||||
* @deprecated in favor of {@link #setAuthExceptionRetryInterval(Duration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setAuthorizationExceptionRetryInterval(Duration authorizationExceptionRetryInterval) {
|
||||
this.authExceptionRetryInterval = authorizationExceptionRetryInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication/authorization retry interval.
|
||||
* @return the interval.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -27,8 +27,9 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface ContainerAwareBatchErrorHandler extends ConsumerAwareBatchErrorHandler {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -31,8 +31,10 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface ContainerAwareErrorHandler extends RemainingRecordsErrorHandler {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2021 the original author or authors.
|
||||
* Copyright 2016-2022 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.
|
||||
@@ -148,50 +148,15 @@ public class ContainerProperties extends ConsumerProperties {
|
||||
|
||||
/**
|
||||
* 'transactional.id' fencing (0.11 - 2.4 brokers).
|
||||
* @deprecated V1 is no longer supported
|
||||
*/
|
||||
@Deprecated
|
||||
V1,
|
||||
|
||||
/**
|
||||
* fetch-offset-request fencing (2.5+ brokers).
|
||||
*/
|
||||
V2,
|
||||
|
||||
/**
|
||||
* 'transactional.id' fencing (0.11 - 2.4 brokers).
|
||||
* @deprecated in favor of {@link #V1}.
|
||||
*/
|
||||
@Deprecated
|
||||
ALPHA(V1),
|
||||
|
||||
/**
|
||||
* fetch-offset-request fencing (2.5+ brokers).
|
||||
* @deprecated in favor of {@link #V2}.
|
||||
*/
|
||||
@Deprecated
|
||||
BETA(V2);
|
||||
|
||||
|
||||
private final EOSMode mode;
|
||||
|
||||
EOSMode() {
|
||||
this.mode = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alias.
|
||||
* @param v12 the mode for which this is an alias.
|
||||
*/
|
||||
EOSMode(EOSMode v12) {
|
||||
this.mode = v12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mode or the aliased mode.
|
||||
* @return the mode.
|
||||
*/
|
||||
public EOSMode getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
V2;
|
||||
|
||||
}
|
||||
|
||||
@@ -696,11 +661,6 @@ public class ContainerProperties extends ConsumerProperties {
|
||||
return this.consumerStartTimeout;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Duration getConsumerStartTimout() {
|
||||
return this.consumerStartTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timeout to wait for a consumer thread to start before logging
|
||||
* an error. Default 30 seconds.
|
||||
@@ -711,11 +671,6 @@ public class ContainerProperties extends ConsumerProperties {
|
||||
this.consumerStartTimeout = consumerStartTimeout;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setConsumerStartTimout(Duration consumerStartTimeout) {
|
||||
setConsumerStartTimeout(consumerStartTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether to split batches by partition.
|
||||
* @return subBatchPerPartition.
|
||||
@@ -793,18 +748,15 @@ public class ContainerProperties extends ConsumerProperties {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the exactly once semantics mode. When {@link EOSMode#V1} a producer per
|
||||
* group/topic/partition is used (enabling 'transactional.id fencing`).
|
||||
* {@link EOSMode#V2} enables fetch-offset-request fencing, and requires brokers 2.5
|
||||
* or later. With the 2.6 client, the default is now V2 because the 2.6 client can
|
||||
* automatically fall back to ALPHA.
|
||||
* IMPORTANT the 3.0 clients cannot be used with {@link EOSMode#V2} unless the broker
|
||||
* is 2.5 or higher.
|
||||
* Set the exactly once semantics mode. Only {@link EOSMode#V2} is supported
|
||||
* since version 3.0.
|
||||
* @param eosMode the mode; default V2.
|
||||
* @since 2.5
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void setEosMode(EOSMode eosMode) {
|
||||
Assert.notNull(eosMode, "'eosMode' cannot be null");
|
||||
Assert.isTrue(!eosMode.equals(EOSMode.V1), "V1 is no longer supported");
|
||||
this.eosMode = eosMode;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.kafka.KafkaException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A container error handler that stops the container after an exception
|
||||
* is thrown by the listener.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
* @deprecated in favor of {@link CommonContainerStoppingErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
public class ContainerStoppingBatchErrorHandler extends KafkaExceptionLogLevelAware
|
||||
implements ContainerAwareBatchErrorHandler {
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
/**
|
||||
* Construct an instance with a {@link SimpleAsyncTaskExecutor}.
|
||||
*/
|
||||
public ContainerStoppingBatchErrorHandler() {
|
||||
this.executor = new SimpleAsyncTaskExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided {@link Executor}.
|
||||
* @param executor the executor.
|
||||
*/
|
||||
public ContainerStoppingBatchErrorHandler(Executor executor) {
|
||||
Assert.notNull(executor, "'executor' cannot be null");
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> data, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
|
||||
this.executor.execute(() -> container.stop());
|
||||
// isRunning is false before the container.stop() waits for listener thread
|
||||
int n = 0;
|
||||
while (container.isRunning() && n++ < 100) { // NOSONAR magic #
|
||||
try {
|
||||
Thread.sleep(100); // NOSONAR magic #
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new KafkaException("Stopped container", getLogLevel(), thrownException);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.kafka.KafkaException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A container error handler that stops the container after an exception
|
||||
* is thrown by the listener.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
* @deprecated in favor of {@link CommonContainerStoppingErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
public class ContainerStoppingErrorHandler extends KafkaExceptionLogLevelAware implements ContainerAwareErrorHandler {
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
/**
|
||||
* Construct an instance with a default {@link SimpleAsyncTaskExecutor}.
|
||||
*/
|
||||
public ContainerStoppingErrorHandler() {
|
||||
this.executor = new SimpleAsyncTaskExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided {@link Executor}.
|
||||
* @param executor the executor.
|
||||
*/
|
||||
public ContainerStoppingErrorHandler(Executor executor) {
|
||||
Assert.notNull(executor, "'executor' cannot be null");
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
this.executor.execute(() -> container.stop());
|
||||
// isRunning is false before the container.stop() waits for listener thread
|
||||
int n = 0;
|
||||
while (container.isRunning() && n++ < 100) { // NOSONAR magic #
|
||||
try {
|
||||
Thread.sleep(100); // NOSONAR magic #
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new KafkaException("Stopped container", getLogLevel(), thrownException);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
* Copyright 2018-2022 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.
|
||||
@@ -247,19 +247,6 @@ public class DeadLetterPublishingRecoverer extends ExceptionClassifier implement
|
||||
this.partitionInfoTimeout = partitionInfoTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to false if you don't want to append the current "original" headers (topic,
|
||||
* partition etc.) if they are already present. When false, only the first "original"
|
||||
* headers are retained.
|
||||
* @param replaceOriginalHeaders set to false not to replace.
|
||||
* @since 2.7
|
||||
* @deprecated in favor of {@link #setAppendOriginalHeaders(boolean)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setReplaceOriginalHeaders(boolean replaceOriginalHeaders) {
|
||||
this.appendOriginalHeaders = replaceOriginalHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to false if you don't want to append the current "original" headers (topic,
|
||||
* partition etc.) if they are already present. When false, only the first "original"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
* Copyright 2018-2022 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.
|
||||
@@ -137,16 +137,9 @@ public class DefaultAfterRollbackProcessor<K, V> extends FailedRecordProcessor
|
||||
getRecoveryStrategy((List) records, exception), container, this.logger)
|
||||
&& isCommitRecovered() && this.kafkaTemplate.isTransactional()) {
|
||||
ConsumerRecord<K, V> skipped = records.get(0);
|
||||
if (EOSMode.V1.equals(eosMode.getMode())) {
|
||||
this.kafkaTemplate.sendOffsetsToTransaction(
|
||||
Collections.singletonMap(new TopicPartition(skipped.topic(), skipped.partition()),
|
||||
new OffsetAndMetadata(skipped.offset() + 1)));
|
||||
}
|
||||
else {
|
||||
this.kafkaTemplate.sendOffsetsToTransaction(
|
||||
Collections.singletonMap(new TopicPartition(skipped.topic(), skipped.partition()),
|
||||
new OffsetAndMetadata(skipped.offset() + 1)), consumer.groupMetadata());
|
||||
}
|
||||
this.kafkaTemplate.sendOffsetsToTransaction(
|
||||
Collections.singletonMap(new TopicPartition(skipped.topic(), skipped.partition()),
|
||||
new OffsetAndMetadata(skipped.offset() + 1)), consumer.groupMetadata());
|
||||
}
|
||||
|
||||
if (!recoverable && this.backOff != null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2021 the original author or authors.
|
||||
* Copyright 2015-2022 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.
|
||||
@@ -26,7 +26,9 @@ import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Gary Russell
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface ErrorHandler extends GenericErrorHandler<ConsumerRecord<?, ?>> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.util.Assert;
|
||||
* @since 2.7.4
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class ErrorHandlerAdapter implements CommonErrorHandler {
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2021 the original author or authors.
|
||||
* Copyright 2019-2022 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.
|
||||
@@ -117,36 +117,6 @@ public abstract class FailedRecordProcessor extends ExceptionClassifier implemen
|
||||
return this.failureTracker.deliveryAttempt(topicPartitionOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link BiPredicate} to call to determine whether the first record in the
|
||||
* list should be skipped.
|
||||
* @param records the records.
|
||||
* @param thrownException the exception.
|
||||
* @return the {@link BiPredicate}.
|
||||
* @deprecated in favor of {@link #getRecoveryStrategy(List, Exception)}.
|
||||
*/
|
||||
@Deprecated
|
||||
protected BiPredicate<ConsumerRecord<?, ?>, Exception> getSkipPredicate(List<ConsumerRecord<?, ?>> records,
|
||||
Exception thrownException) {
|
||||
|
||||
if (getClassifier().classify(thrownException)) {
|
||||
return this.failureTracker::skip;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
this.failureTracker.getRecoverer().accept(records.get(0), thrownException);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (records.size() > 0) {
|
||||
this.logger.error(ex, () -> "Recovery of record ("
|
||||
+ ListenerUtils.recordToString(records.get(0)) + ") failed");
|
||||
}
|
||||
return NEVER_SKIP_PREDICATE;
|
||||
}
|
||||
return ALWAYS_SKIP_PREDICATE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link RecoveryStrategy} to call to determine whether the first record in the
|
||||
* list should be skipped.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2020-2022 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.
|
||||
@@ -16,27 +16,50 @@
|
||||
|
||||
package org.springframework.kafka.listener;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* A batch error handler used by the default error handler when the listener does
|
||||
* not throw a {@link BatchListenerFailedException}.
|
||||
* A batch error handler that invokes the listener according to the supplied
|
||||
* {@link BackOff}. The consumer is paused/polled/resumed before each retry in order to
|
||||
* avoid a rebalance. If/when retries are exhausted, the provided
|
||||
* {@link ConsumerRecordRecoverer} is invoked for each record in the batch. If the
|
||||
* recoverer throws an exception, or the thread is interrupted while sleeping, seeks are
|
||||
* performed so that the batch will be redelivered on the next poll.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.8.3
|
||||
* @since 2.3.7
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class FallbackBatchErrorHandler extends RetryingBatchErrorHandler {
|
||||
class FallbackBatchErrorHandler extends KafkaExceptionLogLevelAware
|
||||
implements ListenerInvokingBatchErrorHandler {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
|
||||
|
||||
private final BackOff backOff;
|
||||
|
||||
private final BiConsumer<ConsumerRecords<?, ?>, Exception> recoverer;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private final CommonErrorHandler seeker = new ErrorHandlerAdapter(new SeekToCurrentBatchErrorHandler());
|
||||
|
||||
private boolean ackAfterHandle = true;
|
||||
|
||||
/**
|
||||
* Construct an instance with a default {@link FixedBackOff} (unlimited attempts with
|
||||
* a 5 second back off).
|
||||
*/
|
||||
FallbackBatchErrorHandler() {
|
||||
super();
|
||||
this(new FixedBackOff(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +70,37 @@ class FallbackBatchErrorHandler extends RetryingBatchErrorHandler {
|
||||
* @param recoverer the recoverer.
|
||||
*/
|
||||
FallbackBatchErrorHandler(BackOff backOff, @Nullable ConsumerRecordRecoverer recoverer) {
|
||||
super(backOff, recoverer);
|
||||
this.backOff = backOff;
|
||||
this.recoverer = (crs, ex) -> {
|
||||
if (recoverer == null) {
|
||||
this.logger.error(ex, () -> "Records discarded: " + ErrorHandlingUtils.recordsToString(crs));
|
||||
}
|
||||
else {
|
||||
crs.spliterator().forEachRemaining(rec -> recoverer.accept(rec, ex));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAckAfterHandle() {
|
||||
return this.ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAckAfterHandle(boolean ackAfterHandle) {
|
||||
this.ackAfterHandle = ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container, Runnable invokeListener) {
|
||||
|
||||
if (records == null || records.count() == 0) {
|
||||
this.logger.error(thrownException, "Called with no records; consumer exception");
|
||||
return;
|
||||
}
|
||||
ErrorHandlingUtils.retryBatch(thrownException, records, consumer, container, invokeListener, this.backOff,
|
||||
this.seeker, this.recoverer, this.logger, getLogLevel());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
* Copyright 2016-2022 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.
|
||||
@@ -27,8 +27,10 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 1.1
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface GenericErrorHandler<T> {
|
||||
|
||||
|
||||
@@ -846,10 +846,10 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Nullable
|
||||
private CommonErrorHandler determineCommonErrorHandler() {
|
||||
CommonErrorHandler common = getCommonErrorHandler();
|
||||
@SuppressWarnings("deprecation")
|
||||
GenericErrorHandler<?> errHandler = getGenericErrorHandler();
|
||||
if (common != null) {
|
||||
if (errHandler != null) {
|
||||
@@ -931,10 +931,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
if (subBatching != null) {
|
||||
return subBatching;
|
||||
}
|
||||
if (this.transactionManager == null) {
|
||||
return false;
|
||||
}
|
||||
return this.eosMode.getMode().equals(EOSMode.V1);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -1202,6 +1199,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void validateErrorHandler(boolean batch, @Nullable GenericErrorHandler<?> errHandler) {
|
||||
if (errHandler == null) {
|
||||
return;
|
||||
@@ -2722,12 +2720,7 @@ public class KafkaMessageListenerContainer<K, V> // NOSONAR line count
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void doSendOffsets(Producer<?, ?> prod, Map<TopicPartition, OffsetAndMetadata> commits) {
|
||||
if (this.eosMode.getMode().equals(EOSMode.V1)) {
|
||||
prod.sendOffsetsToTransaction(commits, this.consumerGroupId);
|
||||
}
|
||||
else {
|
||||
prod.sendOffsetsToTransaction(commits, this.consumer.groupMetadata());
|
||||
}
|
||||
prod.sendOffsetsToTransaction(commits, this.consumer.groupMetadata());
|
||||
if (this.fixTxOffsets) {
|
||||
this.lastCommits.putAll(commits);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
* Copyright 2020-2022 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.
|
||||
@@ -26,8 +26,10 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.3.7
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface ListenerInvokingBatchErrorHandler extends ContainerAwareBatchErrorHandler {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -20,11 +20,8 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectStreamClass;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.common.Metric;
|
||||
import org.apache.kafka.common.MetricName;
|
||||
import org.apache.kafka.common.header.Header;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
import org.apache.kafka.common.header.internals.RecordHeaders;
|
||||
@@ -191,53 +188,6 @@ public final class ListenerUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep according to the {@link BackOff}; when the {@link BackOffExecution} returns
|
||||
* {@link BackOffExecution#STOP} sleep for the previous backOff.
|
||||
* @param backOff the {@link BackOff} to create a new {@link BackOffExecution}.
|
||||
* @param executions a thread local containing the {@link BackOffExecution} for this
|
||||
* thread.
|
||||
* @param lastIntervals a thread local containing the previous {@link BackOff}
|
||||
* interval for this thread.
|
||||
* @since 2.3.12
|
||||
* @deprecated since 2.7 in favor of
|
||||
* {@link #unrecoverableBackOff(BackOff, ThreadLocal, ThreadLocal, MessageListenerContainer)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static void unrecoverableBackOff(BackOff backOff, ThreadLocal<BackOffExecution> executions,
|
||||
ThreadLocal<Long> lastIntervals) {
|
||||
|
||||
try {
|
||||
unrecoverableBackOff(backOff, executions, lastIntervals, new MessageListenerContainer() { // NOSONAR
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupMessageListener(Object messageListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<MetricName, ? extends Metric>> metrics() {
|
||||
return null; // NOSONAR
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep according to the {@link BackOff}; when the {@link BackOffExecution} returns
|
||||
* {@link BackOffExecution#STOP} sleep for the previous backOff.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link ErrorHandler} implementation for logging purpose.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Gary Russell
|
||||
* @deprecated - use the {@link CommonLoggingErrorHandler} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class LoggingErrorHandler implements ErrorHandler {
|
||||
|
||||
private static final LogAccessor LOGGER = new LogAccessor(LogFactory.getLog(LoggingErrorHandler.class));
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecord<?, ?> record) {
|
||||
LOGGER.error(thrownException, () -> "Error while processing: " + ListenerUtils.recordToString(record));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2021 the original author or authors.
|
||||
* Copyright 2019-2022 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.
|
||||
@@ -35,18 +35,6 @@ import org.springframework.lang.Nullable;
|
||||
@FunctionalInterface
|
||||
public interface RecordInterceptor<K, V> extends ThreadStateProcessor {
|
||||
|
||||
/**
|
||||
* Perform some action on the record or return a different one. If null is returned
|
||||
* the record will be skipped. Invoked before the listener.
|
||||
* @param record the record.
|
||||
* @return the record or null.
|
||||
* @deprecated in favor of {@link #intercept(ConsumerRecord, Consumer)} which will
|
||||
* become the required method in a future release.
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record);
|
||||
|
||||
/**
|
||||
* Perform some action on the record or return a different one. If null is returned
|
||||
* the record will be skipped. Invoked before the listener.
|
||||
@@ -56,11 +44,7 @@ public interface RecordInterceptor<K, V> extends ThreadStateProcessor {
|
||||
* @since 2.7
|
||||
*/
|
||||
@Nullable
|
||||
default ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record,
|
||||
@SuppressWarnings("unused") Consumer<K, V> consumer) {
|
||||
|
||||
return intercept(record);
|
||||
}
|
||||
ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> record, Consumer<K, V> consumer);
|
||||
|
||||
/**
|
||||
* Called after the listener exits normally.
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
|
||||
/**
|
||||
* An error handler that seeks to the current offset for each topic in a batch of records.
|
||||
* Used to rewind partitions after a message failure so that the batch can be replayed. If
|
||||
* the listener throws a {@link BatchListenerFailedException}, with the failed record. The
|
||||
* records before the record will have their offsets committed and the partitions for the
|
||||
* remaining records will be repositioned and/or the failed record can be recovered and
|
||||
* skipped. If some other exception is thrown, or a valid record is not provided in the
|
||||
* exception, error handling is delegated to a {@link SeekToCurrentBatchErrorHandler} with
|
||||
* this handler's {@link BackOff}. If the record is recovered, its offset is committed.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 2.5
|
||||
* @deprecated in favor of {@link DefaultErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class RecoveringBatchErrorHandler extends FailedBatchProcessor
|
||||
implements ContainerAwareBatchErrorHandler {
|
||||
|
||||
private boolean ackAfterHandle = true;
|
||||
|
||||
/**
|
||||
* Construct an instance with the default recoverer which simply logs the record after
|
||||
* {@value SeekUtils#DEFAULT_MAX_FAILURES} (maxFailures) have occurred for a
|
||||
* topic/partition/offset.
|
||||
*/
|
||||
public RecoveringBatchErrorHandler() {
|
||||
this(null, SeekUtils.DEFAULT_BACK_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the default recoverer which simply logs the record after
|
||||
* the backOff returns STOP for a topic/partition/offset.
|
||||
* @param backOff the {@link BackOff}.
|
||||
*/
|
||||
public RecoveringBatchErrorHandler(BackOff backOff) {
|
||||
this(null, backOff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided recoverer which will be called after
|
||||
* {@value SeekUtils#DEFAULT_MAX_FAILURES} (maxFailures) have occurred for a
|
||||
* topic/partition/offset.
|
||||
* @param recoverer the recoverer.
|
||||
*/
|
||||
public RecoveringBatchErrorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
|
||||
this(recoverer, SeekUtils.DEFAULT_BACK_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided recoverer which will be called after the
|
||||
* backOff returns STOP for a topic/partition/offset.
|
||||
* @param recoverer the recoverer; if null, the default (logging) recoverer is used.
|
||||
* @param backOff the {@link BackOff}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public RecoveringBatchErrorHandler(@Nullable BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer,
|
||||
BackOff backOff) {
|
||||
|
||||
super(recoverer, backOff, createFallback(backOff));
|
||||
}
|
||||
|
||||
private static CommonErrorHandler createFallback(BackOff backOff) {
|
||||
SeekToCurrentBatchErrorHandler eh = new SeekToCurrentBatchErrorHandler();
|
||||
eh.setBackOff(backOff);
|
||||
return new ErrorHandlerAdapter(eh);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAckAfterHandle() {
|
||||
return this.ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAckAfterHandle(boolean ackAfterHandle) {
|
||||
this.ackAfterHandle = ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> data, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
|
||||
doHandle(thrownException, data, consumer, container, () -> { }); // NOSONAR
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2020 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -31,8 +31,10 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.0.1
|
||||
* @deprecated in favor of {@link CommonErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface RemainingRecordsErrorHandler extends ConsumerAwareErrorHandler {
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* A batch error handler that invokes the listener according to the supplied
|
||||
* {@link BackOff}. The consumer is paused/polled/resumed before each retry in order to
|
||||
* avoid a rebalance. If/when retries are exhausted, the provided
|
||||
* {@link ConsumerRecordRecoverer} is invoked for each record in the batch. If the
|
||||
* recoverer throws an exception, or the thread is interrupted while sleeping, seeks are
|
||||
* performed so that the batch will be redelivered on the next poll.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.3.7
|
||||
* @deprecated in favor of {@link DefaultErrorHandler}.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
public class RetryingBatchErrorHandler extends KafkaExceptionLogLevelAware
|
||||
implements ListenerInvokingBatchErrorHandler {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
|
||||
|
||||
private final BackOff backOff;
|
||||
|
||||
private final BiConsumer<ConsumerRecords<?, ?>, Exception> recoverer;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private final CommonErrorHandler seeker = new ErrorHandlerAdapter(new SeekToCurrentBatchErrorHandler());
|
||||
|
||||
private boolean ackAfterHandle = true;
|
||||
|
||||
/**
|
||||
* Construct an instance with a default {@link FixedBackOff} (unlimited attempts with
|
||||
* a 5 second back off).
|
||||
*/
|
||||
public RetryingBatchErrorHandler() {
|
||||
this(new FixedBackOff(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided {@link BackOff} and
|
||||
* {@link ConsumerRecordRecoverer}. If the recoverer is {@code null}, the discarded
|
||||
* records (topic-partition{@literal @}offset) will be logged.
|
||||
* @param backOff the back off.
|
||||
* @param recoverer the recoverer.
|
||||
*/
|
||||
public RetryingBatchErrorHandler(BackOff backOff, @Nullable ConsumerRecordRecoverer recoverer) {
|
||||
this.backOff = backOff;
|
||||
this.recoverer = (crs, ex) -> {
|
||||
if (recoverer == null) {
|
||||
this.logger.error(ex, () -> "Records discarded: " + ErrorHandlingUtils.recordsToString(crs));
|
||||
}
|
||||
else {
|
||||
crs.spliterator().forEachRemaining(rec -> recoverer.accept(rec, ex));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAckAfterHandle() {
|
||||
return this.ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAckAfterHandle(boolean ackAfterHandle) {
|
||||
this.ackAfterHandle = ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable ConsumerRecords<?, ?> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container, Runnable invokeListener) {
|
||||
|
||||
if (records == null || records.count() == 0) {
|
||||
this.logger.error(thrownException, "Called with no records; consumer exception");
|
||||
return;
|
||||
}
|
||||
ErrorHandlingUtils.retryBatch(thrownException, records, consumer, container, invokeListener, this.backOff,
|
||||
this.seeker, this.recoverer, this.logger, getLogLevel());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -33,11 +33,9 @@ import org.springframework.util.backoff.BackOffExecution;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
* @deprecated with no replacement - use {@link DefaultErrorHandler} with an infinite
|
||||
* {@link BackOff}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class SeekToCurrentBatchErrorHandler extends KafkaExceptionLogLevelAware
|
||||
@SuppressWarnings("deprecation")
|
||||
class SeekToCurrentBatchErrorHandler extends KafkaExceptionLogLevelAware
|
||||
implements ContainerAwareBatchErrorHandler {
|
||||
|
||||
private final ThreadLocal<BackOffExecution> backOffs = new ThreadLocal<>(); // Intentionally not static
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
|
||||
/**
|
||||
* An error handler that seeks to the current offset for each topic in the remaining
|
||||
* records. Used to rewind partitions after a message failure so that it can be
|
||||
* replayed.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.0.1
|
||||
* @deprecated in favor of {@link DefaultErrorHandler}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class SeekToCurrentErrorHandler extends FailedRecordProcessor implements ContainerAwareErrorHandler {
|
||||
|
||||
private boolean ackAfterHandle = true;
|
||||
|
||||
/**
|
||||
* Construct an instance with the default recoverer which simply logs the record after
|
||||
* {@value SeekUtils#DEFAULT_MAX_FAILURES} (maxFailures) have occurred for a
|
||||
* topic/partition/offset, with the default back off (9 retries, no delay).
|
||||
* @since 2.2
|
||||
*/
|
||||
public SeekToCurrentErrorHandler() {
|
||||
this(null, SeekUtils.DEFAULT_BACK_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the default recoverer which simply logs the record after
|
||||
* the backOff returns STOP for a topic/partition/offset.
|
||||
* @param backOff the {@link BackOff}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public SeekToCurrentErrorHandler(BackOff backOff) {
|
||||
this(null, backOff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided recoverer which will be called after
|
||||
* {@value SeekUtils#DEFAULT_MAX_FAILURES} (maxFailures) have occurred for a
|
||||
* topic/partition/offset.
|
||||
* @param recoverer the recoverer.
|
||||
* @since 2.2
|
||||
*/
|
||||
public SeekToCurrentErrorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
|
||||
this(recoverer, SeekUtils.DEFAULT_BACK_OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided recoverer which will be called after
|
||||
* the backOff returns STOP for a topic/partition/offset.
|
||||
* @param recoverer the recoverer; if null, the default (logging) recoverer is used.
|
||||
* @param backOff the {@link BackOff}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public SeekToCurrentErrorHandler(@Nullable BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer, BackOff backOff) {
|
||||
super(recoverer, backOff);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* The container must be configured with
|
||||
* {@link org.springframework.kafka.listener.ContainerProperties.AckMode#MANUAL_IMMEDIATE}.
|
||||
* Whether or not the commit is sync or async depends on the container's syncCommits
|
||||
* property.
|
||||
* @param commitRecovered true to commit.
|
||||
*/
|
||||
@Override
|
||||
public void setCommitRecovered(boolean commitRecovered) { // NOSONAR enhanced javadoc
|
||||
super.setCommitRecovered(commitRecovered);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAckAfterHandle() {
|
||||
return this.ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAckAfterHandle(boolean ackAfterHandle) {
|
||||
this.ackAfterHandle = ackAfterHandle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, @Nullable List<ConsumerRecord<?, ?>> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
|
||||
SeekUtils.seekOrRecover(thrownException, records, consumer, container, isCommitRecovered(), // NOSONAR
|
||||
getRecoveryStrategy(records, thrownException), this.logger, getLogLevel());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2021 the original author or authors.
|
||||
* Copyright 2016-2022 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.
|
||||
@@ -85,41 +85,6 @@ public class DelegatingInvocableHandler {
|
||||
|
||||
private final PayloadValidator validator;
|
||||
|
||||
/**
|
||||
* Construct an instance with the supplied handlers for the bean.
|
||||
* @param handlers the handlers.
|
||||
* @param bean the bean.
|
||||
* @param beanExpressionResolver the expression resolver.
|
||||
* @param beanExpressionContext the expression context.
|
||||
* @deprecated in favor of
|
||||
* {@link #DelegatingInvocableHandler(List, InvocableHandlerMethod, Object, BeanExpressionResolver, BeanExpressionContext, BeanFactory, Validator)}
|
||||
*/
|
||||
@Deprecated
|
||||
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers, Object bean,
|
||||
BeanExpressionResolver beanExpressionResolver, BeanExpressionContext beanExpressionContext) {
|
||||
|
||||
this(handlers, null, bean, beanExpressionResolver, beanExpressionContext, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the supplied handlers for the bean.
|
||||
* @param handlers the handlers.
|
||||
* @param defaultHandler the default handler.
|
||||
* @param bean the bean.
|
||||
* @param beanExpressionResolver the resolver.
|
||||
* @param beanExpressionContext the context.
|
||||
* @since 2.1.3
|
||||
* @deprecated in favor of
|
||||
* {@link #DelegatingInvocableHandler(List, InvocableHandlerMethod, Object, BeanExpressionResolver, BeanExpressionContext, BeanFactory, Validator)}
|
||||
*/
|
||||
@Deprecated
|
||||
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers,
|
||||
@Nullable InvocableHandlerMethod defaultHandler,
|
||||
Object bean, BeanExpressionResolver beanExpressionResolver, BeanExpressionContext beanExpressionContext) {
|
||||
|
||||
this(handlers, defaultHandler, bean, beanExpressionResolver, beanExpressionContext, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the supplied handlers for the bean.
|
||||
* @param handlers the handlers.
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016-2021 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
|
||||
*
|
||||
* https://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.listener.adapter;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.kafka.listener.AcknowledgingConsumerAwareMessageListener;
|
||||
import org.springframework.kafka.listener.MessageListener;
|
||||
import org.springframework.kafka.support.Acknowledgment;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.retry.RecoveryCallback;
|
||||
import org.springframework.retry.RetryState;
|
||||
import org.springframework.retry.support.DefaultRetryState;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A retrying message listener adapter for {@link MessageListener}s.
|
||||
*
|
||||
* @param <K> the key type.
|
||||
* @param <V> the value type.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @deprecated since 2.8 - use a suitably configured error handler instead.
|
||||
*
|
||||
*/
|
||||
@Deprecated
|
||||
public class RetryingMessageListenerAdapter<K, V>
|
||||
extends AbstractRetryingMessageListenerAdapter<K, V, MessageListener<K, V>>
|
||||
implements AcknowledgingConsumerAwareMessageListener<K, V> {
|
||||
|
||||
/**
|
||||
* {@link org.springframework.retry.RetryContext} attribute key for an acknowledgment
|
||||
* if the listener is capable of acknowledging.
|
||||
*/
|
||||
public static final String CONTEXT_ACKNOWLEDGMENT = "acknowledgment";
|
||||
|
||||
/**
|
||||
* {@link org.springframework.retry.RetryContext} attribute key for the consumer if
|
||||
* the listener is consumer-aware.
|
||||
*/
|
||||
public static final String CONTEXT_CONSUMER = "consumer";
|
||||
|
||||
/**
|
||||
* {@link org.springframework.retry.RetryContext} attribute key for the record.
|
||||
*/
|
||||
public static final String CONTEXT_RECORD = "record";
|
||||
|
||||
private boolean stateful;
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided template and delegate. The exception will
|
||||
* be thrown to the container after retries are exhausted.
|
||||
* @param messageListener the delegate listener.
|
||||
* @param retryTemplate the template.
|
||||
*/
|
||||
public RetryingMessageListenerAdapter(MessageListener<K, V> messageListener, RetryTemplate retryTemplate) {
|
||||
this(messageListener, retryTemplate, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided template, callback and delegate.
|
||||
* @param messageListener the delegate listener.
|
||||
* @param retryTemplate the template.
|
||||
* @param recoveryCallback the recovery callback; if null, the exception will be
|
||||
* thrown to the container after retries are exhausted.
|
||||
*/
|
||||
public RetryingMessageListenerAdapter(MessageListener<K, V> messageListener, RetryTemplate retryTemplate,
|
||||
@Nullable RecoveryCallback<? extends Object> recoveryCallback) {
|
||||
|
||||
this(messageListener, retryTemplate, recoveryCallback, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided template, callback and delegate. When using
|
||||
* stateful retry, the retry context key is a concatenated String
|
||||
* {@code topic-partition-offset}. A
|
||||
* {@link org.springframework.kafka.listener.SeekToCurrentErrorHandler} is required in
|
||||
* the listener container because stateful retry will throw the exception to the
|
||||
* container for each delivery attempt.
|
||||
* @param messageListener the delegate listener.
|
||||
* @param retryTemplate the template.
|
||||
* @param recoveryCallback the recovery callback; if null, the exception will be
|
||||
* thrown to the container after retries are exhausted.
|
||||
* @param stateful true for stateful retry.
|
||||
* @since 2.1.3
|
||||
*/
|
||||
public RetryingMessageListenerAdapter(MessageListener<K, V> messageListener, RetryTemplate retryTemplate,
|
||||
@Nullable RecoveryCallback<? extends Object> recoveryCallback, boolean stateful) {
|
||||
|
||||
super(messageListener, retryTemplate, recoveryCallback);
|
||||
Assert.notNull(messageListener, "'messageListener' cannot be null");
|
||||
this.stateful = stateful;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(final ConsumerRecord<K, V> record, @Nullable final Acknowledgment acknowledgment,
|
||||
final Consumer<?, ?> consumer) {
|
||||
|
||||
RetryState retryState = null;
|
||||
if (this.stateful) {
|
||||
retryState = new DefaultRetryState(record.topic() + "-" + record.partition() + "-" + record.offset());
|
||||
}
|
||||
getRetryTemplate().execute(context -> {
|
||||
context.setAttribute(CONTEXT_RECORD, record);
|
||||
switch (RetryingMessageListenerAdapter.this.delegateType) {
|
||||
case ACKNOWLEDGING_CONSUMER_AWARE:
|
||||
context.setAttribute(CONTEXT_ACKNOWLEDGMENT, acknowledgment);
|
||||
context.setAttribute(CONTEXT_CONSUMER, consumer);
|
||||
RetryingMessageListenerAdapter.this.delegate.onMessage(record, acknowledgment, consumer);
|
||||
break;
|
||||
case ACKNOWLEDGING:
|
||||
context.setAttribute(CONTEXT_ACKNOWLEDGMENT, acknowledgment);
|
||||
RetryingMessageListenerAdapter.this.delegate.onMessage(record, acknowledgment);
|
||||
break;
|
||||
case CONSUMER_AWARE:
|
||||
context.setAttribute(CONTEXT_CONSUMER, consumer);
|
||||
RetryingMessageListenerAdapter.this.delegate.onMessage(record, consumer);
|
||||
break;
|
||||
case SIMPLE:
|
||||
RetryingMessageListenerAdapter.this.delegate.onMessage(record);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getRecoveryCallback(), retryState);
|
||||
}
|
||||
|
||||
/*
|
||||
* Since the container uses the delegate's type to determine which method to call, we
|
||||
* must implement them all.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<K, V> data) {
|
||||
onMessage(data, null, null); // NOSONAR
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<K, V> data, Acknowledgment acknowledgment) {
|
||||
onMessage(data, acknowledgment, null); // NOSONAR
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<K, V> data, Consumer<?, ?> consumer) {
|
||||
onMessage(data, null, consumer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
* Copyright 2018-2022 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.
|
||||
@@ -81,19 +81,6 @@ public class RetryTopicConfigurationBuilder {
|
||||
private Boolean autoStartDltHandler;
|
||||
|
||||
/* ---------------- DLT Behavior -------------- */
|
||||
/**
|
||||
* Configure a DLT handler method.
|
||||
* @param clazz the class containing the method.
|
||||
* @param methodName the method name.
|
||||
* @return the builder.
|
||||
* @deprecated in favor of {@link #dltHandlerMethod(String, String)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public RetryTopicConfigurationBuilder dltHandlerMethod(Class<?> clazz, String methodName) {
|
||||
this.dltHandlerMethod = RetryTopicConfigurer.createHandlerMethodWith(clazz, methodName);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a DLT handler method.
|
||||
* @param beanName the bean name.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
* Copyright 2018-2022 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.
|
||||
@@ -220,15 +220,14 @@ public class RetryTopicConfigurer {
|
||||
|
||||
private final RetryTopicNamesProviderFactory retryTopicNamesProviderFactory;
|
||||
|
||||
@Deprecated
|
||||
public RetryTopicConfigurer(DestinationTopicProcessor destinationTopicProcessor,
|
||||
ListenerContainerFactoryResolver containerFactoryResolver,
|
||||
ListenerContainerFactoryConfigurer listenerContainerFactoryConfigurer,
|
||||
BeanFactory beanFactory) {
|
||||
|
||||
this(destinationTopicProcessor, containerFactoryResolver, listenerContainerFactoryConfigurer, beanFactory, new SuffixingRetryTopicNamesProviderFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance with the provided properties.
|
||||
* @param destinationTopicProcessor the destination topic processor.
|
||||
* @param containerFactoryResolver the container factory resolver.
|
||||
* @param listenerContainerFactoryConfigurer the container factory configurer.
|
||||
* @param beanFactory the bean factory.
|
||||
* @param retryTopicNamesProviderFactory the retry topic names factory.
|
||||
*/
|
||||
@Autowired
|
||||
public RetryTopicConfigurer(DestinationTopicProcessor destinationTopicProcessor,
|
||||
ListenerContainerFactoryResolver containerFactoryResolver,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -55,14 +55,6 @@ public abstract class RetryTopicInternalBeanNames {
|
||||
public static final String DEAD_LETTER_PUBLISHING_RECOVERER_FACTORY_BEAN_NAME =
|
||||
"internalDeadLetterPublishingRecovererProvider";
|
||||
|
||||
/**
|
||||
* {@link DeadLetterPublishingRecovererFactory} bean name.
|
||||
* @deprecated in favor of {@link #DEAD_LETTER_PUBLISHING_RECOVERER_FACTORY_BEAN_NAME}
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String DEAD_LETTER_PUBLISHING_RECOVERER_PROVIDER_NAME =
|
||||
DEAD_LETTER_PUBLISHING_RECOVERER_FACTORY_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* {@link DestinationTopicContainer} bean name.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2021 the original author or authors.
|
||||
* Copyright 2019-2022 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.
|
||||
@@ -60,19 +60,6 @@ public final class JacksonUtils {
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for {@link ObjectMapper} instances with registered well-known modules
|
||||
* and disabled {@link MapperFeature#DEFAULT_VIEW_INCLUSION} and
|
||||
* {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} features.
|
||||
* @param classLoader the {@link ClassLoader} for modules to register.
|
||||
* @return the {@link ObjectMapper} instance.
|
||||
* @deprecated since 2.7.5 in favor of {@link #enhancedObjectMapper()}
|
||||
*/
|
||||
@Deprecated
|
||||
public static ObjectMapper enhancedObjectMapper(ClassLoader classLoader) {
|
||||
return enhancedObjectMapper();
|
||||
}
|
||||
|
||||
private static void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper) {
|
||||
objectMapper.registerModule(new JacksonMimeTypeModule());
|
||||
if (JDK8_MODULE_PRESENT) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-2021 the original author or authors.
|
||||
* Copyright 2018-2022 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.
|
||||
@@ -40,28 +40,6 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
public class ErrorHandlingDeserializer<T> implements Deserializer<T> {
|
||||
|
||||
/**
|
||||
* Header name for deserialization exceptions.
|
||||
* @deprecated in favor of {@link SerializationUtils#DESERIALIZER_EXCEPTION_HEADER_PREFIX}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String KEY_DESERIALIZER_EXCEPTION_HEADER_PREFIX =
|
||||
SerializationUtils.DESERIALIZER_EXCEPTION_HEADER_PREFIX;
|
||||
|
||||
/**
|
||||
* Header name for deserialization exceptions.
|
||||
* @deprecated in favor of {@link SerializationUtils#KEY_DESERIALIZER_EXCEPTION_HEADER}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String KEY_DESERIALIZER_EXCEPTION_HEADER = SerializationUtils.KEY_DESERIALIZER_EXCEPTION_HEADER;
|
||||
|
||||
/**
|
||||
* Header name for deserialization exceptions.
|
||||
* @deprecated in favor of {@link SerializationUtils#VALUE_DESERIALIZER_EXCEPTION_HEADER}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String VALUE_DESERIALIZER_EXCEPTION_HEADER = SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER;
|
||||
|
||||
/**
|
||||
* Supplier for a T when deserialization fails.
|
||||
*/
|
||||
|
||||
@@ -98,9 +98,9 @@ import org.springframework.kafka.core.ProducerFactory;
|
||||
import org.springframework.kafka.event.ListenerContainerIdleEvent;
|
||||
import org.springframework.kafka.event.ListenerContainerNoLongerIdleEvent;
|
||||
import org.springframework.kafka.listener.AbstractConsumerSeekAware;
|
||||
import org.springframework.kafka.listener.CommonErrorHandler;
|
||||
import org.springframework.kafka.listener.CommonLoggingErrorHandler;
|
||||
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
|
||||
import org.springframework.kafka.listener.ConsumerAwareErrorHandler;
|
||||
import org.springframework.kafka.listener.ConsumerAwareListenerErrorHandler;
|
||||
import org.springframework.kafka.listener.ConsumerAwareRebalanceListener;
|
||||
import org.springframework.kafka.listener.ConsumerSeekAware;
|
||||
@@ -144,7 +144,6 @@ import org.springframework.messaging.handler.annotation.support.MethodArgumentNo
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
@@ -310,13 +309,8 @@ public class EnableKafkaIntegrationTests {
|
||||
.isInstanceOf(FilteringMessageListenerAdapter.class);
|
||||
assertThat(KafkaTestUtils.getPropertyValue(manualContainer, "containerProperties.messageListener.ackDiscarded",
|
||||
Boolean.class)).isTrue();
|
||||
assertThat(KafkaTestUtils.getPropertyValue(manualContainer, "containerProperties.messageListener.delegate"))
|
||||
.isInstanceOf(org.springframework.kafka.listener.adapter.RetryingMessageListenerAdapter.class);
|
||||
assertThat(KafkaTestUtils
|
||||
.getPropertyValue(manualContainer, "containerProperties.messageListener.delegate.recoveryCallback")
|
||||
.getClass().getName()).contains("EnableKafkaIntegrationTests$Config$");
|
||||
assertThat(KafkaTestUtils.getPropertyValue(manualContainer,
|
||||
"containerProperties.messageListener.delegate.delegate"))
|
||||
"containerProperties.messageListener.delegate"))
|
||||
.isInstanceOf(MessagingMessageListenerAdapter.class);
|
||||
assertThat(this.listener.listen4Consumer).isNotNull();
|
||||
assertThat(this.listener.listen4Consumer).isSameAs(KafkaTestUtils.getPropertyValue(KafkaTestUtils
|
||||
@@ -1043,9 +1037,16 @@ public class EnableKafkaIntegrationTests {
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setRecordFilterStrategy(recordFilter());
|
||||
factory.setReplyTemplate(partitionZeroReplyTemplate());
|
||||
factory.setErrorHandler((ConsumerAwareErrorHandler) (t, d, c) -> {
|
||||
this.globalErrorThrowable = t;
|
||||
c.seek(new org.apache.kafka.common.TopicPartition(d.topic(), d.partition()), d.offset());
|
||||
factory.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleRecord(Exception thrownException, ConsumerRecord<?, ?> record,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
|
||||
globalErrorThrowable = thrownException;
|
||||
consumer.seek(new org.apache.kafka.common.TopicPartition(record.topic(), record.partition()),
|
||||
record.offset());
|
||||
}
|
||||
});
|
||||
factory.getContainerProperties().setMicrometerTags(Collections.singletonMap("extraTag", "foo"));
|
||||
factory.setMessageConverter(new RecordMessageConverter() {
|
||||
@@ -1133,7 +1134,7 @@ public class EnableKafkaIntegrationTests {
|
||||
ConcurrentKafkaListenerContainerFactory<byte[], String> factory =
|
||||
new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(bytesStringConsumerFactory());
|
||||
factory.setRecordInterceptor(record -> {
|
||||
factory.setRecordInterceptor((record, consumer) -> {
|
||||
this.intercepted = true;
|
||||
return record;
|
||||
});
|
||||
@@ -1236,8 +1237,6 @@ public class EnableKafkaIntegrationTests {
|
||||
props.setPollTimeout(50L);
|
||||
factory.setRecordFilterStrategy(manualFilter());
|
||||
factory.setAckDiscarded(true);
|
||||
factory.setRetryTemplate(new RetryTemplate());
|
||||
factory.setRecoveryCallback(c -> null);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -1279,7 +1278,7 @@ public class EnableKafkaIntegrationTests {
|
||||
factory.setConsumerFactory(configuredConsumerFactory("clientIdViaProps4"));
|
||||
ContainerProperties props = factory.getContainerProperties();
|
||||
props.setAckMode(AckMode.RECORD);
|
||||
factory.setErrorHandler(listen16ErrorHandler());
|
||||
factory.setCommonErrorHandler(listen16ErrorHandler());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -1586,11 +1585,18 @@ public class EnableKafkaIntegrationTests {
|
||||
private final CountDownLatch listen16ErrorLatch = new CountDownLatch(1);
|
||||
|
||||
@Bean
|
||||
public ConsumerAwareErrorHandler listen16ErrorHandler() {
|
||||
return (e, r, c) -> {
|
||||
listen16Exception = e;
|
||||
listen16Message = r.value();
|
||||
listen16ErrorLatch.countDown();
|
||||
public CommonErrorHandler listen16ErrorHandler() {
|
||||
return new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleRecord(Exception thrownException, ConsumerRecord<?, ?> record,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
|
||||
listen16Exception = thrownException;
|
||||
listen16Message = record.value();
|
||||
listen16ErrorLatch.countDown();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018-2021 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
|
||||
*
|
||||
* https://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.annotation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.KafkaException.Level;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.config.KafkaListenerContainerFactory;
|
||||
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.listener.DefaultErrorHandler;
|
||||
import org.springframework.kafka.listener.MessageListenerContainer;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.1.3
|
||||
*
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@EmbeddedKafka(topics = "sr1", partitions = 1)
|
||||
public class StatefulRetryTests {
|
||||
|
||||
private static final String DEFAULT_TEST_GROUP_ID = "statefulRetry";
|
||||
|
||||
@Autowired
|
||||
private Config config;
|
||||
|
||||
@Autowired
|
||||
private KafkaTemplate<Integer, String> template;
|
||||
|
||||
@Test
|
||||
public void testStatefulRetry() throws Exception {
|
||||
this.template.send("sr1", "foo");
|
||||
assertThat(this.config.latch1.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.latch2.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.seekPerformed).isTrue();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public static class Config {
|
||||
|
||||
private final CountDownLatch latch1 = new CountDownLatch(3);
|
||||
|
||||
private final CountDownLatch latch2 = new CountDownLatch(1);
|
||||
|
||||
private boolean seekPerformed;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory(EmbeddedKafkaBroker embeddedKafka) {
|
||||
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
|
||||
new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(consumerFactory(embeddedKafka));
|
||||
DefaultErrorHandler errorHandler = new DefaultErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleRemaining(Exception thrownException, List<ConsumerRecord<?, ?>> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
Config.this.seekPerformed = true;
|
||||
super.handleRemaining(thrownException, records, consumer, container);
|
||||
}
|
||||
|
||||
};
|
||||
errorHandler.setLogLevel(Level.INFO);
|
||||
factory.setCommonErrorHandler(errorHandler);
|
||||
factory.setStatefulRetry(true);
|
||||
factory.setRetryTemplate(new RetryTemplate());
|
||||
factory.setRecoveryCallback(c -> {
|
||||
this.latch2.countDown();
|
||||
return null;
|
||||
});
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultKafkaConsumerFactory<Integer, String> consumerFactory(EmbeddedKafkaBroker embeddedKafka) {
|
||||
return new DefaultKafkaConsumerFactory<>(consumerConfigs(embeddedKafka));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, Object> consumerConfigs(EmbeddedKafkaBroker embeddedKafka) {
|
||||
Map<String, Object> consumerProps =
|
||||
KafkaTestUtils.consumerProps(DEFAULT_TEST_GROUP_ID, "false", embeddedKafka);
|
||||
return consumerProps;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KafkaTemplate<Integer, String> template(EmbeddedKafkaBroker embeddedKafka) {
|
||||
return new KafkaTemplate<>(producerFactory(embeddedKafka));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProducerFactory<Integer, String> producerFactory(EmbeddedKafkaBroker embeddedKafka) {
|
||||
return new DefaultKafkaProducerFactory<>(producerConfigs(embeddedKafka));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, Object> producerConfigs(EmbeddedKafkaBroker embeddedKafka) {
|
||||
return KafkaTestUtils.producerProps(embeddedKafka);
|
||||
}
|
||||
|
||||
@KafkaListener(id = "retry", topics = "sr1", groupId = "sr1")
|
||||
public void listen1(String in) {
|
||||
this.latch1.countDown();
|
||||
throw new RuntimeException("retry");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -125,7 +125,7 @@ public class KafkaTemplateTransactionTests {
|
||||
t.sendDefault("baz", "qux");
|
||||
t.sendOffsetsToTransaction(Collections.singletonMap(
|
||||
new TopicPartition(LOCAL_TX_IN_TOPIC, singleRecord.partition()),
|
||||
new OffsetAndMetadata(singleRecord.offset() + 1L)), "testLocalTx");
|
||||
new OffsetAndMetadata(singleRecord.offset() + 1L)), consumer.groupMetadata());
|
||||
assertThat(KafkaTestUtils.getPropertyValue(
|
||||
KafkaTestUtils.getPropertyValue(template, "producers", ThreadLocal.class).get(),
|
||||
"delegate.transactionManager.transactionalId")).isEqualTo("my.transaction.0");
|
||||
|
||||
@@ -147,9 +147,21 @@ public class ConcurrentMessageListenerContainerMockTests {
|
||||
containerProperties);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<MessageListenerContainer> errorContainer = new AtomicReference<>();
|
||||
container.setErrorHandler((ContainerAwareErrorHandler) (thrownException, records, consumer1, ec) -> {
|
||||
errorContainer.set(ec);
|
||||
latch.countDown();
|
||||
container.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public boolean remainingRecords() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOtherException(Exception thrownException, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container, boolean batchListener) {
|
||||
|
||||
errorContainer.set(container);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
});
|
||||
container.start();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
@@ -562,10 +574,9 @@ public class ConcurrentMessageListenerContainerMockTests {
|
||||
}).given(tm).rollback(any());
|
||||
ConcurrentMessageListenerContainer container = new ConcurrentMessageListenerContainer(consumerFactory,
|
||||
containerProperties);
|
||||
CountDownLatch interceptedLatch = new CountDownLatch(2);
|
||||
CountDownLatch successCalled = new CountDownLatch(1);
|
||||
CountDownLatch failureCalled = new CountDownLatch(1);
|
||||
container.setRecordInterceptor(new ConsumerAwareRecordInterceptor() {
|
||||
container.setRecordInterceptor(new RecordInterceptor() {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
@@ -158,7 +159,7 @@ public class ConcurrentMessageListenerContainerTests {
|
||||
}
|
||||
});
|
||||
CountDownLatch intercepted = new CountDownLatch(4);
|
||||
container.setRecordInterceptor(record -> {
|
||||
container.setRecordInterceptor((record, consumer) -> {
|
||||
intercepted.countDown();
|
||||
return record.value().equals("baz") ? null : record;
|
||||
});
|
||||
@@ -620,8 +621,15 @@ public class ConcurrentMessageListenerContainerTests {
|
||||
new ConcurrentMessageListenerContainer<>(cf, containerProps);
|
||||
container.setConcurrency(2);
|
||||
container.setBeanName("testException");
|
||||
container.setErrorHandler((thrownException, record) -> catchError.set(true));
|
||||
container.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleRecord(Exception thrownException, ConsumerRecord<?, ?> record, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
|
||||
catchError.set(true);
|
||||
}
|
||||
});
|
||||
container.start();
|
||||
ContainerTestUtils.waitForAssignment(container, embeddedKafka.getPartitionsPerTopic());
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.kafka.KafkaException;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.7.4
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ConditionalDelegatingErrorHandlerTests {
|
||||
|
||||
@Test
|
||||
void testRecordDelegates() {
|
||||
var def = mock(ContainerAwareErrorHandler.class);
|
||||
var one = mock(ContainerAwareErrorHandler.class);
|
||||
var two = mock(ContainerAwareErrorHandler.class);
|
||||
var three = mock(ContainerAwareErrorHandler.class);
|
||||
var eh = new ConditionalDelegatingErrorHandler(def);
|
||||
eh.setErrorHandlers(Map.of(IllegalStateException.class, one, IllegalArgumentException.class, two));
|
||||
eh.addDelegate(RuntimeException.class, three);
|
||||
|
||||
eh.handle(wrap(new IOException()), Collections.emptyList(), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class));
|
||||
verify(def).handle(any(), any(), any(), any());
|
||||
eh.handle(wrap(new KafkaException("test")), Collections.emptyList(), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class));
|
||||
verify(three).handle(any(), any(), any(), any());
|
||||
eh.handle(wrap(new IllegalArgumentException()), Collections.emptyList(), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class));
|
||||
verify(two).handle(any(), any(), any(), any());
|
||||
eh.handle(wrap(new IllegalStateException()), Collections.emptyList(), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class));
|
||||
verify(one).handle(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchDelegates() {
|
||||
var def = mock(ContainerAwareBatchErrorHandler.class);
|
||||
var one = mock(ContainerAwareBatchErrorHandler.class);
|
||||
var two = mock(ContainerAwareBatchErrorHandler.class);
|
||||
var three = mock(ContainerAwareBatchErrorHandler.class);
|
||||
var eh = new ConditionalDelegatingBatchErrorHandler(def);
|
||||
eh.setErrorHandlers(Map.of(IllegalStateException.class, one, IllegalArgumentException.class, two));
|
||||
eh.addDelegate(RuntimeException.class, three);
|
||||
|
||||
eh.handle(wrap(new IOException()), mock(ConsumerRecords.class), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class), mock(Runnable.class));
|
||||
verify(def).handle(any(), any(), any(), any(), any());
|
||||
eh.handle(wrap(new KafkaException("test")), mock(ConsumerRecords.class), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class), mock(Runnable.class));
|
||||
verify(three).handle(any(), any(), any(), any(), any());
|
||||
eh.handle(wrap(new IllegalArgumentException()), mock(ConsumerRecords.class), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class), mock(Runnable.class));
|
||||
verify(two).handle(any(), any(), any(), any(), any());
|
||||
eh.handle(wrap(new IllegalStateException()), mock(ConsumerRecords.class), mock(Consumer.class),
|
||||
mock(MessageListenerContainer.class), mock(Runnable.class));
|
||||
verify(one).handle(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
private Exception wrap(Exception ex) {
|
||||
return new ListenerExecutionFailedException("test", ex);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.apache.kafka.common.header.internals.RecordHeaders;
|
||||
import org.apache.kafka.common.record.TimestampType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
public class ContainerStoppingBatchErrorHandlerTests {
|
||||
|
||||
private static final String CONTAINER_ID = "container";
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Autowired
|
||||
private Consumer consumer;
|
||||
|
||||
@Autowired
|
||||
private Config config;
|
||||
|
||||
@Autowired
|
||||
private KafkaListenerEndpointRegistry registry;
|
||||
|
||||
/*
|
||||
* Deliver 6 records from three partitions, fail on the second record second
|
||||
* partition.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void stopContainerAfterException() throws Exception {
|
||||
assertThat(this.config.deliveryLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.pollLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.errorLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.closeLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
MessageListenerContainer container = this.registry.getListenerContainer(CONTAINER_ID);
|
||||
assertThat(container.isRunning()).isFalse();
|
||||
InOrder inOrder = inOrder(this.consumer);
|
||||
inOrder.verify(this.consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
inOrder.verify(this.consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
inOrder.verify(this.consumer).unsubscribe();
|
||||
inOrder.verify(this.consumer).close();
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
assertThat(this.registry.getListenerContainers()).hasSize(1);
|
||||
Collection<MessageListenerContainer> containers = this.registry.getAllListenerContainers();
|
||||
assertThat(containers).hasSize(2);
|
||||
Iterator<MessageListenerContainer> iterator = containers.iterator();
|
||||
MessageListenerContainer one = iterator.next();
|
||||
MessageListenerContainer two = iterator.next();
|
||||
assertThat(one).isNotSameAs(two);
|
||||
assertThat(two).isSameAs(this.config.springManagedContainer());
|
||||
assertThat(one.getListenerId()).isEqualTo(CONTAINER_ID);
|
||||
assertThat(two.getListenerId()).isEqualTo("springManagedContainer");
|
||||
assertThat(this.config.customized).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public static class Config {
|
||||
|
||||
private final CountDownLatch pollLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch deliveryLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch errorLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch commitLatch = new CountDownLatch(3);
|
||||
|
||||
private volatile int customized;
|
||||
|
||||
@KafkaListener(id = CONTAINER_ID, topics = "foo")
|
||||
public void foo(List<String> in) {
|
||||
this.deliveryLatch.countDown();
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
@Bean
|
||||
public ConsumerFactory consumerFactory() {
|
||||
ConsumerFactory consumerFactory = mock(ConsumerFactory.class);
|
||||
final Consumer consumer = consumer();
|
||||
given(consumerFactory.createConsumer(CONTAINER_ID, "", "-0", KafkaTestUtils.defaultPropertyOverrides()))
|
||||
.willReturn(consumer);
|
||||
return consumerFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Bean
|
||||
public Consumer consumer() {
|
||||
final Consumer consumer = mock(Consumer.class);
|
||||
final TopicPartition topicPartition0 = new TopicPartition("foo", 0);
|
||||
final TopicPartition topicPartition1 = new TopicPartition("foo", 1);
|
||||
final TopicPartition topicPartition2 = new TopicPartition("foo", 2);
|
||||
willAnswer(i -> {
|
||||
((ConsumerRebalanceListener) i.getArgument(1)).onPartitionsAssigned(
|
||||
Collections.singletonList(topicPartition1));
|
||||
return null;
|
||||
}).given(consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
Map<TopicPartition, List<ConsumerRecord>> records1 = new LinkedHashMap<>();
|
||||
records1.put(topicPartition0, Arrays.asList(
|
||||
new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 0, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "bar",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
records1.put(topicPartition1, Arrays.asList(
|
||||
new ConsumerRecord("foo", 1, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "baz",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 1, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "qux",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
records1.put(topicPartition2, Arrays.asList(
|
||||
new ConsumerRecord("foo", 2, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "fiz",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 2, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "buz",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
final AtomicInteger which = new AtomicInteger();
|
||||
willAnswer(i -> {
|
||||
this.pollLatch.countDown();
|
||||
switch (which.getAndIncrement()) {
|
||||
case 0:
|
||||
return new ConsumerRecords(records1);
|
||||
default:
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return new ConsumerRecords(Collections.emptyMap());
|
||||
}
|
||||
}).given(consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
willAnswer(i -> {
|
||||
this.commitLatch.countDown();
|
||||
return null;
|
||||
}).given(consumer).commitSync(anyMap(), any());
|
||||
willAnswer(i -> {
|
||||
this.closeLatch.countDown();
|
||||
return null;
|
||||
}).given(consumer).close();
|
||||
return consumer;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
|
||||
@Bean
|
||||
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setBatchErrorHandler(new ContainerStoppingBatchErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, ConsumerRecords<?, ?> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
RuntimeException exception = null;
|
||||
try {
|
||||
super.handle(thrownException, records, consumer, container);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
exception = e;
|
||||
}
|
||||
errorLatch.countDown();
|
||||
throw exception;
|
||||
}
|
||||
|
||||
});
|
||||
factory.setBatchListener(true);
|
||||
factory.setContainerCustomizer(container -> this.customized++);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentMessageListenerContainer<String, String> springManagedContainer() {
|
||||
ConcurrentMessageListenerContainer<String, String> container = kafkaListenerContainerFactory()
|
||||
.createContainer("springManaged");
|
||||
container.setAutoStartup(false);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.apache.kafka.common.header.internals.RecordHeaders;
|
||||
import org.apache.kafka.common.record.TimestampType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
public class ContainerStoppingErrorHandlerBatchModeTests {
|
||||
|
||||
private static final String CONTAINER_ID = "container";
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Autowired
|
||||
private Consumer consumer;
|
||||
|
||||
@Autowired
|
||||
private Config config;
|
||||
|
||||
@Autowired
|
||||
private KafkaListenerEndpointRegistry registry;
|
||||
|
||||
/*
|
||||
* Deliver 6 records from three partitions, fail on the second record second
|
||||
* partition.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void stopContainerAfterException() throws Exception {
|
||||
assertThat(this.config.deliveryLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.pollLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.errorLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.closeLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
MessageListenerContainer container = this.registry.getListenerContainer(CONTAINER_ID);
|
||||
assertThat(container.isRunning()).isFalse();
|
||||
InOrder inOrder = inOrder(this.consumer);
|
||||
inOrder.verify(this.consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
inOrder.verify(this.consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
inOrder.verify(this.consumer).unsubscribe();
|
||||
inOrder.verify(this.consumer).close();
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
assertThat(this.config.count).isEqualTo(4);
|
||||
assertThat(this.config.contents.toArray()).isEqualTo(new String[]
|
||||
{ "foo", "bar", "baz", "qux" });
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public static class Config {
|
||||
|
||||
private final List<String> contents = new ArrayList<>();
|
||||
|
||||
private final CountDownLatch pollLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch deliveryLatch = new CountDownLatch(3);
|
||||
|
||||
private final CountDownLatch errorLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
|
||||
private int count;
|
||||
|
||||
@KafkaListener(id = CONTAINER_ID, topics = "foo")
|
||||
public void foo(String in) {
|
||||
this.contents.add(in);
|
||||
this.deliveryLatch.countDown();
|
||||
if (++this.count == 4) { // part 1, offset 1, first time
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
@Bean
|
||||
public ConsumerFactory consumerFactory() {
|
||||
ConsumerFactory consumerFactory = mock(ConsumerFactory.class);
|
||||
final Consumer consumer = consumer();
|
||||
given(consumerFactory.createConsumer(CONTAINER_ID, "", "-0", KafkaTestUtils.defaultPropertyOverrides()))
|
||||
.willReturn(consumer);
|
||||
return consumerFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Bean
|
||||
public Consumer consumer() {
|
||||
final Consumer consumer = mock(Consumer.class);
|
||||
final TopicPartition topicPartition0 = new TopicPartition("foo", 0);
|
||||
final TopicPartition topicPartition1 = new TopicPartition("foo", 1);
|
||||
final TopicPartition topicPartition2 = new TopicPartition("foo", 2);
|
||||
willAnswer(i -> {
|
||||
((ConsumerRebalanceListener) i.getArgument(1)).onPartitionsAssigned(
|
||||
Collections.singletonList(topicPartition1));
|
||||
return null;
|
||||
}).given(consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
Map<TopicPartition, List<ConsumerRecord>> records1 = new LinkedHashMap<>();
|
||||
records1.put(topicPartition0, Arrays.asList(
|
||||
new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 0, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "bar",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
records1.put(topicPartition1, Arrays.asList(
|
||||
new ConsumerRecord("foo", 1, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "baz",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 1, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "qux",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
records1.put(topicPartition2, Arrays.asList(
|
||||
new ConsumerRecord("foo", 2, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "fiz",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 2, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "buz",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
final AtomicInteger which = new AtomicInteger();
|
||||
willAnswer(i -> {
|
||||
this.pollLatch.countDown();
|
||||
switch (which.getAndIncrement()) {
|
||||
case 0:
|
||||
return new ConsumerRecords(records1);
|
||||
default:
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return new ConsumerRecords(Collections.emptyMap());
|
||||
}
|
||||
}).given(consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
willAnswer(i -> {
|
||||
this.closeLatch.countDown();
|
||||
return null;
|
||||
}).given(consumer).close();
|
||||
return consumer;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
|
||||
@Bean
|
||||
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setErrorHandler(new ContainerStoppingErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
RuntimeException exception = null;
|
||||
try {
|
||||
super.handle(thrownException, records, consumer, container);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
exception = e;
|
||||
}
|
||||
errorLatch.countDown();
|
||||
throw exception;
|
||||
}
|
||||
|
||||
});
|
||||
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.BATCH);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.apache.kafka.common.header.internals.RecordHeaders;
|
||||
import org.apache.kafka.common.record.TimestampType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.1
|
||||
*
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
public class ContainerStoppingErrorHandlerRecordModeTests {
|
||||
|
||||
private static final String CONTAINER_ID = "container";
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Autowired
|
||||
private Consumer consumer;
|
||||
|
||||
@Autowired
|
||||
private Config config;
|
||||
|
||||
@Autowired
|
||||
private KafkaListenerEndpointRegistry registry;
|
||||
|
||||
/*
|
||||
* Deliver 6 records from three partitions, fail on the second record second
|
||||
* partition.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void stopContainerAfterException() throws Exception {
|
||||
assertThat(this.config.deliveryLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.commitLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.pollLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.errorLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.config.closeLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
MessageListenerContainer container = this.registry.getListenerContainer(CONTAINER_ID);
|
||||
assertThat(container.isRunning()).isFalse();
|
||||
InOrder inOrder = inOrder(this.consumer);
|
||||
inOrder.verify(this.consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
inOrder.verify(this.consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
inOrder.verify(this.consumer).commitSync(
|
||||
Collections.singletonMap(new TopicPartition("foo", 0), new OffsetAndMetadata(1L)),
|
||||
Duration.ofSeconds(60));
|
||||
inOrder.verify(this.consumer).commitSync(
|
||||
Collections.singletonMap(new TopicPartition("foo", 0), new OffsetAndMetadata(2L)),
|
||||
Duration.ofSeconds(60));
|
||||
inOrder.verify(this.consumer).commitSync(
|
||||
Collections.singletonMap(new TopicPartition("foo", 1), new OffsetAndMetadata(1L)),
|
||||
Duration.ofSeconds(60));
|
||||
inOrder.verify(this.consumer).unsubscribe();
|
||||
inOrder.verify(this.consumer).close();
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
assertThat(this.config.count).isEqualTo(4);
|
||||
assertThat(this.config.contents.toArray()).isEqualTo(new String[]
|
||||
{ "foo", "bar", "baz", "qux" });
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public static class Config {
|
||||
|
||||
private final List<String> contents = new ArrayList<>();
|
||||
|
||||
private final CountDownLatch pollLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch deliveryLatch = new CountDownLatch(3);
|
||||
|
||||
private final CountDownLatch errorLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
|
||||
private final CountDownLatch commitLatch = new CountDownLatch(3);
|
||||
|
||||
private int count;
|
||||
|
||||
@KafkaListener(id = CONTAINER_ID, topics = "foo")
|
||||
public void foo(String in) {
|
||||
this.contents.add(in);
|
||||
this.deliveryLatch.countDown();
|
||||
if (++this.count == 4) { // part 1, offset 1, first time
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
@Bean
|
||||
public ConsumerFactory consumerFactory() {
|
||||
ConsumerFactory consumerFactory = mock(ConsumerFactory.class);
|
||||
final Consumer consumer = consumer();
|
||||
given(consumerFactory.createConsumer(CONTAINER_ID, "", "-0", KafkaTestUtils.defaultPropertyOverrides()))
|
||||
.willReturn(consumer);
|
||||
return consumerFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Bean
|
||||
public Consumer consumer() {
|
||||
final Consumer consumer = mock(Consumer.class);
|
||||
final TopicPartition topicPartition0 = new TopicPartition("foo", 0);
|
||||
final TopicPartition topicPartition1 = new TopicPartition("foo", 1);
|
||||
final TopicPartition topicPartition2 = new TopicPartition("foo", 2);
|
||||
willAnswer(i -> {
|
||||
((ConsumerRebalanceListener) i.getArgument(1)).onPartitionsAssigned(
|
||||
Collections.singletonList(topicPartition1));
|
||||
return null;
|
||||
}).given(consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
Map<TopicPartition, List<ConsumerRecord>> records1 = new LinkedHashMap<>();
|
||||
records1.put(topicPartition0, Arrays.asList(
|
||||
new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 0, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "bar",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
records1.put(topicPartition1, Arrays.asList(
|
||||
new ConsumerRecord("foo", 1, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "baz",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 1, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "qux",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
records1.put(topicPartition2, Arrays.asList(
|
||||
new ConsumerRecord("foo", 2, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "fiz",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 2, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "buz",
|
||||
new RecordHeaders(), Optional.empty())));
|
||||
final AtomicInteger which = new AtomicInteger();
|
||||
willAnswer(i -> {
|
||||
this.pollLatch.countDown();
|
||||
switch (which.getAndIncrement()) {
|
||||
case 0:
|
||||
return new ConsumerRecords(records1);
|
||||
default:
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return new ConsumerRecords(Collections.emptyMap());
|
||||
}
|
||||
}).given(consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
willAnswer(i -> {
|
||||
this.commitLatch.countDown();
|
||||
return null;
|
||||
}).given(consumer).commitSync(anyMap(), any());
|
||||
willAnswer(i -> {
|
||||
this.closeLatch.countDown();
|
||||
return null;
|
||||
}).given(consumer).close();
|
||||
return consumer;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
|
||||
@Bean
|
||||
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setErrorHandler(new ContainerStoppingErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
RuntimeException exception = null;
|
||||
try {
|
||||
super.handle(thrownException, records, consumer, container);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
exception = e;
|
||||
}
|
||||
errorLatch.countDown();
|
||||
throw exception;
|
||||
}
|
||||
|
||||
});
|
||||
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2021 the original author or authors.
|
||||
* Copyright 2019-2022 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.
|
||||
@@ -23,7 +23,6 @@ import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -76,19 +75,18 @@ public class DefaultAfterRollbackProcessorTests {
|
||||
Consumer<String, String> consumer = mock(Consumer.class);
|
||||
given(consumer.groupMetadata()).willReturn(new ConsumerGroupMetadata("foo"));
|
||||
MessageListenerContainer container = mock(MessageListenerContainer.class);
|
||||
processor.process(records, consumer, container, illegalState, true, EOSMode.V1);
|
||||
processor.process(records, consumer, container, illegalState, true, EOSMode.V2);
|
||||
processor.process(records, consumer, container,
|
||||
new DeserializationException("intended", null, false, illegalState), true, EOSMode.V1);
|
||||
verify(template).sendOffsetsToTransaction(anyMap());
|
||||
verify(template, never()).sendOffsetsToTransaction(anyMap(), any(ConsumerGroupMetadata.class));
|
||||
new DeserializationException("intended", null, false, illegalState), true, EOSMode.V2);
|
||||
verify(template).sendOffsetsToTransaction(anyMap(), any(ConsumerGroupMetadata.class));
|
||||
assertThat(recovered.get()).isSameAs(record1);
|
||||
processor.addNotRetryableExceptions(IllegalStateException.class);
|
||||
recovered.set(null);
|
||||
recovererShouldFail.set(true);
|
||||
processor.process(records, consumer, container, illegalState, true, EOSMode.V1);
|
||||
verify(template, times(1)).sendOffsetsToTransaction(anyMap()); // recovery failed
|
||||
processor.process(records, consumer, container, illegalState, true, EOSMode.V2);
|
||||
verify(template, times(1)).sendOffsetsToTransaction(anyMap(), any(ConsumerGroupMetadata.class));
|
||||
verify(template, times(1)).sendOffsetsToTransaction(anyMap(), any(ConsumerGroupMetadata.class)); // recovery failed
|
||||
processor.process(records, consumer, container, illegalState, true, EOSMode.V2);
|
||||
verify(template, times(2)).sendOffsetsToTransaction(anyMap(), any(ConsumerGroupMetadata.class));
|
||||
assertThat(recovered.get()).isSameAs(record1);
|
||||
InOrder inOrder = inOrder(consumer);
|
||||
inOrder.verify(consumer).seek(new TopicPartition("foo", 0), 0L); // not recovered so seek
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
@@ -156,15 +157,22 @@ public class ErrorHandlingDeserializerTests {
|
||||
ConcurrentKafkaListenerContainerFactory<String, String> factory =
|
||||
new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(cf);
|
||||
factory.setErrorHandler((t, r) -> {
|
||||
if (r.value() == null && t.getCause() instanceof DeserializationException) {
|
||||
this.valueErrorCount.incrementAndGet();
|
||||
this.headers = ((DeserializationException) t.getCause()).getHeaders();
|
||||
factory.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleRecord(Exception t, ConsumerRecord<?, ?> r,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
|
||||
if (r.value() == null && t.getCause() instanceof DeserializationException) {
|
||||
valueErrorCount.incrementAndGet();
|
||||
headers = ((DeserializationException) t.getCause()).getHeaders();
|
||||
}
|
||||
else if (r.key() == null && t.getCause() instanceof DeserializationException) {
|
||||
keyErrorCount.incrementAndGet();
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
else if (r.key() == null && t.getCause() instanceof DeserializationException) {
|
||||
this.keyErrorCount.incrementAndGet();
|
||||
}
|
||||
this.latch.countDown();
|
||||
|
||||
});
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ import org.springframework.kafka.test.condition.EmbeddedKafkaCondition;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
import org.springframework.kafka.test.utils.ContainerTestUtils;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
@@ -1384,10 +1385,15 @@ public class KafkaMessageListenerContainerTests {
|
||||
KafkaMessageListenerContainer<Integer, String> container = spyOnContainer(
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps), stubbingComplete);
|
||||
container.setBeanName("testBatchListenerErrors");
|
||||
container.setBatchErrorHandler((t, messages) -> {
|
||||
for (int i = 0; i < messages.count(); i++) {
|
||||
latch.countDown();
|
||||
container.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleBatch(Exception thrownException, ConsumerRecords<?, ?> data, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container, Runnable invokeListener) {
|
||||
|
||||
data.forEach(rec -> latch.countDown());
|
||||
}
|
||||
|
||||
});
|
||||
container.start();
|
||||
Consumer<?, ?> containerConsumer = spyOnConsumer(container);
|
||||
@@ -3015,8 +3021,14 @@ public class KafkaMessageListenerContainerTests {
|
||||
KafkaMessageListenerContainer<Integer, String> container =
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
final CountDownLatch ehl = new CountDownLatch(1);
|
||||
container.setErrorHandler((r, t) -> {
|
||||
ehl.countDown();
|
||||
container.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleOtherException(Exception thrownException, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container, boolean batchListener) {
|
||||
|
||||
ehl.countDown();
|
||||
}
|
||||
});
|
||||
container.start();
|
||||
assertThat(ehl.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
@@ -3025,8 +3037,15 @@ public class KafkaMessageListenerContainerTests {
|
||||
});
|
||||
container = new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
final CountDownLatch behl = new CountDownLatch(1);
|
||||
container.setBatchErrorHandler((r, t) -> {
|
||||
behl.countDown();
|
||||
container.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handleOtherException(Exception thrownException, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container, boolean batchListener) {
|
||||
|
||||
behl.countDown();
|
||||
}
|
||||
|
||||
});
|
||||
first.set(true);
|
||||
container.start();
|
||||
@@ -3559,7 +3578,10 @@ public class KafkaMessageListenerContainerTests {
|
||||
RecordInterceptor<Integer, String> recordInterceptor = spy(new RecordInterceptor<Integer, String>() {
|
||||
|
||||
@Override
|
||||
public ConsumerRecord<Integer, String> intercept(ConsumerRecord<Integer, String> record) {
|
||||
@Nullable
|
||||
public ConsumerRecord<Integer, String> intercept(ConsumerRecord<Integer, String> record,
|
||||
Consumer<Integer, String> consumer) {
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
@@ -3633,7 +3655,10 @@ public class KafkaMessageListenerContainerTests {
|
||||
RecordInterceptor<Integer, String> recordInterceptor = spy(new RecordInterceptor<Integer, String>() {
|
||||
|
||||
@Override
|
||||
public ConsumerRecord<Integer, String> intercept(ConsumerRecord<Integer, String> record) {
|
||||
@Nullable
|
||||
public ConsumerRecord<Integer, String> intercept(ConsumerRecord<Integer, String> record,
|
||||
Consumer<Integer, String> consumer) {
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||
import org.springframework.kafka.core.KafkaOperations;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.event.ConsumerStoppedEvent;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.condition.EmbeddedKafkaCondition;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.5
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@EmbeddedKafka(topics = {
|
||||
RecoveringBatchErrorHandlerIntegrationTests.topic1,
|
||||
RecoveringBatchErrorHandlerIntegrationTests.topic1DLT,
|
||||
RecoveringBatchErrorHandlerIntegrationTests.topic2,
|
||||
RecoveringBatchErrorHandlerIntegrationTests.topic2DLT })
|
||||
public class RecoveringBatchErrorHandlerIntegrationTests {
|
||||
|
||||
public static final String topic1 = "recoverTopic1";
|
||||
|
||||
public static final String topic1DLT = "recoverTopic1.DLT";
|
||||
|
||||
public static final String topic2 = "recoverTopic2";
|
||||
|
||||
public static final String topic2DLT = "recoverTopic2.DLT";
|
||||
|
||||
private static EmbeddedKafkaBroker embeddedKafka;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
embeddedKafka = EmbeddedKafkaCondition.getBroker();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoveryAndDlt() throws InterruptedException {
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("recoverBatch", "false", embeddedKafka);
|
||||
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1000);
|
||||
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500);
|
||||
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props);
|
||||
ContainerProperties containerProps = new ContainerProperties(topic1);
|
||||
containerProps.setPollTimeout(10_000);
|
||||
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
DefaultKafkaProducerFactory<Object, Object> pf = new DefaultKafkaProducerFactory<>(senderProps);
|
||||
final KafkaOperations<Object, Object> template = new KafkaTemplate<>(pf);
|
||||
final CountDownLatch latch = new CountDownLatch(3);
|
||||
List<ConsumerRecord<Integer, String>> data = new ArrayList<>();
|
||||
containerProps.setMessageListener((BatchMessageListener<Integer, String>) records -> {
|
||||
data.addAll(records);
|
||||
latch.countDown();
|
||||
records.forEach(rec -> {
|
||||
if (rec.value().equals("baz")) {
|
||||
throw new BatchListenerFailedException("fail", rec);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
KafkaMessageListenerContainer<Integer, String> container =
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
container.setBeanName("recoverBatch");
|
||||
DeadLetterPublishingRecoverer recoverer =
|
||||
new DeadLetterPublishingRecoverer(template,
|
||||
(r, e) -> new TopicPartition(topic1DLT, r.partition()));
|
||||
RecoveringBatchErrorHandler errorHandler = new RecoveringBatchErrorHandler(recoverer, new FixedBackOff(0L, 1));
|
||||
container.setBatchErrorHandler(errorHandler);
|
||||
final CountDownLatch stopLatch = new CountDownLatch(1);
|
||||
container.setApplicationEventPublisher(e -> {
|
||||
if (e instanceof ConsumerStoppedEvent) {
|
||||
stopLatch.countDown();
|
||||
}
|
||||
});
|
||||
container.start();
|
||||
|
||||
template.send(topic1, 0, 0, "foo");
|
||||
template.send(topic1, 0, 0, "bar");
|
||||
template.send(topic1, 0, 0, "baz");
|
||||
template.send(topic1, 0, 0, "qux");
|
||||
template.send(topic1, 0, 0, "fiz");
|
||||
template.send(topic1, 0, 0, "buz");
|
||||
assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(data).hasSize(13);
|
||||
assertThat(data)
|
||||
.extracting(rec -> rec.value())
|
||||
.containsExactly(
|
||||
"foo", "bar", "baz", "qux", "fiz", "buz",
|
||||
"baz", "qux", "fiz", "buz",
|
||||
"qux", "fiz", "buz");
|
||||
|
||||
props.put(ConsumerConfig.GROUP_ID_CONFIG, "recoverBatch.dlt");
|
||||
DefaultKafkaConsumerFactory<Integer, String> dltcf = new DefaultKafkaConsumerFactory<>(props);
|
||||
Consumer<Integer, String> consumer = dltcf.createConsumer();
|
||||
embeddedKafka.consumeFromAnEmbeddedTopic(consumer, topic1DLT);
|
||||
ConsumerRecord<Integer, String> dltRecord = KafkaTestUtils.getSingleRecord(consumer, topic1DLT);
|
||||
assertThat(dltRecord.value()).isEqualTo("baz");
|
||||
container.stop();
|
||||
pf.destroy();
|
||||
consumer.close();
|
||||
assertThat(stopLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoveryFails() throws InterruptedException {
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("recoverBatch2", "false", embeddedKafka);
|
||||
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1000);
|
||||
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500);
|
||||
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props);
|
||||
ContainerProperties containerProps = new ContainerProperties(topic2);
|
||||
containerProps.setPollTimeout(10_000);
|
||||
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
DefaultKafkaProducerFactory<Object, Object> pf = new DefaultKafkaProducerFactory<>(senderProps);
|
||||
final KafkaOperations<Object, Object> template = new KafkaTemplate<>(pf);
|
||||
final CountDownLatch latch = new CountDownLatch(4);
|
||||
List<ConsumerRecord<Integer, String>> data = new ArrayList<>();
|
||||
containerProps.setMessageListener((BatchMessageListener<Integer, String>) records -> {
|
||||
data.addAll(records);
|
||||
latch.countDown();
|
||||
records.forEach(rec -> {
|
||||
if (rec.value().equals("baz")) {
|
||||
throw new BatchListenerFailedException("fail", rec);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
KafkaMessageListenerContainer<Integer, String> container =
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
container.setBeanName("recoverBatch");
|
||||
final AtomicBoolean failRecovery = new AtomicBoolean(true);
|
||||
DeadLetterPublishingRecoverer recoverer =
|
||||
new DeadLetterPublishingRecoverer(template,
|
||||
(r, e) -> new TopicPartition(topic2DLT, r.partition())) {
|
||||
|
||||
@Override
|
||||
public void accept(ConsumerRecord<?, ?> record, Consumer<?, ?> consumer, Exception exception) {
|
||||
if (failRecovery.getAndSet(false)) {
|
||||
throw new RuntimeException("Recovery failed");
|
||||
}
|
||||
super.accept(record, consumer, exception);
|
||||
}
|
||||
|
||||
};
|
||||
RecoveringBatchErrorHandler errorHandler = new RecoveringBatchErrorHandler(recoverer, new FixedBackOff(0L, 1));
|
||||
errorHandler.setResetStateOnRecoveryFailure(false);
|
||||
container.setBatchErrorHandler(errorHandler);
|
||||
final CountDownLatch stopLatch = new CountDownLatch(1);
|
||||
container.setApplicationEventPublisher(e -> {
|
||||
if (e instanceof ConsumerStoppedEvent) {
|
||||
stopLatch.countDown();
|
||||
}
|
||||
});
|
||||
container.start();
|
||||
|
||||
template.send(topic2, 0, 0, "foo");
|
||||
template.send(topic2, 0, 0, "bar");
|
||||
template.send(topic2, 0, 0, "baz");
|
||||
template.send(topic2, 0, 0, "qux");
|
||||
template.send(topic2, 0, 0, "fiz");
|
||||
template.send(topic2, 0, 0, "buz");
|
||||
assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(data).hasSize(17);
|
||||
assertThat(data)
|
||||
.extracting(rec -> rec.value())
|
||||
.containsExactly(
|
||||
"foo", "bar", "baz", "qux", "fiz", "buz",
|
||||
"baz", "qux", "fiz", "buz",
|
||||
// recovery failed first time so we get the whole batch again
|
||||
"baz", "qux", "fiz", "buz",
|
||||
"qux", "fiz", "buz");
|
||||
|
||||
props.put(ConsumerConfig.GROUP_ID_CONFIG, "recoverBatch2.dlt");
|
||||
DefaultKafkaConsumerFactory<Integer, String> dltcf = new DefaultKafkaConsumerFactory<>(props);
|
||||
Consumer<Integer, String> consumer = dltcf.createConsumer();
|
||||
embeddedKafka.consumeFromAnEmbeddedTopic(consumer, topic2DLT);
|
||||
ConsumerRecord<Integer, String> dltRecord = KafkaTestUtils.getSingleRecord(consumer, topic2DLT);
|
||||
assertThat(dltRecord.value()).isEqualTo("baz");
|
||||
container.stop();
|
||||
pf.destroy();
|
||||
consumer.close();
|
||||
assertThat(stopLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.apache.kafka.common.header.internals.RecordHeaders;
|
||||
import org.apache.kafka.common.record.TimestampType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.KafkaException;
|
||||
import org.springframework.kafka.annotation.EnableKafka;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 2.5
|
||||
*
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
@SuppressWarnings("deprecation")
|
||||
public class RecoveringBatchErrorHandlerTests {
|
||||
|
||||
private static final String CONTAINER_ID = "container";
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Autowired
|
||||
private Consumer consumer;
|
||||
|
||||
@Autowired
|
||||
private Config config;
|
||||
|
||||
@Autowired
|
||||
private KafkaListenerEndpointRegistry registry;
|
||||
|
||||
/*
|
||||
* Deliver 6 records; record "baz" always fails.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void seekAndRecover() throws Exception {
|
||||
assertThat(this.config.deliveryLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
this.registry.stop();
|
||||
assertThat(this.config.closeLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
InOrder inOrder = inOrder(this.consumer);
|
||||
inOrder.verify(this.consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
inOrder.verify(this.consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
Map<TopicPartition, OffsetAndMetadata> offsets = new LinkedHashMap<>();
|
||||
offsets.put(new TopicPartition("foo", 0), new OffsetAndMetadata(2L));
|
||||
inOrder.verify(this.consumer).commitSync(offsets, Duration.ofMinutes(1));
|
||||
inOrder.verify(this.consumer).seek(new TopicPartition("foo", 0), 2L);
|
||||
inOrder.verify(this.consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
inOrder.verify(this.consumer).seek(new TopicPartition("foo", 0), 3L);
|
||||
offsets = new LinkedHashMap<>();
|
||||
offsets.put(new TopicPartition("foo", 0), new OffsetAndMetadata(3L));
|
||||
inOrder.verify(this.consumer).commitSync(offsets, Duration.ofMinutes(1));
|
||||
inOrder.verify(this.consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
offsets = new LinkedHashMap<>();
|
||||
offsets.put(new TopicPartition("foo", 0), new OffsetAndMetadata(6L));
|
||||
inOrder.verify(this.consumer).commitSync(offsets, Duration.ofMinutes(1));
|
||||
assertThat(config.received).containsExactly(
|
||||
"foo", "bar", "baz", "qux", "fiz", "buz",
|
||||
"baz", "qux", "fiz", "buz",
|
||||
"qux", "fiz", "buz");
|
||||
assertThat(this.config.recovered.value()).isEqualTo("baz");
|
||||
assertThat(this.config.listenerFailed.value()).isEqualTo("baz");
|
||||
assertThat(this.config.listenerRecovered.value()).isEqualTo("baz");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void outOfRange() {
|
||||
Consumer mockConsumer = mock(Consumer.class);
|
||||
RecoveringBatchErrorHandler beh = new RecoveringBatchErrorHandler(new FixedBackOff(0, 0));
|
||||
TopicPartition tp = new TopicPartition("foo", 0);
|
||||
ConsumerRecords<?, ?> records = new ConsumerRecords(Collections.singletonMap(tp,
|
||||
Collections.singletonList(
|
||||
new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()))));
|
||||
assertThatExceptionOfType(KafkaException.class).isThrownBy(() ->
|
||||
beh.handle(new ListenerExecutionFailedException("",
|
||||
new BatchListenerFailedException("", 2)), records, mockConsumer, null))
|
||||
.withMessageStartingWith("Seek to current after exception");
|
||||
verify(mockConsumer).seek(tp, 0L);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void wrappedBatchListenerFailedException() {
|
||||
Consumer mockConsumer = mock(Consumer.class);
|
||||
InOrder inOrder = inOrder(mockConsumer);
|
||||
|
||||
MessageListenerContainer container = mock(MessageListenerContainer.class);
|
||||
ContainerProperties containerProperties = mock(ContainerProperties.class);
|
||||
given(container.getContainerProperties()).willReturn(containerProperties);
|
||||
given(containerProperties.isSyncCommits()).willReturn(true);
|
||||
|
||||
Duration syncCommitTimeout = Duration.ofMillis(1000);
|
||||
given(containerProperties.getSyncCommitTimeout()).willReturn(syncCommitTimeout);
|
||||
|
||||
RecoveringBatchErrorHandler beh = new RecoveringBatchErrorHandler(new FixedBackOff(0, 0));
|
||||
TopicPartition tp = new TopicPartition("foo", 0);
|
||||
ConsumerRecords<?, ?> records = new ConsumerRecords(Collections.singletonMap(tp,
|
||||
Arrays.asList(
|
||||
new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 0, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "bar",
|
||||
new RecordHeaders(), Optional.empty()),
|
||||
new ConsumerRecord("foo", 0, 2L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "baz",
|
||||
new RecordHeaders(), Optional.empty()))
|
||||
));
|
||||
assertThatExceptionOfType(KafkaException.class).isThrownBy(() ->
|
||||
beh.handle(new ListenerExecutionFailedException("", new MessagingException("",
|
||||
new BatchListenerFailedException("", 1))), records, mockConsumer, container)
|
||||
);
|
||||
|
||||
Map<TopicPartition, OffsetAndMetadata> offsets = new LinkedHashMap<>();
|
||||
offsets.put(tp, new OffsetAndMetadata(1L));
|
||||
inOrder.verify(mockConsumer).commitSync(offsets, syncCommitTimeout);
|
||||
|
||||
inOrder.verify(mockConsumer).seek(tp, 2);
|
||||
|
||||
offsets = new LinkedHashMap<>();
|
||||
offsets.put(tp, new OffsetAndMetadata(2L));
|
||||
inOrder.verify(mockConsumer).commitSync(offsets, syncCommitTimeout);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Test
|
||||
void missingRecord() {
|
||||
Consumer mockConsumer = mock(Consumer.class);
|
||||
RecoveringBatchErrorHandler beh = new RecoveringBatchErrorHandler(new FixedBackOff(0, 0));
|
||||
TopicPartition tp = new TopicPartition("foo", 0);
|
||||
ConsumerRecords<?, ?> records = new ConsumerRecords(Collections.singletonMap(tp,
|
||||
Collections.singletonList(
|
||||
new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()))));
|
||||
assertThatExceptionOfType(KafkaException.class).isThrownBy(() ->
|
||||
beh.handle(new ListenerExecutionFailedException("",
|
||||
new BatchListenerFailedException("",
|
||||
new ConsumerRecord("bar", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()))),
|
||||
records, mockConsumer, null))
|
||||
.withMessageStartingWith("Seek to current after exception");
|
||||
verify(mockConsumer).seek(tp, 0L);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableKafka
|
||||
public static class Config {
|
||||
|
||||
final CountDownLatch deliveryLatch = new CountDownLatch(3);
|
||||
|
||||
final CountDownLatch closeLatch = new CountDownLatch(1);
|
||||
|
||||
final List<String> received = new ArrayList<>();
|
||||
|
||||
volatile ConsumerRecord<?, ?> recovered;
|
||||
|
||||
volatile ConsumerRecord<?, ?> listenerFailed;
|
||||
|
||||
volatile ConsumerRecord<?, ?> listenerRecovered;
|
||||
|
||||
@KafkaListener(id = CONTAINER_ID, topics = "foo")
|
||||
public void foo(List<String> in) {
|
||||
received.addAll(in);
|
||||
this.deliveryLatch.countDown();
|
||||
for (int i = 0; i < in.size(); i++) {
|
||||
if (in.get(i).equals("baz")) {
|
||||
throw new BatchListenerFailedException("fail", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
@Bean
|
||||
public ConsumerFactory consumerFactory() {
|
||||
ConsumerFactory consumerFactory = mock(ConsumerFactory.class);
|
||||
final Consumer consumer = consumer();
|
||||
given(consumerFactory.createConsumer(CONTAINER_ID, "", "-0", KafkaTestUtils.defaultPropertyOverrides()))
|
||||
.willReturn(consumer);
|
||||
return consumerFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Bean
|
||||
public Consumer consumer() {
|
||||
final Consumer consumer = mock(Consumer.class);
|
||||
final TopicPartition topicPartition = new TopicPartition("foo", 0);
|
||||
willAnswer(i -> {
|
||||
((ConsumerRebalanceListener) i.getArgument(1)).onPartitionsAssigned(
|
||||
Collections.singletonList(topicPartition));
|
||||
return null;
|
||||
}).given(consumer).subscribe(any(Collection.class), any(ConsumerRebalanceListener.class));
|
||||
List<ConsumerRecord> records1 = new ArrayList<>();
|
||||
records1.add(new ConsumerRecord("foo", 0, 0L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "foo",
|
||||
new RecordHeaders(), Optional.empty()));
|
||||
records1.add(new ConsumerRecord("foo", 0, 1L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "bar",
|
||||
new RecordHeaders(), Optional.empty()));
|
||||
List<ConsumerRecord> records2 = new ArrayList<>();
|
||||
records2.add(new ConsumerRecord("foo", 0, 2L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "baz",
|
||||
new RecordHeaders(), Optional.empty()));
|
||||
List<ConsumerRecord> records3 = new ArrayList<>();
|
||||
records3.add(new ConsumerRecord("foo", 0, 3L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "qux",
|
||||
new RecordHeaders(), Optional.empty()));
|
||||
records3.add(new ConsumerRecord("foo", 0, 4L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "fiz",
|
||||
new RecordHeaders(), Optional.empty()));
|
||||
records3.add(new ConsumerRecord("foo", 0, 5L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "buz",
|
||||
new RecordHeaders(), Optional.empty()));
|
||||
List<ConsumerRecord> recordsOne = new ArrayList<>(records1);
|
||||
recordsOne.addAll(records2);
|
||||
recordsOne.addAll(records3);
|
||||
List<ConsumerRecord> recordsTwo = new ArrayList<>(records2);
|
||||
recordsTwo.addAll(records3);
|
||||
Map<TopicPartition, List<ConsumerRecord>> crs1 = Collections.singletonMap(topicPartition, recordsOne);
|
||||
Map<TopicPartition, List<ConsumerRecord>> crs2 = Collections.singletonMap(topicPartition, recordsTwo);
|
||||
Map<TopicPartition, List<ConsumerRecord>> crs3 = Collections.singletonMap(topicPartition, records3);
|
||||
final AtomicInteger which = new AtomicInteger();
|
||||
willAnswer(i -> {
|
||||
switch (which.getAndIncrement()) {
|
||||
case 0:
|
||||
return new ConsumerRecords(crs1);
|
||||
case 1:
|
||||
return new ConsumerRecords(crs2);
|
||||
case 2:
|
||||
return new ConsumerRecords(crs3);
|
||||
default:
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return new ConsumerRecords(Collections.emptyMap());
|
||||
}
|
||||
}).given(consumer).poll(Duration.ofMillis(ContainerProperties.DEFAULT_POLL_TIMEOUT));
|
||||
willAnswer(i -> {
|
||||
this.closeLatch.countDown();
|
||||
return null;
|
||||
}).given(consumer).close();
|
||||
return consumer;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Bean
|
||||
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
RecoveringBatchErrorHandler errorHandler = new RecoveringBatchErrorHandler((cr, ex) -> this.recovered = cr,
|
||||
new FixedBackOff(0, 1));
|
||||
errorHandler.setRetryListeners(new RetryListener() {
|
||||
|
||||
@Override
|
||||
public void failedDelivery(ConsumerRecord<?, ?> record, Exception ex, int deliveryAttempt) {
|
||||
Config.this.listenerFailed = record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recovered(ConsumerRecord<?, ?> record, Exception ex) {
|
||||
Config.this.listenerRecovered = record;
|
||||
}
|
||||
|
||||
});
|
||||
factory.setBatchErrorHandler(errorHandler);
|
||||
factory.setBatchListener(true);
|
||||
factory.getContainerProperties().setSubBatchPerPartition(false);
|
||||
factory.setMissingTopicsFatal(false);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -199,11 +199,17 @@ public class RemainingRecordsErrorHandlerTests {
|
||||
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.setErrorHandler(new RemainingRecordsErrorHandler() {
|
||||
factory.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records,
|
||||
Consumer<?, ?> consumer) {
|
||||
public boolean remainingRecords() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemaining(Exception thrownException, List<ConsumerRecord<?, ?>> records,
|
||||
Consumer<?, ?> consumer, MessageListenerContainer container) {
|
||||
|
||||
remaining.addAll(records.stream()
|
||||
.map(r -> (String) r.value())
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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
|
||||
*
|
||||
* https://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.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
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.ConsumerRecord;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.apache.kafka.common.errors.SerializationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.kafka.KafkaException;
|
||||
import org.springframework.kafka.support.converter.ConversionException;
|
||||
import org.springframework.kafka.support.serializer.DeserializationException;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.3
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SeekToCurrentErrorHandlerTests {
|
||||
|
||||
@Test
|
||||
public void testClassifier() {
|
||||
ListenerUtils.setLogOnlyMetadata(true);
|
||||
AtomicReference<ConsumerRecord<?, ?>> recovered = new AtomicReference<>();
|
||||
AtomicBoolean recovererShouldFail = new AtomicBoolean(false);
|
||||
SeekToCurrentErrorHandler handler = new SeekToCurrentErrorHandler((r, t) -> {
|
||||
if (recovererShouldFail.getAndSet(false)) {
|
||||
throw new RuntimeException("test recoverer failure");
|
||||
}
|
||||
recovered.set(r);
|
||||
});
|
||||
AtomicInteger failedDeliveryAttempt = new AtomicInteger();
|
||||
AtomicReference<Exception> recoveryFailureEx = new AtomicReference<>();
|
||||
AtomicBoolean isRecovered = new AtomicBoolean();
|
||||
handler.setRetryListeners(new RetryListener() {
|
||||
|
||||
@Override
|
||||
public void failedDelivery(ConsumerRecord<?, ?> record, Exception ex, int deliveryAttempt) {
|
||||
failedDeliveryAttempt.set(deliveryAttempt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recovered(ConsumerRecord<?, ?> record, Exception ex) {
|
||||
isRecovered.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recoveryFailed(ConsumerRecord<?, ?> record, Exception original, Exception failure) {
|
||||
recoveryFailureEx.set(failure);
|
||||
}
|
||||
|
||||
});
|
||||
ConsumerRecord<String, String> record1 = new ConsumerRecord<>("foo", 0, 0L, "foo", "bar");
|
||||
ConsumerRecord<String, String> record2 = new ConsumerRecord<>("foo", 1, 1L, "foo", "bar");
|
||||
List<ConsumerRecord<?, ?>> records = Arrays.asList(record1, record2);
|
||||
IllegalStateException illegalState = new IllegalStateException();
|
||||
Consumer<?, ?> consumer = mock(Consumer.class);
|
||||
assertThatExceptionOfType(KafkaException.class).isThrownBy(() -> handler.handle(illegalState, records,
|
||||
consumer, mock(MessageListenerContainer.class)))
|
||||
.withCause(illegalState);
|
||||
handler.handle(new DeserializationException("intended", null, false, illegalState), records,
|
||||
consumer, mock(MessageListenerContainer.class));
|
||||
assertThat(recovered.get()).isSameAs(record1);
|
||||
recovered.set(null);
|
||||
handler.handle(new ConversionException("intended", null), records,
|
||||
consumer, mock(MessageListenerContainer.class));
|
||||
assertThat(recovered.get()).isSameAs(record1);
|
||||
handler.addNotRetryableExceptions(IllegalStateException.class);
|
||||
recovered.set(null);
|
||||
recovererShouldFail.set(true);
|
||||
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() ->
|
||||
handler.handle(illegalState, records, consumer, mock(MessageListenerContainer.class)));
|
||||
handler.handle(illegalState, records, consumer, mock(MessageListenerContainer.class));
|
||||
assertThat(recovered.get()).isSameAs(record1);
|
||||
InOrder inOrder = inOrder(consumer);
|
||||
inOrder.verify(consumer).seek(new TopicPartition("foo", 0), 0L); // not recovered so seek
|
||||
inOrder.verify(consumer, times(3)).seek(new TopicPartition("foo", 1), 1L);
|
||||
inOrder.verify(consumer).seek(new TopicPartition("foo", 0), 0L); // recovery failed
|
||||
inOrder.verify(consumer, times(2)).seek(new TopicPartition("foo", 1), 1L);
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
assertThat(failedDeliveryAttempt.get()).isEqualTo(1);
|
||||
assertThat(recoveryFailureEx.get())
|
||||
.isInstanceOf(RuntimeException.class)
|
||||
.extracting(ex -> ex.getMessage())
|
||||
.isEqualTo("test recoverer failure");
|
||||
assertThat(isRecovered.get()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializationException() {
|
||||
SeekToCurrentErrorHandler handler = new SeekToCurrentErrorHandler();
|
||||
SerializationException thrownException = new SerializationException();
|
||||
assertThatIllegalStateException().isThrownBy(() -> handler.handle(thrownException, null, null, null))
|
||||
.withCause(thrownException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNotRetryableWithNoRecords() {
|
||||
SeekToCurrentErrorHandler handler = new SeekToCurrentErrorHandler();
|
||||
ClassCastException thrownException = new ClassCastException();
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
() -> handler.handle(thrownException, Collections.emptyList(), null, null))
|
||||
.withCause(thrownException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEarlyExitBackOff() {
|
||||
SeekToCurrentErrorHandler handler = new SeekToCurrentErrorHandler(new FixedBackOff(1, 10_000));
|
||||
Consumer<?, ?> consumer = mock(Consumer.class);
|
||||
ConsumerRecord<String, String> record1 = new ConsumerRecord<>("foo", 0, 0L, "foo", "bar");
|
||||
ConsumerRecord<String, String> record2 = new ConsumerRecord<>("foo", 1, 1L, "foo", "bar");
|
||||
List<ConsumerRecord<?, ?>> records = Arrays.asList(record1, record2);
|
||||
IllegalStateException illegalState = new IllegalStateException();
|
||||
MessageListenerContainer container = mock(MessageListenerContainer.class);
|
||||
given(container.isRunning()).willReturn(false);
|
||||
long t1 = System.currentTimeMillis();
|
||||
assertThatExceptionOfType(KafkaException.class).isThrownBy(() -> handler.handle(illegalState,
|
||||
records, consumer, container));
|
||||
assertThat(System.currentTimeMillis() < t1 + 5_000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoEarlyExitBackOff() {
|
||||
SeekToCurrentErrorHandler handler = new SeekToCurrentErrorHandler(new FixedBackOff(1, 200));
|
||||
Consumer<?, ?> consumer = mock(Consumer.class);
|
||||
ConsumerRecord<String, String> record1 = new ConsumerRecord<>("foo", 0, 0L, "foo", "bar");
|
||||
ConsumerRecord<String, String> record2 = new ConsumerRecord<>("foo", 1, 1L, "foo", "bar");
|
||||
List<ConsumerRecord<?, ?>> records = Arrays.asList(record1, record2);
|
||||
IllegalStateException illegalState = new IllegalStateException();
|
||||
MessageListenerContainer container = mock(MessageListenerContainer.class);
|
||||
given(container.isRunning()).willReturn(true);
|
||||
long t1 = System.currentTimeMillis();
|
||||
assertThatExceptionOfType(KafkaException.class).isThrownBy(() -> handler.handle(illegalState,
|
||||
records, consumer, container));
|
||||
assertThat(System.currentTimeMillis() >= t1 + 200);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -237,7 +237,7 @@ public class SeekToCurrentOnErrorRecordModeTests {
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
factory.getContainerProperties().setAckMode(AckMode.RECORD);
|
||||
factory.getContainerProperties().setDeliveryAttemptHeader(true);
|
||||
factory.setRecordInterceptor(record -> {
|
||||
factory.setRecordInterceptor((record, consumer) -> {
|
||||
Config.this.deliveryAttempt = record.headers().lastHeader(KafkaHeaders.DELIVERY_ATTEMPT);
|
||||
return record;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -59,7 +59,6 @@ import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||
import org.springframework.kafka.listener.ContainerProperties.EOSMode;
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker;
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
@@ -155,27 +154,6 @@ public class SubBatchPerPartitionTests {
|
||||
.isEqualTo(Boolean.TRUE);
|
||||
container.stop();
|
||||
|
||||
containerProps = new ContainerProperties("sbpp");
|
||||
containerProps.setMessageListener(mock(MessageListener.class));
|
||||
containerProps.setTransactionManager(mock(PlatformTransactionManager.class));
|
||||
containerProps.setEosMode(EOSMode.V1);
|
||||
container = new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
container.start();
|
||||
assertThat(KafkaTestUtils.getPropertyValue(container, "listenerConsumer.subBatchPerPartition"))
|
||||
.isEqualTo(Boolean.TRUE);
|
||||
container.stop();
|
||||
|
||||
containerProps = new ContainerProperties("sbpp");
|
||||
containerProps.setMessageListener(mock(MessageListener.class));
|
||||
containerProps.setTransactionManager(mock(PlatformTransactionManager.class));
|
||||
containerProps.setEosMode(EOSMode.V2);
|
||||
container = new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
container.start();
|
||||
assertThat(KafkaTestUtils.getPropertyValue(container, "listenerConsumer.subBatchPerPartition"))
|
||||
.isEqualTo(Boolean.FALSE);
|
||||
container.stop();
|
||||
|
||||
// default is BETA
|
||||
containerProps = new ContainerProperties("sbpp");
|
||||
containerProps.setMessageListener(mock(MessageListener.class));
|
||||
containerProps.setTransactionManager(mock(PlatformTransactionManager.class));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -145,22 +145,22 @@ public class TransactionalContainerTests {
|
||||
|
||||
@Test
|
||||
public void testConsumeAndProduceTransactionKTM() throws Exception {
|
||||
testConsumeAndProduceTransactionGuts(false, AckMode.RECORD, EOSMode.V1);
|
||||
testConsumeAndProduceTransactionGuts(false, AckMode.RECORD, EOSMode.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumeAndProduceTransactionHandleError() throws Exception {
|
||||
testConsumeAndProduceTransactionGuts(true, AckMode.RECORD, EOSMode.V1);
|
||||
testConsumeAndProduceTransactionGuts(true, AckMode.RECORD, EOSMode.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumeAndProduceTransactionKTMManual() throws Exception {
|
||||
testConsumeAndProduceTransactionGuts(false, AckMode.MANUAL_IMMEDIATE, EOSMode.V1);
|
||||
testConsumeAndProduceTransactionGuts(false, AckMode.MANUAL_IMMEDIATE, EOSMode.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumeAndProduceTransactionKTM_BETA() throws Exception {
|
||||
testConsumeAndProduceTransactionGuts(false, AckMode.RECORD, EOSMode.V1);
|
||||
testConsumeAndProduceTransactionGuts(false, AckMode.RECORD, EOSMode.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -257,7 +257,8 @@ public class TransactionalContainerTests {
|
||||
KafkaMessageListenerContainer container = new KafkaMessageListenerContainer<>(cf, props);
|
||||
container.setBeanName("commit");
|
||||
if (handleError) {
|
||||
container.setErrorHandler((e, data) -> { });
|
||||
container.setCommonErrorHandler(new CommonErrorHandler() {
|
||||
});
|
||||
}
|
||||
CountDownLatch stopEventLatch = new CountDownLatch(1);
|
||||
AtomicReference<ConsumerStoppedEvent> stopEvent = new AtomicReference<>();
|
||||
@@ -271,14 +272,8 @@ public class TransactionalContainerTests {
|
||||
assertThat(closeLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
InOrder inOrder = inOrder(producer);
|
||||
inOrder.verify(producer).beginTransaction();
|
||||
if (eosMode.equals(EOSMode.V1)) {
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(0)), "group");
|
||||
}
|
||||
else {
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(0)), consumerGroupMetadata);
|
||||
}
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(0)), consumerGroupMetadata);
|
||||
if (stopWhenFenced) {
|
||||
assertThat(stopEventLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(stopEvent.get().getReason()).isEqualTo(Reason.FENCED);
|
||||
@@ -290,14 +285,8 @@ public class TransactionalContainerTests {
|
||||
ArgumentCaptor<ProducerRecord> captor = ArgumentCaptor.forClass(ProducerRecord.class);
|
||||
inOrder.verify(producer).send(captor.capture(), any(Callback.class));
|
||||
assertThat(captor.getValue()).isEqualTo(new ProducerRecord("bar", "baz"));
|
||||
if (eosMode.equals(EOSMode.V1)) {
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(1)), "group");
|
||||
}
|
||||
else {
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(1)), consumerGroupMetadata);
|
||||
}
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(1)), consumerGroupMetadata);
|
||||
inOrder.verify(producer).commitTransaction();
|
||||
inOrder.verify(producer).close(any());
|
||||
container.stop();
|
||||
@@ -491,10 +480,11 @@ public class TransactionalContainerTests {
|
||||
props.setGroupId("group");
|
||||
props.setTransactionManager(new SomeOtherTransactionManager());
|
||||
final KafkaTemplate template = new KafkaTemplate(pf);
|
||||
ConsumerGroupMetadata meta = mock(ConsumerGroupMetadata.class);
|
||||
props.setMessageListener((MessageListener<String, String>) m -> {
|
||||
template.send("bar", "baz");
|
||||
template.sendOffsetsToTransaction(Collections.singletonMap(new TopicPartition(m.topic(), m.partition()),
|
||||
new OffsetAndMetadata(m.offset() + 1)));
|
||||
new OffsetAndMetadata(m.offset() + 1)), meta);
|
||||
});
|
||||
KafkaMessageListenerContainer container = new KafkaMessageListenerContainer<>(cf, props);
|
||||
container.setBeanName("commit");
|
||||
@@ -508,7 +498,7 @@ public class TransactionalContainerTests {
|
||||
inOrder.verify(producer).send(captor.capture(), any(Callback.class));
|
||||
assertThat(captor.getValue()).isEqualTo(new ProducerRecord("bar", "baz"));
|
||||
inOrder.verify(producer).sendOffsetsToTransaction(Collections.singletonMap(topicPartition,
|
||||
new OffsetAndMetadata(1)), "group");
|
||||
new OffsetAndMetadata(1)), meta);
|
||||
inOrder.verify(producer).commitTransaction();
|
||||
inOrder.verify(producer).close(any());
|
||||
container.stop();
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2019 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
|
||||
*
|
||||
* https://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.listener.adapter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.kafka.listener.AcknowledgingConsumerAwareMessageListener;
|
||||
import org.springframework.kafka.listener.AcknowledgingMessageListener;
|
||||
import org.springframework.kafka.listener.ConsumerAwareMessageListener;
|
||||
import org.springframework.kafka.support.Acknowledgment;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 2.0
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class RetryingMessageListenerAdapterTests {
|
||||
|
||||
@Test
|
||||
public void testRecoveryCallbackSimple() {
|
||||
final AtomicReference<RetryContext> context = new AtomicReference<>();
|
||||
RetryingMessageListenerAdapter<String, String> adapter = new RetryingMessageListenerAdapter<>(
|
||||
r -> {
|
||||
throw new RuntimeException();
|
||||
}, new RetryTemplate(), c -> {
|
||||
context.set(c);
|
||||
return null;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
ConsumerRecord<String, String> record = mock(ConsumerRecord.class);
|
||||
adapter.onMessage(record, mock(Acknowledgment.class), mock(Consumer.class));
|
||||
assertThat(context.get()).isNotNull();
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_ACKNOWLEDGMENT)).isNull();
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_RECORD)).isSameAs(record);
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_CONSUMER)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecoveryCallbackConsumerOnly() {
|
||||
final AtomicReference<RetryContext> context = new AtomicReference<>();
|
||||
RetryingMessageListenerAdapter<String, String> adapter = new RetryingMessageListenerAdapter<>(
|
||||
(ConsumerAwareMessageListener<String, String>) (r, c) -> {
|
||||
throw new RuntimeException();
|
||||
}, new RetryTemplate(), c -> {
|
||||
context.set(c);
|
||||
return null;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
ConsumerRecord<String, String> record = mock(ConsumerRecord.class);
|
||||
Consumer<?, ?> consumer = mock(Consumer.class);
|
||||
adapter.onMessage(record, mock(Acknowledgment.class), consumer);
|
||||
assertThat(context.get()).isNotNull();
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_ACKNOWLEDGMENT)).isNull();
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_RECORD)).isSameAs(record);
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_CONSUMER)).isSameAs(consumer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecoveryCallbackAckOnly() {
|
||||
final AtomicReference<RetryContext> context = new AtomicReference<>();
|
||||
RetryingMessageListenerAdapter<String, String> adapter = new RetryingMessageListenerAdapter<>(
|
||||
(AcknowledgingMessageListener<String, String>) (r, a) -> {
|
||||
throw new RuntimeException();
|
||||
}, new RetryTemplate(), c -> {
|
||||
context.set(c);
|
||||
return null;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
ConsumerRecord<String, String> record = mock(ConsumerRecord.class);
|
||||
Acknowledgment ack = mock(Acknowledgment.class);
|
||||
adapter.onMessage(record, ack, mock(Consumer.class));
|
||||
assertThat(context.get()).isNotNull();
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_ACKNOWLEDGMENT)).isSameAs(ack);
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_RECORD)).isSameAs(record);
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_CONSUMER)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecoveryCallbackAckAndConsumer() {
|
||||
final AtomicReference<RetryContext> context = new AtomicReference<>();
|
||||
RetryingMessageListenerAdapter<String, String> adapter = new RetryingMessageListenerAdapter<>(
|
||||
(AcknowledgingConsumerAwareMessageListener<String, String>) (r, a, c) -> {
|
||||
throw new RuntimeException();
|
||||
}, new RetryTemplate(), c -> {
|
||||
context.set(c);
|
||||
return null;
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
ConsumerRecord<String, String> record = mock(ConsumerRecord.class);
|
||||
Acknowledgment ack = mock(Acknowledgment.class);
|
||||
Consumer<?, ?> consumer = mock(Consumer.class);
|
||||
adapter.onMessage(record, ack, consumer);
|
||||
assertThat(context.get()).isNotNull();
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_ACKNOWLEDGMENT)).isSameAs(ack);
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_RECORD)).isSameAs(record);
|
||||
assertThat(context.get().getAttribute(RetryingMessageListenerAdapter.CONTEXT_CONSUMER)).isSameAs(consumer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -119,13 +119,12 @@ class RetryTopicConfigurationIntegrationTests {
|
||||
return new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, broker.getBrokersAsString()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
RetryTopicConfiguration retryTopicConfiguration1(KafkaTemplate<Integer, String> template) {
|
||||
return RetryTopicConfigurationBuilder.newInstance()
|
||||
.includeTopic(TOPIC1)
|
||||
.exponentialBackoff(100, 1.1, 110)
|
||||
.dltHandlerMethod(getClass(), "dlt")
|
||||
.dltHandlerMethod("retryTopicConfigurationIntegrationTests.Config", "dlt")
|
||||
.create(template);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -134,13 +134,12 @@ class RetryTopicConfigurationManualAssignmentIntegrationTests {
|
||||
return new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, broker.getBrokersAsString()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
RetryTopicConfiguration retryTopicConfiguration1(KafkaTemplate<Integer, String> template) {
|
||||
return RetryTopicConfigurationBuilder.newInstance()
|
||||
.includeTopics(List.of(TOPIC1, TOPIC2))
|
||||
.exponentialBackoff(100, 1.1, 110)
|
||||
.dltHandlerMethod(getClass(), "dlt")
|
||||
.dltHandlerMethod("retryTopicConfigurationManualAssignmentIntegrationTests.Config", "dlt")
|
||||
.create(template);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -314,7 +314,6 @@ public class RetryTopicIntegrationTests {
|
||||
|
||||
private static final String DLT_METHOD_NAME = "processDltMessage";
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public RetryTopicConfiguration firstRetryTopic(KafkaTemplate<String, String> template) {
|
||||
return RetryTopicConfigurationBuilder
|
||||
@@ -324,11 +323,10 @@ public class RetryTopicIntegrationTests {
|
||||
.useSingleTopicForFixedDelays()
|
||||
.includeTopic(FIRST_TOPIC)
|
||||
.doNotRetryOnDltFailure()
|
||||
.dltHandlerMethod(MyCustomDltProcessor.class, DLT_METHOD_NAME)
|
||||
.dltHandlerMethod("myCustomDltProcessor", DLT_METHOD_NAME)
|
||||
.create(template);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public RetryTopicConfiguration secondRetryTopic(KafkaTemplate<String, String> template) {
|
||||
return RetryTopicConfigurationBuilder
|
||||
@@ -338,7 +336,7 @@ public class RetryTopicIntegrationTests {
|
||||
.traversingCauses()
|
||||
.includeTopic(SECOND_TOPIC)
|
||||
.doNotRetryOnDltFailure()
|
||||
.dltHandlerMethod(MyCustomDltProcessor.class, DLT_METHOD_NAME)
|
||||
.dltHandlerMethod("myCustomDltProcessor", DLT_METHOD_NAME)
|
||||
.create(template);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -21,7 +21,6 @@ import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -34,6 +33,7 @@ import org.apache.kafka.streams.KafkaStreams;
|
||||
import org.apache.kafka.streams.KeyValue;
|
||||
import org.apache.kafka.streams.StreamsBuilder;
|
||||
import org.apache.kafka.streams.StreamsConfig;
|
||||
import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler;
|
||||
import org.apache.kafka.streams.kstream.KStream;
|
||||
import org.apache.kafka.streams.kstream.Materialized;
|
||||
import org.apache.kafka.streams.kstream.Printed;
|
||||
@@ -41,7 +41,6 @@ import org.apache.kafka.streams.kstream.Repartitioned;
|
||||
import org.apache.kafka.streams.kstream.TimeWindows;
|
||||
import org.apache.kafka.streams.kstream.ValueMapper;
|
||||
import org.apache.kafka.streams.processor.WallclockTimestampExtractor;
|
||||
import org.apache.kafka.streams.processor.internals.StreamThread;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -132,8 +131,8 @@ public class KafkaStreamsTests {
|
||||
CountDownLatch stateLatch = new CountDownLatch(1);
|
||||
|
||||
this.streamsBuilderFactoryBean.setStateListener((newState, oldState) -> stateLatch.countDown());
|
||||
Thread.UncaughtExceptionHandler exceptionHandler = mock(Thread.UncaughtExceptionHandler.class);
|
||||
this.streamsBuilderFactoryBean.setUncaughtExceptionHandler(exceptionHandler);
|
||||
StreamsUncaughtExceptionHandler exceptionHandler = mock(StreamsUncaughtExceptionHandler.class);
|
||||
this.streamsBuilderFactoryBean.setStreamsUncaughtExceptionHandler(exceptionHandler);
|
||||
|
||||
this.streamsBuilderFactoryBean.start();
|
||||
|
||||
@@ -158,10 +157,8 @@ public class KafkaStreamsTests {
|
||||
|
||||
KafkaStreams kafkaStreams = this.streamsBuilderFactoryBean.getKafkaStreams();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<StreamThread> threads = KafkaTestUtils.getPropertyValue(kafkaStreams, "threads", List.class);
|
||||
assertThat(threads).isNotEmpty();
|
||||
assertThat(threads.get(0).getUncaughtExceptionHandler()).isSameAs(exceptionHandler);
|
||||
assertThat(KafkaTestUtils.getPropertyValue(kafkaStreams, "streamsUncaughtExceptionHandler.arg$2"))
|
||||
.isSameAs(exceptionHandler);
|
||||
assertThat(this.stateChangeCalled.get()).isTrue();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords
|
||||
import org.apache.kafka.clients.producer.ProducerConfig
|
||||
import org.apache.kafka.common.serialization.StringDeserializer
|
||||
import org.apache.kafka.common.serialization.StringSerializer
|
||||
@@ -33,11 +35,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.listener.BatchErrorHandler
|
||||
import org.springframework.kafka.listener.BatchMessageListener
|
||||
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer
|
||||
import org.springframework.kafka.listener.ErrorHandler
|
||||
import org.springframework.kafka.listener.MessageListener
|
||||
import org.springframework.kafka.listener.*
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
@@ -143,38 +141,47 @@ class EnableKafkaKotlinTests {
|
||||
return KafkaTemplate(kpf())
|
||||
}
|
||||
|
||||
val eh = ErrorHandler { _, recs : ConsumerRecord<*, *>? ->
|
||||
if (recs != null) {
|
||||
this.error = true;
|
||||
this.latch2.countDown()
|
||||
val eh = object: CommonErrorHandler {
|
||||
override fun handleRecord(
|
||||
thrownException: Exception,
|
||||
record: ConsumerRecord<*, *>,
|
||||
consumer: Consumer<*, *>,
|
||||
container: MessageListenerContainer
|
||||
) {
|
||||
error = true
|
||||
latch2.countDown()
|
||||
}
|
||||
|
||||
override fun handleBatch(
|
||||
thrownException: Exception,
|
||||
recs: ConsumerRecords<*, *>,
|
||||
consumer: Consumer<*, *>,
|
||||
container: MessageListenerContainer,
|
||||
invokeListener: Runnable
|
||||
) {
|
||||
if (!recs.isEmpty) {
|
||||
batchError = true;
|
||||
batchLatch2.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Suppress("deprecation")
|
||||
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
|
||||
val factory: ConcurrentKafkaListenerContainerFactory<String, String>
|
||||
= ConcurrentKafkaListenerContainerFactory()
|
||||
factory.consumerFactory = kcf()
|
||||
factory.setErrorHandler(eh)
|
||||
factory.setCommonErrorHandler(eh)
|
||||
return factory
|
||||
}
|
||||
|
||||
val beh = BatchErrorHandler { _, recs ->
|
||||
if (!recs.isEmpty) {
|
||||
this.batchError = true;
|
||||
this.batchLatch2.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Suppress("deprecation")
|
||||
fun kafkaBatchListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
|
||||
val factory: ConcurrentKafkaListenerContainerFactory<String, String>
|
||||
= ConcurrentKafkaListenerContainerFactory()
|
||||
factory.isBatchListener = true
|
||||
factory.consumerFactory = kcf()
|
||||
factory.setBatchErrorHandler(beh)
|
||||
factory.setCommonErrorHandler(eh)
|
||||
return factory
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user