Race-condition between #SubscribeMapping and ChannelInterceptorAdapter.preSend - java

In my Spring Boot 1.5.9 application with Spring Websockets, web-socket subscriptions are intercepted with an implementation of ChannelInterceptorAdapter:
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHelper.accessor(message);
StompCommand command = accessor.getCommand();
if (null != command) {
StompHelper.authentication(message, tokenHandler);
switch (command) {
case SUBSCRIBE:
this.stompHandler.subscribe(message);
break;
case UNSUBSCRIBE:
this.stompHandler.unsubscribe(message);
break;
// ...
}
}
return super.preSend(message, channel);
}
which captures the subscriber and destination so subscriber-specific messages can be sent. In the web-socket controller, the #SubscribeMapping implementation publishes initialization data to the subscriber. But the #SubscribeMapping and ChannelInterceptorAdapter.preSend are invoked in different threads, so occasionally the #SubscribeMapping executes before the completion of ChannelInterceptorAdapter.preSend which results in the subscriber not getting the initialization data.
This seems like a Spring Websocket design problem, but is there an (elegant-ish) work-around?
Update: submitted a bug: SPR-16323

Related

Spring Integration: How to send messages from pubsub subscribers to external systems/servers with Http Methods

I have been trying to send messages to external systems(using rest template POST, PUT etc) from the service activators as below.
Below is my pubsub consumer class
public class MyConsumer{
#Autowired
ExternalService externalService;
#Bean
public PubSubInboundChannelAdapter messageChannelAdapter(final #Qualifier("myInputChannel") MessageChannel inputChannel,
PubSubTemplate pubSubTemplate)
{
PubSubInboundChannelAdapter adapter = new PubSubInboundChannelAdapter(pubSubTemplate, pubSubSubscriptionName);
adapter.setOutputChannel(inputChannel);
adapter.setAckMode(AckMode.AUTO_ACK);
adapter.setErrorChannelName("pubsubErrors");
return adapter;
}
#ServiceActivator(inputChannel = "pubsubErrors")
public void pubsubErrorHandler(Message<MessagingException> exceptionMessage) {
BasicAcknowledgeablePubsubMessage originalMessage = (BasicAcknowledgeablePubsubMessage) exceptionMessage
.getPayload().getFailedMessage().getHeaders().get(GcpPubSubHeaders.ORIGINAL_MESSAGE);
originalMessage.nack();
}
#Bean
public MessageChannel myInputChannel() {
return new DirectChannel();
}
#Bean
#ServiceActivator(inputChannel = "myInputChannel")
public MessageHandler messageReceiver_AddCustomer() {
return message -> {
externalService.postDataTOExternalSystems(new String((byte[]) message.getPayload());
};
}
#Bean
#ServiceActivator(inputChannel = "myInputChannel")
public MessageHandler messageReceiver_DeleteCustomer() {
return message -> {
externalService.deleteCustomer(new String((byte[]) message.getPayload());
BasicAcknowledgeablePubsubMessage originalMessage =
message.getHeaders().get(GcpPubSubHeaders.ORIGINAL_MESSAGE, BasicAcknowledgeablePubsubMessage.class);
originalMessage.ack();
};
}
}
ExternalService below is the service which sends data to the external systems.
public class ExternalService{
void postDataTOExternalSystems(Object obj){
// RequestEntity object formed with HttpEntity object using obj(in json) and headers
restTemplate.exchange("https://externalsystems/",HttpMethod.POST,requestEntity,Object.class);
}
void deleteDatafromExternalSystems(Object obj){
// RequestEntity object formed with HttpEntity object using obj(in json) and headers
restTemplate.exchange("https://externalsystems/",HttpMethod.Detele,requestEntity,Object.class);
}
}
Since both the methods messageReceiver_AddCustomer and messageReceiver_deleteCustomer are using same channel whats happening is when I try to just addcustomer, the deleteCustomer is also called by default.
I was thinking of creating a seperate channel for deleteCustomer, but creating in this way leads to creating channels for every usecase.
Hence would like to know three things here.
Is there is any other approach of sending through Spring integration through which I can send data to external systems using a single Channel or a different utilization of Channels.
If any error in the external service calls leads to unending of failure logs in the console
message_id: "6830962001745961"
publish_time {
seconds: 1675783352
nanos: 547000000
}
}, timestamp=1675783353720}]': error occurred in message handler
It's not clear what is your expectation for such a logic. You have two contradicting subscribers for the same input channel. It sounds more like you need a router first to determine where to proceed next with an input message from Pub/Sub: https://docs.spring.io/spring-integration/reference/html/message-routing.html#messaging-routing-chapter.
but creating in this way leads to creating channels for every usecase.
Sure! You can go without Spring Integration and just do everything with the plain if..else. So, what's a difference? Therefore I don't see a reasonable argument in your statement. Yo have different HTTP methods, and that is OK to map them via Spring Integration router to respective sub-flows. The MessageChannel is really first-class citizen in Spring Integration. It makes a solution as loosely-coupled as possible.
Several subscribers on your current DirectChannel will lead to a round-robin logic by default, so you'll be surprised that one message from Pub/Sub creates a customer, another deletes and so on in rounds. The PublishSubscribeChannel will make it bad as well: both of your subscribers are going to be called, so created first, then deleted immediately.

