/* * Copyright 2014, The Sporting Exchange Limited * * 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 com.betfair.cougar.transport.socket; import com.betfair.cougar.api.DehydratedExecutionContext; import com.betfair.cougar.api.LoggableEvent; import com.betfair.cougar.core.api.ev.ConnectedResponse; import com.betfair.cougar.core.api.ev.ExecutionResult; import com.betfair.cougar.core.api.ev.OperationDefinition; import com.betfair.cougar.core.api.ev.Subscription; import com.betfair.cougar.core.api.exception.CougarFrameworkException; import com.betfair.cougar.core.api.logging.EventLogger; import com.betfair.cougar.core.impl.ev.ConnectedResponseImpl; import com.betfair.cougar.core.impl.ev.DefaultSubscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.betfair.cougar.netutil.nio.CougarProtocol; import com.betfair.cougar.netutil.nio.NioLogger; import com.betfair.cougar.netutil.nio.TerminateSubscription; import com.betfair.cougar.netutil.nio.connected.*; import com.betfair.cougar.test.ParameterizedMultiRunner; import com.betfair.cougar.transport.api.protocol.CougarObjectIOFactory; import com.betfair.cougar.transport.api.protocol.socket.NewHeapSubscription; import com.betfair.platform.virtualheap.HeapListener; import com.betfair.platform.virtualheap.MutableHeap; import com.betfair.platform.virtualheap.NodeType; import com.betfair.platform.virtualheap.ObservableHeap; import junit.framework.AssertionFailedError; import org.apache.mina.common.CloseFuture; import org.apache.mina.common.IoSession; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.util.*; import java.util.concurrent.BlockingDeque; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import static com.betfair.platform.virtualheap.projection.ProjectorFactory.objectProjector; import static junit.framework.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; @RunWith(value = ParameterizedMultiRunner.class) public class PooledServerConnectedObjectManagerTest { private static Logger LOGGER = LoggerFactory.getLogger(PooledServerConnectedObjectManagerTest.class); private PooledServerConnectedObjectManager subject; private ExpectingOutput cougarOutput; private int ioSessionId; private int numThreads; public PooledServerConnectedObjectManagerTest(int numThreads) { this.numThreads = numThreads; } @ParameterizedMultiRunner.Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{{1}, {2}}); } @BeforeClass public static void multiSetup() { ParameterizedMultiRunner.setNumRuns(Integer.parseInt(System.getProperty("connectedObjects.numTestRuns", "1"))); } @Before public void defaults() throws Exception { subject = new PooledServerConnectedObjectManager(); subject.setNumProcessingThreads(numThreads); subject.setNioLogger(new NioLogger("ALL")); CougarObjectIOFactory ioFactory; subject.setObjectIOFactory(ioFactory = mock(CougarObjectIOFactory.class)); subject.setEventLogger(new EventLogger() { @Override public void logEvent(LoggableEvent event) { } @Override public void logEvent(LoggableEvent loggableEvent, Object[] extensionFields) { } }); cougarOutput = new ExpectingOutput(1000L); doReturn(cougarOutput).when(ioFactory).newCougarObjectOutput(any(ByteArrayOutputStream.class),anyByte()); subject.start(); } @After public void after() { subject.stop(); } @Test public void firstSubscription() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); IoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("firstSubscription"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); ArgumentCaptor<ExecutionResult> resultCaptor = ArgumentCaptor.forClass(ExecutionResult.class); verify(commandProcessor).writeSuccessResponse(any(SocketTransportRPCCommand.class), resultCaptor.capture(), any(DehydratedExecutionContext.class)); ExecutionResult executionResult = resultCaptor.getValue(); assertTrue(executionResult.getResult() instanceof NewHeapSubscription); NewHeapSubscription response = (NewHeapSubscription) executionResult.getResult(); assertEquals(1, response.getHeapId()); assertEquals("firstSubscription", response.getUri()); } @Test public void secondSubscriptionToSameHeap() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("secondSubscription"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); ConnectedResponse subscriptionResult2 = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); cougarOutput.setExpectedUpdates(expectedUpdates); // 2 subs at about the same time, we're interested in the second.. subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); subject.addSubscription(commandProcessor, command, subscriptionResult2, operationDefinition, requestContext, null); ArgumentCaptor<ExecutionResult> resultCaptor = ArgumentCaptor.forClass(ExecutionResult.class); verify(commandProcessor, times(2)).writeSuccessResponse(any(SocketTransportRPCCommand.class), resultCaptor.capture(), any(DehydratedExecutionContext.class)); ExecutionResult executionResult = resultCaptor.getAllValues().get(1); assertTrue(executionResult.getResult() instanceof NewHeapSubscription); NewHeapSubscription response = (NewHeapSubscription) executionResult.getResult(); assertEquals(1, response.getHeapId()); assertNull(response.getUri()); // There should be only one subscription to the main heap assertEquals(1, getHeapListeners(heap).size()); assertExpectedUpdatesWritten(); assertExpectedSessionWrites(session, 1); } @Test public void twoSubscriptionsToDifferentHeaps() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); IoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); OperationDefinition operationDefinition = mock(OperationDefinition.class); // 2 subs at about the same time, we're interested in the second.. Subscription sub = mock(Subscription.class); subject.addSubscription(commandProcessor, command, new ConnectedResponseImpl(new MutableHeap("firstHeap"), sub), operationDefinition, requestContext, null); subject.addSubscription(commandProcessor, command, new ConnectedResponseImpl(new MutableHeap("secondHeap"), sub), operationDefinition, requestContext, null); ArgumentCaptor<ExecutionResult> resultCaptor = ArgumentCaptor.forClass(ExecutionResult.class); verify(commandProcessor, times(2)).writeSuccessResponse(any(SocketTransportRPCCommand.class), resultCaptor.capture(), any(DehydratedExecutionContext.class)); ExecutionResult executionResult0 = resultCaptor.getAllValues().get(0); assertTrue(executionResult0.getResult() instanceof NewHeapSubscription); NewHeapSubscription response0 = (NewHeapSubscription) executionResult0.getResult(); assertEquals(1, response0.getHeapId()); assertEquals("firstHeap", response0.getUri()); ExecutionResult executionResult1 = resultCaptor.getAllValues().get(1); assertTrue(executionResult1.getResult() instanceof NewHeapSubscription); NewHeapSubscription response1 = (NewHeapSubscription) executionResult1.getResult(); assertEquals(2, response1.getHeapId()); assertEquals("secondHeap", response1.getUri()); } @Test public void subscribeToTerminatedHeap() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); IoSession session = new MyIoSession("1"); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("subscribeToTerminatedHeap"); heap.beginUpdate(); heap.terminateHeap(); heap.endUpdate(); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); verify(commandProcessor).writeErrorResponse(any(SocketTransportCommand.class), any(DehydratedExecutionContext.class), any(CougarFrameworkException.class), eq(true)); assertNull(subject.getHeapsByClient().get(session)); assertEquals(0, subject.getHeapStates().size()); verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void secondSubscribeToTerminatedHeap() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); IoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("subscribeToTerminatedHeap"); Subscription sub1 = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub1); OperationDefinition operationDefinition = mock(OperationDefinition.class); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); verify(commandProcessor).writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class)); heap.beginUpdate(); heap.terminateHeap(); heap.endUpdate(); Subscription sub2 = mock(Subscription.class); subscriptionResult = new ConnectedResponseImpl(heap, sub2); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); verify(commandProcessor).writeErrorResponse(any(SocketTransportCommand.class), any(DehydratedExecutionContext.class), any(CougarFrameworkException.class), eq(true)); assertNull(subject.getHeapsByClient().get(session)); assertEquals(0, subject.getHeapStates().size()); // sub should have been closed verify(sub1).close(Subscription.CloseReason.REQUESTED_BY_PUBLISHER); verify(sub2, never()).close(); verify(sub2, never()).close(any(Subscription.CloseReason.class)); } @Test public void basicUpdate() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("basicUpdate"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void basicMultiUpdate() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("basicMultiUpdate"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1), new SetScalar(1, 2), new SetScalar(1, 3))); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); heap.beginUpdate(); object.value().set(2); heap.endUpdate(); heap.beginUpdate(); object.value().set(3); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void basicUpdateToTwoSessions() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("basicUpdateToTwoSessions"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); MyIoSession session2 = new MyIoSession(String.valueOf(ioSessionId++)); session2.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session2); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); assertExpectedUpdatesWritten(); assertEquals(3, cougarOutput.getAllValues().size()); assertExpectedSessionWrites(session, 2); assertExpectedSessionWrites(session2, 2); verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void basicMultiUpdateToTwoSessions() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); ArgumentCaptor<ExecutionResult> resultCaptor = ArgumentCaptor.forClass(ExecutionResult.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), resultCaptor.capture(), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("basicMultiUpdateToTwoSessions"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1), new SetScalar(1, 2), new SetScalar(1, 3))); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); MyIoSession session2 = new MyIoSession(String.valueOf(ioSessionId++)); session2.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session2); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(3); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 2; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); assertExpectedSessionWrites(session2, updatesWritten + 1); verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void addSubscriptionMidStream() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("addSubscriptionMidStream"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); expectedUpdates.add(createUpdate(new SetScalar(1, 2), new SetScalar(1, 3))); cougarOutput.setExpectedUpdates(expectedUpdates); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(3); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void addSecondSubscriptionMidStream() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("addSecondSubscriptionMidStream"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); expectedUpdates.add(createUpdate(new SetScalar(1, 2))); // new initial for new sub expectedUpdates.add(createInitial(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 2))); expectedUpdates.add(createUpdate(new SetScalar(1, 3))); cougarOutput.setExpectedUpdates(expectedUpdates); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); MyIoSession session2 = new MyIoSession(String.valueOf(ioSessionId++)); session2.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session2); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(3); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 2; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); assertExpectedSessionWrites(session2, updatesWritten); // +1 for initial update, but -1 for the one this doesn't see verify(sub, never()).close(); verify(sub, never()).close(any(Subscription.CloseReason.class)); } @Test public void heapTerminationMidStream() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("heapTerminationMidStream"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1), new SetScalar(1, 2), new TerminateHeap())); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); // we need this later to ensure that all the work associated with the termination is done... Lock heapStateLock = subject.getHeapStates().get("heapTerminationMidStream").getUpdateLock(); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); heap.beginUpdate(); heap.terminateHeap(); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); // if we've locked and unlocked, then it's safe to query stuff affected within the lock.. try { heapStateLock.lock(); } finally { heapStateLock.unlock(); } assertNull(subject.getHeapsByClient().get(session)); assertNull(subject.getHeapStates().get("heapTerminationMidStream")); // sub should have been closed verify(sub).close(Subscription.CloseReason.REQUESTED_BY_PUBLISHER); } @Test public void sessionClosed() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)) { // In practice, PSCOMT gets registered as a handler listener and // thus gets notified automatically when a session is closed // Doing it here explicitly for the purpose of the test @Override public CloseFuture close() { subject.sessionClosed(this); return super.close(); } }; session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("sessionClosed"); Subscription subscription = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, subscription); OperationDefinition operationDefinition = mock(OperationDefinition.class); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); session.close(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(3); heap.endUpdate(); assertNull(subject.getHeapsByClient().get(session)); assertNull(subject.getHeapStates().get("sessionClosed")); // sub should have been closed as it was the last sub verify(subscription).close(Subscription.CloseReason.CONNECTION_CLOSED); // There should be no listeners on the main heap assertEquals(0, getHeapListeners(heap).size()); } @Test public void oneOfTwoSessionsClosed() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("oneOfTwoSessionsClosed"); Subscription subscription1 = mock(Subscription.class); Subscription subscription2 = mock(Subscription.class); ConnectedResponse subscriptionResult1 = new ConnectedResponseImpl(heap, subscription1); ConnectedResponse subscriptionResult2 = new ConnectedResponseImpl(heap, subscription2); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1), new SetScalar(1, 2), new SetScalar(1, 3))); cougarOutput.setExpectedUpdates(expectedUpdates); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); subject.addSubscription(commandProcessor, command, subscriptionResult1, operationDefinition, requestContext, null); MyIoSession session2 = new MyIoSession(String.valueOf(ioSessionId++)); session2.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session2); subject.addSubscription(commandProcessor, command, subscriptionResult2, operationDefinition, requestContext, null); // make sure initial sub goes through.. session.awaitWrite(1, 2000L); session2.awaitWrite(1, 2000L); // we need this later to ensure that all the work associated with the termination is done... Lock heapStateLock = subject.getHeapStates().get("oneOfTwoSessionsClosed").getUpdateLock(); // and has been fully processed try { heapStateLock.lock(); } finally { heapStateLock.unlock(); } heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); session.close(); subject.sessionClosed(session); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(3); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 2; // +1 to include the initial update for that session assertExpectedSessionWrites(session2, updatesWritten + 1); assertNull(subject.getHeapsByClient().get(session)); assertEquals(1, subject.getHeapsByClient().get(session2).keySet().size()); assertEquals(1, subject.getHeapStates().get("oneOfTwoSessionsClosed").getSessions().size()); assertEquals(0, subject.getHeapStates().get("oneOfTwoSessionsClosed").getQueuedChanges().size()); verify(subscription1).close(Subscription.CloseReason.CONNECTION_CLOSED); verify(subscription2, never()).close(); verify(subscription2, never()).close(any(Subscription.CloseReason.class)); } @Test public void exceptionInPusher() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("exceptionInPusher"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); // this last one we'll see on the cougarOutput, but not on the session write (since it's gonna fail) expectedUpdates.add(createUpdate(new SetScalar(1, 2))); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); // we need this later to ensure that all the work associated with the termination is done... Lock heapStateLock = subject.getHeapStates().get("exceptionInPusher").getUpdateLock(); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); session.awaitWrite(2, 2000L); // ensures we've had something through after the initial update session.throwExceptionOnNextWrite(); heap.beginUpdate(); object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(2); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 2; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); // make sure all the work is complete! try { heapStateLock.lock(); } finally { heapStateLock.unlock(); } assertNull(subject.getHeapsByClient().get(session)); assertNull(subject.getHeapStates().get("exceptionInPusher")); verify(sub).close(Subscription.CloseReason.INTERNAL_ERROR); } @Test public void secondSubscriptionClosedByPublisher() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createInitial()); cougarOutput.setExpectedUpdates(expectedUpdates); OperationDefinition operationDefinition = mock(OperationDefinition.class); MutableHeap heap = new MutableHeap("secondSubscriptionClosedByPublisher"); Subscription sub1 = mock(Subscription.class); ConnectedResponse subscriptionResult1 = new ConnectedResponseImpl(heap, sub1); subject.addSubscription(commandProcessor, command, subscriptionResult1, operationDefinition, requestContext, null); Subscription sub2 = new DefaultSubscription(); ConnectedResponse subscriptionResult2 = new ConnectedResponseImpl(heap, sub2); subject.addSubscription(commandProcessor, command, subscriptionResult2, operationDefinition, requestContext, null); String subscriptionId2 = getSubscriptionId(subject.getHeapStates().get("secondSubscriptionClosedByPublisher"), sub2); cougarOutput.getExpectedSubTerminations().add(new TerminateSubscription(1, subscriptionId2, Subscription.CloseReason.REQUESTED_BY_PUBLISHER.name())); session.awaitWrite(1, 2000L); // ensures we've had the initial updates through sub2.close(Subscription.CloseReason.REQUESTED_BY_PUBLISHER); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); verify(sub1, never()).close(); verify(sub1, never()).close(any(Subscription.CloseReason.class)); } @Test public void lastSubscriptionClosedByPublisher() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("lastSubscriptionClosedByPublisher"); Subscription sub = new DefaultSubscription(); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); String subscriptionId = getSubscriptionId(subject.getHeapStates().get("lastSubscriptionClosedByPublisher"), sub); cougarOutput.getExpectedSubTerminations().add(new TerminateSubscription(1, subscriptionId, Subscription.CloseReason.REQUESTED_BY_PUBLISHER.name())); session.awaitWrite(1, 2000L); // ensures we've had the initial update through sub.close(Subscription.CloseReason.REQUESTED_BY_PUBLISHER); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); assertNull(subject.getHeapsByClient().get(session)); assertNull(subject.getHeapStates().get("lastSubscriptionClosedByPublisher")); } @Test public void subscriptionCloseNotificationFails() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("subscriptionCloseNotificationFails"); Subscription sub = new DefaultSubscription(); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); String subscriptionId = getSubscriptionId(subject.getHeapStates().get("subscriptionCloseNotificationFails"), sub); cougarOutput.getExpectedSubTerminations().add(new TerminateSubscription(1, subscriptionId, Subscription.CloseReason.REQUESTED_BY_PUBLISHER.name())); session.awaitWrite(1, 2000L); // ensures we've had the initial update through session.throwExceptionOnNextWrite(); sub.close(Subscription.CloseReason.REQUESTED_BY_PUBLISHER); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten); assertNull(subject.getHeapsByClient().get(session)); assertNull(subject.getHeapStates().get("subscriptionCloseNotificationFails")); } @Test public void secondSubscriptionClosedBySubscriber() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); cougarOutput.setExpectedUpdates(expectedUpdates); OperationDefinition operationDefinition = mock(OperationDefinition.class); MutableHeap heap = new MutableHeap("secondSubscriptionClosedBySubscriber"); Subscription sub1 = mock(Subscription.class); ConnectedResponse subscriptionResult1 = new ConnectedResponseImpl(heap, sub1); subject.addSubscription(commandProcessor, command, subscriptionResult1, operationDefinition, requestContext, null); Subscription sub2 = mock(Subscription.class); ConnectedResponse subscriptionResult2 = new ConnectedResponseImpl(heap, sub2); subject.addSubscription(commandProcessor, command, subscriptionResult2, operationDefinition, requestContext, null); String subscriptionId2 = getSubscriptionId(subject.getHeapStates().get("secondSubscriptionClosedBySubscriber"), sub2); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); subject.terminateSubscription(session, new TerminateSubscription(1, subscriptionId2, Subscription.CloseReason.REQUESTED_BY_SUBSCRIBER.name())); verify(sub1, never()).close(); verify(sub1, never()).close(any(Subscription.CloseReason.class)); verify(sub2).close(Subscription.CloseReason.REQUESTED_BY_SUBSCRIBER); } @Test public void lastSubscriptionClosedBySubscriber() throws Exception { SocketTransportCommandProcessor commandProcessor = mock(SocketTransportCommandProcessor.class); when(commandProcessor.writeSuccessResponse(any(SocketTransportRPCCommand.class), any(ExecutionResult.class), any(DehydratedExecutionContext.class))).thenReturn(true); SocketTransportRPCCommand command = mock(SocketTransportRPCCommand.class); MyIoSession session = new MyIoSession(String.valueOf(ioSessionId++)); session.setAttribute(CougarProtocol.PROTOCOL_VERSION_ATTR_NAME, CougarProtocol.TRANSPORT_PROTOCOL_VERSION_MAX_SUPPORTED); when(command.getSession()).thenReturn(session); DehydratedExecutionContext requestContext = mock(DehydratedExecutionContext.class); MutableHeap heap = new MutableHeap("lastSubscriptionClosedBySubscriber"); Subscription sub = mock(Subscription.class); ConnectedResponse subscriptionResult = new ConnectedResponseImpl(heap, sub); OperationDefinition operationDefinition = mock(OperationDefinition.class); List<Update> expectedUpdates = new ArrayList<Update>(); expectedUpdates.add(createInitial()); expectedUpdates.add(createUpdate(new InstallRoot(0, NodeType.OBJECT), new InstallField(0, 1, "value", NodeType.SCALAR), new SetScalar(1, 1))); cougarOutput.setExpectedUpdates(expectedUpdates); subject.addSubscription(commandProcessor, command, subscriptionResult, operationDefinition, requestContext, null); String subscriptionId = getSubscriptionId(subject.getHeapStates().get("lastSubscriptionClosedBySubscriber"), sub); heap.beginUpdate(); SimpleConnectedObject object = objectProjector(SimpleConnectedObject.class).project(heap.ensureRoot(NodeType.OBJECT)); object.value().set(1); heap.endUpdate(); assertExpectedUpdatesWritten(); // might be related to the optimisation whereby if we need to send the same message to multiple clients we serialise it only once int updatesWritten = cougarOutput.getAllValues().size() - 1; // +1 to include the initial update for that session assertExpectedSessionWrites(session, updatesWritten + 1); subject.terminateSubscription(session, new TerminateSubscription(1, subscriptionId, Subscription.CloseReason.REQUESTED_BY_SUBSCRIBER.name())); verify(sub).close(Subscription.CloseReason.REQUESTED_BY_SUBSCRIBER); assertNull(subject.getHeapsByClient().get(session)); assertNull(subject.getHeapStates().get("lastSubscriptionClosedBySubscriber")); assertEquals(0, getHeapListeners(heap).size()); } private String getSubscriptionId(PooledServerConnectedObjectManager.HeapState heapState, Subscription sub) { Map<String, PooledServerConnectedObjectManager.HeapState.SubscriptionDetails> subs = heapState.getSubscriptions(); String subscriptionId = null; for (String id : subs.keySet()) { PooledServerConnectedObjectManager.HeapState.SubscriptionDetails sd = subs.get(id); if (sd.subscription == sub) { subscriptionId = id; } } return subscriptionId; } private void assertExpectedUpdatesWritten() throws InterruptedException { final AtomicReference<String> failureText = new AtomicReference<String>(); cougarOutput.addListener(new ExpectingOutput.ExpectingOutputListener() { @Override public void failure(String s) { failureText.set(s); } @Override public void complete() { } }); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Starting wait for expected updates"); } cougarOutput.start(); BlockingDeque queue = subject.getHeapsWaitingForUpdate(); while (!queue.isEmpty()) { Thread.sleep(10); } // queue empty, now check the heap stats boolean allDone = false; while (!allDone) { allDone = true; for (PooledServerConnectedObjectManager.HeapState heapState : subject.getHeapStates().values()) { Lock lock = heapState.getUpdateLock(); lock.lock(); try { if (!heapState.getQueuedChanges().isEmpty()) { allDone = false; break; } } finally { lock.unlock(); } } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("CougarObjectOutput.writeObject():"); for (Object o : new ArrayList<Object>(cougarOutput.getAllValues())) { LOGGER.debug(String.valueOf(o)); } } if (failureText.get() != null) { fail(failureText.get()); } // add in checks for the terminate subs assertEquals(cougarOutput.getExpectedSubTerminations(), cougarOutput.getSubTerminations()); } private void assertExpectedSessionWrites(MyIoSession session, int writes) throws InterruptedException { InterruptedException ie = null; AssertionFailedError afe = null; try { session.awaitWrite(writes, 2000L); } catch (InterruptedException ie1) { ie = ie1; } catch (AssertionFailedError afe1) { afe = afe1; } if (ie != null || afe != null || writes != session.getWritten().size()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("session(" + session.getSessionId() + ").write():"); for (Object o : session.getWritten()) { LOGGER.debug(String.valueOf(o)); } } if (ie != null) { throw ie; } if (afe != null) { throw afe; } } assertEquals(writes, session.getWritten().size()); } private InitialUpdate createInitial(UpdateAction... actions) { Update u = new Update(); u.setActions(createActionsList(actions)); return new InitialUpdate(u); } private Update createUpdate(UpdateAction... actions) { Update u = new Update(); u.setActions(createActionsList(actions)); return u; } private List<UpdateAction> createActionsList(UpdateAction... actions) { // not using this as it gives a list we can't call addAll() on //return Arrays.asList(actions); List<UpdateAction> ret = new ArrayList<UpdateAction>(); Collections.addAll(ret, actions); return ret; } private Set<HeapListener> getHeapListeners(MutableHeap heap) throws Exception { Field listeners = ObservableHeap.class.getDeclaredField("listeners"); listeners.setAccessible(true); return (Set<HeapListener>) listeners.get(heap); } }