GH-172: Add Success and Failure Message Channels
Resolves https://github.com/spring-projects/spring-integration-kafka/issues/172 Use `ErrorMessageStrategy` to build an error message in the event of a failure. Update to SI 4.3.12. Polishing - PR Comments - Use standard outputChannel (if configured) for success. Polishing - PR Comments Javadoc Polishing * Polishing `@since` in JavaDocs * Upgrade to Checkstyle 8.1, SI-5.0.B-S * Adjust `RequireThis` rule and fix vulnerabilities # Conflicts: # build.gradle # src/test/java/org/springframework/integration/kafka/outbound/KafkaProducerMessageHandlerTests.java
This commit is contained in:
committed by
Artem Bilan
parent
37ffd4e2e9
commit
857980349e
@@ -4,6 +4,5 @@
|
||||
"http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
<suppressions>
|
||||
<suppress files="package-info\.java" checks=".*" />
|
||||
<suppress files="[\\/]test[\\/]" checks="RequireThis" />
|
||||
<suppress files="[\\/]test[\\/]" checks="Javadoc*" />
|
||||
</suppressions>
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
</module>
|
||||
<module name="MultipleVariableDeclarations" />
|
||||
<module name="RequireThis">
|
||||
<property name="validateOnlyOverlapping" value="false" />
|
||||
<property name="checkMethods" value="false" />
|
||||
</module>
|
||||
<module name="OneStatementPerLine" />
|
||||
|
||||
@@ -289,7 +289,7 @@ public class KafkaProducerMessageHandlerSpec<K, V, S extends KafkaProducerMessag
|
||||
|
||||
@Override
|
||||
public Map<Object, String> getComponentsToRegister() {
|
||||
return Collections.singletonMap(this.kafkaTemplateSpec.get(), kafkaTemplateSpec.getId());
|
||||
return Collections.singletonMap(this.kafkaTemplateSpec.get(), this.kafkaTemplateSpec.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,17 +28,24 @@ import org.springframework.expression.Expression;
|
||||
import org.springframework.integration.MessageTimeoutException;
|
||||
import org.springframework.integration.expression.ExpressionUtils;
|
||||
import org.springframework.integration.expression.ValueExpression;
|
||||
import org.springframework.integration.handler.AbstractMessageHandler;
|
||||
import org.springframework.integration.handler.AbstractMessageProducingHandler;
|
||||
import org.springframework.integration.kafka.support.KafkaSendFailureException;
|
||||
import org.springframework.integration.support.DefaultErrorMessageStrategy;
|
||||
import org.springframework.integration.support.ErrorMessageStrategy;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.support.DefaultKafkaHeaderMapper;
|
||||
import org.springframework.kafka.support.JacksonPresent;
|
||||
import org.springframework.kafka.support.KafkaHeaderMapper;
|
||||
import org.springframework.kafka.support.KafkaHeaders;
|
||||
import org.springframework.kafka.support.KafkaNull;
|
||||
import org.springframework.kafka.support.SendResult;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.support.ErrorMessage;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||
|
||||
/**
|
||||
* Kafka Message Handler.
|
||||
@@ -54,7 +61,7 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||
*
|
||||
* @since 0.5
|
||||
*/
|
||||
public class KafkaProducerMessageHandler<K, V> extends AbstractMessageHandler {
|
||||
public class KafkaProducerMessageHandler<K, V> extends AbstractMessageProducingHandler {
|
||||
|
||||
private static final long DEFAULT_SEND_TIMEOUT = 10000;
|
||||
|
||||
@@ -76,6 +83,12 @@ public class KafkaProducerMessageHandler<K, V> extends AbstractMessageHandler {
|
||||
|
||||
private KafkaHeaderMapper headerMapper;
|
||||
|
||||
private MessageChannel sendFailureChannel;
|
||||
|
||||
private String sendFailureChannelName;
|
||||
|
||||
private ErrorMessageStrategy errorMessageStrategy = new DefaultErrorMessageStrategy();
|
||||
|
||||
public KafkaProducerMessageHandler(final KafkaTemplate<K, V> kafkaTemplate) {
|
||||
Assert.notNull(kafkaTemplate, "kafkaTemplate cannot be null");
|
||||
this.kafkaTemplate = kafkaTemplate;
|
||||
@@ -99,7 +112,6 @@ public class KafkaProducerMessageHandler<K, V> extends AbstractMessageHandler {
|
||||
/**
|
||||
* Specify a SpEL expression to evaluate a timestamp that will be added in the Kafka record.
|
||||
* The resulting value should be a {@link Long} type representing epoch time in milliseconds.
|
||||
*
|
||||
* @param timestampExpression the {@link Expression} for timestamp to wait for result
|
||||
* fo send operation.
|
||||
* @since 2.3
|
||||
@@ -136,10 +148,13 @@ public class KafkaProducerMessageHandler<K, V> extends AbstractMessageHandler {
|
||||
* Specify a timeout in milliseconds for how long this
|
||||
* {@link KafkaProducerMessageHandler} should wait wait for send operation
|
||||
* results. Defaults to 10 seconds. The timeout is applied only in {@link #sync} mode.
|
||||
* Also applies when sending to the success or failure channels.
|
||||
* @param sendTimeout the timeout to wait for result fo send operation.
|
||||
* @since 2.0.1
|
||||
*/
|
||||
@Override
|
||||
public void setSendTimeout(long sendTimeout) {
|
||||
super.setSendTimeout(sendTimeout);
|
||||
setSendTimeoutExpression(new ValueExpression<>(sendTimeout));
|
||||
}
|
||||
|
||||
@@ -156,6 +171,50 @@ public class KafkaProducerMessageHandler<K, V> extends AbstractMessageHandler {
|
||||
this.sendTimeoutExpression = sendTimeoutExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the failure channel. After a send failure, an {@link ErrorMessage} will be sent
|
||||
* to this channel with a payload of a {@link KafkaSendFailureException} with the
|
||||
* failed message and cause.
|
||||
* @param sendFailureChannel the failure channel.
|
||||
* @since 2.1.2
|
||||
*/
|
||||
public void setSendFailureChannel(MessageChannel sendFailureChannel) {
|
||||
this.sendFailureChannel = sendFailureChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the failure channel name. After a send failure, an {@link ErrorMessage} will be
|
||||
* sent to this channel name with a payload of a {@link KafkaSendFailureException}
|
||||
* with the failed message and cause.
|
||||
* @param sendFailureChannelName the failure channel name.
|
||||
* @since 2.1.2
|
||||
*/
|
||||
public void setSendFailureChannelName(String sendFailureChannelName) {
|
||||
this.sendFailureChannelName = sendFailureChannelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error message strategy implementation to use when sending error messages after
|
||||
* send failures. Cannot be null.
|
||||
* @param errorMessageStrategy the implementation.
|
||||
* @since 2.1.2
|
||||
*/
|
||||
public void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) {
|
||||
Assert.notNull(errorMessageStrategy, "'errorMessageStrategy' cannot be null");
|
||||
this.errorMessageStrategy = errorMessageStrategy;
|
||||
}
|
||||
|
||||
protected MessageChannel getSendFailureChannel() {
|
||||
if (this.sendFailureChannel != null) {
|
||||
return this.sendFailureChannel;
|
||||
}
|
||||
else if (this.sendFailureChannelName != null) {
|
||||
this.sendFailureChannel = getChannelResolver().resolveDestination(this.sendFailureChannelName);
|
||||
return this.sendFailureChannel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() throws Exception {
|
||||
super.onInit();
|
||||
@@ -193,9 +252,33 @@ public class KafkaProducerMessageHandler<K, V> extends AbstractMessageHandler {
|
||||
headers = new RecordHeaders();
|
||||
this.headerMapper.fromHeaders(message.getHeaders(), headers);
|
||||
}
|
||||
ProducerRecord<K, V> producerRecord = new ProducerRecord<K, V>(topic, partitionId, timestamp, (K) messageKey,
|
||||
payload, headers);
|
||||
ListenableFuture<?> future = this.kafkaTemplate.send(producerRecord);
|
||||
final ProducerRecord<K, V> producerRecord = new ProducerRecord<K, V>(topic, partitionId, timestamp,
|
||||
(K) messageKey, payload, headers);
|
||||
ListenableFuture<SendResult<K, V>> future = this.kafkaTemplate.send(producerRecord);
|
||||
if (getSendFailureChannel() != null || getOutputChannel() != null) {
|
||||
future.addCallback(new ListenableFutureCallback<SendResult<K, V>>() {
|
||||
|
||||
@Override
|
||||
public void onSuccess(SendResult<K, V> result) {
|
||||
if (getOutputChannel() != null) {
|
||||
KafkaProducerMessageHandler.this.messagingTemplate.send(getOutputChannel(),
|
||||
getMessageBuilderFactory().fromMessage(message)
|
||||
// TODO: change to constant when available
|
||||
.setHeader("kafka_recordMetadata", result.getRecordMetadata()).build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable ex) {
|
||||
if (getSendFailureChannel() != null) {
|
||||
KafkaProducerMessageHandler.this.messagingTemplate.send(getSendFailureChannel(),
|
||||
KafkaProducerMessageHandler.this.errorMessageStrategy.buildErrorMessage(
|
||||
new KafkaSendFailureException(message, producerRecord, ex), null));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if (this.sync) {
|
||||
Long sendTimeout = this.sendTimeoutExpression.getValue(this.evaluationContext, message, Long.class);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.integration.kafka.support;
|
||||
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
/**
|
||||
* An exception that is the payload of an {@code ErrorMessage} when a send fails.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*
|
||||
* @since 2.1.2
|
||||
*
|
||||
*/
|
||||
public class KafkaSendFailureException extends MessagingException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final ProducerRecord<?, ?> record;
|
||||
|
||||
public KafkaSendFailureException(Message<?> message, ProducerRecord<?, ?> record, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
public ProducerRecord<?, ?> getRecord() {
|
||||
return this.record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " [record=" + this.record + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2016 the original author or authors.
|
||||
* Copyright 2015-2017 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.
|
||||
@@ -72,7 +72,7 @@ public class KafkaMessageDrivenChannelAdapterParserTests {
|
||||
TestUtils.getPropertyValue(this.kafkaListener, "messageListenerContainer",
|
||||
KafkaMessageListenerContainer.class);
|
||||
assertThat(container).isNotNull();
|
||||
assertThat(TestUtils.getPropertyValue(kafkaListener, "mode", ListenerMode.class))
|
||||
assertThat(TestUtils.getPropertyValue(this.kafkaListener, "mode", ListenerMode.class))
|
||||
.isEqualTo(ListenerMode.record);
|
||||
assertThat(TestUtils.getPropertyValue(this.kafkaListener, "recordListener.fallbackType"))
|
||||
.isEqualTo(String.class);
|
||||
@@ -91,7 +91,7 @@ public class KafkaMessageDrivenChannelAdapterParserTests {
|
||||
TestUtils.getPropertyValue(this.kafkaBatchListener, "messageListenerContainer",
|
||||
KafkaMessageListenerContainer.class);
|
||||
assertThat(container).isNotNull();
|
||||
assertThat(TestUtils.getPropertyValue(kafkaBatchListener, "mode", ListenerMode.class))
|
||||
assertThat(TestUtils.getPropertyValue(this.kafkaBatchListener, "mode", ListenerMode.class))
|
||||
.isEqualTo(ListenerMode.batch);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,18 @@ import java.util.Map;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.apache.kafka.clients.producer.RecordMetadata;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.expression.FunctionExpression;
|
||||
import org.springframework.integration.expression.ValueExpression;
|
||||
import org.springframework.integration.kafka.support.KafkaSendFailureException;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||
@@ -45,9 +49,15 @@ import org.springframework.kafka.core.ProducerFactory;
|
||||
import org.springframework.kafka.support.DefaultKafkaHeaderMapper;
|
||||
import org.springframework.kafka.support.KafkaHeaders;
|
||||
import org.springframework.kafka.support.KafkaNull;
|
||||
import org.springframework.kafka.support.SendResult;
|
||||
import org.springframework.kafka.test.rule.KafkaEmbedded;
|
||||
import org.springframework.kafka.test.utils.KafkaTestUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.ErrorMessage;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.SettableListenableFuture;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
@@ -64,6 +74,7 @@ public class KafkaProducerMessageHandlerTests {
|
||||
@ClassRule
|
||||
public static KafkaEmbedded embeddedKafka = new KafkaEmbedded(1, true, topic1, topic2);
|
||||
|
||||
|
||||
private static Consumer<Integer, String> consumer;
|
||||
|
||||
@BeforeClass
|
||||
@@ -194,4 +205,63 @@ public class KafkaProducerMessageHandlerTests {
|
||||
assertThat(record2.timestamp()).isGreaterThanOrEqualTo(currentTimeMarker);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutboundWithAsyncResults() {
|
||||
ProducerFactory<Integer, String> producerFactory = new DefaultKafkaProducerFactory<>(
|
||||
KafkaTestUtils.producerProps(embeddedKafka));
|
||||
KafkaTemplate<Integer, String> template = new KafkaTemplate<>(producerFactory);
|
||||
KafkaProducerMessageHandler<Integer, String> handler = new KafkaProducerMessageHandler<>(template);
|
||||
handler.setBeanFactory(mock(BeanFactory.class));
|
||||
PollableChannel successes = new QueueChannel();
|
||||
handler.setOutputChannel(successes);
|
||||
handler.afterPropertiesSet();
|
||||
|
||||
Message<?> message = MessageBuilder.withPayload("foo")
|
||||
.setHeader(KafkaHeaders.TOPIC, topic1)
|
||||
.setHeader(KafkaHeaders.MESSAGE_KEY, 2)
|
||||
.setHeader(KafkaHeaders.PARTITION_ID, 1)
|
||||
.build();
|
||||
handler.handleMessage(message);
|
||||
|
||||
ConsumerRecord<Integer, String> record = KafkaTestUtils.getSingleRecord(consumer, topic1);
|
||||
assertThat(record).has(key(2));
|
||||
assertThat(record).has(partition(1));
|
||||
assertThat(record).has(value("foo"));
|
||||
Message<?> received = successes.receive(10000);
|
||||
assertThat(received).isNotNull();
|
||||
assertThat(received.getPayload()).isEqualTo("foo");
|
||||
// TODO: Change to constant when available
|
||||
assertThat(received.getHeaders().get("kafka_recordMetadata")).isInstanceOf(RecordMetadata.class);
|
||||
|
||||
final RuntimeException fooException = new RuntimeException("foo");
|
||||
|
||||
handler = new KafkaProducerMessageHandler<>(new KafkaTemplate<Integer, String>(producerFactory) {
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<SendResult<Integer, String>> doSend(
|
||||
ProducerRecord<Integer, String> producerRecord) {
|
||||
SettableListenableFuture<SendResult<Integer, String>> future = new SettableListenableFuture<>();
|
||||
future.setException(fooException);
|
||||
return future;
|
||||
}
|
||||
|
||||
});
|
||||
PollableChannel failures = new QueueChannel();
|
||||
handler.setSendFailureChannel(failures);
|
||||
handler.setBeanFactory(mock(BeanFactory.class));
|
||||
handler.afterPropertiesSet();
|
||||
message = MessageBuilder.withPayload("bar")
|
||||
.setHeader(KafkaHeaders.TOPIC, "foo")
|
||||
.setHeader(KafkaHeaders.PARTITION_ID, 0)
|
||||
.build();
|
||||
handler.handleMessage(message);
|
||||
|
||||
received = failures.receive(10000);
|
||||
assertThat(received).isNotNull();
|
||||
assertThat(received).isInstanceOf(ErrorMessage.class);
|
||||
assertThat(((MessagingException) received.getPayload()).getFailedMessage()).isSameAs(message);
|
||||
assertThat(((MessagingException) received.getPayload()).getCause()).isSameAs(fooException);
|
||||
assertThat(((KafkaSendFailureException) received.getPayload()).getRecord()).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user