How to receive message from Spring Integration using Direct Channel

I have created DirectChannel and have sent an object to my channel and want to receive it abck to store it in DB and send it in another service bus queue. Can you suggest how to receive the object from channel?
My Channel -
#Bean("tempChannel")
public MessageChannel tempChannel() {
return new DirectChannel();
}
Integration flow -
#Bean
public IntegrationFlow tempMessageFlow() {
return IntegrationFlows.from("tempChannel").handle().get();
}
For handle method I need to pass MessageHandler, how to I declare it and pass here?
I am sending message to channel using below piece of code, please do tell if this is alright-
tempChannel().send(messageObj);
The DirectChannel implements a SubscribableChannel. So, to get messages sent to this channel you need to subscribe(MessageHandler handler). What you have so far with that IntegrationFlow definition is OK: adding that handle() you subscribe to the tempChannel. Just handle message and forget you can do this:
.handle(m - > System.out.println("Processed message: " + m))
This is a lambda for that MessageHandler functional interface. There are many other handle() variants for other use-cases. For example process-n-reply is like this:
.handle((p, h) - > {
System.out.println("Processed message: " + m);
return "My new payload";
})
If you say that you need to do several operations on the same message, then look into a PublishSubscribeChannel. In Java DSL we have a publishSubscribeChannel(Consumer<PublishSubscribeSpec> publishSubscribeChannelConfigurer) to configure several subscribers as sub-flows.
To store into DB, you can use a JdbcMessageHandler: https://docs.spring.io/spring-integration/docs/current/reference/html/jdbc.html#jdbc-outbound-channel-adapter

How to rollback in saga pattern when a service fails

