Fix NPE for retryTemp!=null but recoveryCall==null
Starting with version `3.0.x`, the `KafkaInboundGateway` and `KafkaMessageDrivenChannelAdapter` rely on the `RetryContext.getRetryCount()` for the `IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT` message header, but the `RetryContext` is obtained from the `TheadLocal` `attributesHolder`, which is populated from the `RetryListener.open()` only if `recoveryCallback != null` * Fix `KafkaInboundGateway` and `KafkaMessageDrivenChannelAdapter` to populate `TheadLocal` `attributesHolder` with the `RetryContext` from the `RetryListener.open()` when only `retryTemplate != null` **Cherry-pick to `master` & 3.0.x** # Conflicts: # src/test/java/org/springframework/integration/kafka/inbound/MessageDrivenAdapterTests.java
This commit is contained in:
@@ -63,6 +63,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @author Urs Keller
|
||||
*
|
||||
* @since 3.0.2
|
||||
*
|
||||
@@ -281,8 +282,7 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
}
|
||||
}
|
||||
else {
|
||||
KafkaInboundGateway.this.logger.debug("Converter returned a null message for: "
|
||||
+ record);
|
||||
KafkaInboundGateway.this.logger.debug("Converter returned a null message for: " + record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
|
||||
if (KafkaInboundGateway.this.recoveryCallback != null) {
|
||||
if (KafkaInboundGateway.this.retryTemplate != null) {
|
||||
attributesHolder.set(context);
|
||||
}
|
||||
return true;
|
||||
@@ -354,6 +354,7 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
@Override
|
||||
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
|
||||
Throwable throwable) {
|
||||
|
||||
attributesHolder.remove();
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ import org.springframework.util.Assert;
|
||||
* @author Marius Bogoevici
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @author Urs Keller
|
||||
*
|
||||
*/
|
||||
public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSupport implements OrderlyShutdownCapable,
|
||||
@@ -472,7 +473,7 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
|
||||
if (KafkaMessageDrivenChannelAdapter.this.recoveryCallback != null) {
|
||||
if (KafkaMessageDrivenChannelAdapter.this.retryTemplate != null) {
|
||||
attributesHolder.set(context);
|
||||
}
|
||||
return true;
|
||||
@@ -481,6 +482,7 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
@Override
|
||||
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
|
||||
Throwable throwable) {
|
||||
|
||||
attributesHolder.remove();
|
||||
}
|
||||
|
||||
@@ -527,7 +529,7 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
|
||||
if (KafkaMessageDrivenChannelAdapter.this.recoveryCallback != null) {
|
||||
if (KafkaMessageDrivenChannelAdapter.this.retryTemplate != null) {
|
||||
attributesHolder.set(context);
|
||||
}
|
||||
return true;
|
||||
@@ -536,6 +538,7 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
@Override
|
||||
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
|
||||
Throwable throwable) {
|
||||
|
||||
attributesHolder.remove();
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,10 @@ import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.ErrorMessage;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.backoff.NoBackOffPolicy;
|
||||
import org.springframework.retry.listener.RetryListenerSupport;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
@@ -68,6 +71,7 @@ import org.springframework.retry.support.RetryTemplate;
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @author Urs Keller
|
||||
*
|
||||
* @since 3.0.2
|
||||
*
|
||||
@@ -86,9 +90,11 @@ public class InboundGatewayTests {
|
||||
|
||||
private static String topic6 = "testTopic6";
|
||||
|
||||
private static String topic7 = "testTopic7";
|
||||
|
||||
@ClassRule
|
||||
public static EmbeddedKafkaRule embeddedKafka =
|
||||
new EmbeddedKafkaRule(1, true, topic1, topic2, topic3, topic4, topic5, topic6);
|
||||
new EmbeddedKafkaRule(1, true, topic1, topic2, topic3, topic4, topic5, topic6, topic7);
|
||||
|
||||
@Rule
|
||||
public Log4j2LevelAdjuster adjuster = Log4j2LevelAdjuster.trace().categories("org.apache.kafka.clients",
|
||||
@@ -229,7 +235,7 @@ public class InboundGatewayTests {
|
||||
Message<?> failed = ((MessagingException) em.getPayload()).getFailedMessage();
|
||||
assertThat(failed).isNotNull();
|
||||
assertThat(failed.getHeaders().get(IntegrationMessageHeaderAccessor.SOURCE_DATA))
|
||||
.isSameAs(em.getHeaders().get(KafkaHeaders.RAW_DATA));
|
||||
.isSameAs(em.getHeaders().get(KafkaHeaders.RAW_DATA));
|
||||
MessageChannel reply = (MessageChannel) em.getHeaders().getReplyChannel();
|
||||
MessageHeaders headers = failed.getHeaders();
|
||||
reply.send(MessageBuilder.withPayload("ERROR").copyHeaders(headers).build());
|
||||
@@ -335,4 +341,62 @@ public class InboundGatewayTests {
|
||||
gateway.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInboundRetryErrorRecoverWithoutRecocveryCallback() throws Exception {
|
||||
EmbeddedKafkaBroker embeddedKafka = InboundGatewayTests.embeddedKafka.getEmbeddedKafka();
|
||||
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps("replyHandler4", "false", embeddedKafka);
|
||||
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
ConsumerFactory<Integer, String> cf2 = new DefaultKafkaConsumerFactory<>(consumerProps);
|
||||
Consumer<Integer, String> consumer = cf2.createConsumer();
|
||||
embeddedKafka.consumeFromAnEmbeddedTopic(consumer, topic7);
|
||||
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("test4", "false", embeddedKafka);
|
||||
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props);
|
||||
ContainerProperties containerProps = new ContainerProperties(topic7);
|
||||
KafkaMessageListenerContainer<Integer, String> container =
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
ProducerFactory<Integer, String> pf = new DefaultKafkaProducerFactory<>(senderProps);
|
||||
KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf);
|
||||
template.setDefaultTopic(topic7);
|
||||
KafkaInboundGateway<Integer, String, String> gateway = new KafkaInboundGateway<>(container, template);
|
||||
MessageChannel out = new DirectChannel() {
|
||||
|
||||
@Override
|
||||
protected boolean doSend(Message<?> message, long timeout) {
|
||||
throw new RuntimeException("intended");
|
||||
}
|
||||
|
||||
};
|
||||
gateway.setRequestChannel(out);
|
||||
gateway.setBeanFactory(mock(BeanFactory.class));
|
||||
gateway.setReplyTimeout(30_000);
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
|
||||
retryPolicy.setMaxAttempts(5);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
retryTemplate.setBackOffPolicy(new NoBackOffPolicy());
|
||||
final CountDownLatch retryCountLatch = new CountDownLatch(retryPolicy.getMaxAttempts());
|
||||
retryTemplate.registerListener(new RetryListenerSupport() {
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
|
||||
Throwable throwable) {
|
||||
retryCountLatch.countDown();
|
||||
}
|
||||
});
|
||||
gateway.setRetryTemplate(retryTemplate);
|
||||
gateway.afterPropertiesSet();
|
||||
gateway.start();
|
||||
ContainerTestUtils.waitForAssignment(container, 2);
|
||||
|
||||
template.sendDefault(0, 1487694048607L, 1, "foo");
|
||||
|
||||
assertThat(retryCountLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
gateway.stop();
|
||||
consumer.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.ErrorMessage;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.listener.RetryListenerSupport;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
@@ -94,6 +97,7 @@ import org.springframework.retry.support.RetryTemplate;
|
||||
* @author Artem Bilan
|
||||
* @author Biju Kunjummen
|
||||
* @author Cameron Mayfield
|
||||
* @author Urs Keller
|
||||
*
|
||||
* @since 2.0
|
||||
*
|
||||
@@ -114,7 +118,7 @@ public class MessageDrivenAdapterTests {
|
||||
|
||||
@ClassRule
|
||||
public static EmbeddedKafkaRule embeddedKafkaRule =
|
||||
new EmbeddedKafkaRule(1, true, topic1, topic2, topic3, topic4, topic5, topic6);
|
||||
new EmbeddedKafkaRule(1, true, topic1, topic2, topic3, topic4, topic5, topic6);
|
||||
|
||||
private static EmbeddedKafkaBroker embeddedKafka = embeddedKafkaRule.getEmbeddedKafka();
|
||||
|
||||
@@ -256,6 +260,61 @@ public class MessageDrivenAdapterTests {
|
||||
adapter.stop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* the recovery callback is not mandatory, if not set and retries are exhausted the last throwable is rethrown
|
||||
* to the consumer.
|
||||
*/
|
||||
@Test
|
||||
public void testInboundRecordRetryRecoverWithoutRecoveryCallback() throws Exception {
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("test6", "true", embeddedKafka);
|
||||
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props);
|
||||
ContainerProperties containerProps = new ContainerProperties(topic6);
|
||||
KafkaMessageListenerContainer<Integer, String> container =
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
|
||||
KafkaMessageDrivenChannelAdapter<Integer, String> adapter = new KafkaMessageDrivenChannelAdapter<>(container);
|
||||
MessageChannel out = new DirectChannel() {
|
||||
|
||||
@Override
|
||||
protected boolean doSend(Message<?> message, long timeout) {
|
||||
throw new RuntimeException("intended");
|
||||
}
|
||||
|
||||
};
|
||||
adapter.setOutputChannel(out);
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
|
||||
retryPolicy.setMaxAttempts(2);
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
final CountDownLatch retryCountLatch = new CountDownLatch(retryPolicy.getMaxAttempts());
|
||||
retryTemplate.registerListener(new RetryListenerSupport() {
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
|
||||
Throwable throwable) {
|
||||
retryCountLatch.countDown();
|
||||
}
|
||||
});
|
||||
adapter.setRetryTemplate(retryTemplate);
|
||||
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
ContainerTestUtils.waitForAssignment(container, 2);
|
||||
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
DefaultKafkaProducerFactory<Integer, String> pf = new DefaultKafkaProducerFactory<>(senderProps);
|
||||
KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf);
|
||||
template.setDefaultTopic(topic6);
|
||||
template.sendDefault(1, "foo");
|
||||
|
||||
assertThat(retryCountLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
adapter.stop();
|
||||
pf.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInboundRecordNoRetryRecover() {
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("test5", "true", embeddedKafka);
|
||||
@@ -294,11 +353,11 @@ public class MessageDrivenAdapterTests {
|
||||
MessageHeaders headers = received.getHeaders();
|
||||
assertThat(headers.get(KafkaHeaders.RAW_DATA)).isNotNull();
|
||||
assertThat(headers.get(IntegrationMessageHeaderAccessor.SOURCE_DATA))
|
||||
.isSameAs(headers.get(KafkaHeaders.RAW_DATA));
|
||||
.isSameAs(headers.get(KafkaHeaders.RAW_DATA));
|
||||
Message<?> originalMessage = ((ErrorMessage) received).getOriginalMessage();
|
||||
assertThat(originalMessage).isNotNull();
|
||||
assertThat(originalMessage.getHeaders().get(IntegrationMessageHeaderAccessor.SOURCE_DATA))
|
||||
.isSameAs(headers.get(KafkaHeaders.RAW_DATA));
|
||||
.isSameAs(headers.get(KafkaHeaders.RAW_DATA));
|
||||
headers = originalMessage.getHeaders();
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_MESSAGE_KEY)).isEqualTo(1);
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_TOPIC)).isEqualTo(topic5);
|
||||
|
||||
Reference in New Issue
Block a user