package alma.acs.nc.testsupport; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx; import alma.acs.container.ContainerServicesBase; import alma.acs.logging.testsupport.JUnit4StandaloneTestBase; import alma.acs.nc.AcsEventPublisher; import alma.acs.nc.AcsEventSubscriber; import alma.acs.nc.AcsEventSubscriber.GenericCallback; import alma.acs.nc.AcsEventSubscriberImplBase; import alma.acs.util.StopWatch; import alma.acsnc.EventDescription; public class InMemoryNcTest extends JUnit4StandaloneTestBase { private ContainerServicesBase services; //////////////////////////////////////////////////////////////////////////////////////// /////////////////////////// Dummy data and receivers /////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// /** * Event base type, corresponds to IDLEntity in the world of IDL-defined event structs. */ private static class TestEventTypeBase { // some data.. } private static class TestEventType1 extends TestEventTypeBase { } private static class TestEventType2 extends TestEventTypeBase { } private static class ReceivedEventInfo { Object eventData; EventDescription eventDesc; CollectingReceiver receiver; ReceivedEventInfo(Object eventData, EventDescription eventDesc, CollectingReceiver receiver) { this.eventData = eventData; this.eventDesc = eventDesc; this.receiver = receiver; } } /** * Meant to be used as a single instance, to which multiple receivers dump their data * so that the unit test can see all events together. */ private static interface EventCollector { void storeEvent(TestEventTypeBase eventData, EventDescription eventDescrip, CollectingReceiver receiver); } /** * Collects the events received by one or more test receivers, * for later verification in the tests. */ private static class StoringEventCollector implements EventCollector { /** * Even with a ConcurrentLinkedQueue and no 'synchronized' instead of a synchronized ArrayList we would get warnings about slow receiver, e.g. * "WARNING [testThroughput] More events came in from the NC than the receiver processed. eventName= alma.acs.nc.testsupport.InMemoryNcTest$TestEventType1; numEventsDiscarded=0; logOcurrencesNumber=1. */ private final List<ReceivedEventInfo> collectedEvents = new ArrayList<ReceivedEventInfo>(); @Override public synchronized void storeEvent(TestEventTypeBase eventData, EventDescription eventDescrip, CollectingReceiver receiver) { collectedEvents.add(new ReceivedEventInfo(eventData, eventDescrip, receiver)); // System.out.println("Got an event of type " + eventData.getClass().getSimpleName() + " from receiver " + receiver); // new Exception().printStackTrace(); } synchronized List<ReceivedEventInfo> getAndClearEvents() { List<ReceivedEventInfo> ret = new ArrayList<ReceivedEventInfo>(collectedEvents); collectedEvents.clear(); return ret; } } /** * Collector that allows publishers to wait until a batch of events has been collected. * @see InMemoryNcTest#testConcurrentUse() */ private static class SyncingEventCollector implements EventCollector { final CyclicBarrier sync; private final int numEventsToReceivePerBatch; private final int numEventsToReceiveTotal; private long numEventsReceivedTotal; private long numEventsReceivedInBatch; SyncingEventCollector(int numPublishers, int numEventsToReceivePerBatch, int numEventsToReceiveTotal) { this.numEventsToReceivePerBatch = numEventsToReceivePerBatch; this.numEventsToReceiveTotal = numEventsToReceiveTotal; sync = new CyclicBarrier(numPublishers+1); } @Override public synchronized void storeEvent(TestEventTypeBase eventData, EventDescription eventDescrip, CollectingReceiver receiver) { numEventsReceivedTotal++; numEventsReceivedInBatch++; if (numEventsReceivedInBatch == numEventsToReceivePerBatch || numEventsReceivedTotal == numEventsToReceiveTotal) { // logger.fine("Received " + numEventsReceivedTotal + ", releasing the waiting publishers..."); try { sync.await(1, TimeUnit.MINUTES); if (numEventsReceivedTotal == numEventsToReceiveTotal) { sync.await(1, TimeUnit.MINUTES); // extra call when totally done, as expected by publishers } } catch (Exception ex) { ex.printStackTrace(); } numEventsReceivedInBatch = 0; } } /** * To be called by publishers */ void awaitEventBatchReception() throws Exception { sync.await(1, TimeUnit.MINUTES); // timeout 1 min just to protect failing test from hanging forever } synchronized long getNumEventsReceivedTotal() { return numEventsReceivedTotal; } } private static class CollectingReceiver { protected final EventCollector collector; CollectingReceiver(EventCollector collector) { this.collector = collector; } } /** * This receiver makes no sense in the analogy of IDL structs where * no event is directly of type IDLEntity and structs don't have inheritance. * However it may be possible to publish instantiated event base classes in the future * with other pub-sub frameworks. */ private static class TestEventReceiverBase extends CollectingReceiver implements AcsEventSubscriber.Callback<TestEventTypeBase> { TestEventReceiverBase(EventCollector collector) { super(collector); } @Override public void receive(TestEventTypeBase eventData, EventDescription eventDescrip) { collector.storeEvent(eventData, eventDescrip, this); } @Override public Class<TestEventTypeBase> getEventType() { return TestEventTypeBase.class; } } private static class TestEventReceiver1 extends CollectingReceiver implements AcsEventSubscriber.Callback<TestEventType1> { TestEventReceiver1(EventCollector collector) { super(collector); } @Override public void receive(TestEventType1 eventData, EventDescription eventDescrip) { collector.storeEvent(eventData, eventDescrip, this); } @Override public Class<TestEventType1> getEventType() { return TestEventType1.class; } } private static class TestEventReceiver2 extends CollectingReceiver implements AcsEventSubscriber.Callback<TestEventType2>, GenericCallback { TestEventReceiver2(EventCollector collector) { super(collector); } @Override public void receive(TestEventType2 eventData, EventDescription eventDescrip) { collector.storeEvent(eventData, eventDescrip, this); } @Override public Class<TestEventType2> getEventType() { return TestEventType2.class; } @Override public void receiveGeneric(Object eventData, EventDescription eventDescrip) { collector.storeEvent((TestEventTypeBase)eventData, eventDescrip, this); } } //////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// Test methods ///////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////// @Before public void setUp() throws Exception { super.setUp(); services = new DummyContainerServicesBase(testMethodName, logger); } @After public void tearDown() throws Exception { super.tearDown(); } /** * Tests that events get delivered correctly, * using various typed and generic subscriptions. */ @Test public void testSubscriptions() throws Exception { InMemoryNcFake nc = new InMemoryNcFake(services, "myTestChannel"); // create subscribers and register typed receivers AcsEventSubscriber<TestEventType1> sub1 = nc.createSubscriber("myTestSubscriber1", TestEventType1.class); sub1.startReceivingEvents(); AcsEventSubscriber<TestEventType2> sub2 = nc.createSubscriber("myTestSubscriber2", TestEventType2.class); sub2.startReceivingEvents(); AcsEventSubscriber<TestEventTypeBase> subBase = nc.createSubscriber("myTestSubscriberBase", TestEventTypeBase.class); subBase.startReceivingEvents(); StoringEventCollector eventCollector = new StoringEventCollector(); TestEventReceiver1 rec1 = new TestEventReceiver1(eventCollector); TestEventReceiver2 rec2 = new TestEventReceiver2(eventCollector); TestEventReceiverBase recBaseBase = new TestEventReceiverBase(eventCollector); TestEventReceiver1 recBase1 = new TestEventReceiver1(eventCollector); TestEventReceiver2 recBase2 = new TestEventReceiver2(eventCollector); sub1.addSubscription(rec1); // subscriber 1 subscribes only to event 1 sub2.addSubscription(rec2); // subscriber 2 subscribes only to event 2 subBase.addSubscription(recBase1); // subscriber base subscribes to event 1 subBase.addSubscription(recBase2); // subscriber base subscribes to event 2 subBase.addSubscription(recBaseBase); // subscriber base subscribes to event base // publishers AcsEventPublisher<TestEventType1> pub1 = nc.createPublisher("myTestPublisher1", TestEventType1.class); AcsEventPublisher<TestEventType2> pub2 = nc.createPublisher("myTestPublisher2", TestEventType2.class); AcsEventPublisher<TestEventTypeBase> pubBase = nc.createPublisher("myTestPublisherBase", TestEventTypeBase.class); // event1 should be received by sub1.rec1 and subBase.recBase1 TestEventType1 event1 = new TestEventType1(); pub1.publishEvent(event1); Thread.sleep(100); // Wait for async delivery from subscribers to receivers List<ReceivedEventInfo> events = eventCollector.getAndClearEvents(); assertThat(events.size(), equalTo(2)); assertThat((TestEventType1)events.get(0).eventData, sameInstance(event1)); assertThat((TestEventType1)events.get(1).eventData, sameInstance(event1)); assertThat(Arrays.asList(events.get(0).receiver, events.get(1).receiver), containsInAnyOrder((CollectingReceiver)rec1, (CollectingReceiver)recBase1)); logger.info("OK: Received event1 by rec1 and recBase1."); // now add a generic subscription to sub2, which should then also receive event1, // in addition to the type-specific receivers. sub2.addGenericSubscription(rec2); pub1.publishEvent(event1); Thread.sleep(100); events = eventCollector.getAndClearEvents(); assertThat(events.size(), equalTo(3)); assertThat(Arrays.asList(events.get(0).receiver, events.get(1).receiver, events.get(2).receiver), containsInAnyOrder((CollectingReceiver)rec1, (CollectingReceiver)rec2, (CollectingReceiver)recBase1)); logger.info("OK: Received event1 by rec1 and recBase1 (typed) and by rec2 (generic)"); // remove the typed subscriptions to event1 and also recBase2.event2, // and check that sub2.event2 is received only once, in spite of generic subscription sub1.removeSubscription(TestEventType1.class); subBase.removeSubscription(TestEventType1.class); subBase.removeSubscription(TestEventType2.class); TestEventType2 event2 = new TestEventType2(); pub2.publishEvent(event2); Thread.sleep(100); events = eventCollector.getAndClearEvents(); assertThat(events.size(), equalTo(1)); assertThat(events.get(0).receiver, sameInstance((CollectingReceiver)rec2)); logger.info("OK: Received event2 by rec2."); // One subscriber gets two events, one through typed and one through generic subscription. // event2 and eventBase should be received by sub2 sub1.disconnect(); subBase.removeSubscription(TestEventTypeBase.class); TestEventTypeBase eventBase = new TestEventTypeBase(); pub2.publishEvent(event2); pubBase.publishEvent(eventBase); Thread.sleep(100); events = eventCollector.getAndClearEvents(); assertThat(events.size(), equalTo(2)); assertThat(Arrays.asList(events.get(0).eventData, events.get(1).eventData), containsInAnyOrder((Object)event2, (Object)eventBase)); assertThat(events.get(0).receiver, sameInstance((CollectingReceiver)rec2)); assertThat(events.get(1).receiver, sameInstance((CollectingReceiver)rec2)); logger.info("OK: received event2 and eventData in rec2 after disconnecting / unsubscribing other subscribers."); } /** * Tests the correct behavior of suspend / resume. * The events sent during subscriber suspension must be delivered right after resume gets called. */ @Test public void testSuspendResume() throws Exception { InMemoryNcFake nc = new InMemoryNcFake(services, "myTestChannel"); // subscriber AcsEventSubscriber<TestEventType1> sub = nc.createSubscriber("myTestSubscriber", TestEventType1.class); sub.startReceivingEvents(); StoringEventCollector eventCollector = new StoringEventCollector(); TestEventReceiver1 rec = new TestEventReceiver1(eventCollector); sub.addSubscription(rec); // publisher AcsEventPublisher<TestEventType1> pub = nc.createPublisher("myTestPublisher", TestEventType1.class); TestEventType1 event1 = new TestEventType1(); final int numEventsBeforeSuspend = 7; final int numEventsDuringSuspend = 5; final int numEventsAfterResume = 3; for (int i = 0; i < numEventsBeforeSuspend; i++) { pub.publishEvent(event1); } // Suspend the subscriber. This can no longer lose already published events, because those calls return // only after putting the event in the subscriber's queue. // Still chances are good that the subscriber is still processing the events, which would then test concurrency behavior sub.suspend(); for (int i = 0; i < numEventsDuringSuspend; i++) { pub.publishEvent(event1); } // wait a bit for the receiver. Alternatively sync on the receiver as done in the concurrency test. Thread.sleep(100); // make sure we do not see the numEventsDuringSuspend included... assertThat(eventCollector.getAndClearEvents(), hasSize(numEventsBeforeSuspend)); sub.resume(); Thread.sleep(100); assertThat(eventCollector.getAndClearEvents(), hasSize(numEventsDuringSuspend)); for (int i = 0; i < numEventsAfterResume; i++) { pub.publishEvent(event1); } Thread.sleep(100); assertThat(eventCollector.getAndClearEvents(), hasSize(numEventsAfterResume)); } /** * Heavy-duty test to check for concurrency problems * and to do basic verification of throughput performance (which is limited by having a single receiver). * <p> * We want to test also the asynchronous event processing in AcsEventSubscriberImplBase, * but then must throttle the publishers so that the subscribers don't lose data. * Still we want the publishers to fire fast enough so that the subscribers get stressed at times. * This is achieved by letting the publishers fire batches of events at maximum speed, * but then wait for the entire batch to be received by the registered receiver class. * These pulses of events are calculated to at most fill up the subscriber queue completely, * which means that we may get warnings about slow receivers ("More events came in from the NC than the receiver processed"), * but still no data should be lost ("numEventsDiscarded=0"). */ @Test public void testConcurrentUse() throws Exception { InMemoryNcFake nc = new InMemoryNcFake(services, "myTestChannel"); final int numEventsPerPublisher = 2000; final int numPublishers = 5; final int numEventsPublishedTotal = numEventsPerPublisher * numPublishers; final int numActiveSubscribers = 5; final int numInactiveSubscribers = 2; final int numEventsToReceiveTotal = numEventsPublishedTotal * numActiveSubscribers; final int eventBatchSize = Math.min(numEventsPerPublisher, AcsEventSubscriberImplBase.EVENT_QUEUE_CAPACITY / numPublishers); assertThat("Current choice of test parameters leads to illegal batch size.", eventBatchSize, greaterThanOrEqualTo(1)); final int numEventsToReceivePerBatch = eventBatchSize * numPublishers * numActiveSubscribers; logger.info("Will use " + numPublishers + " publishers to each publish " + numEventsPerPublisher + " events (in batches of " + eventBatchSize + " each synchronized with the receivers), and " + numActiveSubscribers + " subscribers for these events. In addition we have " + numInactiveSubscribers + " subscribers that should not receive these events."); StopWatch sw = new StopWatch(logger); // set up publishers (unlike above we do it before the subscribers, just to make sure that works as well) List<InMemoryPublisher<TestEventType1>> publishers = new ArrayList<InMemoryPublisher<TestEventType1>>(numPublishers); for (int i = 1; i <= numPublishers; i++) { AcsEventPublisher<TestEventType1> pub = nc.createPublisher("myTestPublisher"+i, TestEventType1.class); publishers.add((InMemoryPublisher)pub); } sw.logLapTime("create " + numPublishers + " publishers"); // set up subscribers final SyncingEventCollector eventCollector = new SyncingEventCollector(numPublishers, numEventsToReceivePerBatch, numEventsToReceiveTotal); TestEventReceiver1 sharedReceiver = new TestEventReceiver1(eventCollector); List<AcsEventSubscriber<?>> subscribers = new ArrayList<AcsEventSubscriber<?>>(numActiveSubscribers); for (int i = 1; i <= numActiveSubscribers; i++) { AcsEventSubscriber<TestEventType1> sub = nc.createSubscriber("myTestSubscriber"+i, TestEventType1.class); subscribers.add(sub); sub.addSubscription(sharedReceiver); sub.startReceivingEvents(); } for (int i = 1; i <= numInactiveSubscribers; i++) { // make the inactive subscribers a mix of subscribers for the right event but disconnected, and subscribers for a different event if (i % 2 == 0) { AcsEventSubscriber<TestEventType1> sub = nc.createSubscriber("myInactiveTestSubscriber"+i, TestEventType1.class); subscribers.add(sub); sub.addSubscription(sharedReceiver); // do not call sub.startReceivingEvents() for this inactive subscriber } else { AcsEventSubscriber<TestEventType2> sub = nc.createSubscriber("myTestSubscriber"+i, TestEventType2.class); subscribers.add(sub); sub.startReceivingEvents(); } } sw.logLapTime("create " + (numActiveSubscribers + numInactiveSubscribers) + " subscribers"); // Publish and receive "event1" as specified above final TestEventType1 event1 = new TestEventType1(); final List<Throwable> asyncThrowables = Collections.synchronizedList(new ArrayList<Throwable>()); final CountDownLatch synchOnPublishers = new CountDownLatch(numPublishers); class PublisherRunnable implements Runnable { private final InMemoryPublisher<TestEventType1> publisher; PublisherRunnable(InMemoryPublisher<TestEventType1> publisher) { this.publisher = publisher; } @Override public void run() { for (int i = 1; i <= numEventsPerPublisher; i++) { try { publisher.publishEvent(event1); if (i % eventBatchSize == 0) { awaitEventReception(); } } catch (Exception ex) { asyncThrowables.add(ex); } } // test getEventCount() if (publisher.getEventCount() != numEventsPerPublisher) { asyncThrowables.add(new Exception("Published only " + publisher.getEventCount() + " events when " + numEventsPerPublisher + " were expected.")); } try { publisher.disconnect(); } catch (AcsJIllegalStateEventEx ex) { asyncThrowables.add(ex); } // the last batch may be smaller than eventBatchSize, so that we need to sync on their reception with this extra call awaitEventReception(); synchOnPublishers.countDown(); } private void awaitEventReception() { try { // StopWatch swWait = new StopWatch(logger); eventCollector.awaitEventBatchReception(); // logger.fine("Publisher in thread " + Thread.currentThread().getName() + " returned from awaitEventBatchReception() in " + swWait.getLapTimeMillis() + " ms."); } catch (Exception ex) { asyncThrowables.add(ex); } } } // let each publisher fire its events from a separate thread for (InMemoryPublisher<TestEventType1> publisher : publishers) { services.getThreadFactory().newThread(new PublisherRunnable(publisher)).start(); } // wait for publishers to fire all events (which includes already their waiting for event reception) assertThat(synchOnPublishers.await(1, TimeUnit.MINUTES), is(true)); // verify results assertThat(asyncThrowables, is(empty())); assertThat(eventCollector.getNumEventsReceivedTotal(), equalTo((long)numEventsToReceiveTotal)); sw.logLapTime("publish " + numEventsPublishedTotal + " and receive " + numEventsToReceiveTotal + " events"); } }