/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * 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.jboss.errai.bus.server; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import org.jboss.errai.bus.client.api.QueueSession; import org.jboss.errai.bus.client.api.RoutingFlag; import org.jboss.errai.bus.client.api.base.MessageBuilder; import org.jboss.errai.bus.client.api.messaging.Message; import org.jboss.errai.bus.client.framework.BuiltInServices; import org.jboss.errai.bus.client.protocols.BusCommand; import org.jboss.errai.bus.server.api.MessageQueue; import org.jboss.errai.bus.server.api.ServerMessageBus; import org.jboss.errai.bus.server.io.MessageDeliveryHandler; import org.jboss.errai.bus.server.service.ErraiService; import org.jboss.errai.common.client.protocols.MessageParts; import org.jboss.errai.marshalling.server.MappingContextSingleton; import junit.framework.TestCase; /** * @author Mike Brock */ public class ClusteringTests extends TestCase { private final List<ErraiService<?>> startedInstances = new ArrayList<>(); private final AtomicInteger counter = new AtomicInteger(0); private ErraiService<?> startInstance() { final ErraiService<?> newService = InVMBusUtil.startService(counter.incrementAndGet()); startedInstances.add(newService); return newService; } @Override protected void setUp() throws Exception { MappingContextSingleton.get(); } @Override protected void tearDown() throws Exception { super.tearDown(); startedInstances.forEach(svc -> svc.stopService()); startedInstances.clear(); } public void testGlobalMessageInCluster() throws Exception { final ErraiService<?> nodeA = startInstance(); final ErraiService<?> nodeB = startInstance(); final QueueSession sessionA = MockQueueSessionFactory.newSession("client1"); final QueueSession sessionB = MockQueueSessionFactory.newSession("client2"); final QueueSession broadCastBlockingSession = MockQueueSessionFactory.newSession("dummy"); associateQueueSessionToBus(sessionA, nodeA.getBus()); associateQueueSessionToBus(sessionB, nodeB.getBus()); // Prevents broadcasting of messages so that they can be intercepted associateQueueSessionToBus(broadCastBlockingSession, nodeA.getBus()); associateQueueSessionToBus(broadCastBlockingSession, nodeB.getBus()); final Set<String> resultsSet = new HashSet<>(); final String localService = "localTest"; final CountDownLatch latch = new CountDownLatch(2); remoteSubscibeToTopic(sessionA, nodeA.getBus(), localService); remoteSubscibeToTopic(sessionB, nodeB.getBus(), localService); mockTransportWithAction(nodeA.getBus(), sessionA, msg -> { if (localService.equals(msg.getSubject())) { final String val = "Client 1:" + msg.get(String.class, MessageParts.Value); if (resultsSet.add(val)) { latch.countDown(); } else { fail("Received duplicate message to Client 1."); } } }); mockTransportWithAction(nodeB.getBus(), sessionB, msg -> { if (localService.equals(msg.getSubject())) { final String val = "Client 2:" + msg.get(String.class, MessageParts.Value); if (resultsSet.add(val)) { latch.countDown(); } else { fail("Received duplicate message to Client 2."); } } }); MessageBuilder.createMessage() .toSubject(localService) .signalling() .withValue("MSG") .noErrorHandling() .sendGlobalWith(nodeA.getBus()); assertTrue("Timeout while waiting for messages from servers. Received: " + resultsSet, latch.await(30, TimeUnit.SECONDS)); assertEquals(new HashSet<>(Arrays.asList("Client 1:MSG", "Client 2:MSG")), resultsSet); } public void testPointToPointMessageInCluster() throws Exception { final ErraiService<?> nodeA = startInstance(); final ErraiService<?> nodeB = startInstance(); final QueueSession sessionA = MockQueueSessionFactory.newSession("client1"); final QueueSession sessionB = MockQueueSessionFactory.newSession("client2"); final QueueSession broadCastBlockingSession = MockQueueSessionFactory.newSession("dummy"); associateQueueSessionToBus(sessionA, nodeA.getBus()); associateQueueSessionToBus(sessionB, nodeB.getBus()); // Prevents broadcasting of messages so that they can be intercepted associateQueueSessionToBus(broadCastBlockingSession, nodeA.getBus()); associateQueueSessionToBus(broadCastBlockingSession, nodeB.getBus()); final Set<String> resultsSet = new HashSet<>(); final String localService = "localTest"; final CountDownLatch latch = new CountDownLatch(2); remoteSubscibeToTopic(sessionA, nodeA.getBus(), localService); remoteSubscibeToTopic(sessionB, nodeB.getBus(), localService); mockTransportWithAction(nodeA.getBus(), sessionA, msg -> { if (localService.equals(msg.getSubject())) { final String val = "Client 1:" + msg.get(String.class, MessageParts.Value); if (resultsSet.add(val)) { latch.countDown(); } else { fail("Received duplicate message to Client 1."); } } }); mockTransportWithAction(nodeB.getBus(), sessionB, msg -> { if (localService.equals(msg.getSubject())) { final String val = "Client 2:" + msg.get(String.class, MessageParts.Value); if (resultsSet.add(val)) { latch.countDown(); } else { fail("Received duplicate message to Client 2."); } } }); MessageBuilder.createMessage() .toSubject(localService) .signalling() .withValue("ServerA") .with(MessageParts.SessionID, sessionB.getSessionId()) .noErrorHandling() .sendNowWith(nodeA.getBus()); MessageBuilder.createMessage() .toSubject(localService) .signalling() .withValue("ServerB") .with(MessageParts.SessionID, sessionA.getSessionId()) .noErrorHandling() .sendNowWith(nodeB.getBus()); assertTrue("Timeout while waiting for messages from servers. Received: " + resultsSet, latch.await(30, TimeUnit.SECONDS)); assertEquals(new HashSet<>(Arrays.asList("Client 1:ServerB", "Client 2:ServerA")), resultsSet); } public void testPointToPointMessageInClusterAfterClientChangesNodes() throws Exception { final ErraiService<?> nodeA = startInstance(); final ErraiService<?> nodeB = startInstance(); final ErraiService<?> nodeC = startInstance(); final QueueSession session = MockQueueSessionFactory.newSession("client1"); final QueueSession controlSession = MockQueueSessionFactory.newSession("control"); final QueueSession broadCastBlockingSession = MockQueueSessionFactory.newSession("dummy"); associateQueueSessionToBus(session, nodeA.getBus()); // Listens for sent topic to control that it is not being broadcast associateQueueSessionToBus(controlSession, nodeA.getBus()); associateQueueSessionToBus(controlSession, nodeB.getBus()); associateQueueSessionToBus(controlSession, nodeC.getBus()); // Prevents broadcasting of messages so that they can be intercepted associateQueueSessionToBus(broadCastBlockingSession, nodeA.getBus()); associateQueueSessionToBus(broadCastBlockingSession, nodeB.getBus()); associateQueueSessionToBus(broadCastBlockingSession, nodeC.getBus()); final List<String> results = new ArrayList<>(); final String localService = "localTest"; final CountDownLatch latch1 = new CountDownLatch(1); remoteSubscibeToTopic(session, nodeA.getBus(), localService); mockTransportWithAction(nodeA.getBus(), session, msg -> { if (localService.equals(msg.getSubject())) { final String val = "Client:" + msg.get(String.class, MessageParts.Value); results.add(val); latch1.countDown(); } }); final Consumer<Message> failAction = msg -> { if (localService.equals(msg.getSubject())) { fail("Received message to wrong client session."); } }; mockTransportWithAction(nodeA.getBus(), controlSession, failAction); mockTransportWithAction(nodeB.getBus(), controlSession, failAction); mockTransportWithAction(nodeC.getBus(), controlSession, failAction); MessageBuilder.createMessage() .toSubject(localService) .signalling() .withValue("ServerA") .with(MessageParts.SessionID, session.getSessionId()) .noErrorHandling() .sendNowWith(nodeC.getBus()); assertTrue("Timeout while waiting for first message from servers. Received: " + results, latch1.await(30, TimeUnit.SECONDS)); assertEquals(Arrays.asList("Client:ServerA"), results); final CountDownLatch latch2 = new CountDownLatch(1); // Change bus and resubscribe to topic with new bus associateToNewBus(session, nodeA.getBus(), nodeB.getBus()); remoteSubscibeToTopic(session, nodeB.getBus(), localService); mockTransportWithAction(nodeB.getBus(), session, msg -> { if (localService.equals(msg.getSubject())) { final String val = "Client:" + msg.get(String.class, MessageParts.Value); results.add(val); latch2.countDown(); } }); MessageBuilder.createMessage() .toSubject(localService) .signalling() .withValue("ServerB") .with(MessageParts.SessionID, session.getSessionId()) .noErrorHandling() .sendNowWith(nodeC.getBus()); assertTrue("Timeout while waiting for messages from servers. Received: " + results, latch2.await(30, TimeUnit.SECONDS)); assertEquals(Arrays.asList("Client:ServerA", "Client:ServerB"), results); } /* * When a client switches servers in a cluster, it must associate with the new bus. This tests that * a message from an unassociated client is rejected with a QueueUnavailableException. Because * all Errai servlets catch this exception and send a disconnect, this behaviour ensures that * clients re-associate when a load-balancer switches them to a new server. */ public void testBusThrowsQueueUnavailableExceptionForMessageFromClientThatHasNotAssociated() throws Exception { final ErraiService<?> node = startInstance(); final QueueSession session = MockQueueSessionFactory.newSession("client1"); final List<Message> received = new ArrayList<>(); final String service = "service"; node.getBus().subscribe(service, msg -> received.add(msg)); final Message msg = MessageBuilder .createMessage(service) .signalling() .noErrorHandling() .getMessage() .setFlag(RoutingFlag.FromRemote) .setResource("Session", session) .setResource("SessionID", session); try { node.getBus().sendGlobal(msg); fail("No exception was thrown after message sent from unassociated bus!"); } catch (final QueueUnavailableException ex) { // success } catch (final AssertionError ae) { throw ae; } catch (final Throwable t) { throw new AssertionError("Unexpected error after sending message from unassociated client.", t); } assertTrue(received.isEmpty()); associateQueueSessionToBus(session, node.getBus()); try { node.getBus().sendGlobal(msg); assertEquals("received = " + received, 1, received.size()); assertSame(msg, received.get(0)); } catch (final QueueUnavailableException ex) { fail("QueueUnavailableException thrown after associating."); } catch (final AssertionError ae) { throw ae; } catch (final Throwable t) { throw new AssertionError("Unexpected error after sending message from associated client.", t); } } private void associateToNewBus(final QueueSession session, final ServerMessageBus oldBus, final ServerMessageBus newBus) { final Message disconnectMsg = MessageBuilder .createMessage(BuiltInServices.ServerBus.name()) .command(BusCommand.Disconnect) .noErrorHandling() .getMessage() .setResource("Session", session) .setResource("SessionID", session.getSessionId()) .setFlag(RoutingFlag.FromRemote); oldBus.sendGlobal(disconnectMsg); associateQueueSessionToBus(session, newBus); } private void remoteSubscibeToTopic(final QueueSession session, final ServerMessageBus bus, final String subject) { final Message msg = MessageBuilder .createMessage(BuiltInServices.ServerBus.name()) .command(BusCommand.RemoteSubscribe) .with(MessageParts.Subject, subject) .noErrorHandling() .getMessage() .setResource("Session", session) .setResource("SessionID", session.getSessionId()) .setFlag(RoutingFlag.FromRemote); bus.sendGlobal(msg); } private void associateQueueSessionToBus(final QueueSession qs, final ServerMessageBus bus) { final Message msg = MessageBuilder .createMessage(BuiltInServices.ServerBus.name()) .command(BusCommand.Associate) .with(MessageParts.RemoteServices, "ClientBus") .with(MessageParts.PriorityProcessing, 1) .noErrorHandling() .getMessage() .setResource("Session", qs) .setResource("SessionID", qs.getSessionId()) .setFlag(RoutingFlag.FromRemote); bus.sendGlobal(msg); } private void mockTransportWithAction(final ServerMessageBus bus, final QueueSession qs, final Consumer<Message> action) { bus.getQueue(qs).setDeliveryHandler(new MessageDeliveryHandler() { @Override public void noop(final MessageQueue queue) throws IOException {} @Override public boolean deliver(final MessageQueue queue, final Message message) throws IOException { action.accept(message); return true; } }); } }