When sending UNSUBSCRIBE and then SUBSCRIBE to the same destination with minimal delay, sometimes first subscription is not removed from cache which results in duplicating each message sent to this destination. This subscription will stay in cache until websocket disconnect.

Screenshot:

Websocket

Minimal project to reproduce:

WebsocketBug.zip

How to reproduce:

  1. Launch server.App.
  2. Open index.html in any browser that supports showing websocket frames.
  3. See websocket frames.

This doesn't work all the time, but has a pretty high change of reproducing. If it didn't reproduce once - just refresh the page. For me it didn't took more than 5-6 refreshes each time I tried.

spring-messaging versions I reproduced this for: 5.0.10, 5.1.7

Comment From: rstoyanchev

The sample is a bit more extensive, from what I can make out, it's detecting SUBSCRIPTION messages through a controller method and keeping track of those, so that WsService can broadcast messages to user destinations periodically. Perhaps you've seen the SimpUserRegistry bean in the config? I don't know that you need all the logic to keep track of subscriptions.

The sample doesn't have an index.html page by the way. Regardless, I'm afraid this is expected behavior. If SUBSCRIBE and UNSUBSCRIBE arrive around the same time, they pass through the channel executor, and could indeed be processed out of order.

The only thing I can suggest is to ensure UNSUBSCRIBE cannot be sent immediately after. It would help if the simple broker supported RECEIPTs (see see #21848) since then you could wait until the SUBSCRIBE is completes before allowing further SUBSCRIBE / UNSUBSCRIBE for the destination, but some delay could also be used as a workaround.

Comment From: Amartel1986

Hello, @rstoyanchev. Thank for you answer.

The sample is a bit more extensive

I tried to make the sample as small as possible, while containing all the details necessary for it to reproduce.

from what I can make out, it's detecting SUBSCRIPTION messages through a controller method and keeping track of those, so that WsService can broadcast messages to user destinations periodically.

Yes.

Perhaps you've seen the SimpUserRegistry bean in the config? I don't know that you need all the logic to keep track of subscriptions.

No, I haven't. Thanks for the hint, I'll try to use it in future.

The sample doesn't have an index.html page by the way.

I've downloaded the archive to double check it. Maybe you were expecting to see it somewhere else?

Screenshot-WebsocketBug zip

Regardless, I'm afraid this is expected behavior. If SUBSCRIBE and UNSUBSCRIBE arrive around the same time, they pass through the channel executor, and could indeed be processed out of order.

That sounds odd to me. Unlike REST, websocket has a permanent connection and all messages in it come and go in the exact order. If you process them in a different order then they arrive, that sounds like a bug to me not a feature.

Also, if those messages are really processed in a different order, why are we left with both subscriptions, while we should have only one or even none of them left? Let's say executor processes them like this: SUBSCRIBE, SUBSCRIBE, UNSUBSCRIBE. What result are you expecting to see, really?

From what I see, the problem here is in SessionSubscriptionRegistry.SessionSubscriptionInfo.destinationLookup map and how it stores data. For each subscription it has two records: /v1/feed-user94218266-c5fa-1bfe-3c93-e6b24cca817e and /user/v1/feed. In my case, when UNSUBSCRIBE comes, it only deletes one of them, leaving the other so that when SimpleBrokerMessageHandler.sendMessageToSubscribers scans the subscription map, it finds both alive, which is wrong.

The only thing I can suggest is to ensure UNSUBSCRIBE cannot be sent immediately after. It would help if the simple broker supported RECEIPTs (see see #21848) since then you could wait until the SUBSCRIBE is completes before allowing further SUBSCRIBE / UNSUBSCRIBE for the destination, but some delay could also be used as a workaround.

I guess that's the only solution for now, but I really hope you'll fix the problem some time in the future.

Comment From: rstoyanchev

We can experiment with something to ensure messages within a session are ordered.

One thought is to enhance ExecutorSubsribableChannel to invoke some callback (e.g. passed via a header) when a message has been handled. We'd have to decorate the per-handler tasks sent to the executor, and track their completion.

We can then hold up sending further messages from the same session until the previous one is handled. This could also help to improve event publication which currently also competes with handling on the inbound side.

It could help to create a custom MessageChannel decorator that is configured with how to find the session id, and then queues messages by session until the previous message is processed.

Comment From: rstoyanchev

As of 6.1 there is an option to enable ordered handling of inbound messages, see https://github.com/spring-projects/spring-framework/issues/27173#issuecomment-1758051084.