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:
Artem Bilan
2015-11-07 23:31:56 -05:00
committed by Gary Russell
parent f1bd6e3bac
commit b9730cd183
14 changed files with 426 additions and 77 deletions

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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