diff --git a/spring-kafka-docs/src/main/asciidoc/whats-new.adoc b/spring-kafka-docs/src/main/asciidoc/whats-new.adoc index e447de6c..be1c745f 100644 --- a/spring-kafka-docs/src/main/asciidoc/whats-new.adoc +++ b/spring-kafka-docs/src/main/asciidoc/whats-new.adoc @@ -6,9 +6,9 @@ For changes in earlier version, see <>. [[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 <> and https://cwiki.apache.org/confluence/display/KAFKA/KIP-447%3A+Producer+scalability+for+exactly+once+semantics[KIP-447] for more information. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/config/AbstractKafkaListenerContainerFactory.java b/spring-kafka/src/main/java/org/springframework/kafka/config/AbstractKafkaListenerContainerFactory.java index ee7b7d38..6f31ee82 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/config/AbstractKafkaListenerContainerFactory.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/config/AbstractKafkaListenerContainerFactory.java @@ -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 errorHandler; + @SuppressWarnings("deprecation") + private org.springframework.kafka.listener.GenericErrorHandler errorHandler; private CommonErrorHandler commonErrorHandler; @@ -90,10 +87,6 @@ public abstract class AbstractKafkaListenerContainerFactory recoveryCallback; - private Boolean statefulRetry; private Boolean batchListener; @@ -177,25 +170,6 @@ public abstract class AbstractKafkaListenerContainerFactory 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 "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 private boolean ackDiscarded; - private RetryTemplate retryTemplate; - private RecoveryCallback recoveryCallback; private boolean statefulRetry; @@ -326,34 +324,6 @@ public abstract class AbstractKafkaListenerEndpoint 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 recoveryCallback) { - this.recoveryCallback = recoveryCallback; - } - protected boolean isStatefulRetry() { return this.statefulRetry; } @@ -510,7 +480,7 @@ public abstract class AbstractKafkaListenerEndpoint protected abstract MessagingMessageListenerAdapter 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 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) messageListener, - this.retryTemplate, this.recoveryCallback, this.statefulRetry); - } if (this.recordFilterStrategy != null) { if (isBatchListener) { if (((MessagingMessageListenerAdapter) messageListener).isConsumerRecords()) { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/config/StreamsBuilderFactoryBean.java b/spring-kafka/src/main/java/org/springframework/kafka/config/StreamsBuilderFactoryBean.java index 6db3fed4..59e49b03 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/config/StreamsBuilderFactoryBean.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/config/StreamsBuilderFactoryBean.java @@ -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 { */ 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 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 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 diff --git a/spring-kafka/src/main/java/org/springframework/kafka/core/KafkaTemplate.java b/spring-kafka/src/main/java/org/springframework/kafka/core/KafkaTemplate.java index fb0ff9f5..c8960dc0 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/core/KafkaTemplate.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/core/KafkaTemplate.java @@ -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 implements KafkaOperations, ApplicationCo } } - - @Override - @Deprecated - public void sendOffsetsToTransaction(Map offsets) { - sendOffsetsToTransaction(offsets, KafkaUtils.getConsumerGroupId()); - } - - @SuppressWarnings("deprecation") - @Override - @Deprecated - public void sendOffsetsToTransaction(Map offsets, String consumerGroupId) { - producerForOffsets().sendOffsetsToTransaction(offsets, consumerGroupId); - } - @Override public void sendOffsetsToTransaction(Map offsets, ConsumerGroupMetadata groupMetadata) { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/core/RoutingKafkaTemplate.java b/spring-kafka/src/main/java/org/springframework/kafka/core/RoutingKafkaTemplate.java index 87beae7a..9495c280 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/core/RoutingKafkaTemplate.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/core/RoutingKafkaTemplate.java @@ -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 { throw new UnsupportedOperationException(THIS_METHOD_IS_NOT_SUPPORTED); } - @Override - @Deprecated - public void sendOffsetsToTransaction(Map offsets, String consumerGroupId) { - throw new UnsupportedOperationException(THIS_METHOD_IS_NOT_SUPPORTED); - } - - @Override - @Deprecated - public void sendOffsetsToTransaction(Map offsets) { - throw new UnsupportedOperationException(THIS_METHOD_IS_NOT_SUPPORTED); - } - @Override public void sendOffsetsToTransaction(Map offsets, ConsumerGroupMetadata groupMetadata) { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/core/reactive/ReactiveKafkaProducerTemplate.java b/spring-kafka/src/main/java/org/springframework/kafka/core/reactive/ReactiveKafkaProducerTemplate.java index 063568b0..904bedbb 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/core/reactive/ReactiveKafkaProducerTemplate.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/core/reactive/ReactiveKafkaProducerTemplate.java @@ -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 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 partitionsFromProducerFor(String topic) { Mono> partitionsInfo = doOnProducer(producer -> producer.partitionsFor(topic)); return partitionsInfo.flatMapIterable(Function.identity()); diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java index d7f65813..d08eb5b9 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/AbstractMessageListenerContainer.java @@ -86,6 +86,7 @@ public abstract class AbstractMessageListenerContainer private ApplicationEventPublisher applicationEventPublisher; + @SuppressWarnings("deprecation") private GenericErrorHandler errorHandler; private CommonErrorHandler commonErrorHandler; diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchErrorHandler.java index cee9d70b..6f6433f5 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchErrorHandler.java @@ -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> { /** diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchLoggingErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchLoggingErrorHandler.java deleted file mode 100644 index edd8349a..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/BatchLoggingErrorHandler.java +++ /dev/null @@ -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)); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/CompositeRecordInterceptor.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/CompositeRecordInterceptor.java index a583debf..491c41f9 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/CompositeRecordInterceptor.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/CompositeRecordInterceptor.java @@ -38,7 +38,7 @@ import org.springframework.util.Assert; * @since 2.3 * */ -public class CompositeRecordInterceptor implements ConsumerAwareRecordInterceptor { +public class CompositeRecordInterceptor implements RecordInterceptor { private final Collection> delegates = new ArrayList<>(); diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConditionalDelegatingBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConditionalDelegatingBatchErrorHandler.java deleted file mode 100644 index 76a78a32..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConditionalDelegatingBatchErrorHandler.java +++ /dev/null @@ -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, 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, 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 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 causeClass = cause.getClass(); - for (Entry, 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); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConditionalDelegatingErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConditionalDelegatingErrorHandler.java deleted file mode 100644 index 51a72475..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConditionalDelegatingErrorHandler.java +++ /dev/null @@ -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, 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, 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 throwable, ContainerAwareErrorHandler handler) { - this.delegates.put(throwable, handler); - } - - @Override - public void handle(Exception thrownException, @Nullable List> records, Consumer consumer, - MessageListenerContainer container) { - - Throwable cause = thrownException; - if (cause instanceof ListenerExecutionFailedException) { - cause = thrownException.getCause(); - } - if (cause != null) { - Class causeClass = cause.getClass(); - for (Entry, 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); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareBatchErrorHandler.java index d5b0ef27..b96350a7 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareBatchErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareBatchErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareErrorHandler.java index 54db386b..8910168d 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareRecordInterceptor.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareRecordInterceptor.java index 9b279264..101d307c 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareRecordInterceptor.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerAwareRecordInterceptor.java @@ -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 extends RecordInterceptor { - @SuppressWarnings("deprecation") - @Override - @Nullable - default ConsumerRecord intercept(ConsumerRecord record) { - throw new UnsupportedOperationException("Container should never call this"); - } - - @Override - @Nullable - ConsumerRecord intercept(ConsumerRecord record, Consumer consumer); - } diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerProperties.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerProperties.java index 6591f9dc..2431cc4d 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerProperties.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ConsumerProperties.java @@ -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. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareBatchErrorHandler.java index 3db052c7..5aaa50bb 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareBatchErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareBatchErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareErrorHandler.java index 4c473d47..da238e67 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerAwareErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerProperties.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerProperties.java index a71e7252..e4b119e9 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerProperties.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerProperties.java @@ -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; } diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerStoppingBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerStoppingBatchErrorHandler.java deleted file mode 100644 index 1f8218e5..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerStoppingBatchErrorHandler.java +++ /dev/null @@ -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); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerStoppingErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerStoppingErrorHandler.java deleted file mode 100644 index b0e70d6c..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerStoppingErrorHandler.java +++ /dev/null @@ -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> 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); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/DeadLetterPublishingRecoverer.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/DeadLetterPublishingRecoverer.java index 399a50c2..d2515cca 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/DeadLetterPublishingRecoverer.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/DeadLetterPublishingRecoverer.java @@ -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" diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessor.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessor.java index 0add9f65..ab2cb0a5 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessor.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessor.java @@ -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 extends FailedRecordProcessor getRecoveryStrategy((List) records, exception), container, this.logger) && isCommitRecovered() && this.kafkaTemplate.isTransactional()) { ConsumerRecord 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) { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandler.java index 9aa77692..86bf0a64 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandler.java @@ -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> { /** diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandlerAdapter.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandlerAdapter.java index ef638629..ce1cf702 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandlerAdapter.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ErrorHandlerAdapter.java @@ -33,6 +33,7 @@ import org.springframework.util.Assert; * @since 2.7.4 * */ +@SuppressWarnings("deprecation") class ErrorHandlerAdapter implements CommonErrorHandler { @SuppressWarnings({ "rawtypes", "unchecked" }) diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/FailedRecordProcessor.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/FailedRecordProcessor.java index 620bdde2..276e1c64 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/FailedRecordProcessor.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/FailedRecordProcessor.java @@ -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, Exception> getSkipPredicate(List> 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. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/FallbackBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/FallbackBatchErrorHandler.java index 7995f33f..91159cdc 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/FallbackBatchErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/FallbackBatchErrorHandler.java @@ -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, 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()); } } diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/GenericErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/GenericErrorHandler.java index f375a755..00bbaca9 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/GenericErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/GenericErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java index 5b76c32b..36121049 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/KafkaMessageListenerContainer.java @@ -846,10 +846,10 @@ public class KafkaMessageListenerContainer // 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 // 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 // NOSONAR line count } } + @SuppressWarnings("deprecation") private void validateErrorHandler(boolean batch, @Nullable GenericErrorHandler errHandler) { if (errHandler == null) { return; @@ -2722,12 +2720,7 @@ public class KafkaMessageListenerContainer // NOSONAR line count @SuppressWarnings("deprecation") private void doSendOffsets(Producer prod, Map 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); } diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerInvokingBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerInvokingBatchErrorHandler.java index 8facc73d..6153923d 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerInvokingBatchErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerInvokingBatchErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerUtils.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerUtils.java index f919060d..25eb302b 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerUtils.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/ListenerUtils.java @@ -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 executions, - ThreadLocal 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> 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. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/LoggingErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/LoggingErrorHandler.java deleted file mode 100644 index e3f97f8f..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/LoggingErrorHandler.java +++ /dev/null @@ -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)); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/RecordInterceptor.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/RecordInterceptor.java index ef513dc8..1fa2b91b 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/RecordInterceptor.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/RecordInterceptor.java @@ -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 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 intercept(ConsumerRecord 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 extends ThreadStateProcessor { * @since 2.7 */ @Nullable - default ConsumerRecord intercept(ConsumerRecord record, - @SuppressWarnings("unused") Consumer consumer) { - - return intercept(record); - } + ConsumerRecord intercept(ConsumerRecord record, Consumer consumer); /** * Called after the listener exits normally. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/RecoveringBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/RecoveringBatchErrorHandler.java deleted file mode 100644 index eb4ab587..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/RecoveringBatchErrorHandler.java +++ /dev/null @@ -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, 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, 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 - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/RemainingRecordsErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/RemainingRecordsErrorHandler.java index ba6c8a6e..f0b298b7 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/RemainingRecordsErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/RemainingRecordsErrorHandler.java @@ -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 { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/RetryingBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/RetryingBatchErrorHandler.java deleted file mode 100644 index 5059b0a7..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/RetryingBatchErrorHandler.java +++ /dev/null @@ -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, 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()); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentBatchErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentBatchErrorHandler.java index 3197c749..f9ebae14 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentBatchErrorHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentBatchErrorHandler.java @@ -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 backOffs = new ThreadLocal<>(); // Intentionally not static diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentErrorHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentErrorHandler.java deleted file mode 100644 index 2c2289f6..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/SeekToCurrentErrorHandler.java +++ /dev/null @@ -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, 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, 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> records, - Consumer consumer, MessageListenerContainer container) { - - SeekUtils.seekOrRecover(thrownException, records, consumer, container, isCommitRecovered(), // NOSONAR - getRecoveryStrategy(records, thrownException), this.logger, getLogLevel()); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/DelegatingInvocableHandler.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/DelegatingInvocableHandler.java index fe1ef33c..1f8fc850 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/DelegatingInvocableHandler.java @@ -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 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 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. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/RetryingMessageListenerAdapter.java b/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/RetryingMessageListenerAdapter.java deleted file mode 100644 index f5b20592..00000000 --- a/spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/RetryingMessageListenerAdapter.java +++ /dev/null @@ -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 the key type. - * @param the value type. - * - * @author Gary Russell - * @deprecated since 2.8 - use a suitably configured error handler instead. - * - */ -@Deprecated -public class RetryingMessageListenerAdapter - extends AbstractRetryingMessageListenerAdapter> - implements AcknowledgingConsumerAwareMessageListener { - - /** - * {@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 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 messageListener, RetryTemplate retryTemplate, - @Nullable RecoveryCallback 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 messageListener, RetryTemplate retryTemplate, - @Nullable RecoveryCallback recoveryCallback, boolean stateful) { - - super(messageListener, retryTemplate, recoveryCallback); - Assert.notNull(messageListener, "'messageListener' cannot be null"); - this.stateful = stateful; - } - - @Override - public void onMessage(final ConsumerRecord 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 data) { - onMessage(data, null, null); // NOSONAR - } - - @Override - public void onMessage(ConsumerRecord data, Acknowledgment acknowledgment) { - onMessage(data, acknowledgment, null); // NOSONAR - } - - @Override - public void onMessage(ConsumerRecord data, Consumer consumer) { - onMessage(data, null, consumer); - } - -} diff --git a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationBuilder.java b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationBuilder.java index 2ac0680c..dcc7e851 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationBuilder.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationBuilder.java @@ -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. diff --git a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurer.java b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurer.java index 9fba6cf3..a2db6b0b 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurer.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicConfigurer.java @@ -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, diff --git a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicInternalBeanNames.java b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicInternalBeanNames.java index ef56912a..f7bf40d4 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicInternalBeanNames.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/retrytopic/RetryTopicInternalBeanNames.java @@ -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. */ diff --git a/spring-kafka/src/main/java/org/springframework/kafka/support/JacksonUtils.java b/spring-kafka/src/main/java/org/springframework/kafka/support/JacksonUtils.java index cb625e42..31214092 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/support/JacksonUtils.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/support/JacksonUtils.java @@ -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) { diff --git a/spring-kafka/src/main/java/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer.java b/spring-kafka/src/main/java/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer.java index 8c5120cb..43b6edb5 100644 --- a/spring-kafka/src/main/java/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer.java +++ b/spring-kafka/src/main/java/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer.java @@ -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 implements Deserializer { - /** - * 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. */ diff --git a/spring-kafka/src/test/java/org/springframework/kafka/annotation/EnableKafkaIntegrationTests.java b/spring-kafka/src/test/java/org/springframework/kafka/annotation/EnableKafkaIntegrationTests.java index e32fb76f..e6070e79 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/annotation/EnableKafkaIntegrationTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/annotation/EnableKafkaIntegrationTests.java @@ -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 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(); + } + }; } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/annotation/StatefulRetryTests.java b/spring-kafka/src/test/java/org/springframework/kafka/annotation/StatefulRetryTests.java deleted file mode 100644 index 6af6f36f..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/annotation/StatefulRetryTests.java +++ /dev/null @@ -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 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 factory = - new ConcurrentKafkaListenerContainerFactory<>(); - factory.setConsumerFactory(consumerFactory(embeddedKafka)); - DefaultErrorHandler errorHandler = new DefaultErrorHandler() { - - @Override - public void handleRemaining(Exception thrownException, List> 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 consumerFactory(EmbeddedKafkaBroker embeddedKafka) { - return new DefaultKafkaConsumerFactory<>(consumerConfigs(embeddedKafka)); - } - - @Bean - public Map consumerConfigs(EmbeddedKafkaBroker embeddedKafka) { - Map consumerProps = - KafkaTestUtils.consumerProps(DEFAULT_TEST_GROUP_ID, "false", embeddedKafka); - return consumerProps; - } - - @Bean - public KafkaTemplate template(EmbeddedKafkaBroker embeddedKafka) { - return new KafkaTemplate<>(producerFactory(embeddedKafka)); - } - - @Bean - public ProducerFactory producerFactory(EmbeddedKafkaBroker embeddedKafka) { - return new DefaultKafkaProducerFactory<>(producerConfigs(embeddedKafka)); - } - - @Bean - public Map 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"); - } - - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/core/KafkaTemplateTransactionTests.java b/spring-kafka/src/test/java/org/springframework/kafka/core/KafkaTemplateTransactionTests.java index 9a9434e8..b10f00f2 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/core/KafkaTemplateTransactionTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/core/KafkaTemplateTransactionTests.java @@ -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"); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerMockTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerMockTests.java index d5d95928..55c9465d 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerMockTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerMockTests.java @@ -147,9 +147,21 @@ public class ConcurrentMessageListenerContainerMockTests { containerProperties); CountDownLatch latch = new CountDownLatch(1); AtomicReference 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 diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerTests.java index 2697e057..24397a75 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/ConcurrentMessageListenerContainerTests.java @@ -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 senderProps = KafkaTestUtils.producerProps(embeddedKafka); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ConditionalDelegatingErrorHandlerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ConditionalDelegatingErrorHandlerTests.java deleted file mode 100644 index 76cb7f30..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ConditionalDelegatingErrorHandlerTests.java +++ /dev/null @@ -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); - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingBatchErrorHandlerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingBatchErrorHandlerTests.java deleted file mode 100644 index 2992d56b..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingBatchErrorHandlerTests.java +++ /dev/null @@ -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 containers = this.registry.getAllListenerContainers(); - assertThat(containers).hasSize(2); - Iterator 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 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> 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.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 springManagedContainer() { - ConcurrentMessageListenerContainer container = kafkaListenerContainerFactory() - .createContainer("springManaged"); - container.setAutoStartup(false); - return container; - } - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingErrorHandlerBatchModeTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingErrorHandlerBatchModeTests.java deleted file mode 100644 index 0c7b2f4b..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingErrorHandlerBatchModeTests.java +++ /dev/null @@ -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 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> 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> 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; - } - - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingErrorHandlerRecordModeTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingErrorHandlerRecordModeTests.java deleted file mode 100644 index 992dac41..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ContainerStoppingErrorHandlerRecordModeTests.java +++ /dev/null @@ -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 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> 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> 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; - } - - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessorTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessorTests.java index 65e46712..efcde9de 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessorTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/DefaultAfterRollbackProcessorTests.java @@ -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 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 diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/ErrorHandlingDeserializerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/ErrorHandlingDeserializerTests.java index 63726115..d9769bc2 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/ErrorHandlingDeserializerTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/ErrorHandlingDeserializerTests.java @@ -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 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; } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/KafkaMessageListenerContainerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/KafkaMessageListenerContainerTests.java index 68a23244..00e25d4f 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/KafkaMessageListenerContainerTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/KafkaMessageListenerContainerTests.java @@ -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 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 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 recordInterceptor = spy(new RecordInterceptor() { @Override - public ConsumerRecord intercept(ConsumerRecord record) { + @Nullable + public ConsumerRecord intercept(ConsumerRecord record, + Consumer consumer) { + return record; } @@ -3633,7 +3655,10 @@ public class KafkaMessageListenerContainerTests { RecordInterceptor recordInterceptor = spy(new RecordInterceptor() { @Override - public ConsumerRecord intercept(ConsumerRecord record) { + @Nullable + public ConsumerRecord intercept(ConsumerRecord record, + Consumer consumer) { + return record; } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/RecoveringBatchErrorHandlerIntegrationTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/RecoveringBatchErrorHandlerIntegrationTests.java deleted file mode 100644 index 76aadada..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/RecoveringBatchErrorHandlerIntegrationTests.java +++ /dev/null @@ -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 props = KafkaTestUtils.consumerProps("recoverBatch", "false", embeddedKafka); - props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1000); - props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(props); - ContainerProperties containerProps = new ContainerProperties(topic1); - containerProps.setPollTimeout(10_000); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - final KafkaOperations template = new KafkaTemplate<>(pf); - final CountDownLatch latch = new CountDownLatch(3); - List> data = new ArrayList<>(); - containerProps.setMessageListener((BatchMessageListener) records -> { - data.addAll(records); - latch.countDown(); - records.forEach(rec -> { - if (rec.value().equals("baz")) { - throw new BatchListenerFailedException("fail", rec); - } - }); - }); - - KafkaMessageListenerContainer 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 dltcf = new DefaultKafkaConsumerFactory<>(props); - Consumer consumer = dltcf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, topic1DLT); - ConsumerRecord 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 props = KafkaTestUtils.consumerProps("recoverBatch2", "false", embeddedKafka); - props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1000); - props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(props); - ContainerProperties containerProps = new ContainerProperties(topic2); - containerProps.setPollTimeout(10_000); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - final KafkaOperations template = new KafkaTemplate<>(pf); - final CountDownLatch latch = new CountDownLatch(4); - List> data = new ArrayList<>(); - containerProps.setMessageListener((BatchMessageListener) records -> { - data.addAll(records); - latch.countDown(); - records.forEach(rec -> { - if (rec.value().equals("baz")) { - throw new BatchListenerFailedException("fail", rec); - } - }); - }); - - KafkaMessageListenerContainer 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 dltcf = new DefaultKafkaConsumerFactory<>(props); - Consumer consumer = dltcf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, topic2DLT); - ConsumerRecord dltRecord = KafkaTestUtils.getSingleRecord(consumer, topic2DLT); - assertThat(dltRecord.value()).isEqualTo("baz"); - container.stop(); - pf.destroy(); - consumer.close(); - assertThat(stopLatch.await(10, TimeUnit.SECONDS)).isTrue(); - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/RecoveringBatchErrorHandlerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/RecoveringBatchErrorHandlerTests.java deleted file mode 100644 index d3231647..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/RecoveringBatchErrorHandlerTests.java +++ /dev/null @@ -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 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 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 received = new ArrayList<>(); - - volatile ConsumerRecord recovered; - - volatile ConsumerRecord listenerFailed; - - volatile ConsumerRecord listenerRecovered; - - @KafkaListener(id = CONTAINER_ID, topics = "foo") - public void foo(List 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 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 records2 = new ArrayList<>(); - records2.add(new ConsumerRecord("foo", 0, 2L, 0L, TimestampType.NO_TIMESTAMP_TYPE, 0, 0, null, "baz", - new RecordHeaders(), Optional.empty())); - List 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 recordsOne = new ArrayList<>(records1); - recordsOne.addAll(records2); - recordsOne.addAll(records3); - List recordsTwo = new ArrayList<>(records2); - recordsTwo.addAll(records3); - Map> crs1 = Collections.singletonMap(topicPartition, recordsOne); - Map> crs2 = Collections.singletonMap(topicPartition, recordsTwo); - Map> 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; - } - - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/RemainingRecordsErrorHandlerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/RemainingRecordsErrorHandlerTests.java index 6bc64592..53990684 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/RemainingRecordsErrorHandlerTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/RemainingRecordsErrorHandlerTests.java @@ -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> records, - Consumer consumer) { + public boolean remainingRecords() { + return true; + } + + @Override + public void handleRemaining(Exception thrownException, List> records, + Consumer consumer, MessageListenerContainer container) { + remaining.addAll(records.stream() .map(r -> (String) r.value()) .collect(Collectors.toList())); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentErrorHandlerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentErrorHandlerTests.java deleted file mode 100644 index 323b8d0b..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentErrorHandlerTests.java +++ /dev/null @@ -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> 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 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 record1 = new ConsumerRecord<>("foo", 0, 0L, "foo", "bar"); - ConsumerRecord record2 = new ConsumerRecord<>("foo", 1, 1L, "foo", "bar"); - List> 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 record1 = new ConsumerRecord<>("foo", 0, 0L, "foo", "bar"); - ConsumerRecord record2 = new ConsumerRecord<>("foo", 1, 1L, "foo", "bar"); - List> 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 record1 = new ConsumerRecord<>("foo", 0, 0L, "foo", "bar"); - ConsumerRecord record2 = new ConsumerRecord<>("foo", 1, 1L, "foo", "bar"); - List> 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); - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentOnErrorRecordModeTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentOnErrorRecordModeTests.java index 4673d20d..b4ad2b37 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentOnErrorRecordModeTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/SeekToCurrentOnErrorRecordModeTests.java @@ -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; }); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/SubBatchPerPartitionTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/SubBatchPerPartitionTests.java index 98670674..fc0a781f 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/SubBatchPerPartitionTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/SubBatchPerPartitionTests.java @@ -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)); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/TransactionalContainerTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/TransactionalContainerTests.java index 5b0080df..10f73493 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/TransactionalContainerTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/listener/TransactionalContainerTests.java @@ -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 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 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) 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(); diff --git a/spring-kafka/src/test/java/org/springframework/kafka/listener/adapter/RetryingMessageListenerAdapterTests.java b/spring-kafka/src/test/java/org/springframework/kafka/listener/adapter/RetryingMessageListenerAdapterTests.java deleted file mode 100644 index b975a855..00000000 --- a/spring-kafka/src/test/java/org/springframework/kafka/listener/adapter/RetryingMessageListenerAdapterTests.java +++ /dev/null @@ -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 context = new AtomicReference<>(); - RetryingMessageListenerAdapter adapter = new RetryingMessageListenerAdapter<>( - r -> { - throw new RuntimeException(); - }, new RetryTemplate(), c -> { - context.set(c); - return null; - }); - @SuppressWarnings("unchecked") - ConsumerRecord 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 context = new AtomicReference<>(); - RetryingMessageListenerAdapter adapter = new RetryingMessageListenerAdapter<>( - (ConsumerAwareMessageListener) (r, c) -> { - throw new RuntimeException(); - }, new RetryTemplate(), c -> { - context.set(c); - return null; - }); - @SuppressWarnings("unchecked") - ConsumerRecord 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 context = new AtomicReference<>(); - RetryingMessageListenerAdapter adapter = new RetryingMessageListenerAdapter<>( - (AcknowledgingMessageListener) (r, a) -> { - throw new RuntimeException(); - }, new RetryTemplate(), c -> { - context.set(c); - return null; - }); - @SuppressWarnings("unchecked") - ConsumerRecord 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 context = new AtomicReference<>(); - RetryingMessageListenerAdapter adapter = new RetryingMessageListenerAdapter<>( - (AcknowledgingConsumerAwareMessageListener) (r, a, c) -> { - throw new RuntimeException(); - }, new RetryTemplate(), c -> { - context.set(c); - return null; - }); - @SuppressWarnings("unchecked") - ConsumerRecord 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); - } - -} diff --git a/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationIntegrationTests.java b/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationIntegrationTests.java index 5cb13a7d..c2ac9866 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationIntegrationTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationIntegrationTests.java @@ -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 template) { return RetryTopicConfigurationBuilder.newInstance() .includeTopic(TOPIC1) .exponentialBackoff(100, 1.1, 110) - .dltHandlerMethod(getClass(), "dlt") + .dltHandlerMethod("retryTopicConfigurationIntegrationTests.Config", "dlt") .create(template); } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationManualAssignmentIntegrationTests.java b/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationManualAssignmentIntegrationTests.java index 267298f4..ccd6df68 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationManualAssignmentIntegrationTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicConfigurationManualAssignmentIntegrationTests.java @@ -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 template) { return RetryTopicConfigurationBuilder.newInstance() .includeTopics(List.of(TOPIC1, TOPIC2)) .exponentialBackoff(100, 1.1, 110) - .dltHandlerMethod(getClass(), "dlt") + .dltHandlerMethod("retryTopicConfigurationManualAssignmentIntegrationTests.Config", "dlt") .create(template); } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicIntegrationTests.java b/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicIntegrationTests.java index 5a79d668..535ea85a 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicIntegrationTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/retrytopic/RetryTopicIntegrationTests.java @@ -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 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 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); } diff --git a/spring-kafka/src/test/java/org/springframework/kafka/streams/KafkaStreamsTests.java b/spring-kafka/src/test/java/org/springframework/kafka/streams/KafkaStreamsTests.java index 338b87d2..1f72f8c4 100644 --- a/spring-kafka/src/test/java/org/springframework/kafka/streams/KafkaStreamsTests.java +++ b/spring-kafka/src/test/java/org/springframework/kafka/streams/KafkaStreamsTests.java @@ -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 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(); } diff --git a/spring-kafka/src/test/kotlin/org/springframework/kafka/listener/EnableKafkaKotlinTests.kt b/spring-kafka/src/test/kotlin/org/springframework/kafka/listener/EnableKafkaKotlinTests.kt index 01cba16c..ef1a4728 100644 --- a/spring-kafka/src/test/kotlin/org/springframework/kafka/listener/EnableKafkaKotlinTests.kt +++ b/spring-kafka/src/test/kotlin/org/springframework/kafka/listener/EnableKafkaKotlinTests.kt @@ -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 { val factory: ConcurrentKafkaListenerContainerFactory = 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 { val factory: ConcurrentKafkaListenerContainerFactory = ConcurrentKafkaListenerContainerFactory() factory.isBatchListener = true factory.consumerFactory = kcf() - factory.setBatchErrorHandler(beh) + factory.setCommonErrorHandler(eh) return factory }