Spring integration : testing async flows - spring

I'm trying to write a test case for a scenario in which I send a message to a JMS endpoint and then the message is processed using Spring Integration. Here is a test case that works but its not entirely clear to me why it works. Here is the test:
public class MessageListenerTest {
#Autowired ApplicationContext context;
#Test
public void testEventListener() {
SubscribableChannel eventsChannel = (SubscribableChannel) context.getBean("events");
class TestMessageHandler implements MessageHandler {
public Boolean received = false;
#Override
public void handleMessage(Message<?> message) throws MessagingException {
received = true;
}
}
TestMessageHandler handler = new TestMessageHandler();
eventsChannel.subscribe(handler);
PollableChannel outputChannel = new QueueChannel();
PollingConsumer consumer = new PollingConsumer(outputChannel, handler);
consumer.setBeanFactory(context);
consumer.start();
String msg = "hello world!";
JmsTemplate template = (JmsTemplate) context.getBean("jmsTemplate");
template.convertAndSend("myQueue", msg);
outputChannel.receive(2000);
Assert.assertTrue(handler.received);
}
}
This tests that the message sent to the queue is received. The message is received by a SubscribableChannel named events. What is not clear to me is how the PollableChannel is connected to the SubscribableChannel. Is it because of TestMessageHandler? If so, how? If this is working entirely by accident, it would be useful if someone could fix this or provide a simpler test case for this scenario.
UPDATE: Based on the suggestions in the comments, here is a modified test case:
public class MessageListenerTest {
#Autowired ApplicationContext context;
#Test
public void testEventListener() throws InterruptedException {
SubscribableChannel eventsChannel = (SubscribableChannel) context.getBean("events");
final CountDownLatch countDownLatch = new CountDownLatch(1);
MessageHandler handler = new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
countDownLatch.countDown();
}
};
eventsChannel.subscribe(handler);
String msg = "hello world!";
JmsTemplate template = (JmsTemplate) context.getBean("jmsTemplate");
template.convertAndSend("myQueue", msg);
Assert.assertTrue(countDownLatch.await(2, TimeUnit.SECONDS));
}
}

Your outputChannel just doesn't work. This channel is a test-case scope. so no one sends messages to it.
You can check that with the assert:
Assert.notNull(outputChannel.receive(2000));
It doesn't matter for how much channel the MessageHandler is subscribed. It just handles messages. So, it continues to handle messages from your events. And the binding to that inline outputChannel just is dead and doesn't matter for other application.

Related

Spring JMS listener acknowledge

I am using JMS to send receive message from IBM MQ message broker. I am currently working on listener service throwing unhandled excepion and message sent
back to queue without acknowledgement.
I want the service to retry a configurable number of time and throw meaning full exception message that listener service is unavailable.
My listener and container factory looks like below.
#JmsListener(destination = "testqueue", containerFactory = "queuejmsfactory")
public void consumer(String message) throws JMSException
{ handle(message); }
#Bean(name = "queuejmsfactory") public JmsListenerContainerFactory getQueueTopicFactory(ConnectionFactory con ,
DefaultJmsListenerContainerFactoryConfigurer config)
{ DefaultJmsListenerContainerFactory d = new DefaultJmsListenerContainerFactory();
d.setSessionTransacted(true);
d.setSessionAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
config.configure(d,con);
return d; }
I short, I have an existing code using the SessionawareMessageListener onMessage which i am trying to
replicate to #JmsListener. How do i handle the session commit and rollback automatically and
how do i get the session in JmsListener if have to handle manually similar to onMessage.
#Override
public void onMessage(Mesage mes, Session ses) throws JMSException
{ try
{ TestMessage txtMessage = (TextMessage)message;
handle(txtMessage); ses.commit();
} catch (Exception exp)
{ if (shouldRollback(message))
{ ses.rollback();}
else{logger,warn("moved to dlq");
ses.commit();
}
} }
private boolean shouldRollback(Message mes) throws JMSException
{ int rollbackcount = mes.getIntProperty("JMSXDeliveryCount");
return (rollbackcount <= maxRollBackCountFromApplication.properties)
}
Updated code:
#JmsListener(destination = "testqueue", containerFactory = "queuejmsfactory")
public void consumer(Message message) throws JMSException
{
try {TestMessage txtMessage = (TextMessage)message;
handle(txtMessage);}
catch(Excepton ex) {
if shouldRollback(message)
{throw ex;}
else {logger.warn("moved to dlq")}
}}
private boolean shouldRollback(Message mes) throws JMSException
{ int rollbackcount = mes.getIntProperty("JMSXDeliveryCount");
return (rollbackcount <= maxRollBackCountFromApplication.properties)
}
#Bean(name = "queuejmsfactory") public JmsListenerContainerFactory getQueueTopicFactory(ConnectionFactory con ,
DefaultJmsListenerContainerFactoryConfigurer config)
{ DefaultJmsListenerContainerFactory d = new DefaultJmsListenerContainerFactory();
d.setSessionTransacted(true);
d.setSessionAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
config.configure(d,con);
return d; }
I have also tried to access the JMSXDeliveryCount from Headers, but couldnt get the exact object to access delivery count. Can you clarify plz.
#JmsListener(destination = "testqueue", containerFactory = "queuejmsfactory")
public void consumer(Message message,
#Header(JmsHeaders.CORRELATION_ID) String correlationId,
#Header(name = "jms-header-not-exists") String nonExistingHeader,
#Headers Map<String, Object> headers,
MessageHeaders messageHeaders,
JmsMessageHeaderAccessor jmsMessageHeaderAccessor) {}
You can add the Session as another parameter to the JmsListener method.

