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:
Gary Russell
2022-02-10 15:00:10 -05:00
committed by GitHub
parent b30e4b3a48
commit 7c2178ac43
73 changed files with 327 additions and 3456 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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()) {

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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());

View File

@@ -86,6 +86,7 @@ public abstract class AbstractMessageListenerContainer<K, V>
private ApplicationEventPublisher applicationEventPublisher;
@SuppressWarnings("deprecation")
private GenericErrorHandler<?> errorHandler;
private CommonErrorHandler commonErrorHandler;

View File

@@ -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<?, ?>> {
/**

View File

@@ -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));
}
}

View File

@@ -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<>();

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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<?, ?>> {
/**

View File

@@ -33,6 +33,7 @@ import org.springframework.util.Assert;
* @since 2.7.4
*
*/
@SuppressWarnings("deprecation")
class ErrorHandlerAdapter implements CommonErrorHandler {
@SuppressWarnings({ "rawtypes", "unchecked" })

View File

@@ -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.

View File

@@ -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());
}
}

View File

@@ -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> {

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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));
}
}

View File

@@ -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.

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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());
}
}

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -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.
*/

View File

@@ -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) {

View File

@@ -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.
*/

View File

@@ -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();
}
};
}

View File

@@ -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");
}
}
}

View File

@@ -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");

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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()));

View File

@@ -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);
}
}

View File

@@ -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;
});

View File

@@ -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));

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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
}