SIGH-2958: Add source record to common SI Header
Resolves https://github.com/spring-projects/spring-integration/issues/2958 Enables applications to be agnostic regarding the source of a message. e.g Kafka Vs. AMQP.
This commit is contained in:
committed by
Artem Bilan
parent
c25b3b45f4
commit
14302088c5
@@ -83,6 +83,8 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
|
||||
private BiConsumer<Map<TopicPartition, Long>, ConsumerSeekAware.ConsumerSeekCallback> onPartitionsAssignedSeekCallback;
|
||||
|
||||
private boolean bindSourceRecord;
|
||||
|
||||
/**
|
||||
* Construct an instance with the provided container.
|
||||
* @param messageListenerContainer the container.
|
||||
@@ -155,6 +157,16 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
this.onPartitionsAssignedSeekCallback = onPartitionsAssignedCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to bind the source consumer record in the header named
|
||||
* {@link IntegrationMessageHeaderAccessor#SOURCE_DATA}.
|
||||
* @param bindSourceRecord true to bind.
|
||||
* @since 3.1.4
|
||||
*/
|
||||
public void setBindSourceRecord(boolean bindSourceRecord) {
|
||||
this.bindSourceRecord = bindSourceRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
super.onInit();
|
||||
@@ -245,10 +257,7 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
public void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
|
||||
Message<?> message = null;
|
||||
try {
|
||||
message = toMessagingMessage(record, acknowledgment, consumer);
|
||||
if (KafkaInboundGateway.this.retryTemplate != null) {
|
||||
message = addDeliveryAttemptHeader(message);
|
||||
}
|
||||
message = enhanceHeaders(toMessagingMessage(record, acknowledgment, consumer), record);
|
||||
setAttributesIfNecessary(record, message);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
@@ -277,18 +286,30 @@ public class KafkaInboundGateway<K, V, R> extends MessagingGatewaySupport implem
|
||||
}
|
||||
}
|
||||
|
||||
private Message<?> addDeliveryAttemptHeader(Message<?> message) {
|
||||
private Message<?> enhanceHeaders(Message<?> message, ConsumerRecord<K, V> record) {
|
||||
Message<?> messageToReturn = message;
|
||||
AtomicInteger deliveryAttempt =
|
||||
new AtomicInteger(((RetryContext) attributesHolder.get()).getRetryCount() + 1);
|
||||
if (message.getHeaders() instanceof KafkaMessageHeaders) {
|
||||
((KafkaMessageHeaders) message.getHeaders()).getRawHeaders()
|
||||
.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt);
|
||||
Map<String, Object> rawHeaders = ((KafkaMessageHeaders) message.getHeaders()).getRawHeaders();
|
||||
if (KafkaInboundGateway.this.retryTemplate != null) {
|
||||
AtomicInteger deliveryAttempt =
|
||||
new AtomicInteger(((RetryContext) attributesHolder.get()).getRetryCount() + 1);
|
||||
rawHeaders.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt);
|
||||
}
|
||||
if (KafkaInboundGateway.this.bindSourceRecord) {
|
||||
rawHeaders.put(IntegrationMessageHeaderAccessor.SOURCE_DATA, record);
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageToReturn = MessageBuilder.fromMessage(message)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt)
|
||||
.build();
|
||||
MessageBuilder<?> builder = MessageBuilder.fromMessage(message);
|
||||
if (KafkaInboundGateway.this.retryTemplate != null) {
|
||||
AtomicInteger deliveryAttempt =
|
||||
new AtomicInteger(((RetryContext) attributesHolder.get()).getRetryCount() + 1);
|
||||
builder.setHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt);
|
||||
}
|
||||
if (KafkaInboundGateway.this.bindSourceRecord) {
|
||||
builder.setHeader(IntegrationMessageHeaderAccessor.SOURCE_DATA, record);
|
||||
}
|
||||
messageToReturn = builder.build();
|
||||
}
|
||||
return messageToReturn;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
|
||||
private BiConsumer<Map<TopicPartition, Long>, ConsumerSeekAware.ConsumerSeekCallback> onPartitionsAssignedSeekCallback;
|
||||
|
||||
private boolean bindSourceRecord;
|
||||
|
||||
/**
|
||||
* Construct an instance with mode {@link ListenerMode#record}.
|
||||
* @param messageListenerContainer the container.
|
||||
@@ -247,6 +249,17 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
this.onPartitionsAssignedSeekCallback = onPartitionsAssignedCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to bind the source consumer record in the header named
|
||||
* {@link IntegrationMessageHeaderAccessor#SOURCE_DATA}.
|
||||
* Does not apply to batch listeners.
|
||||
* @param bindSourceRecord true to bind.
|
||||
* @since 3.1.4
|
||||
*/
|
||||
public void setBindSourceRecord(boolean bindSourceRecord) {
|
||||
this.bindSourceRecord = bindSourceRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComponentType() {
|
||||
return "kafka:message-driven-channel-adapter";
|
||||
@@ -264,10 +277,10 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
if (this.mode.equals(ListenerMode.record)) {
|
||||
MessageListener<K, V> listener = this.recordListener;
|
||||
|
||||
boolean filterInRetry = this.filterInRetry && this.retryTemplate != null
|
||||
boolean doFilterInRetry = this.filterInRetry && this.retryTemplate != null
|
||||
&& this.recordFilterStrategy != null;
|
||||
|
||||
if (filterInRetry) {
|
||||
if (doFilterInRetry) {
|
||||
listener = new FilteringMessageListenerAdapter<>(listener, this.recordFilterStrategy,
|
||||
this.ackDiscarded);
|
||||
listener = new RetryingMessageListenerAdapter<>(listener, this.retryTemplate,
|
||||
@@ -418,10 +431,7 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
public void onMessage(ConsumerRecord<K, V> record, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
|
||||
Message<?> message = null;
|
||||
try {
|
||||
message = toMessagingMessage(record, acknowledgment, consumer);
|
||||
if (KafkaMessageDrivenChannelAdapter.this.retryTemplate != null) {
|
||||
message = addDeliveryAttemptHeader(message);
|
||||
}
|
||||
message = enhanceHeaders(toMessagingMessage(record, acknowledgment, consumer), record);
|
||||
setAttributesIfNecessary(record, message);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
@@ -432,18 +442,30 @@ public class KafkaMessageDrivenChannelAdapter<K, V> extends MessageProducerSuppo
|
||||
sendMessageIfAny(message, record);
|
||||
}
|
||||
|
||||
private Message<?> addDeliveryAttemptHeader(Message<?> message) {
|
||||
private Message<?> enhanceHeaders(Message<?> message, ConsumerRecord<K, V> record) {
|
||||
Message<?> messageToReturn = message;
|
||||
AtomicInteger deliveryAttempt =
|
||||
new AtomicInteger(((RetryContext) attributesHolder.get()).getRetryCount() + 1);
|
||||
if (message.getHeaders() instanceof KafkaMessageHeaders) {
|
||||
((KafkaMessageHeaders) message.getHeaders()).getRawHeaders()
|
||||
.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt);
|
||||
Map<String, Object> rawHeaders = ((KafkaMessageHeaders) message.getHeaders()).getRawHeaders();
|
||||
if (KafkaMessageDrivenChannelAdapter.this.retryTemplate != null) {
|
||||
AtomicInteger deliveryAttempt =
|
||||
new AtomicInteger(((RetryContext) attributesHolder.get()).getRetryCount() + 1);
|
||||
rawHeaders.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt);
|
||||
}
|
||||
if (KafkaMessageDrivenChannelAdapter.this.bindSourceRecord) {
|
||||
rawHeaders.put(IntegrationMessageHeaderAccessor.SOURCE_DATA, record);
|
||||
}
|
||||
}
|
||||
else {
|
||||
messageToReturn = MessageBuilder.fromMessage(message)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt)
|
||||
.build();
|
||||
MessageBuilder<?> builder = MessageBuilder.fromMessage(message);
|
||||
if (KafkaMessageDrivenChannelAdapter.this.retryTemplate != null) {
|
||||
AtomicInteger deliveryAttempt =
|
||||
new AtomicInteger(((RetryContext) attributesHolder.get()).getRetryCount() + 1);
|
||||
builder.setHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, deliveryAttempt);
|
||||
}
|
||||
if (KafkaMessageDrivenChannelAdapter.this.bindSourceRecord) {
|
||||
builder.setHeader(IntegrationMessageHeaderAccessor.SOURCE_DATA, record);
|
||||
}
|
||||
messageToReturn = builder.build();
|
||||
}
|
||||
return messageToReturn;
|
||||
}
|
||||
|
||||
@@ -310,9 +310,10 @@ public class KafkaMessageSource<K, V> extends AbstractMessageSource<Object> impl
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to include the raw {@link ConsumerRecord} as a header
|
||||
* with key {@link KafkaHeaders#RAW_DATA},
|
||||
* enabling callers to have access to the record to process errors.
|
||||
* Set to true to include the raw {@link ConsumerRecord} as headers with keys
|
||||
* {@link KafkaHeaders#RAW_DATA} and
|
||||
* {@link IntegrationMessageHeaderAccessor#SOURCE_DATA}. enabling callers to have
|
||||
* access to the record to process errors.
|
||||
* @param rawMessageHeader true to include the header.
|
||||
*/
|
||||
public void setRawMessageHeader(boolean rawMessageHeader) {
|
||||
@@ -458,6 +459,7 @@ public class KafkaMessageSource<K, V> extends AbstractMessageSource<Object> impl
|
||||
rawHeaders.put(REMAINING_RECORDS, this.remainingCount.get());
|
||||
if (this.rawMessageHeader) {
|
||||
rawHeaders.put(KafkaHeaders.RAW_DATA, record);
|
||||
rawHeaders.put(IntegrationMessageHeaderAccessor.SOURCE_DATA, record);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
@@ -467,6 +469,7 @@ public class KafkaMessageSource<K, V> extends AbstractMessageSource<Object> impl
|
||||
.setHeader(REMAINING_RECORDS, this.remainingCount.get());
|
||||
if (this.rawMessageHeader) {
|
||||
builder.setHeader(KafkaHeaders.RAW_DATA, record);
|
||||
builder.setHeader(IntegrationMessageHeaderAccessor.SOURCE_DATA, record);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
|
||||
package org.springframework.integration.kafka.support;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.AttributeAccessor;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.support.ErrorMessageStrategy;
|
||||
import org.springframework.integration.support.ErrorMessageUtils;
|
||||
import org.springframework.kafka.support.KafkaHeaders;
|
||||
@@ -41,9 +42,13 @@ public class RawRecordHeaderErrorMessageStrategy implements ErrorMessageStrategy
|
||||
|
||||
@Override
|
||||
public ErrorMessage buildErrorMessage(Throwable throwable, @Nullable AttributeAccessor context) {
|
||||
Object inputMessage = context.getAttribute(ErrorMessageUtils.INPUT_MESSAGE_CONTEXT_KEY);
|
||||
Map<String, Object> headers =
|
||||
Collections.singletonMap(KafkaHeaders.RAW_DATA, context.getAttribute(KafkaHeaders.RAW_DATA));
|
||||
Object inputMessage = null;
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
if (context != null) {
|
||||
inputMessage = context.getAttribute(ErrorMessageUtils.INPUT_MESSAGE_CONTEXT_KEY);
|
||||
headers.put(KafkaHeaders.RAW_DATA, context.getAttribute(KafkaHeaders.RAW_DATA));
|
||||
headers.put(IntegrationMessageHeaderAccessor.SOURCE_DATA, context.getAttribute(KafkaHeaders.RAW_DATA));
|
||||
}
|
||||
if (inputMessage instanceof Message) {
|
||||
return new ErrorMessage(throwable, headers, (Message<?>) inputMessage);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer;
|
||||
@@ -170,20 +171,20 @@ public class InboundGatewayTests {
|
||||
|
||||
@Test
|
||||
public void testInboundErrorRecover() throws Exception {
|
||||
EmbeddedKafkaBroker embeddedKafka = InboundGatewayTests.embeddedKafka.getEmbeddedKafka();
|
||||
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps("replyHandler2", "false", embeddedKafka);
|
||||
EmbeddedKafkaBroker broker = InboundGatewayTests.embeddedKafka.getEmbeddedKafka();
|
||||
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps("replyHandler2", "false", broker);
|
||||
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
ConsumerFactory<Integer, String> cf2 = new DefaultKafkaConsumerFactory<>(consumerProps);
|
||||
Consumer<Integer, String> consumer = cf2.createConsumer();
|
||||
embeddedKafka.consumeFromAnEmbeddedTopic(consumer, topic4);
|
||||
broker.consumeFromAnEmbeddedTopic(consumer, topic4);
|
||||
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("test2", "false", embeddedKafka);
|
||||
Map<String, Object> props = KafkaTestUtils.consumerProps("test2", "false", broker);
|
||||
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
DefaultKafkaConsumerFactory<Integer, String> cf = new DefaultKafkaConsumerFactory<>(props);
|
||||
ContainerProperties containerProps = new ContainerProperties(topic3);
|
||||
KafkaMessageListenerContainer<Integer, String> container =
|
||||
new KafkaMessageListenerContainer<>(cf, containerProps);
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
|
||||
Map<String, Object> senderProps = KafkaTestUtils.producerProps(broker);
|
||||
ProducerFactory<Integer, String> pf = new DefaultKafkaProducerFactory<>(senderProps);
|
||||
KafkaTemplate<Integer, String> template = new KafkaTemplate<>(pf);
|
||||
template.setDefaultTopic(topic3);
|
||||
@@ -215,6 +216,7 @@ public class InboundGatewayTests {
|
||||
|
||||
});
|
||||
gateway.setReplyTimeout(30_000);
|
||||
gateway.setBindSourceRecord(true);
|
||||
gateway.afterPropertiesSet();
|
||||
gateway.start();
|
||||
ContainerTestUtils.waitForAssignment(container, 2);
|
||||
@@ -222,8 +224,11 @@ public class InboundGatewayTests {
|
||||
template.sendDefault(0, 1487694048607L, 1, "foo");
|
||||
ErrorMessage em = (ErrorMessage) errors.receive(30_000);
|
||||
assertThat(em).isNotNull();
|
||||
assertThat(em.getHeaders().get(KafkaHeaders.RAW_DATA)).isNotNull();
|
||||
Message<?> failed = ((MessagingException) em.getPayload()).getFailedMessage();
|
||||
assertThat(failed).isNotNull();
|
||||
assertThat(failed.getHeaders().get(IntegrationMessageHeaderAccessor.SOURCE_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());
|
||||
@@ -308,6 +313,7 @@ public class InboundGatewayTests {
|
||||
assertThat(em).isNotNull();
|
||||
Message<?> failed = ((MessagingException) em.getPayload()).getFailedMessage();
|
||||
assertThat(failed).isNotNull();
|
||||
assertThat(failed.getHeaders().get(IntegrationMessageHeaderAccessor.SOURCE_DATA)).isNull();
|
||||
MessageChannel reply = (MessageChannel) em.getHeaders().getReplyChannel();
|
||||
MessageHeaders headers = failed.getHeaders();
|
||||
reply.send(MessageBuilder.withPayload("ERROR").copyHeaders(headers).build());
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.apache.kafka.common.header.internals.RecordHeaders;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.StaticMessageHeaderAccessor;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
@@ -238,14 +239,15 @@ public class MessageDrivenAdapterTests {
|
||||
assertThat(received).isInstanceOf(ErrorMessage.class);
|
||||
MessageHeaders headers = received.getHeaders();
|
||||
assertThat(headers.get(KafkaHeaders.RAW_DATA)).isNotNull();
|
||||
received = ((ErrorMessage) received).getOriginalMessage();
|
||||
assertThat(received).isNotNull();
|
||||
headers = received.getHeaders();
|
||||
Message<?> originalMessage = ((ErrorMessage) received).getOriginalMessage();
|
||||
assertThat(originalMessage).isNotNull();
|
||||
assertThat(originalMessage.getHeaders().get(IntegrationMessageHeaderAccessor.SOURCE_DATA)).isNull();
|
||||
headers = originalMessage.getHeaders();
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_MESSAGE_KEY)).isEqualTo(1);
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_TOPIC)).isEqualTo(topic4);
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_PARTITION_ID)).isEqualTo(0);
|
||||
assertThat(headers.get(KafkaHeaders.OFFSET)).isEqualTo(0L);
|
||||
assertThat(StaticMessageHeaderAccessor.getDeliveryAttempt(received).get()).isEqualTo(2);
|
||||
assertThat(StaticMessageHeaderAccessor.getDeliveryAttempt(originalMessage).get()).isEqualTo(2);
|
||||
|
||||
adapter.stop();
|
||||
}
|
||||
@@ -272,6 +274,7 @@ public class MessageDrivenAdapterTests {
|
||||
adapter.setErrorChannel(errorChannel);
|
||||
adapter.setRecoveryCallback(
|
||||
new ErrorMessageSendingRecoverer(errorChannel, new RawRecordHeaderErrorMessageStrategy()));
|
||||
adapter.setBindSourceRecord(true);
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
ContainerTestUtils.waitForAssignment(container, 2);
|
||||
@@ -286,9 +289,13 @@ public class MessageDrivenAdapterTests {
|
||||
assertThat(received).isInstanceOf(ErrorMessage.class);
|
||||
MessageHeaders headers = received.getHeaders();
|
||||
assertThat(headers.get(KafkaHeaders.RAW_DATA)).isNotNull();
|
||||
received = ((ErrorMessage) received).getOriginalMessage();
|
||||
assertThat(received).isNotNull();
|
||||
headers = received.getHeaders();
|
||||
assertThat(headers.get(IntegrationMessageHeaderAccessor.SOURCE_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));
|
||||
headers = originalMessage.getHeaders();
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_MESSAGE_KEY)).isEqualTo(1);
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_TOPIC)).isEqualTo(topic5);
|
||||
assertThat(headers.get(KafkaHeaders.RECEIVED_PARTITION_ID)).isEqualTo(0);
|
||||
|
||||
@@ -56,6 +56,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.StaticMessageHeaderAccessor;
|
||||
import org.springframework.integration.acks.AcknowledgmentCallback;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
@@ -116,6 +117,8 @@ public class MessageSourceTests {
|
||||
Message<?> received = source.receive();
|
||||
assertThat(received).isNotNull();
|
||||
assertThat(received.getHeaders().get(KafkaHeaders.RAW_DATA)).isInstanceOf(ConsumerRecord.class);
|
||||
assertThat(received.getHeaders().get(IntegrationMessageHeaderAccessor.SOURCE_DATA))
|
||||
.isSameAs(received.getHeaders().get(KafkaHeaders.RAW_DATA));
|
||||
StaticMessageHeaderAccessor.getAcknowledgmentCallback(received)
|
||||
.acknowledge(AcknowledgmentCallback.Status.ACCEPT);
|
||||
received = source.receive();
|
||||
|
||||
Reference in New Issue
Block a user