Spring Integration - Convert Service Activator with Java Configuration

I try to convert the "Hello World example" from Spring Integration samples (https://github.com/spring-projects/spring-integration-samples/tree/master/basic/helloworld) from XML, to Java Configuration, (so with the #Configuration annotation).
The configuration class looks like this :
#Configuration
#EnableIntegration
public class BasicIntegrationConfig{
#Bean
public DirectChannel inputCHannel() {
return new DirectChannel();
}
#Bean
public QueueChannel outputChannel() {
return new QueueChannel();
}
#Bean
#ServiceActivator(inputChannel= "inputChannel", outputChannel= "outputChannel" )
public MessageHandler fileWritingMessageHandler() {
MessageHandler mh = new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println("Message payload: " + message.getPayload());
}
};
return mh;
}
}
To test it, I use the main() supplied from sample project :
DirectChannel fileChannel = applicationContext.getBean("inputChannel", DirectChannel.class);
QueueChannel outputChannel = applicationContext.getBean("outputChannel", QueueChannel.class);
System.out.println("********** SENDING MESSAGE");
fileChannel.send(new GenericMessage<>("test"));
System.out.println(outputChannel.receive(0).getPayload());
I see in the console "Message payload: test", but unfortunately, I don't receive the message on the outputchannel (I have a NullPointerException on outputChannel.receive(0).
Do you have an idea why the Service Activator does not send the message to the output channel?
Your MessageHandler returns void.
You need to subclass AbstractReplyProducingMessageHandler instead.
Thank you Gary, it works perfectly after switching to :
#Bean
#ServiceActivator(inputChannel= "inputChannel")
public AbstractReplyProducingMessageHandler fileWritingMessageHandler() {
AbstractReplyProducingMessageHandler mh = new AbstractReplyProducingMessageHandler() {
#Override
protected Object handleRequestMessage(Message<?> message) {
String payload= (String)message.getPayload();
return "Message Payload : ".concat(payload);
}
};
mh.setOutputChannelName("outputChannel");
return mh;
}
As a side note, I had to remove the output channel attribute in #ServiceActivator annotation, and put it in method body instead (Bean Validation Exception if not).

_AMQ_GROUP_ID present in message but JMSXGroupID null in #JmsListener

