I would like to understand how returning values work for PublishSubscribeChannel having multiple subscribers.
#Bean
public PublishSubscribeChannel channel(){
return new PublishSubscribeChannel();
}
#Bean
#ServiceActivator(inputChannel = "channel")
public MessageHandler handler1() {
//...
return handler1;
}
#Bean
#ServiceActivator(inputChannel = "channel")
public MessageHandler handler2() {
//...
return handler2;
}
#Bean
#ServiceActivator(inputChannel = "channel")
public MessageHandler handler3() {
//...
return handler3;
}
#MessagingGateway
public interface TestGateway{
#Gateway(requestChannel = "channel")
String method(String payload);
}
method expects some String as a return type. If a message is sent to all three handlers via channel, the value coming from which handler would be returned? From what I understand, messages are sent to each subscriber one by one, so would it be the value returned by the last handler?
Also, would it be possible to have handlers returning type different than the method return type, also if it wouldn't necessarily expect String?
When it comes to a scenario where any Exception occurs, I believe if setIgnoreFailures = false, the processing would stop on it and not process to the next handler. Otherwise, the last exception would be thrown.
Thanks in advance
I'm sure there is a specific business task behind your question.
But i you really are about an academic knowledge to see how Spring Integration works internally, then here is some answer for you.
Since your PublishSubscribeChannel is not configure with an Executor, then all your subscribers are called one by one, and only when the previous has done its job. And the part of that job is really a reply producing. So, if your first MessageHandler produced some reply, then exactly this one fulfills CountDownLatch in the TempraryReplyChannel for a gateway request-reply functionality.
The replies from the rest of handlers are going to be ignored and they may throw a late reply error.
Yes, you can return any type as long as it can be converted to the expected return type. See more info about ConversionService: https://docs.spring.io/spring-integration/reference/html/messaging-endpoints.html#payload-type-conversion
About ignoreFailures I'd suggest to look into a PublishSubscribeChannel source code and how it is propagated down to BroadcastingDispatcher:
private boolean invokeHandler(MessageHandler handler, Message<?> message) {
try {
handler.handleMessage(message);
return true;
}
catch (RuntimeException e) {
if (!this.ignoreFailures) {
if (e instanceof MessagingException && ((MessagingException) e).getFailedMessage() == null) { // NOSONAR
throw new MessagingException(message, "Failed to handle Message", e);
}
throw e;
}
else if (this.logger.isWarnEnabled()) {
logger.warn("Suppressing Exception since 'ignoreFailures' is set to TRUE.", e);
}
return false;
}
}
And no: otherwise none exception will be thrown. See that code again.
I have written a spring cloud stream application where producers are publishing messages to the designated kafka topics. My query is how can I add a producer callback to receive ack/confirmation that the message has been successfully published on the topic? Like how we do in spring kafka producer.send(record, new callback { ... }) (maintaining async producer). Below is my code:
private final Sinks.Many<Message<?>> responseProcessor = Sinks.many().multicast().onBackpressureBuffer();
#Bean
public Supplier<Flux<Message<?>>> event() {
return responseProcessor::asFlux;
}
public Message<?> publishEvent(String status) {
try {
String key = ...;
response = MessageBuilder.withPayload(payload)
.setHeader(KafkaHeaders.MESSAGE_KEY, key)
.build();
responseProcessor.tryEmitNext(response);
}
How can I make sure that tryEmitNext has successfully written to the topic?
Is implementing ProducerListener a solution and possible? Couldn't find a concrete solution/documentation in Spring Cloud Stream
UPDATE
I have implemented below now, seems to work as expected
#Component
public class MyProducerListener<K, V> implements ProducerListener<K, V> {
#Override
public void onSuccess(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata) {
// Do nothing on onSuccess
}
#Override
public void onError(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata, Exception exception) {
log.error("Producer exception occurred while publishing message : {}, exception : {}", producerRecord, exception);
}
}
#Bean
ProducerMessageHandlerCustomizer<KafkaProducerMessageHandler<?, ?>> customizer(MyProducerListener pl) {
return (handler, destinationName) -> handler.getKafkaTemplate().setProducerListener(pl);
}
See the Kafka Producer Properties.
recordMetadataChannel
The bean name of a MessageChannel to which successful send results should be sent; the bean must exist in the application context. The message sent to the channel is the sent message (after conversion, if any) with an additional header KafkaHeaders.RECORD_METADATA. The header contains a RecordMetadata object provided by the Kafka client; it includes the partition and offset where the record was written in the topic.
ResultMetadata meta = sendResultMsg.getHeaders().get(KafkaHeaders.RECORD_METADATA, RecordMetadata.class)
Failed sends go the producer error channel (if configured); see Error Channels. Default: null
You can add a #ServiceActivator to consume from this channel asynchronously.
I am looking for a way to delivery a message, and once the message is delivered (and routed) successfully, i need to perform some operations.
I have enabled publisher confirms and returns by:
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
I have configured return and confirm callback on the rabbit template:
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("Message returned");
});
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
System.out.println("confirm"); //correlationData.returnedMessage has the original message
});
Here is my publish code:
CorrelationData crd = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("X-ORDERS", "ORDER_PLACED", request, crd);
crd.getFuture().addCallback(new ListenableFutureCallback<Confirm>() {
#Override
public void onFailure(Throwable throwable) {
log.info("Failure received");
}
#Override
public void onSuccess(Confirm confirm) {
if(confirm.isAck()){
log.info("Success received");
doSomethingAfterSuccess();
}}
});
Now, when i publish a message that is unable to route the message :-
rabbitTemplate's returnCallBack AND confirmCallBack are also being
called
the onSuccess(..) of the correlationData is still called with
isAck() = true
So, how can I check if the message is delivered successfully and routed?
EDIT: Found solution. The publish code :
CorrelationData crd = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("X-ORDERS", "ORDER_PLACED", request, crd);
crd.getFuture().addCallback(new ListenableFutureCallback<Confirm>() {
#Override
public void onFailure(Throwable throwable) {
log.info("Failure received");
}
#Override
public void onSuccess(Confirm confirm) {
if(confirm.isAck() && crd.getReturnedMessage == null){
log.info("Success received");
doSomethingAfterSuccess();
}}
});
basically changed the condition in onSuccess to "confirm.isAck() && crd.getReturnedMessage == null"
That is per the RabbitMQ documentation - you still get a positive ack, but it is guaranteed to be delievered after the return.
So simply check that the future.returnedMessage is not null in onSuccess().
See the documentation.
In addition, when both confirms and returns are enabled, the CorrelationData is populated with the returned message. It is guaranteed that this occurs before the future is set with the ack.
Summarization of the problem
In my project, we are trying to use Spring-Cloud-Stream (SCS) to connect to Solace. Eventually we plan to move to Kafka. So using SCS will help us move over to Kafka quite easily without any code changes and very minimal configuration & dependency changes.
We had been using Solace for a while using JMS. Now when we tried to publish messages to Solace using SCS, we observed that in the message, some crucial JMS Headers (JMSMessageID, JMSType, JMSPriority,JMSCorrelationID, JMSExpiration) are blank.
Do we need to configure the JMS headers separately ? If yes, how ?
What I've already tried
I tried to set headers like this, but this is just resulting in duplicate headers with the same name.
#Output(SendReport.TO_NMR)
public void sendMessage(String request) {
log.info("****************** Got this Report Request: " + request);
MessageBuilder<String> builder = MessageBuilder.withPayload(request);
builder.setHeader("JMSType","report-request");
builder.setHeader("JMSMessageId","1");
builder.setHeader("JMSCorrelationId","11");
builder.setHeader("JMSMessageID","4");
builder.setHeader("JMSCorrelationID","114");
builder.setHeader("ApplicationMessageId","111");
builder.setHeader("ApplicationMessageID","112");
builder.setCorrelationId("23434");
Message message = builder.build();
sendReport.output().send(message);
}
JMS Header of the message in Solace looks like this
JMSMessageID
JMSDestination TOPIC_NAME
JMSTimestamp Wed Dec 31 18:00:00 CST 1969
JMSType
JMSReplyTo
JMSCorrelationID
JMSExpiration 0
JMSPriority 0
JMSType nmr-report-request
JMSMessageId 1
JMSMessageID 4
_isJavaSerializedObject-contentType true
_isJavaSerializedObject-id true
solaceSpringCloudStreamBinderVersion 0.1.0
ApplicationMessageId 111
ApplicationMessageID 112
JMSCorrelationId 11
JMSCorrelationID 114
correlationId 23434
id [-84,-19,0,5,115,114,0,14,106,97,118,97,46,117,116,105,108,46,85,85,73,68,-68,-103,3,-9,-104,109,-123,47,2,0,2,74,0,12,108,101,97,115,116,83,105,103,66,105,116,115,74,0,11,109,111,115,116,83,105,103,66,105,116,115,120,112,13,-26,2,-51,111,-17,73,73,-18,-32,-26,-11,-46,-89,50,-37] (offset=377, length=80)
contentType [-84,-19,0,5,115,114,0,33,111,114,103,46,115,112,114,105,110,103,102,114,97,109,101,119,111,114,107,46,117,116,105,108,46,77,105,109,101,84,121,112,101,56,-76,29,-63,64,96,-36,-81,2,0,3,76,0,10,112,97,114,97,109,101,116,101,114,115,116,0,15,76,106,97,118,97,47,117,116,105,108,47,77,97,112,59,76,0,7,115,117,98,116,121,112,101,116,0,18,76,106,97,118] (offset=473, length=190)
timestamp 1555707627482
Code used to connect to Solace
Spring Boot Main Class
#SpringBootApplication
#EnableDiscoveryClient
#Slf4j
#EnableBinding({SendReport.class})
public class ReportServerApplication {
public static void main(final String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-server.xml");
new SpringApplicationBuilder(ReportServerApplication.class).listeners(new EnvironmentPreparedListener()) .run(args);
}
Class to connect channel to topic:
public interface SendReport {
String TO_NMR = "solace-poc-outbound";
#Output(SendReport.TO_NMR)
MessageChannel output();
}
Message Handler:
#Slf4j
#Component
#EnableBinding({SendReport.class})
public class MessageHandler {
private SendReport sendReport;
public MessageHandler(SendReport sendReport){
this.sendReport = sendReport;
}
#Output(SendReport.TO_NMR)
public void sendMessage(String request) {
log.info("****************** Got this Report Request: " + request);
var message = MessageBuilder.withPayload(request).build();
sendReport.output().send(message);
}
}
Properties used for configuration : application.yml
spring:
cloud:
# spring cloud stream binding
stream:
bindings:
solace-poc-outbound:
destination: TOPIC_NAME
contentType: text/plain
solace:
java:
host: tcp://xyz.abc.com
#port: xxx
msgVpn: yyy
clientUsername: aaa
Dependencies used:
'org.springframework.cloud:spring-cloud-stream',
'com.solace.spring.cloud:spring-cloud-starter-stream-solace:1.1.+'
Observation
Expected result : All JMS headers should get populated by SCS.
Actual result : Some JMS headers are not getting populated.
See JMS Message JavaDocs:
/** Sets the message ID.
*
* <P>This method is for use by JMS providers only to set this field
* when a message is sent. This message cannot be used by clients
* to configure the message ID. This method is public
* to allow a JMS provider to set this field when sending a message
* whose implementation is not its own.
*
* #param id the ID of the message
*
* #exception JMSException if the JMS provider fails to set the message ID
* due to some internal error.
*
* #see javax.jms.Message#getJMSMessageID()
*/
void
setJMSMessageID(String id) throws JMSException;
So, this property cannot be populated from the application level.
In the ActiveMQ I see the code like this:
msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));
// Set the message id.
if (msg != message) {
message.setJMSMessageID(msg.getMessageId().toString());
But still: it is not what we can control from the application level.
The priority, deliveryMode and timeToLive ca be populated from the JmsSendingMessageHandler:
if (this.jmsTemplate instanceof DynamicJmsTemplate && this.jmsTemplate.isExplicitQosEnabled()) {
Integer priority = StaticMessageHeaderAccessor.getPriority(message);
if (priority != null) {
DynamicJmsTemplateProperties.setPriority(priority);
}
if (this.deliveryModeExpression != null) {
Integer deliveryMode =
this.deliveryModeExpression.getValue(this.evaluationContext, message, Integer.class);
if (deliveryMode != null) {
DynamicJmsTemplateProperties.setDeliveryMode(deliveryMode);
}
}
if (this.timeToLiveExpression != null) {
Long timeToLive = this.timeToLiveExpression.getValue(this.evaluationContext, message, Long.class);
if (timeToLive != null) {
DynamicJmsTemplateProperties.setTimeToLive(timeToLive);
}
}
}
The JmsCorrelationID must be populated by the JmsHeaders.CORRELATION_ID. The JmsType by the JmsHeaders.TYPE, respectively:
public void fromHeaders(MessageHeaders headers, javax.jms.Message jmsMessage) {
try {
Object jmsCorrelationId = headers.get(JmsHeaders.CORRELATION_ID);
if (jmsCorrelationId instanceof Number) {
jmsCorrelationId = jmsCorrelationId.toString();
}
if (jmsCorrelationId instanceof String) {
try {
jmsMessage.setJMSCorrelationID((String) jmsCorrelationId);
}
catch (Exception e) {
this.logger.info("failed to set JMSCorrelationID, skipping", e);
}
}
Object jmsReplyTo = headers.get(JmsHeaders.REPLY_TO);
if (jmsReplyTo instanceof Destination) {
try {
jmsMessage.setJMSReplyTo((Destination) jmsReplyTo);
}
catch (Exception e) {
this.logger.info("failed to set JMSReplyTo, skipping", e);
}
}
Object jmsType = headers.get(JmsHeaders.TYPE);
if (jmsType instanceof String) {
try {
jmsMessage.setJMSType((String) jmsType);
}
catch (Exception e) {
this.logger.info("failed to set JMSType, skipping", e);
}
}
See DefaultJmsHeaderMapper for more info.