Issue link: https://github.com/spring-cloud/spring-cloud-stream/issues/3066

Signed-off-by: DidierLoiseau <didierloiseau+github@gmail.com>
This commit is contained in:
DidierLoiseau
2025-03-02 00:43:08 +01:00
committed by Soby Chacko
parent e0fdfdecc8
commit 48659f9e92
2 changed files with 104 additions and 14 deletions

View File

@@ -165,6 +165,7 @@ import org.springframework.util.backoff.FixedBackOff;
* @author Byungjun You
* @author Oliver Führer
* @author Omer Celik
* @author Didier Loiseau
*/
public class KafkaMessageChannelBinder extends
// @checkstyle:off
@@ -566,10 +567,16 @@ public class KafkaMessageChannelBinder extends
}
@Override
@SuppressWarnings("unchecked")
protected MessageProducer createConsumerEndpoint(
final ConsumerDestination destination, final String group,
final ExtendedConsumerProperties<KafkaConsumerProperties> extendedConsumerProperties) {
return createConsumerEndpointCaptureHelper(destination, group, extendedConsumerProperties);
}
@SuppressWarnings("unchecked")
private <K, V> MessageProducer createConsumerEndpointCaptureHelper(
final ConsumerDestination destination, final String group,
final ExtendedConsumerProperties<KafkaConsumerProperties> extendedConsumerProperties) {
boolean anonymous = !StringUtils.hasText(group);
Assert.isTrue(
@@ -577,7 +584,7 @@ public class KafkaMessageChannelBinder extends
"DLQ support is not available for anonymous subscriptions");
String consumerGroup = anonymous ? "anonymous." + UUID.randomUUID().toString()
: group;
final ConsumerFactory<?, ?> consumerFactory = createKafkaConsumerFactory(
final ConsumerFactory<K, V> consumerFactory = (ConsumerFactory<K, V>) createKafkaConsumerFactory(
anonymous, consumerGroup, extendedConsumerProperties, destination.getName() + ".consumer", destination.getName());
int partitionCount = extendedConsumerProperties.getInstanceCount()
* extendedConsumerProperties.getConcurrency();
@@ -647,9 +654,8 @@ public class KafkaMessageChannelBinder extends
}
resetOffsetsForAutoRebalance(extendedConsumerProperties, consumerFactory, containerProperties);
containerProperties.setAuthExceptionRetryInterval(this.configurationProperties.getAuthorizationExceptionRetryInterval());
@SuppressWarnings("rawtypes")
final ConcurrentMessageListenerContainer<?, ?> messageListenerContainer = new ConcurrentMessageListenerContainer(
consumerFactory, containerProperties) {
final ConcurrentMessageListenerContainer<K, V> messageListenerContainer =
new ConcurrentMessageListenerContainer<>(consumerFactory, containerProperties) {
@Override
public void stop(Runnable callback) {
@@ -677,8 +683,7 @@ public class KafkaMessageChannelBinder extends
ContainerProperties.AckMode ackMode = extendedConsumerProperties.getExtension().getAckMode();
if (ackMode != null) {
if ((extendedConsumerProperties.isBatchMode() && ackMode != ContainerProperties.AckMode.RECORD) ||
!extendedConsumerProperties.isBatchMode()) {
if (!extendedConsumerProperties.isBatchMode() || ackMode != ContainerProperties.AckMode.RECORD) {
messageListenerContainer.getContainerProperties()
.setAckMode(ackMode);
}
@@ -715,7 +720,7 @@ public class KafkaMessageChannelBinder extends
}
}
else if (!extendedConsumerProperties.isBatchMode() && transMan != null) {
messageListenerContainer.setAfterRollbackProcessor(new DefaultAfterRollbackProcessor<>(
var afterRollbackProcessor = new DefaultAfterRollbackProcessor<K, V>(
(record, exception) -> {
MessagingException payload =
new MessagingException(((RecordMessageConverter) messageConverter)
@@ -740,7 +745,31 @@ public class KafkaMessageChannelBinder extends
}
}, createBackOff(extendedConsumerProperties),
new KafkaTemplate<>(transMan.getProducerFactory()),
extendedConsumerProperties.getExtension().isTxCommitRecovered()));
extendedConsumerProperties.getExtension().isTxCommitRecovered());
if (!CollectionUtils.isEmpty(extendedConsumerProperties.getRetryableExceptions())) {
// mimic AbstractBinder.buildRetryTemplate(properties)s retryPolicy
if (!extendedConsumerProperties.isDefaultRetryable()) {
afterRollbackProcessor.defaultFalse(true);
}
extendedConsumerProperties.getRetryableExceptions()
.forEach((t, retry) -> {
if (Exception.class.isAssignableFrom(t)) {
var ex = t.asSubclass(Exception.class);
if (retry) {
afterRollbackProcessor.addRetryableExceptions(ex);
}
else {
afterRollbackProcessor.addNotRetryableExceptions(ex);
}
}
else {
throw new IllegalArgumentException(
"Only Exception types can be configured as retryable-exceptions together with transactions. "
+ "Unsupported type: " + t.getName());
}
});
}
messageListenerContainer.setAfterRollbackProcessor(afterRollbackProcessor);
}
else {
kafkaMessageDrivenChannelAdapter.setErrorChannel(errorInfrastructure.getErrorChannel());

View File

@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -172,7 +173,8 @@ import static org.mockito.Mockito.verify;
* @author Henryk Konsek
* @author Gary Russell
* @author Chris Bono
* @Author Oliver Führer
* @author Oliver Führer
* @author Didier Loiseau
*/
@EmbeddedKafka(count = 1, controlledShutdown = true, topics = "error.pollableDlq.group-pcWithDlq", brokerProperties = {"transaction.state.log.replication.factor=1",
"transaction.state.log.min.isr=1"})
@@ -1052,11 +1054,41 @@ class KafkaBinderTests extends
testDlqGuts(true, null, null, false, false);
}
@Test
void dlqAndRetryWithNonRetryableException() throws Exception {
testDlqGuts(true, null, null, false, false, true, true);
}
@Test
void dlqAndRetryDefaultFalse() throws Exception {
testDlqGuts(true, null, null, false, false, false, false);
}
@Test
void dlqAndRetryDefaultFalseWithRetryableException() throws Exception {
testDlqGuts(true, null, null, false, false, false, true);
}
@Test
void dlqAndRetryTransactional() throws Exception {
testDlqGuts(true, null, null, true, false);
}
@Test
void dlqAndRetryWithNonRetryableExceptionTransactional() throws Exception {
testDlqGuts(true, null, null, true, false, true, true);
}
@Test
void dlqAndRetryDefaultFalseTransactional() throws Exception {
testDlqGuts(true, null, null, true, false, false, false);
}
@Test
void dlqAndRetryDefaultFalseWithRetryableExceptionTransactional() throws Exception {
testDlqGuts(true, null, null, true, false, false, true);
}
@Test
void dlq() throws Exception {
testDlqGuts(false, null, 3, false, false);
@@ -1084,6 +1116,14 @@ class KafkaBinderTests extends
private void testDlqGuts(boolean withRetry, HeaderMode headerMode, Integer dlqPartitions,
boolean transactional, boolean useDlqDestResolver) throws Exception {
testDlqGuts(withRetry, headerMode, dlqPartitions, transactional,
useDlqDestResolver, true, false);
}
private void testDlqGuts(boolean withRetry, HeaderMode headerMode,
Integer dlqPartitions, boolean transactional, boolean useDlqDestResolver,
boolean defaultRetryable, boolean useConfiguredRetryableException)
throws Exception {
int expectedDlqPartition = dlqPartitions == null ? 0 : dlqPartitions - 1;
KafkaBinderConfigurationProperties binderConfig = createConfigurationProperties();
@@ -1128,12 +1168,18 @@ class KafkaBinderTests extends
consumerProperties.getExtension().setDlqPartitions(dlqPartitions);
consumerProperties.setConcurrency(2);
consumerProperties.populateBindingName("foobar");
consumerProperties.setDefaultRetryable(defaultRetryable);
consumerProperties.getRetryableExceptions().put(NumberFormatException.class,
!defaultRetryable);
DirectChannel moduleInputChannel = createBindableChannel("input",
createConsumerBindingProperties(consumerProperties));
var dlqChannel = new QueueChannel();
var handler = new FailingInvocationCountingMessageHandler();
var handler = new FailingInvocationCountingMessageHandler(
() -> useConfiguredRetryableException
? new NumberFormatException("fail")
: new RuntimeException("fail"));
moduleInputChannel.subscribe(handler);
long uniqueBindingId = System.currentTimeMillis();
@@ -1253,8 +1299,10 @@ class KafkaBinderTests extends
.get(KafkaHeaders.RECEIVED_PARTITION)).isEqualTo(expectedDlqPartition);
}
else if (!HeaderMode.none.equals(headerMode)) {
boolean shouldHaveRetried = defaultRetryable != useConfiguredRetryableException;
assertThat(handler.getInvocationCount())
.isEqualTo(consumerProperties.getMaxAttempts());
.isEqualTo(
shouldHaveRetried ? consumerProperties.getMaxAttempts() : 1);
assertThat(receivedMessage.getHeaders()
.get(KafkaMessageChannelBinder.X_ORIGINAL_TOPIC))
@@ -4090,14 +4138,27 @@ class KafkaBinderTests extends
private final class FailingInvocationCountingMessageHandler
implements MessageHandler {
private final Supplier<? extends RuntimeException> exceptionProvider;
private volatile int invocationCount;
private final LinkedHashMap<Long, Message<?>> receivedMessages = new LinkedHashMap<>();
private final CountDownLatch latch;
private FailingInvocationCountingMessageHandler(int latchSize) {
private FailingInvocationCountingMessageHandler(int latchSize,
Supplier<? extends RuntimeException> exceptionProvider) {
latch = new CountDownLatch(latchSize);
this.exceptionProvider = exceptionProvider;
}
private FailingInvocationCountingMessageHandler(
Supplier<? extends RuntimeException> exceptionProvider) {
this(1, exceptionProvider);
}
private FailingInvocationCountingMessageHandler(int latchSize) {
this(latchSize, () -> new RuntimeException("fail"));
}
private FailingInvocationCountingMessageHandler() {
@@ -4115,7 +4176,7 @@ class KafkaBinderTests extends
receivedMessages.put(offset, message);
latch.countDown();
}
throw new RuntimeException("fail");
throw exceptionProvider.get();
}
public LinkedHashMap<Long, Message<?>> getReceivedMessages() {