10

On the client side javascript I have

    stomp.subscribe("/topic/path", function (message) {
        console.info("message received");
    });

And on the server side

public class Controller {
  private final MessageSendingOperations<String> messagingTemplate;
  @Autowired
  public Controller(MessageSendingOperations<String> messagingTemplate) {
      this.messagingTemplate = messagingTemplate;
  }
  @SubscribeMapping("/topic/path")
  public void subscribe() {
     LOGGER.info("before send");
     messagingTemplate.convertAndSend(/topic/path, "msg");
  }
}

From this setup, I am occasionally (around once in 30 page refreshes) experiencing message dropping, which means I can see neither "message received" msg on the client side nor the websocket traffic from Chrome debugging tool.

"before send" is always logged on the server side.

This looks like that the MessageSendingOperations is not ready when I call it in the subscribe() method. (if I put Thread.sleep(50); before calling messagingTemplate.convertAndSend the problem would disappear (or much less likely to be reproduced))

I wonder if anyone experienced the same before and if there is an event that can tell me MessageSendingOperations is ready or not.

user2001850
  • 477
  • 4
  • 18
  • is stomp.subscribe executed after the dom is ready? – guido Mar 22 '15 at 13:07
  • @ᴳᵁᴵᴰᴼ Yes. that's right. I can see the subscribe msg was sent from Chrome debugging for websocket network traffic. So I don't think its the client side problem. – user2001850 Mar 22 '15 at 13:10

3 Answers3

4

The issue you are facing is laying in the nature of clientInboundChannel which is ExecutorSubscribableChannel by default.

It has 3 subscribers:

0 = {SimpleBrokerMessageHandler@5276} "SimpleBroker[DefaultSubscriptionRegistry[cache[0 destination(s)], registry[0 sessions]]]"
1 = {UserDestinationMessageHandler@5277} "UserDestinationMessageHandler[DefaultUserDestinationResolver[prefix=/user/]]"
2 = {SimpAnnotationMethodMessageHandler@5278} "SimpAnnotationMethodMessageHandler[prefixes=[/app/]]"

which are invoked within taskExecutor, hence asynchronously.

The first one here (SimpleBrokerMessageHandler (or StompBrokerRelayMessageHandler) if you use broker-relay) is responsible to register subscription for the topic.

Your messagingTemplate.convertAndSend(/topic/path, "msg") operation may be performed before the subscription registration for that WebSocket session, because they are performed in the separate threads. Hence the Broker handler doesn't know you to send the message to the session.

The @SubscribeMapping can be configured on method with return, where the result of this method will be sent as a reply to that subscription function on the client.

HTH

Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • inside my subscribe method, I was asynchronously calling service layer like subscriableService.registerAndHandleWith(new Handler(){}). So that I can't return immediately in this method. What would you recommend in this scenario? Thanks. – user2001850 Mar 22 '15 at 15:35
  • `Future.get()` or `CountDownLatch` from that `registerAndHandleWith`. From other side you can access to the `SimpleBrokerMessageHandler` and populate some your custom impl of `DefaultSubscriptionRegistry` to override `addSubscriptionInternal` to raise some custom `ApplicationEvent` to listen to it from some another component to send that message to the topic, when `subscription` will be there already. It is for case, when you really need an async stuff and don't overload `clientInboundChannel` executor to wait for that `Future`. – Artem Bilan Mar 22 '15 at 18:39
  • 1
    Thanks again Artem, I assume that I can't use the existing SessionSubscribeEvent as it only tells me that the client has requested but doesn't mean the subscription registration is completed (it would be nice to have something like SessionSubscribe**d**Event). – user2001850 Mar 22 '15 at 20:34
  • 1
    Yes, you are correct: a `SessionSubscribeEvent` event is emitted before the real subscription is done. Feel free to raise a [JIRA](https://jira.spring.io/browse/SPR) ticket for the `SessionSubscribedEvent`. – Artem Bilan Mar 23 '15 at 06:37
1

Here is my solution. It is along the same lines. Added a ExecutorChannelInterceptor and published a custom SubscriptionSubscribedEvent. The key is to publish the event after the message has been handled by AbstractBrokerMessageHandler which means the subscription has been registered with the broker.

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(new ExecutorChannelInterceptorAdapter() {

        @Override
        public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
            SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(message);
            if (accessor.getMessageType() == SimpMessageType.SUBSCRIBE && handler instanceof AbstractBrokerMessageHandler) {
                /*
                 * Publish a new session subscribed event AFTER the client
                 * has been subscribed to the broker. Before spring was
                 * publishing the event after receiving the message but not
                 * necessarily after the subscription occurred. There was a
                 * race condition because the subscription was being done on
                 * a separate thread.
                 */
                applicationEventPublisher.publishEvent(new SessionSubscribedEvent(this, message));
            }
        }
    });

}
chenson42
  • 1,108
  • 6
  • 13
0

A little late but I thought I'd add my solution. I was having the same problem with the subscription not being registered before I was sending data through the messaging template. This issue happened rarely and unpredictable because of the race with the DefaultSubscriptionRegistry.

Unfortunately, I could not just use the return method of the @SubscriptionMapping because we were using a custom object mapper that changed dynamically based on the type of user (attribute filtering essentially).

I searched through the Spring code and found SubscriptionMethodReturnValueHandler was responsible for sending the return value of subscription mappings and had a different messagingTemplate than the autowired SimpMessagingTemplate of my async controller!!

So the solution was autowiring MessageChannel clientOutboundChannel into my async controller and using that to create a SimpMessagingTemplate. (You can't directly wire it in because you'll just get the template going to the broker).

In subscription methods, I then used the direct template while in other methods I used the template that went to the broker.

Laplie Anderson
  • 6,345
  • 4
  • 33
  • 37