/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2004 * Copyright by ESO (in the framework of the ALMA collaboration), * All rights reserved * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA */ package alma.ACS.MasterComponentImpl; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import junit.framework.AssertionFailedError; import junit.framework.TestCase; import alma.acs.container.CleaningDaemonThreadFactory; import alma.acs.logging.ClientLogManager; /** * Test for {@link SubsysResourceMonitor}. * @author hsommer */ public class SubsysResourceMonitorTest extends TestCase { private static final int checkDelaySeconds = 5; private SubsysResourceMonitor subsysResourceMonitor; private Logger logger; private CleaningDaemonThreadFactory threadFactory; protected void setUp() throws Exception { logger = ClientLogManager.getAcsLogManager().getLoggerForApplication(getName(), false); logger.info("-------------------- setUp " + getName() + " --------------------"); threadFactory = new CleaningDaemonThreadFactory("monitor", logger) { public Thread newThread(Runnable command) { Thread newThread = super.newThread(command); logger.info("Created thread '" + newThread.getName() + "'."); return newThread; } }; // create a monitor that checks every 5 seconds subsysResourceMonitor = new SubsysResourceMonitor(logger, threadFactory, checkDelaySeconds); } protected void tearDown() throws Exception { logger.info("tearDown: will destroy scheduler and threads"); subsysResourceMonitor.destroy(10, TimeUnit.SECONDS); super.tearDown(); } /** * Verifies the regular resource status checks under ideal conditions. * Also tests the basic functioning of the test handlers and listeners used by this class. */ public void testMonitorEasy() throws Exception { // a well-behaved resource TestResource resource = new TestResource(logger); resource.setState(TestResource.STATE_OK); int testDelaySeconds = 1; resource.setTestDelaySeconds(testDelaySeconds); int numMonitorCalls = 4; CountDownLatch resourceCheckCounter = new CountDownLatch(numMonitorCalls); resource.setStateCheckCounter(resourceCheckCounter); // a benign resource checker TestResourceChecker checker = new TestResourceChecker(resource, logger); assertSame(resource, checker.getResource()); assertEquals(resource.getName(), checker.getResourceName()); // and a sweet error handler TestErrorHandler handler = new TestErrorHandler(logger); assertEquals(0, handler.getUnreachableCount()); assertEquals(0, handler.getBadStateCount()); assertFalse(handler.isPermanentlyUnreachable()); assertEquals(0, subsysResourceMonitor.getNumberOfMonitorTasks()); // the call to monitorResource returns immediately, thus we need to wait afterwards subsysResourceMonitor.monitorResource(checker, handler); int maxTotalTimeSeconds = (testDelaySeconds + checkDelaySeconds) * numMonitorCalls + 1; assertTrue("timeout occured while waiting for resource check calls", resourceCheckCounter.await(maxTotalTimeSeconds, TimeUnit.SECONDS)); assertEquals(0, handler.getUnreachableCount()); assertEquals(0, handler.getBadStateCount()); // we expect one thread for the monitor scheduler, and another thread for the asynchronous call to the resource List<Thread> threadsCreated = threadFactory._getAllThreadsCreated(); assertEquals("The monitoring should have created 2 threads", 2, threadsCreated.size()); } /** * Tests if resource unreachability is propagated to the client when it is not detected by a timeout * in the monitoring framework, but by getting a RuntimeException such as TRANSIENT for a Corba resource. * */ public void testResourceCheckException() throws Exception { TestResource resource = new TestResource(logger); TestResourceChecker checker = new TestResourceChecker(resource, logger); TestErrorHandler handler = new TestErrorHandler(logger); int delaySeconds = 2; // (1) NPE, as if the check had failed for a stupid error. // The NPE is not expected, so an additional warning gets logged. // The unreachable handler must be called (it will decide to continue monitoring). int numMonitorCalls = 2; resource.setCheckStateRuntimeEx(NullPointerException.class); CountDownLatch timeoutSync = new CountDownLatch(numMonitorCalls); handler.setUnreachabilitySync(timeoutSync); handler.setIsPermanentlyUnreachable(false); subsysResourceMonitor.monitorResource(checker, handler, delaySeconds); assertTrue("timeout occured while waiting for unreachability notification", timeoutSync.await(delaySeconds * numMonitorCalls, TimeUnit.SECONDS)); logger.info("Survived the " + numMonitorCalls + " NPEs"); // (2) org.omg.CORBA.TRANSIENT, as if a remote container had disappeared. // The TRANSIENT exception is treated the same way as a timeout. // The unreachable handler must be called (it will decide to stop monitoring). Thread.sleep(100); // because above some stuff happens after timeoutSync was called numMonitorCalls = 1; resource.setCheckStateRuntimeEx(org.omg.CORBA.TRANSIENT.class); timeoutSync = new CountDownLatch(numMonitorCalls); handler.setUnreachabilitySync(timeoutSync); handler.setIsPermanentlyUnreachable(true); assertTrue("timeout occured while waiting for unreachability notification", timeoutSync.await(delaySeconds * numMonitorCalls, TimeUnit.SECONDS)); Thread.sleep(100); // because above some stuff happens after timeoutSync was called } /** * Simulates hanging monitor calls, and tests the notification of the error handler, * the creation and reuse of threads, the continuation and eventual termination of monitoring calls. */ public void testResourceUnavailable() throws Exception { try { TestResource resource = new TestResource(logger); TestResourceChecker checker = new TestResourceChecker(resource, logger); TestErrorHandler handler = new TestErrorHandler(logger); subsysResourceMonitor.monitorResource(checker, handler); SubsysResourceMonitor.ResourceCheckRunner runner = subsysResourceMonitor.getResourceCheckRunner(checker); assertNotNull(runner); runner.setCallTimeoutSeconds(3); // to speed up the test a bit, compared to the default of 10 // (1) we simulate an unavailable resource, as in a CORBA call that eventually times out int timeoutSeconds = runner.getCallTimeoutSeconds(); int testDelaySeconds = 6 * timeoutSeconds + 1; // must be greater than timeoutSeconds to allow testing the behavior for unavailable resources assertTrue("for testing, the artifical resource check hanging time should not be an integer multiple of (timeoutSeconds + checkDelaySeconds), to avoid hitting the boundary where an exisitng thread can be reused vs. creating a new thread", testDelaySeconds % (timeoutSeconds + checkDelaySeconds) > 0); resource.setTestDelaySeconds(testDelaySeconds); int numMonitorCalls = 10; CountDownLatch timeoutSync = new CountDownLatch(numMonitorCalls); handler.setUnreachabilitySync(timeoutSync); int maxTotalTimeSeconds = (timeoutSeconds + checkDelaySeconds) * numMonitorCalls + 1; logger.info("Test thread will wait for " + numMonitorCalls + " unavailability notifications (at most " + maxTotalTimeSeconds + " seconds)"); assertTrue("timeout occured while waiting for deliberately slow resource check calls", timeoutSync.await(maxTotalTimeSeconds, TimeUnit.SECONDS)); logger.info("Test thread continues..."); assertEquals(1, subsysResourceMonitor.getNumberOfMonitorTasks()); assertEquals(numMonitorCalls, handler.getUnreachableCount()); assertEquals(0, handler.getBadStateCount()); List<Thread> threadsCreated = threadFactory._getAllThreadsCreated(); // we expect one thread for the monitor scheduler, and some more threads for the asynchronous calls to the resource. // Their number depends on how many checker threads had to be started before an initially hanging thread could be reused. int numCheckerThreads = 1 + testDelaySeconds/(timeoutSeconds + checkDelaySeconds); assertEquals("The monitoring should have created " + (numCheckerThreads + 1) + " threads.", numCheckerThreads+1, threadsCreated.size()); // (2) we simulate a resource that's gone beyond repair, and whose error handler will request no further monitoring handler.setIsPermanentlyUnreachable(true); Thread.sleep((checkDelaySeconds + timeoutSeconds + 1)*1000); // wait to make sure that the permanent failure has been discovered assertEquals("Monitoring should have been cancelled due to the permanent failure.", 0, subsysResourceMonitor.getNumberOfMonitorTasks()); logger.info("done"); } catch (AssertionFailedError e) { // we want to log this in order to compare timestamps with the other asynchronous activities logger.log(Level.SEVERE, "assertion failure", e); throw e; } } /** * Tests whether bad user-supplied parameters are handled gracefully. * @throws Exception */ public void testUsageErrorResponse() throws Exception { TestResource resourceOk = new TestResource(logger); TestResourceChecker checkerOk = new TestResourceChecker(resourceOk, logger); TestErrorHandler handler = new TestErrorHandler(logger); // (1) ResourceChecker and error handler are null try { subsysResourceMonitor.monitorResource(null, null); fail("IllegalArgumentException expected"); } catch (IllegalArgumentException ex) { assertEquals("ResourceChecker must be non-null and must deliver non-null resource and resource name.", ex.getMessage()); } // (2) ResourceChecker wraps a null resource TestResourceChecker checkerNullResource = new TestResourceChecker(null, logger); try { subsysResourceMonitor.monitorResource(checkerNullResource, handler); fail("IllegalArgumentException expected"); } catch (IllegalArgumentException ex) { assertEquals("ResourceChecker must be non-null and must deliver non-null resource and resource name.", ex.getMessage()); } // (3) ResourceChecker wraps a resource with a null name TestResource resourceNullName = new TestResource(logger); resourceNullName.setName(null); TestResourceChecker checkerResourceNullName = new TestResourceChecker(resourceNullName, logger); try { subsysResourceMonitor.monitorResource(checkerResourceNullName, handler); fail("IllegalArgumentException expected"); } catch (IllegalArgumentException ex) { assertEquals("ResourceChecker must be non-null and must deliver non-null resource and resource name.", ex.getMessage()); } // (4) decent ResourceChecker, but null error handler try { subsysResourceMonitor.monitorResource(checkerOk, null); fail("IllegalArgumentException expected"); } catch (IllegalArgumentException ex) { assertEquals("ResourceErrorHandler must not be null", ex.getMessage()); } // (5) logger.info("Will submit the same resource checker twice"); subsysResourceMonitor.monitorResource(checkerOk, handler); subsysResourceMonitor.monitorResource(checkerOk, handler); // will re-schedule the task Thread.sleep(10000); // to allow the canceled first thread to disappear assertEquals("Only one monitoring task expected.", 1, subsysResourceMonitor.getNumberOfMonitorTasks()); // (6) logger.info("Will submit a new resource checker for a resource that is already being monitored"); TestResourceChecker checkerOk2 = new TestResourceChecker(resourceOk, logger); subsysResourceMonitor.monitorResource(checkerOk2, handler); // will re-schedule the task Thread.sleep(10000); // to allow the canceled first thread to disappear assertEquals("Only one monitoring task expected.", 1, subsysResourceMonitor.getNumberOfMonitorTasks()); } private static class TestResource { public static final String STATE_OK = "OK"; private volatile String state; private int testDelaySeconds; private final Logger logger; private volatile CountDownLatch counter; private String name; private Class<? extends RuntimeException> checkStateRuntimeExClass; // != null to simulate a problem // private Error checkStateError; // != null to simulate a problem public TestResource(Logger logger) { this.logger = logger; setTestDelaySeconds(1); setState(STATE_OK); setName("Your faithful test resource"); this.checkStateRuntimeExClass = null; // this.checkStateError = null; } String getName() { return name; } void setName(String name) { this.name = name; } /** * This method must be reentrant so that the next scheduled check can work even the last thread is still sleeping. * Otherwise the timeouts in the tests get confused. */ String getState() { if (checkStateRuntimeExClass != null) { logger.info("TestResource#getState called in thread '" + Thread.currentThread().getName() + "'. Will throw an exception of type " + checkStateRuntimeExClass.getName()); RuntimeException ex = null; try { ex = checkStateRuntimeExClass.newInstance(); } catch (Throwable thr) { logger.log(Level.WARNING, "unexpected exception while instantiating test exception", thr); } throw ex; } logger.info("TestResource#getState called in thread '" + Thread.currentThread().getName() + "'. Will wait " + testDelaySeconds + " seconds unless interrupted."); try { Thread.sleep(testDelaySeconds * 1000); } catch (InterruptedException ex) { logger.log(Level.WARNING, "Sleeping TestResource#getState() interrupted in thread '" + Thread.currentThread().getName() + "'."); } if (counter != null) { counter.countDown(); logger.info("TestResource#getState (thread " + Thread.currentThread().getName() + ") is done and will return state '" + state + "'. Counter=" + counter.getCount()); } else { logger.info("TestResource#getState (thread " + Thread.currentThread().getName() + ") is done and will return state '" + state + "'."); } return state; } void setState(String state) { this.state = state; } void setStateCheckCounter(CountDownLatch counter) { this.counter = counter; } int getTestDelaySeconds() { return testDelaySeconds; } void setTestDelaySeconds(int testDelaySeconds) { this.testDelaySeconds = testDelaySeconds; } Class<? extends RuntimeException> getCheckStateRuntimeEx() { return this.checkStateRuntimeExClass; } void setCheckStateRuntimeEx(Class<? extends RuntimeException> exClass) { this.checkStateRuntimeExClass = exClass; } } private static class TestResourceChecker implements SubsysResourceMonitor.ResourceChecker<TestResource> { private final TestResource resource; private Logger logger; TestResourceChecker(TestResource resource, Logger logger) { this.resource = resource; this.logger = logger; } public String checkState() { String state = resource.getState(); if (state.equals(TestResource.STATE_OK)) { return null; } else { return state; } } public TestResource getResource() { return resource; } public String getResourceName() { return resource.getName(); } } private static class TestErrorHandler implements SubsysResourceMonitor.ResourceErrorHandler<TestResource> { private final Logger logger; private volatile int badStateCount; private volatile int unreachableCount; private boolean isPermanentlyUnreachable; private volatile CountDownLatch unreachabilitySync; TestErrorHandler(Logger logger) { this.logger = logger; isPermanentlyUnreachable = false; resetCounters(); } public void badState(TestResource resource, String stateName) { badStateCount++; logger.info("TestErrorHandler#badState (thread " + Thread.currentThread().getName() + ") called - " + unreachableCount); } public boolean resourceUnreachable(TestResource resource) { unreachableCount++; logger.info("TestErrorHandler#resourceUnreachable (thread " + Thread.currentThread().getName() + ") called - " + unreachableCount); if (unreachabilitySync != null) { unreachabilitySync.countDown(); } return isPermanentlyUnreachable; } public void resourceRecovered(TestResource resource) { //@todo test also invocations of this method. } void resetCounters() { badStateCount = 0; unreachableCount = 0; } int getBadStateCount() { return badStateCount; } int getUnreachableCount() { return unreachableCount; } boolean isPermanentlyUnreachable() { return isPermanentlyUnreachable; } void setIsPermanentlyUnreachable(boolean isPermanentlyUnreachable) { this.isPermanentlyUnreachable = isPermanentlyUnreachable; } void setUnreachabilitySync(CountDownLatch sync) { this.unreachabilitySync = sync; } } }