I am starting with Saga pattern using Spring cloud and rabbit mq. Following is the problem statement:
I call /service1 (producer) which publishes a message in rabbit mq and that message is consumed by the consumer service.
Now occurs tow cases:
Case 1: Consumer service does its part successfully.
Case 2: Consumer service fails to do its part, thus /service1 has to rollback its changes.
How does /service1 know if consumer is successful or not, so that it can send a success/failure response. Following is the project structure:
Producer:
#RestController
public class ProducerController {
private MessageChannel greet;
public ProducerController(HelloBinding binding) {
greet = binding.greeting();
}
#GetMapping("/greet/{name}")
public void publish(#PathVariable String name) {
String greeting = "Hello, "+name+"!";
Message<String> msg = MessageBuilder.withPayload(greeting)
.build();
this.greet.send(msg);
System.out.println("Message sent to the queue");
AMQP.Basic.Ack;
}
Consumer:
#EnableBinding(HelloBinding.class)
public class HelloListener {
#StreamListener(target=HelloBinding.GREETING)
public void processHelloChannelGreeting(String msg) {
System.out.println("Message received:- "+msg);
}
}
Now how do I tell the producer whether consumer's action is a success or a failure so that producer service sends appropriate response?
The producer can not know what happens after a message has been successfully published to a topic. If you want feedback from the consumer then you need to create a new "response" topic on which the consumer communicates success or failure of processing that message.
You can map the messages by keys.

How to globally handle Spring WebSockets/Spring Messaging exception?

Question
Is there a way to globally handle Spring Messaging MessageDeliveryException caused by error (usualy insufficient authorities) in Spring WebSocket module?
Use case
I have implemented Spring WebSockets over STOMP to support ws connection in my webapp. To secure websocket endpoint I have created interceptor that authorizes user to start STOMP session at STOMP CONNECT time (as suggested in Spring documentation here in 22.4.11 section):
#Component
public class StompMessagingInterceptor extends ChannelInterceptorAdapter {
// Some code not important to the problem
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
switch (headerAccessor.getCommand()) {
// Authenticate STOMP session on CONNECT using jwt token passed as a STOMP login header - it's working great
case CONNECT:
authorizeStompSession(headerAccessor);
break;
}
// Returns processed message
return message;
}
// Another part of code not important for the problem
}
and included spring-security-messaging configuration to add some fine-grained control over authorities when messaging:
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpTypeMatchers(
SimpMessageType.CONNECT,
SimpMessageType.DISCONNECT,
SimpMessageType.HEARTBEAT
).authenticated()
.simpSubscribeDestMatchers("/queue/general").authenticated()
.simpSubscribeDestMatchers("/user/queue/priv").authenticated()
.simpDestMatchers("/app/general").authenticated()
.simpDestMatchers("/user/*/queue/priv").hasAuthority("ADMIN")
.anyMessage().denyAll();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
First of all - this configuration works as expected, the problem is when some security exception happens during websocket communication (say user without admin authority tries to send message on "/user/{something}/queue/priv" endpoint) it will end in org.springframework.messaging.MessageDeliveryException being rised and:
Full exception stack trace being written down to my server log
Returning STOMP ERROR frame containing part of stack trace as it's message field.
What I would like to do is catching (if possible globally) DeliveryException, checking what caused it and accoridingly to that create my own message for returning in STOMP ERROR frame (lets say with some error code like just 403 to mimic HTTP) and instead of throwing original exception further just logging some warning with my logger. Is it possible?
What I tried
When looking for solution I found some people using #MessageExceptionHandler to catch messaging exceptions, Spring 4.2.3 (which is version I use) documentation mentions it only once here in 25.4.11 section. I tried to use it like this:
#Controller
#ControllerAdvice
public class WebSocketGeneralController {
...
#MessageExceptionHandler
public WebSocketMessage handleException(org.springframework.messaging.MessageDeliveryException e) {
WebSocketMessage errorMessage = new WebSocketMessage();
errorMessage.setMessage(e.getClass().getName());
return errorMessage;
}
}
but it seems like method isn't called at any point (tried catching different exceptions, just Exception including - no results). What else should I look into?
#ControllerAdvice and #MessageExceptionHandler are working on business-logic level (like #MessageMapping or SimpMessagingTemplate).
To handle STOMP exceptions, you need to set STOMP error handler in STOMP registry:
#Configuration
#EnableWebSocketMessageBroker
class WebSocketConfiguration : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
// ...
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/ws")
// Handle exceptions in interceptors and Spring library itself.
// Will terminate a connection and send ERROR frame to the client.
registry.setErrorHandler(object : StompSubProtocolErrorHandler() {
override fun handleInternal(
errorHeaderAccessor: StompHeaderAccessor,
errorPayload: ByteArray,
cause: Throwable?,
clientHeaderAccessor: StompHeaderAccessor?
): Message<ByteArray> {
errorHeaderAccessor.message = null
val message = "..."
return MessageBuilder.createMessage(message.toByteArray(), errorHeaderAccessor.messageHeaders)
}
})
}
}
It does not work because of #ControllerAdvice catch exception from the request that passed dispatcher servlet. When you secure your endpoint and someone makes an unauthorized request it does not pass through dispatcher servlet. The request is caught by spring interceptors.

Spring AMQP with RabbitMQ receiving same message

I have small rabbitmq spring boot application:
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory)
{
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
return factory;
}
Simple queue definition:
#Bean
public Queue queue()
{
return new Queue("queue");
}
And a simple listener which logs the message:
#RabbitHandler
public void process(#Payload Message message)....
I am seeing that the same message is being sent multiple times before the
removed from queue.
How can i change this config to ensure the message is sent exactly once.
If your listener throws an exception the message will be requeued and sent.
It cannot be sent multiple times otherwise - impossible.
Turn on DEBUG logging to watch the message flow.

Categories