From this documentation:
Messages in a message group share the same group id, i.e. they have same group identifier property (JMSXGroupID for JMS, _AMQ_GROUP_ID for Apache ActiveMQ Artemis Core API).
I can see why the property originally set via JMSXGroupID becomes _AMQ_GROUP_ID when I browse the messages in the broker with a value of product=paper. However, In my #JmsListener annotated method I can see the _AMQ_GROUP_ID property is missing and the JMSXGroupID is coming through as null in the Message's headers hashmap.
#JmsListener(destination = "${artemis.destination}", subscription = "${artemis.subscriptionName}",
containerFactory = "containerFactory", concurrency = "15-15")
public void consumeMessage(Message<StatefulSpineEvent<?>> eventMessage)
So
My Producer application sends the message to the queue after setting the string property JMSXGroupID to 'product=paper'
I can see _AMQ_GROUP_ID has a value of 'product=paper' when I browse that message's headers in the Artemis UI
When I debug my listener application and look at the map of headers, _AMQ_GROUP_ID is absent and JMSXGroupID has a value of null instead of 'product=paper'.
Is the character '=' invalid or is there something else that can cause this? I'm running out of things to try.
Edit, with new code:
HeaderMapper:
#Component
public class GroupIdMessageMapper extends SimpleJmsHeaderMapper {
#Override
public MessageHeaders toHeaders(Message jmsMessage) {
MessageHeaders messageHeaders = super.toHeaders(jmsMessage);
Map<String, Object> messageHeadersMap = new HashMap<>(messageHeaders);
try {
messageHeadersMap.put("JMSXGroupID", jmsMessage.getStringProperty("_AMQ_GROUP_ID"));
} catch (JMSException e) {
e.printStackTrace();
}
// can see while debugging that this returns the correct headers
return new MessageHeaders(messageHeadersMap);
}
}
Listener:
#Component
public class CustomSpringJmsListener {
protected final Logger LOG = LoggerFactory.getLogger(getClass());
#JmsListener(destination = "local-queue", subscription = "groupid-example",
containerFactory = "myContainerFactory", concurrency = "15-15")
public void receive(Message message) throws JMSException {
LOG.info("Received message: " + message);
}
}
Application code:
#SpringBootApplication
#EnableJms
public class GroupidApplication implements CommandLineRunner {
private static Logger LOG = LoggerFactory
.getLogger(GroupidApplication.class);
#Autowired
private JmsTemplate jmsTemplate;
#Autowired MessageConverter messageConverter;
public static void main(String[] args) {
LOG.info("STARTING THE APPLICATION");
SpringApplication.run(GroupidApplication.class, args);
LOG.info("APPLICATION FINISHED");
}
#Override
public void run(String... args) {
LOG.info("EXECUTING : command line runner");
jmsTemplate.setPubSubDomain(true);
createAndSendObjectMessage("Message1");
createAndSendTextMessage("Message2");
createAndSendTextMessage("Message3");
createAndSendTextMessage("Message4");
createAndSendTextMessage("Message5");
createAndSendTextMessage("Message6");
}
private void createAndSendTextMessage(String messageBody) {
jmsTemplate.send("local-queue", session -> {
Message message = session.createTextMessage(messageBody);
message.setStringProperty("JMSXGroupID", "product=paper");
return message;
});
}
// BEANS
#Bean
public JmsListenerContainerFactory<?> myContainerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// This provides all boot's default to this factory, including the message converter
configurer.configure(factory, connectionFactory);
// You could still override some of Boot's default if necessary.
factory.setSubscriptionDurable(true);
factory.setSubscriptionShared(true);
factory.setMessageConverter(messagingMessageConverter());
return factory;
}
#Bean
public MessagingMessageConverter messagingMessageConverter() {
return new MessagingMessageConverter(messageConverter, new GroupIdMessageMapper());
}
}
Stack trace of where SimpleJmsHeaderMapper is being called:
toHeaders:130, SimpleJmsHeaderMapper (org.springframework.jms.support)
toHeaders:57, SimpleJmsHeaderMapper (org.springframework.jms.support)
extractHeaders:148, MessagingMessageConverter
(org.springframework.jms.support.converter) access$100:466,
AbstractAdaptableMessageListener$MessagingMessageConverterAdapter
(org.springframework.jms.listener.adapter) getHeaders:552,
AbstractAdaptableMessageListener$MessagingMessageConverterAdapter$LazyResolutionMessage
(org.springframework.jms.listener.adapter) resolveArgumentInternal:68,
HeaderMethodArgumentResolver
(org.springframework.messaging.handler.annotation.support)
resolveArgument:100, AbstractNamedValueMethodArgumentResolver
(org.springframework.messaging.handler.annotation.support)
resolveArgument:117, HandlerMethodArgumentResolverComposite
(org.springframework.messaging.handler.invocation)
getMethodArgumentValues:148, InvocableHandlerMethod
(org.springframework.messaging.handler.invocation) invoke:116,
InvocableHandlerMethod
(org.springframework.messaging.handler.invocation) invokeHandler:114,
MessagingMessageListenerAdapter
(org.springframework.jms.listener.adapter) onMessage:77,
MessagingMessageListenerAdapter
(org.springframework.jms.listener.adapter) doInvokeListener:736,
AbstractMessageListenerContainer (org.springframework.jms.listener)
invokeListener:696, AbstractMessageListenerContainer
(org.springframework.jms.listener) doExecuteListener:674,
AbstractMessageListenerContainer (org.springframework.jms.listener)
doReceiveAndExecute:318, AbstractPollingMessageListenerContainer
(org.springframework.jms.listener) receiveAndExecute:257,
AbstractPollingMessageListenerContainer
(org.springframework.jms.listener) invokeListener:1190,
DefaultMessageListenerContainer$AsyncMessageListenerInvoker
(org.springframework.jms.listener) executeOngoingLoop:1180,
DefaultMessageListenerContainer$AsyncMessageListenerInvoker
(org.springframework.jms.listener) run:1077,
DefaultMessageListenerContainer$AsyncMessageListenerInvoker
(org.springframework.jms.listener) run:748, Thread (java.lang)
Try subclassing the SimpleJmsHeaderMapper and override toHeaders(). Call super.toHeaders(), create a new Map<> from the result; put() any additional headers you want into the map and return a new MessageHeaders from the map.
Pass the custom mapper into a new MessagingMessageConverter and pass that into the container factory.
If you are using Spring Boot, simply add the converter as a #Bean and boot will auto wire it into the factory.
EDIT
After all this; I just wrote an app and it works just fine for me without any customization at all...
#SpringBootApplication
public class So58399905Application {
public static void main(String[] args) {
SpringApplication.run(So58399905Application.class, args);
}
#JmsListener(destination = "foo")
public void listen(String in, MessageHeaders headers) {
System.out.println(in + headers);
}
#Bean
public ApplicationRunner runner(JmsTemplate template) {
return args -> template.convertAndSend("foo", "bar", msg -> {
msg.setStringProperty("JMSXGroupID", "product=x");
return msg;
});
}
}
and
bar{jms_redelivered=false, JMSXGroupID=product=x, jms_deliveryMode=2, JMSXDeliveryCount=1, ...
EDIT2
It's a bug in the artemis client - with 2.6.4 (Boot 2.1.9) only getStringProperty() returns the value of the _AMQ_GROUP_ID property when getting JMSXGroupID.
The mapper uses getObjectProperty() which returned null. With the 2.10.1 client; the message properly returns the value of the _AMQ_GROUP_ID property from getObjectProperty().

Overridden onMessage of MessageListner not getting called in Spring Kafka Consumer Unit Test

I am writing Kafka Consumer Unit Test, and need to Mock the Service of my KafkaConsumer for testing the Kafka Consumer independently. But, the mockObject of Service is not getting invoked, instead Spring is creating the original Service class object and calling it. Thus, my mock class object not getting called.
KafkaConsumer :
#Slf4j
#Component
#RequiredArgsConstructor (onConstructor = #__(#Autowired))
public class KafkaEventConsumer {
private final MyService requestService;
#KafkaListener (topics = "${kafka.topic:topic-name}")
public void receive(#Payload String message) throws Exception {
try {
LOGGER.debug("Received message:{} ", message);
ObjectMapper mapper = new ObjectMapper();
ForecastRequest forecastRequest = mapper.readValue(message, ForecastRequest.class);
JobDetail jobDetail = requestForecastService.refreshForecasts(forecastRequest);
if (jobDetail.getJobStatus() != JobStatus.complete) {
LOGGER.error("Failed to Refresh Forecast for ProgramId-{}, JobId-{}, JobStatus-{}",
forecastRequest.getProgramId(), jobDetail.getJobId(), jobDetail.getJobStatus());
throw new Exception("Internal Server Error");
}
} catch (Exception e) {
LOGGER.error("Failed to Refresh Forecast for Forecast Request {}", message, e);
throw e;
}
}
}
Kafka Consumer Test :
#RunWith (SpringRunner.class)
#ActiveProfiles ("kafkatest")
#SpringBootTest (classes = ForecastEventConsumerApplication.class)
#DirtiesContext
public class KafkaEventConsumerTest {
private static String TOPIC = "topic-name";
#Mock
private MyServiceImpl myServiceMock;
#InjectMocks
private KafkaEventConsumer kafkaEventConsumer;
private KafkaTemplate<String, String> template;
#Autowired
private KafkaListenerEndpointRegistry kafkaListenerEndpointRegistry;
#ClassRule
public static final KafkaEmbedded embeddedKafka = new KafkaEmbedded(1, true,3, TOPIC);
#Before
public void setUp() throws Exception {
kafkaEventConsumer = new KafkaEventConsumer(myServiceMock);
// set up the Kafka producer properties
Map<String, Object> senderProperties = KafkaTestUtils.senderProps(embeddedKafka.getBrokersAsString());
// create a Kafka producer factory
ProducerFactory<String, String> producerFactory = new DefaultKafkaProducerFactory<String, String>(senderProperties);
// create a Kafka template
template = new KafkaTemplate<>(producerFactory);
// set the default topic to send to
template.setDefaultTopic(TOPIC);
// wait until the partitions are assigned
for (MessageListenerContainer messageListenerContainer : kafkaListenerEndpointRegistry.getListenerContainers()) {
messageListenerContainer.setupMessageListener(new MessageListener<String, String>() {
#Override
public void onMessage(ConsumerRecord<String, String> record) {
try {
kafkaEventConsumer.receive(record.value());
} catch (Exception e) {
e.printStackTrace();
}
}
});
ContainerTestUtils.waitForAssignment(messageListenerContainer, embeddedKafka.getPartitionsPerTopic());
}
}
#AfterClass
public static void tearDown() throws Exception {
embeddedKafka.destroy();
}
#Test
public void testReceive() throws Exception {
String forecastRequestMessage = "{\"programId\":100011770}";
ForecastRequest forecastRequest = ForecastRequest.builder().programId(100011770L).build();
JobDetail jobDetail = JobDetail.builder().jobStatus(JobStatus.complete).build();
Mockito.when(forecastServiceMock.refreshForecasts(Matchers.any())).thenReturn(jobDetail);
template.sendDefault(forecastRequestMessage);
Thread.sleep(2000L);
// validate something
}
}
The problem is, in the above #Test method instead of calling the mocked version of MyService it is calling the original MyService implementation. Also, while debugging my code I found that overridden onMessage() is also not getting called. Please help me in finding what am I doing wrong here.
You have to stop() all the MessageListenerContainers before calling their setupMessageListener(). Then you will need to start() them back to let them to pick up a fresh listener:
protected void doStart() {
...
Object messageListener = containerProperties.getMessageListener();
Assert.state(messageListener != null, "A MessageListener is required");
Anyway that sounds like you really would like to mock only your MyService which is injected into the real KafkaEventConsumer. So, how about to consider to use that like this:
#MockBean
private MyServiceImpl myServiceMock;
And you won't need to do anything in your #Before and no need in the #InjectMocks.
The KafkaEmbedded can expose its host/port (or brokers) properties to the expected Spring Boot conventional configuration properties like this:
#BeforeClass
public static void setup() {
System.setProperty("spring.kafka.bootstrap-servers", kafkaEmbedded.getBrokersAsString());
}
https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/reference/htmlsingle/#boot-features-testing-spring-boot-applications-mocking-beans

Spring Kafka asynchronous send calls block

I'm using Spring-Kafka version 1.2.1 and, when the Kafka server is down/unreachable, the asynchronous send calls block for a time. It seems to be the TCP timeout. The code is something like this:
ListenableFuture<SendResult<K, V>> future = kafkaTemplate.send(topic, key, message);
future.addCallback(new ListenableFutureCallback<SendResult<K, V>>() {
#Override
public void onSuccess(SendResult<K, V> result) {
...
}
#Override
public void onFailure(Throwable ex) {
...
}
});
I've taken a really quick look at the Spring-Kafka code and it seems to just pass the task along to the kafka client library, translating a callback interaction to a future object interaction. Looking at the kafka client library, the code gets more complex and I didn't take the time to understand it all, but I guess it may be making remote calls (metadata, at least?) in the same thread.
As a user, I expected the Spring-Kafka methods that return a future to return immediately, even if the remote kafka server is unreachable.
Any confirmation if my understanding is wrong or if this is a bug would be welcome. I ended up making it asynchronous on my end for now.
Another problem is that Spring-Kafka documentation says, at the beginning, that it provides synchronous and asynchronous send methods. I couldn't find any methods that do not return futures, maybe the documentation needs updating.
I'm happy to provide any further details if needed. Thanks.
In addition to the #EnableAsync annotation on a configuration class, the #Async annotation needs to be used on the method were you invoke this code.
http://www.baeldung.com/spring-async
Here some code fragements. Kafka producer config:
#EnableAsync
#Configuration
public class KafkaProducerConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProducerConfig.class);
#Value("${kafka.brokers}")
private String servers;
#Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
return props;
}
#Bean
public ProducerFactory<String, GenericMessage> producerFactory(ObjectMapper objectMapper) {
return new DefaultKafkaProducerFactory<>(producerConfigs(), new StringSerializer(), new JsonSerializer(objectMapper));
}
#Bean
public KafkaTemplate<String, GenericMessage> kafkaTemplate(ObjectMapper objectMapper) {
return new KafkaTemplate<String, GenericMessage>(producerFactory(objectMapper));
}
#Bean
public Producer producer() {
return new Producer();
}
}
And the producer itself:
public class Producer {
public static final Logger LOGGER = LoggerFactory.getLogger(Producer.class);
#Autowired
private KafkaTemplate<String, GenericMessage> kafkaTemplate;
#Async
public void send(String topic, GenericMessage message) {
ListenableFuture<SendResult<String, GenericMessage>> future = kafkaTemplate.send(topic, message);
future.addCallback(new ListenableFutureCallback<SendResult<String, GenericMessage>>() {
#Override
public void onSuccess(final SendResult<String, GenericMessage> message) {
LOGGER.info("sent message= " + message + " with offset= " + message.getRecordMetadata().offset());
}
#Override
public void onFailure(final Throwable throwable) {
LOGGER.error("unable to send message= " + message, throwable);
}
});
}
}
If I look at the KafkaProducer itself, there are two parts of sending a message:
Storing the message into the internal buffer.
Uploading the message from the buffer into Kafka.
KafkaProducer is asynchronous for the second part, not the first part.
The send() method can still be blocked on the first part and eventually throw TimeoutExceptions, e.g:
The metadata for the topics is not cached or stale, so the producer tries to get the metadata from the server to know if the topic still exists and how many partitions it has.
The buffer is full (32MB by default).
If the server is completely unresponsive, you will probably encounter both issues.
Update:
I tested and confirmed this in Kafka 2.2.1. It looks like this behaviour might be different in 2.4 and/or 2.6: KAFKA-3720
Best solution is to add a 'Callback' Listener at the level of the Producer.
#Bean
public KafkaTemplate<String, WebUserOperation> operationKafkaTemplate() {
KafkaTemplate<String, WebUserOperation> kt = new KafkaTemplate<>(operationProducerFactory());
kt.setProducerListener(new ProducerListener<String, WebUserOperation>() {
#Override
public void onSuccess(ProducerRecord<String, WebUserOperation> record, RecordMetadata recordMetadata) {
System.out.println("### Callback :: " + recordMetadata.topic() + " ; partition = "
+ recordMetadata.partition() +" with offset= " + recordMetadata.offset()
+ " ; Timestamp : " + recordMetadata.timestamp() + " ; Message Size = " + recordMetadata.serializedValueSize());
}
#Override
public void onError(ProducerRecord<String, WebUserOperation> producerRecord, Exception exception) {
System.out.println("### Topic = " + producerRecord.topic() + " ; Message = " + producerRecord.value().getOperation());
exception.printStackTrace();
}
});
return kt;
}
Just to be sure. Do you have the #EnableAsync annotation applied? I want to say that could be the key to specifying the behavior of Future<>
Below code works for me to get response asynchronously
ProducerRecord<UUID, Person> person = new ProducerRecord<>(kafkaTemplate.getDefaultTopic(), messageKey,Person);
Runnable runnable = () -> kafkaTemplate.send(person).addCallback(new MessageAckHandler());
new Thread(runnable).start();
public class MessageAckHandler implements ListenableFutureCallback<SendResult<UUID,Person>> {
#Override
public void onFailure(Throwable exception) {
log.error("unable to send message: " + exception.getMessage());
}
#Override
public void onSuccess(SendResult<UUID, ScreeningEvent> result) {
log.debug("sent message with offset={} messageID={}", result.getRecordMetadata().offset(), result.getProducerRecord().key());
}
}
public class SendResult<K, V> {
private final ProducerRecord<K, V> producerRecord;
private final RecordMetadata recordMetadata;
public SendResult(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata) {
this.producerRecord = producerRecord;
this.recordMetadata = recordMetadata;
}
public ProducerRecord<K, V> getProducerRecord() {
return this.producerRecord;
}
public RecordMetadata getRecordMetadata() {
return this.recordMetadata;
}
#Override
public String toString() {
return "SendResult [producerRecord=" + this.producerRecord + ", recordMetadata=" + this.recordMetadata + "]";
}
}

Resources