package alma.acs.nc; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import junit.framework.AssertionFailedError; import org.omg.CORBA.IntHolder; import org.omg.CosNaming.NameComponent; import org.omg.CosNaming.NamingContext; import org.omg.CosNaming.NamingContextHelper; import org.omg.CosNaming.NamingContextPackage.NotFound; import org.omg.CosNotification.Property; import org.omg.CosNotification.UnsupportedQoS; import Monitor.Data; import Monitor.DataType; import gov.sandia.CosNotification.NotificationServiceMonitorControl; import gov.sandia.CosNotification.NotificationServiceMonitorControlHelper; import gov.sandia.CosNotification.NotificationServiceMonitorControlPackage.InvalidName; import gov.sandia.NotifyMonitoringExt.ActiveEventChannelCount; import gov.sandia.NotifyMonitoringExt.ActiveEventChannelNames; import gov.sandia.NotifyMonitoringExt.EventChannel; import gov.sandia.NotifyMonitoringExt.EventChannelCreationTime; import gov.sandia.NotifyMonitoringExt.EventChannelFactory; import gov.sandia.NotifyMonitoringExt.EventChannelFactoryNames; import gov.sandia.NotifyMonitoringExt.NameAlreadyUsed; import alma.ACSErrTypeCORBA.wrappers.AcsJNarrowFailedEx; import alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx; import alma.acs.component.client.ComponentClientTestCase; import alma.acs.container.ContainerServicesBase; import alma.acs.exceptions.AcsJException; import alma.acs.util.IsoDateFormat; import alma.acscommon.NAMING_SERVICE_NAME; import alma.acscommon.NC_KIND; /** * Tests creation of notification channels and usage of the TAO notification extensions * that were introduced to fix the race conditions described in COMP-2808. * * @author hsommer */ public class HelperTest extends ComponentClientTestCase { private NamingContext nctx; private NotificationServiceMonitorControl nsmc; public HelperTest() throws Exception { super("HelperTest"); } protected void setUp() throws Exception { System.out.println("------------------------- " + getName() + " -------------------------"); super.setUp(); nctx = NamingContextHelper.narrow( m_acsManagerProxy.get_service(NAMING_SERVICE_NAME.value, false) ); String factoryMonitorRegName = "MC_NotifyEventChannelFactory"; try { nsmc = NotificationServiceMonitorControlHelper.narrow( nctx.resolve(new NameComponent[]{new NameComponent("MC_NotifyEventChannelFactory", "")})); } catch (NotFound ex) { fail("Failed to resolve factory's MC extension object in the naming service: " + factoryMonitorRegName + ". This is a recurrent problem, probably linked to the missing line 'ReBound ID: MC_NotifyEventChannelFactory' in ./tmp/acsStart.log."); } assertNotNull(nsmc); } protected void tearDown() throws Exception { super.tearDown(); } // public void testNamingServiceBindings() { // BindingListHolder blh = new BindingListHolder(); // helper.getNamingService().list(100, blh, new BindingIteratorHolder()); // for (Binding binding : blh.value) { // System.out.println(binding.binding_name[0].id + " " + binding.binding_type.value()); // } // } /** * */ public void testTaoMonitorAndControlService() throws Exception { String[] nsmcNames = nsmc.get_statistic_names(); String nsmcNamesMsg = "NotificationServiceMonitorControl service offers statistcs for "; for (String nsmcName : nsmcNames) { nsmcNamesMsg += nsmcName + ", "; } m_logger.info("The following strings can be used as args to getStatistics: " + nsmcNamesMsg); m_logger.info("EventChannelFactoryNames: " + getStatistic(EventChannelFactoryNames.value)); m_logger.info("ActiveEventChannelCount: " + getStatistic("NotifyEventChannelFactory/" + ActiveEventChannelCount.value)); m_logger.info("ActiveEventChannelNames: " + getStatistic("NotifyEventChannelFactory/" + ActiveEventChannelNames.value)); Date t1 = getStatisticTime("NotifyEventChannelFactory/" + EventChannelCreationTime.value); m_logger.info("NotifyEventChannelFactory/EventChannelCreationTime: " + IsoDateFormat.formatDate(t1)); } /** * Creates and destroys a test channel. * Also tests creating a second instance of that channel after the first one has been created, * to check for the NameAlreadyUsed ex. */ public void testCreateChannel() throws Exception { String channelName = "singleChannel"; Helper helper = new HelperWithChannelCreationSynch(channelName, getContainerServices(), nctx); String factoryName = helper.getNotificationFactoryNameForChannel(); EventChannel myChannel = null; try { //precondition: channel not there (e.g. from previous run) assertChannel(false, channelName); // The call to "getNotificationChannel" should create the channel, because reuse will not be possible. myChannel = helper.getNotificationChannel(factoryName); assertChannel(true, channelName); // Now we try to create that channel again, without allowing reuse. Should fail. try { helper.createNotificationChannel(NC_KIND.value, factoryName); fail("Expected NameAlreadyUsed exception for creating the channel twice."); } catch (Exception ex) { m_logger.info("Got a NameAlreadyUsed exception as expected."); } // But with reuse it should work EventChannel myChannel2 = helper.getNotificationChannel(factoryName); assertTrue(myChannel._is_equivalent(myChannel2)); } finally { // Destroy the channel if (myChannel != null) { helper.destroyNotificationChannel(NC_KIND.value, myChannel); } assertChannel(false, channelName); } } /** * Tests the collision case where many threads create the same channel concurrently. * <p> * Note that we would need to hack TAO to slow down (or otherwise synchronize with) channel creation, * so that we can be sure that the second request comes in before the first request has finished. * We optimize this by synchronizing the test threads right before they make the call to the channel factory, * for which we overload the method {@link HelperWithChannelCreationSynch#createNotifyChannel_internal(EventChannelFactory, Property[], Property[], String, IntHolder)}. * This eliminates jitter from thread creation, thread starting, and contact with the naming service, all happening before actual channel creation. */ public void testConcurrentChannelCreation() throws Exception { final String channelName = "testChannelForConcurrentCreation"; // one channel tried to be created concurrently final HelperWithChannelCreationSynch helper = new HelperWithChannelCreationSynch(channelName, getContainerServices(), nctx); assertChannel(false, channelName); // @TODO Refactor the following code to use alma.acs.concurrent.ThreadBurstExecutorService now that we have it class ChannelCreator implements Callable<EventChannel> { private final CountDownLatch synchStart; ChannelCreator(CountDownLatch synchStart) { this.synchStart = synchStart; } public EventChannel call() throws Exception { String factoryName = helper.getNotificationFactoryNameForChannel(); return helper.createNotificationChannel(NC_KIND.value, factoryName, synchStart); } } // we need at least two threads, but more threads may improve collision chances final int numCreators = 4; assertTrue(numCreators >= 2); ExecutorService pool = Executors.newFixedThreadPool(numCreators, getContainerServices().getThreadFactory()); CountDownLatch synchCreationStart = new CountDownLatch(numCreators); List<Future<EventChannel>> results = new ArrayList<Future<EventChannel>>(); // check the results EventChannel uniqueChannel = null; try { // Run the threads that create the same channel for (int i = 0; i < numCreators; i++) { results.add(pool.submit(new ChannelCreator(synchCreationStart))); } // wait for all threads to finish. Waiting here instead of waiting on the future.get() calls // has the advantage that we can exit this method with a fail() without leaving an ongoing channel creation behind. pool.shutdown(); assertTrue(pool.awaitTermination(30, TimeUnit.SECONDS)); for (Future<EventChannel> future : results) { try { EventChannel threadResult = future.get(); // we only get here if threadResult != null, otherwise ex if (uniqueChannel != null) { fail("Only one thread should have managed to create the channel without exception!"); } else { uniqueChannel = threadResult; } } catch (ExecutionException ex) { if (ex.getCause() instanceof NameAlreadyUsed) { m_logger.info("Got a NameAlreadyUsed exception"); } else { fail("Unexpected exception "+ ex.getCause().toString()); } } catch (AssertionFailedError ex) { throw ex; } catch (Throwable thr) { fail("Unexpected exception "+ thr.toString()); } } assertNotNull("One thread should have succeeded", uniqueChannel); } finally { if (uniqueChannel != null) { helper.destroyNotificationChannel(NC_KIND.value, uniqueChannel); } } } /** * One step up from {@link #testConcurrentChannelCreation()}, here we test concurrent calls to * {@link Helper#getNotificationChannel(String, String, String)} which are supposed to handle the * <code>NameAlreadyUsed</code> exception by making those later threads wait until the channel has * been created for the first thread, then sharing the channel object. */ public void testConcurrentChannelRetrieval() throws Throwable { final String channelName = "testChannelForConcurrentRetrieval"; // one channel to be retrieved concurrently final HelperWithChannelCreationSynch helper = new HelperWithChannelCreationSynch(channelName, getContainerServices(), nctx); assertChannel(false, channelName); class ChannelRetriever implements Callable<EventChannel> { private final CountDownLatch synchStart; ChannelRetriever(CountDownLatch synchStart) { this.synchStart = synchStart; } public EventChannel call() throws Exception { String factoryName = helper.getNotificationFactoryNameForChannel(); return helper.getNotificationChannel(factoryName, synchStart); } } // we need at least two threads, but more threads may improve collision chances final int numCreators = 3; assertTrue(numCreators >= 2); ExecutorService pool = Executors.newFixedThreadPool(numCreators, getContainerServices().getThreadFactory()); CountDownLatch synchCreationStart = new CountDownLatch(numCreators); List<Future<EventChannel>> results = new ArrayList<Future<EventChannel>>(); // check the results EventChannel uniqueChannel = null; try { // Run the threads that request the same channel for (int i = 0; i < numCreators; i++) { results.add(pool.submit(new ChannelRetriever(synchCreationStart))); } // wait for all threads to finish. Waiting here instead of waiting on the future.get() calls // has the advantage that we can exit this method with a fail() without leaving an ongoing channel creation behind. pool.shutdown(); assertTrue(pool.awaitTermination(30, TimeUnit.SECONDS)); for (Future<EventChannel> future : results) { try { EventChannel threadResult = future.get(); // we only get here if threadResult != null, otherwise ex if (uniqueChannel != null) { assertTrue(uniqueChannel._is_equivalent(threadResult)); } uniqueChannel = threadResult; } catch (ExecutionException ex) { throw ex.getCause(); } catch (AssertionFailedError ex) { throw ex; } catch (Throwable thr) { fail("Unexpected exception "+ thr.toString()); } } m_logger.info("All concurrent calls to getNotificationChannel got the same channel object."); } finally { if (uniqueChannel != null) { helper.destroyNotificationChannel(NC_KIND.value, uniqueChannel); } } } ///////////////////////////// //// Test helper methods ///////////////////////////// /** * Helper method that checks (non-/)existence of a channel * @param existing */ private void assertChannel(boolean existing, String channelName) { // Check notify service factory // Check naming service binding NameComponent[] t_NameSequence = { new NameComponent( Helper.combineChannelAndDomainName(channelName, null), NC_KIND.value) }; try { nctx.resolve(t_NameSequence); if (existing) { m_logger.info("Verified that channel " + channelName + " is registered with the naming service."); } else { fail("Channel " + channelName + " is erroneously registered with the naming service."); } } catch (NotFound ex) { if (existing) { fail("Channel " + channelName + " is NOT registered with the naming service."); } else { m_logger.info("Verified that channel " + channelName + " is NOT registered with the naming service."); } } catch (Throwable thr) { fail("Unexpected exception " + thr.toString()); } } private String getStatistic(String statName) throws InvalidName { Data data = nsmc.get_statistic(statName); if (data.data_union.discriminator() == DataType.DATA_TEXT) { String[] list = data.data_union.list(); return Arrays.toString(list); } else if (data.data_union.discriminator() == DataType.DATA_NUMERIC) { return Double.toString(data.data_union.num().last); } return null; } private Date getStatisticTime(String statName) throws InvalidName { Data data = nsmc.get_statistic(statName); if (data.data_union.discriminator() == DataType.DATA_NUMERIC) { long t = (long) data.data_union.num().last; return new Date(t*1000); } throw new org.omg.CORBA.BAD_OPERATION(); } /** * We extend the tested Helper class so that this test can synchronize on the actual channel creation */ private static class HelperWithChannelCreationSynch extends Helper { private volatile CountDownLatch synch; public HelperWithChannelCreationSynch(String channelName, ContainerServicesBase services, NamingContext namingService) throws AcsJException { super(channelName, services, namingService); } /** * Counts down the optional CountDownLatch, then delegates to parent method * @throws AcsJCORBAProblemEx */ @Override protected EventChannel createNotifyChannel_internal(Property[] initial_qos, Property[] initial_admin, IntHolder channelIdHolder) throws NameAlreadyUsed, UnsupportedQoS, AcsJNarrowFailedEx, AcsJCORBAProblemEx { if (synch != null) { // count down and wait for other threads to reach the same state before going on. synch.countDown(); m_logger.info("Thread "+ Thread.currentThread().getName() + " is ready to proceed with channel creation. Count is " + synch.getCount()); try { synch.await(10, TimeUnit.SECONDS); } catch (InterruptedException ex) { ex.printStackTrace(); } } return super.createNotifyChannel_internal(initial_qos, initial_admin, channelIdHolder); } protected EventChannel createNotificationChannel(String channelKind, String notifyFactoryName, CountDownLatch synch) throws AcsJException, NameAlreadyUsed { this.synch = synch; return super.createNotificationChannel(channelKind, notifyFactoryName); } protected EventChannel getNotificationChannel(String notifyFactoryName, CountDownLatch synch) throws AcsJException { this.synch = synch; return super.getNotificationChannel(notifyFactoryName); } } }