Option to preserve publish order
Issue: SPR-13989
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.messaging.simp.broker;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link OrderedMessageSender}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class OrderedMessageSenderTests {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(OrderedMessageSenderTests.class);
|
||||
|
||||
|
||||
private OrderedMessageSender sender;
|
||||
|
||||
ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(this.executor);
|
||||
|
||||
private ThreadPoolTaskExecutor executor;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.executor = new ThreadPoolTaskExecutor();
|
||||
this.executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
|
||||
this.executor.setAllowCoreThreadTimeOut(true);
|
||||
this.executor.afterPropertiesSet();
|
||||
|
||||
this.channel = new ExecutorSubscribableChannel(this.executor);
|
||||
OrderedMessageSender.configureOutboundChannel(this.channel, true);
|
||||
|
||||
this.sender = new OrderedMessageSender(this.channel, logger);
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
this.executor.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test() throws InterruptedException {
|
||||
|
||||
int start = 1;
|
||||
int end = 1000;
|
||||
|
||||
AtomicInteger index = new AtomicInteger(start);
|
||||
AtomicReference<Object> result = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
this.channel.subscribe(message -> {
|
||||
int expected = index.getAndIncrement();
|
||||
Integer actual = (Integer) message.getHeaders().getOrDefault("seq", -1);
|
||||
if (actual != expected) {
|
||||
result.set("Expected: " + expected + ", but was: " + actual);
|
||||
latch.countDown();
|
||||
return;
|
||||
}
|
||||
if (actual == 100 || actual == 200) {
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
result.set(ex.toString());
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
if (actual == end) {
|
||||
result.set("Done");
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = start; i <= end; i++) {
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
accessor.setHeader("seq", i);
|
||||
accessor.setLeaveMutable(true);
|
||||
this.sender.send(MessageBuilder.createMessage("payload", accessor.getMessageHeaders()));
|
||||
}
|
||||
|
||||
latch.await(10, TimeUnit.SECONDS);
|
||||
assertEquals("Done", result.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,20 +38,8 @@ import org.springframework.messaging.simp.TestPrincipal;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for SimpleBrokerMessageHandler.
|
||||
@@ -65,10 +53,10 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
private SimpleBrokerMessageHandler messageHandler;
|
||||
|
||||
@Mock
|
||||
private SubscribableChannel clientInboundChannel;
|
||||
private SubscribableChannel clientInChannel;
|
||||
|
||||
@Mock
|
||||
private MessageChannel clientOutboundChannel;
|
||||
private MessageChannel clientOutChannel;
|
||||
|
||||
@Mock
|
||||
private SubscribableChannel brokerChannel;
|
||||
@@ -83,15 +71,16 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.messageHandler = new SimpleBrokerMessageHandler(this.clientInboundChannel,
|
||||
this.clientOutboundChannel, this.brokerChannel, Collections.emptyList());
|
||||
this.messageHandler = new SimpleBrokerMessageHandler(
|
||||
this.clientInChannel, this.clientOutChannel, this.brokerChannel, Collections.emptyList());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void subcribePublish() {
|
||||
|
||||
this.messageHandler.start();
|
||||
startSession("sess1");
|
||||
startSession("sess2");
|
||||
|
||||
this.messageHandler.handleMessage(createSubscriptionMessage("sess1", "sub1", "/foo"));
|
||||
this.messageHandler.handleMessage(createSubscriptionMessage("sess1", "sub2", "/foo"));
|
||||
@@ -104,7 +93,7 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
this.messageHandler.handleMessage(createMessage("/foo", "message1"));
|
||||
this.messageHandler.handleMessage(createMessage("/bar", "message2"));
|
||||
|
||||
verify(this.clientOutboundChannel, times(6)).send(this.messageCaptor.capture());
|
||||
verify(this.clientOutChannel, times(6)).send(this.messageCaptor.capture());
|
||||
assertTrue(messageCaptured("sess1", "sub1", "/foo"));
|
||||
assertTrue(messageCaptured("sess1", "sub2", "/foo"));
|
||||
assertTrue(messageCaptured("sess2", "sub1", "/foo"));
|
||||
@@ -119,7 +108,8 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
String sess1 = "sess1";
|
||||
String sess2 = "sess2";
|
||||
|
||||
this.messageHandler.start();
|
||||
startSession(sess1);
|
||||
startSession(sess2);
|
||||
|
||||
this.messageHandler.handleMessage(createSubscriptionMessage(sess1, "sub1", "/foo"));
|
||||
this.messageHandler.handleMessage(createSubscriptionMessage(sess1, "sub2", "/foo"));
|
||||
@@ -138,9 +128,9 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
this.messageHandler.handleMessage(createMessage("/foo", "message1"));
|
||||
this.messageHandler.handleMessage(createMessage("/bar", "message2"));
|
||||
|
||||
verify(this.clientOutboundChannel, times(4)).send(this.messageCaptor.capture());
|
||||
verify(this.clientOutChannel, times(4)).send(this.messageCaptor.capture());
|
||||
|
||||
Message<?> captured = this.messageCaptor.getAllValues().get(0);
|
||||
Message<?> captured = this.messageCaptor.getAllValues().get(2);
|
||||
assertEquals(SimpMessageType.DISCONNECT_ACK, SimpMessageHeaderAccessor.getMessageType(captured.getHeaders()));
|
||||
assertSame(message, captured.getHeaders().get(SimpMessageHeaderAccessor.DISCONNECT_MESSAGE_HEADER));
|
||||
assertEquals(sess1, SimpMessageHeaderAccessor.getSessionId(captured.getHeaders()));
|
||||
@@ -154,14 +144,9 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
@Test
|
||||
public void connect() {
|
||||
|
||||
this.messageHandler.start();
|
||||
|
||||
String id = "sess1";
|
||||
Message<String> connectMessage = createConnectMessage(id, new TestPrincipal("joe"), null);
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.handleMessage(connectMessage);
|
||||
|
||||
verify(this.clientOutboundChannel, times(1)).send(this.messageCaptor.capture());
|
||||
Message<String> connectMessage = startSession(id);
|
||||
Message<?> connectAckMessage = this.messageCaptor.getValue();
|
||||
|
||||
SimpMessageHeaderAccessor connectAckHeaders = SimpMessageHeaderAccessor.wrap(connectAckMessage);
|
||||
@@ -173,7 +158,7 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heartbeatValueWithAndWithoutTaskScheduler() throws Exception {
|
||||
public void heartbeatValueWithAndWithoutTaskScheduler() {
|
||||
|
||||
assertNull(this.messageHandler.getHeartbeatValue());
|
||||
|
||||
@@ -184,14 +169,14 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void startWithHeartbeatValueWithoutTaskScheduler() throws Exception {
|
||||
public void startWithHeartbeatValueWithoutTaskScheduler() {
|
||||
this.messageHandler.setHeartbeatValue(new long[] {10000, 10000});
|
||||
this.messageHandler.start();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void startAndStopWithHeartbeatValue() throws Exception {
|
||||
public void startAndStopWithHeartbeatValue() {
|
||||
|
||||
ScheduledFuture future = mock(ScheduledFuture.class);
|
||||
when(this.taskScheduler.scheduleWithFixedDelay(any(Runnable.class), eq(15000L))).thenReturn(future);
|
||||
@@ -211,7 +196,7 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void startWithOneZeroHeartbeatValue() throws Exception {
|
||||
public void startWithOneZeroHeartbeatValue() {
|
||||
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.setHeartbeatValue(new long[] {0, 10000});
|
||||
@@ -240,7 +225,7 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
Thread.sleep(10);
|
||||
heartbeatTask.run();
|
||||
|
||||
verify(this.clientOutboundChannel, atLeast(2)).send(this.messageCaptor.capture());
|
||||
verify(this.clientOutChannel, atLeast(2)).send(this.messageCaptor.capture());
|
||||
List<Message<?>> messages = this.messageCaptor.getAllValues();
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
@@ -272,7 +257,7 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
Thread.sleep(10);
|
||||
heartbeatTask.run();
|
||||
|
||||
verify(this.clientOutboundChannel, times(2)).send(this.messageCaptor.capture());
|
||||
verify(this.clientOutChannel, times(2)).send(this.messageCaptor.capture());
|
||||
List<Message<?>> messages = this.messageCaptor.getAllValues();
|
||||
assertEquals(2, messages.size());
|
||||
|
||||
@@ -304,13 +289,25 @@ public class SimpleBrokerMessageHandlerTests {
|
||||
Thread.sleep(10);
|
||||
heartbeatTask.run();
|
||||
|
||||
verify(this.clientOutboundChannel, times(1)).send(this.messageCaptor.capture());
|
||||
verify(this.clientOutChannel, times(1)).send(this.messageCaptor.capture());
|
||||
List<Message<?>> messages = this.messageCaptor.getAllValues();
|
||||
assertEquals(1, messages.size());
|
||||
assertEquals(SimpMessageType.CONNECT_ACK,
|
||||
messages.get(0).getHeaders().get(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER));
|
||||
}
|
||||
|
||||
private Message<String> startSession(String id) {
|
||||
this.messageHandler.start();
|
||||
|
||||
Message<String> connectMessage = createConnectMessage(id, new TestPrincipal("joe"), null);
|
||||
this.messageHandler.setTaskScheduler(this.taskScheduler);
|
||||
this.messageHandler.handleMessage(connectMessage);
|
||||
|
||||
verify(this.clientOutChannel, times(1)).send(this.messageCaptor.capture());
|
||||
reset(this.clientOutChannel);
|
||||
return connectMessage;
|
||||
}
|
||||
|
||||
private Message<String> createSubscriptionMessage(String sessionId, String subcriptionId, String destination) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
|
||||
headers.setSubscriptionId(subcriptionId);
|
||||
|
||||
@@ -157,7 +157,7 @@ public class MessageBrokerConfigurationTests {
|
||||
public void clientOutboundChannelUsedBySimpleBroker() {
|
||||
ApplicationContext context = loadConfig(SimpleBrokerConfig.class);
|
||||
|
||||
TestChannel channel = context.getBean("clientOutboundChannel", TestChannel.class);
|
||||
TestChannel outboundChannel = context.getBean("clientOutboundChannel", TestChannel.class);
|
||||
SimpleBrokerMessageHandler broker = context.getBean(SimpleBrokerMessageHandler.class);
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
|
||||
@@ -167,6 +167,7 @@ public class MessageBrokerConfigurationTests {
|
||||
Message<?> message = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
||||
|
||||
// subscribe
|
||||
broker.handleMessage(createConnectMessage("sess1", new long[] {0,0}));
|
||||
broker.handleMessage(message);
|
||||
|
||||
headers = StompHeaderAccessor.create(StompCommand.SEND);
|
||||
@@ -177,7 +178,7 @@ public class MessageBrokerConfigurationTests {
|
||||
// message
|
||||
broker.handleMessage(message);
|
||||
|
||||
message = channel.messages.get(0);
|
||||
message = outboundChannel.messages.get(1);
|
||||
headers = StompHeaderAccessor.wrap(message);
|
||||
|
||||
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
|
||||
@@ -192,7 +193,7 @@ public class MessageBrokerConfigurationTests {
|
||||
AbstractSubscribableChannel channel = context.getBean(
|
||||
"clientOutboundChannel", AbstractSubscribableChannel.class);
|
||||
|
||||
assertEquals(3, channel.getInterceptors().size());
|
||||
assertEquals(4, channel.getInterceptors().size());
|
||||
|
||||
ThreadPoolTaskExecutor taskExecutor = context.getBean(
|
||||
"clientOutboundChannelExecutor", ThreadPoolTaskExecutor.class);
|
||||
@@ -200,6 +201,10 @@ public class MessageBrokerConfigurationTests {
|
||||
assertEquals(21, taskExecutor.getCorePoolSize());
|
||||
assertEquals(22, taskExecutor.getMaxPoolSize());
|
||||
assertEquals(23, taskExecutor.getKeepAliveSeconds());
|
||||
|
||||
SimpleBrokerMessageHandler broker =
|
||||
context.getBean("simpleBrokerMessageHandler", SimpleBrokerMessageHandler.class);
|
||||
assertTrue(broker.isPreservePublishOrder());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -479,6 +484,7 @@ public class MessageBrokerConfigurationTests {
|
||||
TestChannel outChannel = context.getBean("clientOutboundChannel", TestChannel.class);
|
||||
MessageChannel brokerChannel = context.getBean("brokerChannel", MessageChannel.class);
|
||||
|
||||
inChannel.send(createConnectMessage("sess1", new long[] {0,0}));
|
||||
|
||||
// 1. Subscribe to user destination
|
||||
|
||||
@@ -497,13 +503,14 @@ public class MessageBrokerConfigurationTests {
|
||||
message = MessageBuilder.createMessage("123".getBytes(), headers.getMessageHeaders());
|
||||
inChannel.send(message);
|
||||
|
||||
assertEquals(1, outChannel.messages.size());
|
||||
Message<?> outputMessage = outChannel.messages.remove(0);
|
||||
assertEquals(2, outChannel.messages.size());
|
||||
Message<?> outputMessage = outChannel.messages.remove(1);
|
||||
headers = StompHeaderAccessor.wrap(outputMessage);
|
||||
|
||||
assertEquals(SimpMessageType.MESSAGE, headers.getMessageType());
|
||||
assertEquals(expectLeadingSlash ? "/queue.q1-usersess1" : "queue.q1-usersess1", headers.getDestination());
|
||||
assertEquals("123", new String((byte[]) outputMessage.getPayload()));
|
||||
outChannel.messages.clear();
|
||||
|
||||
|
||||
// 3. Send message via broker channel
|
||||
@@ -527,6 +534,13 @@ public class MessageBrokerConfigurationTests {
|
||||
return new AnnotationConfigApplicationContext(configClass);
|
||||
}
|
||||
|
||||
private Message<String> createConnectMessage(String sessionId, long[] heartbeat) {
|
||||
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
|
||||
accessor.setSessionId(sessionId);
|
||||
accessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, heartbeat);
|
||||
return MessageBuilder.createMessage("", accessor.getMessageHeaders());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Controller
|
||||
@@ -635,6 +649,7 @@ public class MessageBrokerConfigurationTests {
|
||||
.corePoolSize(31).maxPoolSize(32).keepAliveSeconds(33).queueCapacity(34);
|
||||
registry.setPathMatcher(new AntPathMatcher(".")).enableSimpleBroker("/topic", "/queue");
|
||||
registry.setCacheLimit(8192);
|
||||
registry.setPreservePublishOrder(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,12 +102,14 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
|
||||
}
|
||||
|
||||
private void createAndStartRelay() throws InterruptedException {
|
||||
this.relay = new StompBrokerRelayMessageHandler(new StubMessageChannel(),
|
||||
this.responseChannel, new StubMessageChannel(), Arrays.asList("/queue/", "/topic/"));
|
||||
StubMessageChannel channel = new StubMessageChannel();
|
||||
List<String> prefixes = Arrays.asList("/queue/", "/topic/");
|
||||
this.relay = new StompBrokerRelayMessageHandler(channel, this.responseChannel, channel, prefixes);
|
||||
this.relay.setRelayPort(this.port);
|
||||
this.relay.setApplicationEventPublisher(this.eventPublisher);
|
||||
this.relay.setSystemHeartbeatReceiveInterval(0);
|
||||
this.relay.setSystemHeartbeatSendInterval(0);
|
||||
this.relay.setPreservePublishOrder(true);
|
||||
|
||||
this.relay.start();
|
||||
this.eventPublisher.expectBrokerAvailabilityEvent(true);
|
||||
|
||||
Reference in New Issue
Block a user