Skip to content

convertAndSendToUser loses the message for every session but the first when preserveReceiveOrder is enabled ("Expected mutable SimpMessageHeaderAccessor") #37680

Open

Description

@tonicolet

Setup

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws");
    registry.setPreserveReceiveOrder(true);
}

A user connected with two STOMP sessions (e.g. two browser tabs), subscribed to /user/queue/notifications:

messagingTemplate.convertAndSendToUser("user", "/queue/notifications", payload);

Actual

Only one session receives the message. For the other one, UserDestinationMessageHandler logs:

ERROR o.s.m.simp.user.UserDestinationMessageHandler -- Failed to send GenericMessage [payload=A, headers={simpNextMessageTask=..., simpDestination=/queue/notifications-users2}]
java.lang.IllegalArgumentException: Expected mutable SimpMessageHeaderAccessor
	at org.springframework.util.Assert.isTrue(Assert.java:117)
	at org.springframework.messaging.simp.broker.OrderedMessageChannelDecorator.setTaskHeader(OrderedMessageChannelDecorator.java:134)
	at org.springframework.messaging.simp.broker.OrderedMessageChannelDecorator.sendNextMessage(OrderedMessageChannelDecorator.java:96)
	at org.springframework.messaging.simp.broker.OrderedMessageChannelDecorator.trySend(OrderedMessageChannelDecorator.java:87)
	at org.springframework.messaging.simp.broker.OrderedMessageChannelDecorator.send(OrderedMessageChannelDecorator.java:76)
	at org.springframework.messaging.simp.broker.OrderedMessageChannelDecorator.send(OrderedMessageChannelDecorator.java:70)
	at org.springframework.messaging.simp.SimpMessagingTemplate.sendInternal(SimpMessagingTemplate.java:185)
	at org.springframework.messaging.simp.SimpMessagingTemplate.doSend(SimpMessagingTemplate.java:177)
	at org.springframework.messaging.simp.SimpMessagingTemplate.doSend(SimpMessagingTemplate.java:48)
	at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:108)
	at org.springframework.messaging.simp.user.UserDestinationMessageHandler$SendHelper.send(UserDestinationMessageHandler.java:282)
	at org.springframework.messaging.simp.user.UserDestinationMessageHandler.handleMessage(UserDestinationMessageHandler.java:239)
	...

It happens on every send, without any concurrency.

Expected

Every session of the user receives the message, as it does without setPreserveReceiveOrder(true).

Cause (as far as I can tell)

With preserveReceiveOrder, UserDestinationMessageHandler.SendHelper sends to each target session through its own SimpMessagingTemplate(new OrderedMessageChannelDecorator(brokerChannel)), and passes the same message instance to all of them.

  1. First session: the message is still mutable (handleMessage calls accessor.setLeaveMutable(true)), so SimpMessagingTemplate.doSend sends it as is. OrderedMessageChannelDecorator sets its task header and sends it to the broker channel, whose ImmutableMessageChannelInterceptor makes it immutable.
  2. Second session: the message is now immutable, so SimpMessagingTemplate.doSend takes the other branch: getMutableAccessor(message) + MessageBuilder.createMessage(payload, simpAccessor.getMessageHeaders()). getMessageHeaders() makes the new accessor immutable, because leaveMutable is not set on it.
  3. OrderedMessageChannelDecorator.setTaskHeader then requires a mutable accessor and fails. The message is removed from the queue and never sent.

Possible fixes: send a separate mutable copy per session in SendHelper.send, or setLeaveMutable(true) in the copy branch of SimpMessagingTemplate.doSend (as handleMessage already does).

Reproducer

Plain JUnit test with the real Spring classes, wired as AbstractMessageBrokerConfiguration does with preserveReceiveOrder (attached / below): convertAndSendToUserReachesEverySession fails; the same without preserveReceiveOrder passes, and so does the workaround.

Workaround

Send to each session separately, with its session id in the headers, so that every send has a single target and the message stays mutable:

SimpUser simpUser = userRegistry.getUser(user);
if (simpUser != null)
    for (SimpSession session : simpUser.getSessions()) {
        SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        accessor.setSessionId(session.getId());
        accessor.setLeaveMutable(true);
        messagingTemplate.convertAndSendToUser(user, destination, payload, accessor.getMessageHeaders());
    }

UserDestinationPreserveOrderTest.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions