Support user destinations with multiple app servers

This change adds support for broadcasting messages with unresolved
user destinations so that other servers can try to resolve it.
That enables sending messages to users who may be connected to a
different server.

Issue: SPR-11620
This commit is contained in:
Rossen Stoyanchev
2015-04-14 20:50:48 -04:00
parent c384945a17
commit c29eae3307
14 changed files with 550 additions and 150 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -79,8 +79,11 @@ import static org.mockito.Mockito.*;
public class MessageBrokerConfigurationTests {
private ApplicationContext defaultContext = new AnnotationConfigApplicationContext(DefaultConfig.class);
private ApplicationContext simpleBrokerContext = new AnnotationConfigApplicationContext(SimpleBrokerConfig.class);
private ApplicationContext brokerRelayContext = new AnnotationConfigApplicationContext(BrokerRelayConfig.class);
private ApplicationContext customContext = new AnnotationConfigApplicationContext(CustomConfig.class);
@@ -401,7 +404,17 @@ public class MessageBrokerConfigurationTests {
assertEquals("a.a", handler.getPathMatcher().combine("a", "a"));
}
@Test
public void userDestinationBroadcast() throws Exception {
StompBrokerRelayMessageHandler relay = this.brokerRelayContext.getBean(StompBrokerRelayMessageHandler.class);
UserDestinationMessageHandler userHandler = this.brokerRelayContext.getBean(UserDestinationMessageHandler.class);
assertEquals("/topic/unresolved", userHandler.getUserDestinationBroadcast());
assertNotNull(relay.getSystemSubscriptions());
assertSame(userHandler, relay.getSystemSubscriptions().get("/topic/unresolved"));
}
@SuppressWarnings("unused")
@Controller
static class TestController {
@@ -417,7 +430,7 @@ public class MessageBrokerConfigurationTests {
}
}
@SuppressWarnings("unused")
@Configuration
static class SimpleBrokerConfig extends AbstractMessageBrokerConfiguration {
@@ -451,6 +464,7 @@ public class MessageBrokerConfigurationTests {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue").setAutoStartup(true);
registry.setUserDestinationBroadcast("/topic/unresolved");
}
}

View File

@@ -15,15 +15,22 @@
*/
package org.springframework.messaging.simp.stomp;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.StubMessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
@@ -37,8 +44,6 @@ import org.springframework.messaging.tcp.TcpOperations;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureTask;
import static org.junit.Assert.*;
/**
* Unit tests for StompBrokerRelayMessageHandler.
*
@@ -74,62 +79,52 @@ public class StompBrokerRelayMessageHandlerTests {
@Test
public void testVirtualHostHeader() throws Exception {
public void virtualHost() throws Exception {
this.brokerRelay.setVirtualHost("ABC");
String virtualHost = "ABC";
this.brokerRelay.setVirtualHost(virtualHost);
this.brokerRelay.start();
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
String sessionId = "sess1";
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
assertEquals(2, this.tcpClient.getSentMessages().size());
List<Message<byte[]>> sent = this.tcpClient.connection.messages;
assertEquals(2, sent.size());
StompHeaderAccessor headers1 = this.tcpClient.getSentHeaders(0);
assertEquals(StompCommand.CONNECT, headers1.getCommand());
assertEquals(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID, headers1.getSessionId());
assertEquals("ABC", headers1.getHost());
StompHeaderAccessor headers1 = StompHeaderAccessor.wrap(sent.get(0));
assertEquals(virtualHost, headers1.getHost());
assertNotNull("The prepared message does not have an accessor",
MessageHeaderAccessor.getAccessor(sent.get(0), MessageHeaderAccessor.class));
StompHeaderAccessor headers2 = StompHeaderAccessor.wrap(sent.get(1));
assertEquals(sessionId, headers2.getSessionId());
assertEquals(virtualHost, headers2.getHost());
assertNotNull("The prepared message does not have an accessor",
MessageHeaderAccessor.getAccessor(sent.get(1), MessageHeaderAccessor.class));
StompHeaderAccessor headers2 = this.tcpClient.getSentHeaders(1);
assertEquals(StompCommand.CONNECT, headers2.getCommand());
assertEquals("sess1", headers2.getSessionId());
assertEquals("ABC", headers2.getHost());
}
@Test
public void testLoginPasscode() throws Exception {
this.brokerRelay.setClientLogin("clientlogin");
this.brokerRelay.setClientPasscode("clientpasscode");
public void loginAndPasscode() throws Exception {
this.brokerRelay.setSystemLogin("syslogin");
this.brokerRelay.setSystemPasscode("syspasscode");
this.brokerRelay.setClientLogin("clientlogin");
this.brokerRelay.setClientPasscode("clientpasscode");
this.brokerRelay.start();
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
String sessionId = "sess1";
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
assertEquals(2, this.tcpClient.getSentMessages().size());
List<Message<byte[]>> sent = this.tcpClient.connection.messages;
assertEquals(2, sent.size());
StompHeaderAccessor headers1 = StompHeaderAccessor.wrap(sent.get(0));
StompHeaderAccessor headers1 = this.tcpClient.getSentHeaders(0);
assertEquals(StompCommand.CONNECT, headers1.getCommand());
assertEquals("syslogin", headers1.getLogin());
assertEquals("syspasscode", headers1.getPasscode());
StompHeaderAccessor headers2 = StompHeaderAccessor.wrap(sent.get(1));
StompHeaderAccessor headers2 = this.tcpClient.getSentHeaders(1);
assertEquals(StompCommand.CONNECT, headers2.getCommand());
assertEquals("clientlogin", headers2.getLogin());
assertEquals("clientpasscode", headers2.getPasscode());
}
@Test
public void testDestinationExcluded() throws Exception {
public void destinationExcluded() throws Exception {
this.brokerRelay.start();
@@ -138,89 +133,113 @@ public class StompBrokerRelayMessageHandlerTests {
headers.setDestination("/user/daisy/foo");
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
List<Message<byte[]>> sent = this.tcpClient.connection.messages;
assertEquals(1, sent.size());
assertEquals(StompCommand.CONNECT, StompHeaderAccessor.wrap(sent.get(0)).getCommand());
assertNotNull("The prepared message does not have an accessor",
MessageHeaderAccessor.getAccessor(sent.get(0), MessageHeaderAccessor.class));
assertEquals(1, this.tcpClient.getSentMessages().size());
StompHeaderAccessor headers1 = this.tcpClient.getSentHeaders(0);
assertEquals(StompCommand.CONNECT, headers1.getCommand());
assertEquals(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID, headers1.getSessionId());
}
@Test
public void testOutboundMessageIsEnriched() throws Exception {
public void messageFromBrokerIsEnriched() throws Exception {
this.brokerRelay.start();
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
String sessionId = "sess1";
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal("joe"));
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
assertEquals(2, this.tcpClient.getSentMessages().size());
assertEquals(StompCommand.CONNECT, this.tcpClient.getSentHeaders(0).getCommand());
assertEquals(StompCommand.CONNECT, this.tcpClient.getSentHeaders(1).getCommand());
List<Message<byte[]>> sent = this.tcpClient.connection.messages;
assertEquals(2, sent.size());
this.tcpClient.handleMessage(message(StompCommand.MESSAGE, null, null, null));
StompHeaderAccessor responseHeaders = StompHeaderAccessor.create(StompCommand.MESSAGE);
responseHeaders.setLeaveMutable(true);
Message<byte[]> response = MessageBuilder.createMessage(new byte[0], responseHeaders.getMessageHeaders());
this.tcpClient.connectionHandler.handleMessage(response);
Message<byte[]> actual = this.outboundChannel.getMessages().get(0);
StompHeaderAccessor actualHeaders = StompHeaderAccessor.getAccessor(actual, StompHeaderAccessor.class);
assertEquals(sessionId, actualHeaders.getSessionId());
assertEquals("joe", actualHeaders.getUser().getName());
Message<byte[]> message = this.outboundChannel.getMessages().get(0);
StompHeaderAccessor accessor = StompHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assertEquals("sess1", accessor.getSessionId());
assertEquals("joe", accessor.getUser().getName());
}
// SPR-12820
@Test
public void testConnectWhenBrokerNotAvailable() throws Exception {
public void connectWhenBrokerNotAvailable() throws Exception {
this.brokerRelay.start();
this.brokerRelay.stopInternal();
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
String sessionId = "sess1";
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal("joe"));
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
Message<byte[]> actual = this.outboundChannel.getMessages().get(0);
StompHeaderAccessor actualHeaders = StompHeaderAccessor.getAccessor(actual, StompHeaderAccessor.class);
assertEquals(StompCommand.ERROR, actualHeaders.getCommand());
assertEquals(sessionId, actualHeaders.getSessionId());
assertEquals("joe", actualHeaders.getUser().getName());
assertEquals("Broker not available.", actualHeaders.getMessage());
Message<byte[]> message = this.outboundChannel.getMessages().get(0);
StompHeaderAccessor accessor = StompHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assertEquals(StompCommand.ERROR, accessor.getCommand());
assertEquals("sess1", accessor.getSessionId());
assertEquals("joe", accessor.getUser().getName());
assertEquals("Broker not available.", accessor.getMessage());
}
@Test
public void testSendAfterBrokerUnavailable() throws Exception {
public void sendAfterBrokerUnavailable() throws Exception {
this.brokerRelay.start();
assertEquals(1, this.brokerRelay.getConnectionCount());
String sessionId = "sess1";
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal("joe"));
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
assertEquals(2, this.brokerRelay.getConnectionCount());
this.brokerRelay.stopInternal();
headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal("joe"));
headers.setDestination("/foo");
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
this.brokerRelay.handleMessage(message(StompCommand.SEND, "sess1", "joe", "/foo"));
assertEquals(1, this.brokerRelay.getConnectionCount());
Message<byte[]> actual = this.outboundChannel.getMessages().get(0);
StompHeaderAccessor actualHeaders = StompHeaderAccessor.getAccessor(actual, StompHeaderAccessor.class);
assertEquals(StompCommand.ERROR, actualHeaders.getCommand());
assertEquals(sessionId, actualHeaders.getSessionId());
assertEquals("joe", actualHeaders.getUser().getName());
assertEquals("Broker not available.", actualHeaders.getMessage());
Message<byte[]> message = this.outboundChannel.getMessages().get(0);
StompHeaderAccessor accessor = StompHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assertEquals(StompCommand.ERROR, accessor.getCommand());
assertEquals("sess1", accessor.getSessionId());
assertEquals("joe", accessor.getUser().getName());
assertEquals("Broker not available.", accessor.getMessage());
}
@Test
public void systemSubscription() throws Exception {
MessageHandler handler = mock(MessageHandler.class);
this.brokerRelay.setSystemSubscriptions(Collections.singletonMap("/topic/foo", handler));
this.brokerRelay.start();
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
accessor.setLeaveMutable(true);
MessageHeaders headers = accessor.getMessageHeaders();
this.tcpClient.handleMessage(MessageBuilder.createMessage(new byte[0], headers));
assertEquals(2, this.tcpClient.getSentMessages().size());
assertEquals(StompCommand.CONNECT, this.tcpClient.getSentHeaders(0).getCommand());
assertEquals(StompCommand.SUBSCRIBE, this.tcpClient.getSentHeaders(1).getCommand());
assertEquals("/topic/foo", this.tcpClient.getSentHeaders(1).getDestination());
Message<byte[]> message = message(StompCommand.MESSAGE, null, null, "/topic/foo");
this.tcpClient.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
verify(handler).handleMessage(captor.capture());
assertSame(message, captor.getValue());
}
private Message<byte[]> connectMessage(String sessionId, String user) {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal(user));
return MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
}
private Message<byte[]> message(StompCommand command, String sessionId, String user, String destination) {
StompHeaderAccessor accessor = StompHeaderAccessor.create(command);
if (sessionId != null) {
accessor.setSessionId(sessionId);
}
if (user != null) {
accessor.setUser(new TestPrincipal(user));
}
if (destination != null) {
accessor.setDestination(destination);
}
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders());
}
@@ -254,17 +273,29 @@ public class StompBrokerRelayMessageHandlerTests {
private TcpConnectionHandler<byte[]> connectionHandler;
public List<Message<byte[]>> getSentMessages() {
return this.connection.getMessages();
}
public StompHeaderAccessor getSentHeaders(int index) {
assertTrue("Size: " + getSentMessages().size(), getSentMessages().size() > index);
Message<byte[]> message = getSentMessages().get(index);
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assertNotNull(accessor);
return accessor;
}
@Override
public ListenableFuture<Void> connect(TcpConnectionHandler<byte[]> connectionHandler) {
this.connectionHandler = connectionHandler;
connectionHandler.afterConnected(this.connection);
public ListenableFuture<Void> connect(TcpConnectionHandler<byte[]> handler) {
this.connectionHandler = handler;
handler.afterConnected(this.connection);
return getVoidFuture();
}
@Override
public ListenableFuture<Void> connect(TcpConnectionHandler<byte[]> connectionHandler, ReconnectStrategy reconnectStrategy) {
this.connectionHandler = connectionHandler;
connectionHandler.afterConnected(this.connection);
public ListenableFuture<Void> connect(TcpConnectionHandler<byte[]> handler, ReconnectStrategy strategy) {
this.connectionHandler = handler;
handler.afterConnected(this.connection);
return getVoidFuture();
}
@@ -272,6 +303,11 @@ public class StompBrokerRelayMessageHandlerTests {
public ListenableFuture<Boolean> shutdown() {
return getBooleanFuture();
}
public void handleMessage(Message<byte[]> message) {
this.connectionHandler.handleMessage(message);
}
}
@@ -280,6 +316,10 @@ public class StompBrokerRelayMessageHandlerTests {
private final List<Message<byte[]>> messages = new ArrayList<>();
public List<Message<byte[]>> getMessages() {
return this.messages;
}
@Override
public ListenableFuture<Void> send(Message<byte[]> message) {
this.messages.add(message);

View File

@@ -18,7 +18,9 @@ package org.springframework.messaging.simp.user;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.messaging.simp.SimpMessageHeaderAccessor.ORIGINAL_DESTINATION;
import static org.springframework.messaging.simp.SimpMessageHeaderAccessor.*;
import java.nio.charset.Charset;
import org.junit.Before;
import org.junit.Test;
@@ -33,6 +35,8 @@ import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.TestPrincipal;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.MessageBuilder;
/**
@@ -62,7 +66,6 @@ public class UserDestinationMessageHandlerTests {
@Test
@SuppressWarnings("rawtypes")
public void handleSubscribe() {
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
this.handler.handleMessage(createWith(SimpMessageType.SUBSCRIBE, "joe", SESSION_ID, "/user/queue/foo"));
@@ -75,7 +78,6 @@ public class UserDestinationMessageHandlerTests {
}
@Test
@SuppressWarnings("rawtypes")
public void handleUnsubscribe() {
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
this.handler.handleMessage(createWith(SimpMessageType.UNSUBSCRIBE, "joe", "123", "/user/queue/foo"));
@@ -88,7 +90,6 @@ public class UserDestinationMessageHandlerTests {
}
@Test
@SuppressWarnings("rawtypes")
public void handleMessage() {
this.registry.registerSessionId("joe", "123");
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
@@ -102,6 +103,69 @@ public class UserDestinationMessageHandlerTests {
assertEquals("/user/queue/foo", accessor.getFirstNativeHeader(ORIGINAL_DESTINATION));
}
@Test
public void handleMessageWithoutActiveSession() {
this.handler.setUserDestinationBroadcast("/topic/unresolved");
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
this.handler.handleMessage(createWith(SimpMessageType.MESSAGE, "joe", "123", "/user/joe/queue/foo"));
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
Mockito.verify(this.brokerChannel).send(captor.capture());
Message message = captor.getValue();
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(message);
assertEquals("/topic/unresolved", accessor.getDestination());
assertEquals("/user/joe/queue/foo", accessor.getFirstNativeHeader(ORIGINAL_DESTINATION));
// Should ignore our own broadcast to brokerChannel
this.handler.handleMessage(message);
Mockito.verifyNoMoreInteractions(this.brokerChannel);
}
@Test
public void handleMessageFromBrokerWithActiveSession() {
this.registry.registerSessionId("joe", "123");
this.handler.setUserDestinationBroadcast("/topic/unresolved");
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.MESSAGE);
accessor.setSessionId("system123");
accessor.setDestination("/topic/unresolved");
accessor.setNativeHeader(ORIGINAL_DESTINATION, "/user/joe/queue/foo");
accessor.setNativeHeader("customHeader", "customHeaderValue");
accessor.setLeaveMutable(true);
byte[] payload = "payload".getBytes(Charset.forName("UTF-8"));
this.handler.handleMessage(MessageBuilder.createMessage(payload, accessor.getMessageHeaders()));
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
Mockito.verify(this.brokerChannel).send(captor.capture());
assertNotNull(captor.getValue());
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(captor.getValue());
assertEquals("/queue/foo-user123", headers.getDestination());
assertEquals("/user/queue/foo", headers.getFirstNativeHeader(ORIGINAL_DESTINATION));
assertEquals("customHeaderValue", headers.getFirstNativeHeader("customHeader"));
assertArrayEquals(payload, (byte[]) captor.getValue().getPayload());
}
@Test
public void handleMessageFromBrokerWithoutActiveSession() {
this.handler.setUserDestinationBroadcast("/topic/unresolved");
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.MESSAGE);
accessor.setSessionId("system123");
accessor.setDestination("/topic/unresolved");
accessor.setNativeHeader(ORIGINAL_DESTINATION, "/user/joe/queue/foo");
accessor.setLeaveMutable(true);
byte[] payload = "payload".getBytes(Charset.forName("UTF-8"));
this.handler.handleMessage(MessageBuilder.createMessage(payload, accessor.getMessageHeaders()));
// No re-broadcast
verifyNoMoreInteractions(this.brokerChannel);
}
@Test
public void ignoreMessage() {