Skip to content

Commit a205eab

Browse files
committed
Handle STOMP messages from client in order
See gh-21798
1 parent 4195e69 commit a205eab

File tree

6 files changed

+133
-46
lines changed

6 files changed

+133
-46
lines changed

spring-messaging/src/main/java/org/springframework/messaging/simp/broker/OrderedMessageChannelDecorator.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ private void sendNextMessage() {
113113
}
114114
}
115115

116+
/**
117+
* Remove the message from the top of the queue, but only if it matches,
118+
* i.e. hasn't been removed already.
119+
*/
116120
private boolean removeMessage(Message<?> message) {
121+
// Remove only if not removed already
117122
Message<?> next = this.messages.peek();
118123
if (next == message) {
119124
this.messages.remove();
@@ -181,7 +186,7 @@ private PostHandleTask(Message<?> message) {
181186
@Override
182187
public void run() {
183188
if (this.handledCount == null || this.handledCount.addAndGet(1) == subscriberCount) {
184-
if (OrderedMessageChannelDecorator.this.removeMessage(message)) {
189+
if (OrderedMessageChannelDecorator.this.removeMessage(this.message)) {
185190
sendNextMessage();
186191
}
187192
}

spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompEndpointRegistry.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -52,4 +52,18 @@ public interface StompEndpointRegistry {
5252
*/
5353
WebMvcStompEndpointRegistry setErrorHandler(StompSubProtocolErrorHandler errorHandler);
5454

55+
/**
56+
* Whether to handle client messages sequentially in the order in which
57+
* they were received.
58+
* <p>By default messages sent to the {@code "clientInboundChannel"} may
59+
* be handled in parallel and not in the same order as they were received
60+
* because the channel is backed by a ThreadPoolExecutor that in turn does
61+
* not guarantee processing in order.
62+
* <p>When this flag is set to {@code true} messages within the same session
63+
* will be sent to the {@code "clientInboundChannel"} one at a time in
64+
* order to preserve the order in which they were received.
65+
* @since 6.1
66+
*/
67+
WebMvcStompEndpointRegistry setPreserveReceiveOrder(boolean preserveReceiveOrder);
68+
5569
}

spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompEndpointRegistry.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ public WebMvcStompEndpointRegistry setErrorHandler(StompSubProtocolErrorHandler
142142
return this;
143143
}
144144

145+
public WebMvcStompEndpointRegistry setPreserveReceiveOrder(boolean preserveReceiveOrder) {
146+
this.stompHandler.setPreserveReceiveOrder(preserveReceiveOrder);
147+
return this;
148+
}
149+
150+
protected boolean isPreserveReceiveOrder() {
151+
return this.stompHandler.isPreserveReceiveOrder();
152+
}
153+
145154
protected void setApplicationContext(ApplicationContext applicationContext) {
146155
this.stompHandler.setApplicationEventPublisher(applicationContext);
147156
}

spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.messaging.simp.SimpSessionScope;
2929
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
3030
import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler;
31+
import org.springframework.messaging.simp.broker.OrderedMessageChannelDecorator;
3132
import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration;
3233
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
3334
import org.springframework.messaging.simp.user.SimpUserRegistry;
@@ -80,7 +81,8 @@ protected SimpUserRegistry createLocalUserRegistry(@Nullable Integer order) {
8081

8182
@Bean
8283
public HandlerMapping stompWebSocketHandlerMapping(
83-
WebSocketHandler subProtocolWebSocketHandler, TaskScheduler messageBrokerTaskScheduler) {
84+
WebSocketHandler subProtocolWebSocketHandler, TaskScheduler messageBrokerTaskScheduler,
85+
AbstractSubscribableChannel clientInboundChannel) {
8486

8587
WebSocketHandler handler = decorateWebSocketHandler(subProtocolWebSocketHandler);
8688
WebMvcStompEndpointRegistry registry =
@@ -90,6 +92,7 @@ public HandlerMapping stompWebSocketHandlerMapping(
9092
registry.setApplicationContext(applicationContext);
9193
}
9294
registerStompEndpoints(registry);
95+
OrderedMessageChannelDecorator.configureInterceptor(clientInboundChannel, registry.isPreserveReceiveOrder());
9396
return registry.getHandlerMapping();
9497
}
9598

spring-websocket/src/main/java/org/springframework/web/socket/messaging/StompSubProtocolHandler.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
108108
@Nullable
109109
private MessageHeaderInitializer headerInitializer;
110110

111+
private boolean preserveReceiveOrder;
112+
113+
private final Map<String, MessageChannel> messageChannels = new ConcurrentHashMap<>();
114+
111115
private final Map<String, Principal> stompAuthentications = new ConcurrentHashMap<>();
112116

113117
@Nullable
@@ -193,6 +197,30 @@ public MessageHeaderInitializer getHeaderInitializer() {
193197
return this.headerInitializer;
194198
}
195199

200+
/**
201+
* Whether client messages must be handled in the order received.
202+
* <p>By default messages sent to the {@code "clientInboundChannel"} may
203+
* not be handled in the same order because the channel is backed by a
204+
* ThreadPoolExecutor that in turn does not guarantee processing in order.
205+
* <p>When this flag is set to {@code true} messages within the same session
206+
* will be sent to the {@code "clientInboundChannel"} one at a time to
207+
* preserve the order in which they were received.
208+
* @param preserveReceiveOrder whether to publish in order
209+
* @since 6.1
210+
*/
211+
public void setPreserveReceiveOrder(boolean preserveReceiveOrder) {
212+
this.preserveReceiveOrder = preserveReceiveOrder;
213+
}
214+
215+
/**
216+
* Whether the handler is configured to handle inbound messages in the
217+
* order in which they were received.
218+
* @since 6.1
219+
*/
220+
public boolean isPreserveReceiveOrder() {
221+
return this.preserveReceiveOrder;
222+
}
223+
196224
@Override
197225
public List<String> getSupportedProtocols() {
198226
return Arrays.asList("v10.stomp", "v11.stomp", "v12.stomp");
@@ -268,6 +296,12 @@ else if (webSocketMessage instanceof BinaryMessage binaryMessage) {
268296
return;
269297
}
270298

299+
MessageChannel channelToUse =
300+
(this.messageChannels.computeIfAbsent(session.getId(),
301+
id -> this.preserveReceiveOrder ?
302+
new OrderedMessageChannelDecorator(outputChannel, logger) :
303+
outputChannel));
304+
271305
for (Message<byte[]> message : messages) {
272306
StompHeaderAccessor headerAccessor =
273307
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
@@ -307,7 +341,7 @@ else if (StompCommand.DISCONNECT.equals(command)) {
307341

308342
try {
309343
SimpAttributesContextHolder.setAttributesFromMessage(message);
310-
sent = outputChannel.send(message);
344+
sent = channelToUse.send(message);
311345

312346
if (sent) {
313347
if (this.eventPublisher != null) {
@@ -652,6 +686,7 @@ public void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus,
652686
outputChannel.send(message);
653687
}
654688
finally {
689+
this.messageChannels.remove(session.getId());
655690
this.stompAuthentications.remove(session.getId());
656691
SimpAttributesContextHolder.resetAttributes();
657692
simpAttributes.sessionCompleted();

0 commit comments

Comments
 (0)