INT-3882: StompSession Reconnection Logic
JIRA: https://jira.spring.io/browse/INT-3882 Add reconnect support for the StompSession: * Introduce `recoveryInterval` for the `AbstractStompSessionManager` * Add reconnect scheduled task * Add handling of the `ConnectionLostException` into the `AbstractStompSessionManager`, as well as for the `StompInboundChannelAdapter` and `StompMessageHandler` * Cover adapters reconnection feature with tests * Documentation polishing Rework logic according PR comments * Make `StompMessageHandler` as a "lazy-load" for the connection * Add "direct" connect interaction for the `AbstractStompSessionManager` * Polishing tests Polishing Fix `StompAdaptersParserTests#testStompSessionManagerReconnect()` to use "fake" server port Address PR comments The further polishing Some further polishing * `AbstractStompSessionManager`: `reconnectFuture.cancel(true)` on each `scheduleReconnect()` to avoid something like "DDoS attack" * Add reconnect feature test for the `StompInboundChannelAdapterWebSocketIntegrationTests`, closing and then refreshing again the `serverContext`
This commit is contained in:
committed by
Gary Russell
parent
f1bd6e3bac
commit
b9730cd183
@@ -16,17 +16,18 @@
|
||||
|
||||
package org.springframework.integration.stomp.client;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.core.IsInstanceOf.instanceOf;
|
||||
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.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.apache.activemq.broker.BrokerService;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
@@ -42,14 +43,17 @@ import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer;
|
||||
import org.springframework.integration.stomp.Reactor2TcpStompSessionManager;
|
||||
import org.springframework.integration.stomp.StompSessionManager;
|
||||
import org.springframework.integration.stomp.event.StompConnectionFailedEvent;
|
||||
import org.springframework.integration.stomp.event.StompIntegrationEvent;
|
||||
import org.springframework.integration.stomp.event.StompReceiptEvent;
|
||||
import org.springframework.integration.stomp.event.StompSessionConnectedEvent;
|
||||
import org.springframework.integration.stomp.inbound.StompInboundChannelAdapter;
|
||||
import org.springframework.integration.stomp.outbound.StompMessageHandler;
|
||||
import org.springframework.integration.support.converter.PassThruMessageConverter;
|
||||
import org.springframework.integration.test.support.LongRunningIntegrationTest;
|
||||
import org.springframework.integration.test.support.LogAdjustingTestSupport;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageDeliveryException;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.simp.stomp.Reactor2TcpStompClient;
|
||||
@@ -63,15 +67,17 @@ import org.springframework.util.SocketUtils;
|
||||
* @author Gary Russell
|
||||
* @since 4.2
|
||||
*/
|
||||
public class StompServerIntegrationTests {
|
||||
|
||||
@Rule
|
||||
public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest();
|
||||
public class StompServerIntegrationTests extends LogAdjustingTestSupport {
|
||||
|
||||
private static BrokerService activeMQBroker;
|
||||
|
||||
private static Reactor2TcpStompClient stompClient;
|
||||
|
||||
|
||||
public StompServerIntegrationTests() {
|
||||
super("org.springframework", "org.springframework.integration.stomp");
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception {
|
||||
int port = SocketUtils.findAvailableTcpPort(61613);
|
||||
@@ -97,7 +103,7 @@ public class StompServerIntegrationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStompAdapters() {
|
||||
public void testStompAdapters() throws Exception {
|
||||
ConfigurableApplicationContext context1 = new AnnotationConfigApplicationContext(ContextConfiguration.class);
|
||||
ConfigurableApplicationContext context2 = new AnnotationConfigApplicationContext(ContextConfiguration.class);
|
||||
|
||||
@@ -112,11 +118,19 @@ public class StompServerIntegrationTests {
|
||||
|
||||
Message<?> eventMessage = stompEvents1.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
assertThat(eventMessage.getPayload(), instanceOf(StompSessionConnectedEvent.class));
|
||||
|
||||
eventMessage = stompEvents1.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
assertThat(eventMessage.getPayload(), instanceOf(StompReceiptEvent.class));
|
||||
StompReceiptEvent stompReceiptEvent = (StompReceiptEvent) eventMessage.getPayload();
|
||||
assertEquals(StompCommand.SUBSCRIBE, stompReceiptEvent.getStompCommand());
|
||||
assertEquals("/topic/myTopic", stompReceiptEvent.getDestination());
|
||||
|
||||
eventMessage = stompEvents2.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
assertThat(eventMessage.getPayload(), instanceOf(StompSessionConnectedEvent.class));
|
||||
|
||||
eventMessage = stompEvents2.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
assertThat(eventMessage.getPayload(), instanceOf(StompReceiptEvent.class));
|
||||
@@ -180,6 +194,42 @@ public class StompServerIntegrationTests {
|
||||
assertNotNull(receive24);
|
||||
assertArrayEquals("???".getBytes(), (byte[]) receive24.getPayload());
|
||||
|
||||
activeMQBroker.stop();
|
||||
|
||||
do {
|
||||
eventMessage = stompEvents1.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
}
|
||||
while (!(eventMessage.getPayload() instanceof StompConnectionFailedEvent));
|
||||
|
||||
try {
|
||||
stompOutputChannel1.send(new GenericMessage<byte[]>("foo".getBytes()));
|
||||
fail("MessageDeliveryException is expected");
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertThat(e, instanceOf(MessageDeliveryException.class));
|
||||
assertThat(e.getMessage(), containsString("could not deliver message"));
|
||||
}
|
||||
|
||||
activeMQBroker.start(false);
|
||||
|
||||
do {
|
||||
eventMessage = stompEvents1.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
}
|
||||
while (!(eventMessage.getPayload() instanceof StompReceiptEvent));
|
||||
|
||||
do {
|
||||
eventMessage = stompEvents2.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
}
|
||||
while (!(eventMessage.getPayload() instanceof StompReceiptEvent));
|
||||
|
||||
stompOutputChannel1.send(new GenericMessage<byte[]>("foo".getBytes()));
|
||||
Message<?> receive25 = stompInputChannel2.receive(10000);
|
||||
assertNotNull(receive25);
|
||||
assertArrayEquals("foo".getBytes(), (byte[]) receive25.getPayload());
|
||||
|
||||
context1.close();
|
||||
context2.close();
|
||||
}
|
||||
@@ -192,6 +242,7 @@ public class StompServerIntegrationTests {
|
||||
public StompSessionManager stompSessionManager() {
|
||||
Reactor2TcpStompSessionManager stompSessionManager = new Reactor2TcpStompSessionManager(stompClient);
|
||||
stompSessionManager.setAutoReceipt(true);
|
||||
stompSessionManager.setRecoveryInterval(500);
|
||||
return stompSessionManager;
|
||||
}
|
||||
|
||||
@@ -213,6 +264,7 @@ public class StompServerIntegrationTests {
|
||||
public MessageHandler stompMessageHandler() {
|
||||
StompMessageHandler handler = new StompMessageHandler(stompSessionManager());
|
||||
handler.setDestination("/topic/myTopic");
|
||||
handler.setConnectTimeout(1000);
|
||||
return handler;
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,6 @@ public class StompAdaptersParserTests {
|
||||
List<SmartLifecycle> bars = lifecycles.get("bar");
|
||||
bars.contains(this.customInboundAdapter);
|
||||
assertTrue(lifecycles.containsKey("foo"));
|
||||
List<SmartLifecycle> foos = lifecycles.get("bar");
|
||||
bars.contains(this.customOutboundAdapter);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -46,8 +47,10 @@ import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer;
|
||||
import org.springframework.integration.stomp.StompSessionManager;
|
||||
import org.springframework.integration.stomp.WebSocketStompSessionManager;
|
||||
import org.springframework.integration.stomp.event.StompConnectionFailedEvent;
|
||||
import org.springframework.integration.stomp.event.StompIntegrationEvent;
|
||||
import org.springframework.integration.stomp.event.StompReceiptEvent;
|
||||
import org.springframework.integration.stomp.event.StompSessionConnectedEvent;
|
||||
import org.springframework.integration.test.rule.Log4jLevelAdjuster;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.integration.websocket.TomcatWebSocketTestServer;
|
||||
@@ -96,7 +99,7 @@ public class StompInboundChannelAdapterWebSocketIntegrationTests {
|
||||
public Log4jLevelAdjuster adjuster = new Log4jLevelAdjuster(Level.TRACE, "org.springframework");
|
||||
|
||||
@Value("#{server.serverContext}")
|
||||
private ApplicationContext serverContext;
|
||||
private ConfigurableApplicationContext serverContext;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("stompInputChannel")
|
||||
@@ -114,7 +117,11 @@ public class StompInboundChannelAdapterWebSocketIntegrationTests {
|
||||
private StompInboundChannelAdapter stompInboundChannelAdapter;
|
||||
|
||||
@Test
|
||||
public void testWebSocketStompClient() throws InterruptedException {
|
||||
public void testWebSocketStompClient() throws Exception {
|
||||
Message<?> eventMessage = this.stompEvents.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
assertThat(eventMessage.getPayload(), instanceOf(StompSessionConnectedEvent.class));
|
||||
|
||||
Message<?> receive = this.stompEvents.receive(10000);
|
||||
assertNotNull(receive);
|
||||
assertThat(receive.getPayload(), instanceOf(StompReceiptEvent.class));
|
||||
@@ -165,6 +172,27 @@ public class StompInboundChannelAdapterWebSocketIntegrationTests {
|
||||
assertThat(throwable, instanceOf(MessageHandlingException.class));
|
||||
assertThat(throwable.getCause(), instanceOf(MessageConversionException.class));
|
||||
assertThat(throwable.getMessage(), containsString("No suitable converter, payloadType=interface java.util.Map"));
|
||||
|
||||
this.serverContext.close();
|
||||
|
||||
eventMessage = this.stompEvents.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
assertThat(eventMessage.getPayload(), instanceOf(StompConnectionFailedEvent.class));
|
||||
|
||||
this.serverContext.refresh();
|
||||
|
||||
do {
|
||||
eventMessage = this.stompEvents.receive(10000);
|
||||
assertNotNull(eventMessage);
|
||||
}
|
||||
while (!(eventMessage.getPayload() instanceof StompSessionConnectedEvent));
|
||||
|
||||
waitForSubscribe("myTopic");
|
||||
|
||||
messagingTemplate = this.serverContext.getBean("brokerMessagingTemplate", SimpMessagingTemplate.class);
|
||||
messagingTemplate.convertAndSend("/topic/myTopic", "foo");
|
||||
receive = this.errorChannel.receive(10000);
|
||||
assertNotNull(receive);
|
||||
}
|
||||
|
||||
private void waitForSubscribe(String destination) throws InterruptedException {
|
||||
@@ -225,6 +253,7 @@ public class StompInboundChannelAdapterWebSocketIntegrationTests {
|
||||
WebSocketStompSessionManager webSocketStompSessionManager =
|
||||
new WebSocketStompSessionManager(stompClient, server().getWsBaseUrl() + "/ws");
|
||||
webSocketStompSessionManager.setAutoReceipt(true);
|
||||
webSocketStompSessionManager.setRecoveryInterval(1000);
|
||||
StompHeaders stompHeaders = new StompHeaders();
|
||||
stompHeaders.setHeartbeat(new long[] {10000, 10000});
|
||||
webSocketStompSessionManager.setConnectHeaders(stompHeaders);
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.springframework.integration.stomp.WebSocketStompSessionManager;
|
||||
import org.springframework.integration.stomp.event.StompExceptionEvent;
|
||||
import org.springframework.integration.stomp.event.StompIntegrationEvent;
|
||||
import org.springframework.integration.stomp.event.StompReceiptEvent;
|
||||
import org.springframework.integration.stomp.event.StompSessionConnectedEvent;
|
||||
import org.springframework.integration.test.support.LongRunningIntegrationTest;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.integration.websocket.TomcatWebSocketTestServer;
|
||||
@@ -103,10 +104,6 @@ public class StompMessageHandlerWebSocketIntegrationTests {
|
||||
@Value("#{server.serverContext}")
|
||||
private ApplicationContext serverContext;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("stompMessageHandler")
|
||||
private MessageHandler stompMessageHandler;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("webSocketOutputChannel")
|
||||
private MessageChannel webSocketOutputChannel;
|
||||
@@ -117,12 +114,6 @@ public class StompMessageHandlerWebSocketIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void testStompMessageHandler() throws InterruptedException {
|
||||
int n = 0;
|
||||
while (TestUtils.getPropertyValue(this.stompMessageHandler, "stompSession") == null && n++ < 100) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue(n < 100);
|
||||
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
|
||||
headers.setDestination("/app/simple");
|
||||
Message<String> message = MessageBuilder.withPayload("foo").setHeaders(headers).build();
|
||||
@@ -131,9 +122,13 @@ public class StompMessageHandlerWebSocketIntegrationTests {
|
||||
SimpleController controller = this.serverContext.getBean(SimpleController.class);
|
||||
assertTrue(controller.latch.await(10, TimeUnit.SECONDS));
|
||||
|
||||
// Simple Broker Relay doesn't support RECEIPT Frame, so we check here the 'lost' StompReceiptEvent
|
||||
Message<?> receive = this.stompEvents.receive(10000);
|
||||
assertNotNull(receive);
|
||||
assertThat(receive.getPayload(), instanceOf(StompSessionConnectedEvent.class));
|
||||
|
||||
// Simple Broker Relay doesn't support RECEIPT Frame, so we check here the 'lost' StompReceiptEvent
|
||||
receive = this.stompEvents.receive(10000);
|
||||
assertNotNull(receive);
|
||||
assertThat(receive.getPayload(), instanceOf(StompReceiptEvent.class));
|
||||
StompReceiptEvent stompReceiptEvent = (StompReceiptEvent) receive.getPayload();
|
||||
assertEquals(StompCommand.SEND, stompReceiptEvent.getStompCommand());
|
||||
@@ -195,13 +190,16 @@ public class StompMessageHandlerWebSocketIntegrationTests {
|
||||
WebSocketStompSessionManager webSocketStompSessionManager =
|
||||
new WebSocketStompSessionManager(stompClient, server().getWsBaseUrl() + "/ws");
|
||||
webSocketStompSessionManager.setAutoReceipt(true);
|
||||
webSocketStompSessionManager.setRecoveryInterval(1000);
|
||||
return webSocketStompSessionManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ServiceActivator(inputChannel = "webSocketOutputChannel")
|
||||
public MessageHandler stompMessageHandler(StompSessionManager stompSessionManager) {
|
||||
return new StompMessageHandler(stompSessionManager);
|
||||
StompMessageHandler stompMessageHandler = new StompMessageHandler(stompSessionManager);
|
||||
stompMessageHandler.setConnectTimeout(10000);
|
||||
return stompMessageHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -2,8 +2,8 @@ log4j.rootCategory=WARN, stdout
|
||||
|
||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d %c{1} [%t] : %m%n
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m%n
|
||||
|
||||
#log4j.category.org.springframework.messaging=DEBUG
|
||||
#log4j.category.org.springframework=DEBUG
|
||||
#log4j.category.org.springframework.integration=DEBUG
|
||||
log4j.category.org.springframework.integration.stomp=WARN
|
||||
|
||||
Reference in New Issue
Block a user