package alma.acs.concurrent; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.LogRecord; import java.util.logging.Logger; import junit.framework.TestCase; import alma.acs.concurrent.ThreadLoopRunner.ScheduleDelayMode; import alma.acs.testsupport.LogRecordCollectingLogger; import alma.acs.testsupport.TestLogger; import alma.acs.util.StopWatch; public class ThreadLoopRunnerTest extends TestCase { private ThreadFactory tf; private Logger logger; protected void setUp() throws Exception { super.setUp(); tf = new DaemonThreadFactory("ThreadLoopRunner"); logger = TestLogger.getLogger(getClass().getName()); logger.info("\n------------ " + getName() + " --------------"); } public void testSimple() throws Exception { final LogRecordCollectingLogger logger2 = LogRecordCollectingLogger.getCollectingLogger("testSimple_collecting_logger"); logger2.setDelegateLogger(logger); Runnable action = new Runnable() { public void run() { logger2.info("Simple action run."); } }; ThreadLoopRunner threadLoopRunner = new ThreadLoopRunner(action, 1, TimeUnit.SECONDS, tf, logger, "testSimple"); assertEquals(ScheduleDelayMode.FIXED_RATE, threadLoopRunner.getDelayMode()); assertFalse(threadLoopRunner.isLoopRunning()); threadLoopRunner.runLoop(); assertTrue(threadLoopRunner.isLoopRunning()); assertFalse(threadLoopRunner.isDisabled()); assertEquals(ScheduleDelayMode.FIXED_RATE, threadLoopRunner.getDelayMode()); // sleep 2.5 seconds, so that the loop can run 3 times (with our 1 sec delay) at around times 0, 1, 2 seconds int expectedNumberOfInvocations = 3; Thread.sleep((expectedNumberOfInvocations-1) * 1000 + 500); boolean shutdownRet = threadLoopRunner.shutdown(100, TimeUnit.MILLISECONDS); assertTrue("should have finished within 100 ms", shutdownRet); LogRecord[] records = logger2.getCollectedLogRecords(); assertEquals("three records expected", expectedNumberOfInvocations, records.length); for (int i = 0; i < records.length; i++) { assertEquals("Simple action run.", records[i].getMessage()); } } /** * Tests a well-behaved but complex usage scenario, * including calls to suspend and start again the execution loop. */ public void testComplexWithSuspendAndRestart() throws Exception { int actionWaitMillis = 100; int delayMillis = 300; int numberOnOffCycles = 3; int expectedInvocationsPerCycle = 33; int allowedThreadJitterMillis = 200; CountDownLatch sync = new CountDownLatch(expectedInvocationsPerCycle); MyAction myAction = new MyAction(sync, actionWaitMillis, logger); ThreadLoopRunner threadLoopRunner = null; try { threadLoopRunner = new ThreadLoopRunner(myAction, delayMillis, TimeUnit.MILLISECONDS, tf, logger, "testComplex"); StopWatch sw = new StopWatch(); for (int i = 0; i < numberOnOffCycles; i++) { ScheduleDelayMode delayMode = i % 2 == 0 ? ScheduleDelayMode.FIXED_RATE : ScheduleDelayMode.FIXED_DELAY; logger.info("Will start the thread loop with delay mode " + delayMode.toString()); threadLoopRunner.setDelayMode(delayMode); threadLoopRunner.runLoop(); sw.reset(); // wait till the last execution of the action is over int expectedDurationMillis = (expectedInvocationsPerCycle - 1)* delayMillis + actionWaitMillis; if (delayMode == ScheduleDelayMode.FIXED_DELAY) { expectedDurationMillis += (expectedInvocationsPerCycle - 1) * actionWaitMillis; } int timeoutMillis = expectedDurationMillis + allowedThreadJitterMillis+1000; boolean awaitRet = sync.await(timeoutMillis, TimeUnit.MILLISECONDS); int actualDuration = (int) sw.getLapTimeMillis(); int actualInvocations = myAction.getCount(); assertTrue("Timed out after " + timeoutMillis + " ms", awaitRet); assertEquals(expectedInvocationsPerCycle, actualInvocations); assertTrue("Tasks were run faster (" + actualDuration + ") than expected (" + expectedDurationMillis + ")", actualDuration > expectedDurationMillis - allowedThreadJitterMillis); assertTrue(threadLoopRunner.isLoopRunning()); assertFalse(threadLoopRunner.isDisabled()); // suspend and assert that no further actions get executed logger.info("Will suspend the thread loop"); threadLoopRunner.suspendLoop(); assertFalse(threadLoopRunner.isLoopRunning()); assertFalse(threadLoopRunner.isDisabled()); assertEquals(delayMode, threadLoopRunner.getDelayMode()); Thread.sleep((actionWaitMillis + delayMillis)*2); assertEquals(expectedInvocationsPerCycle, myAction.getCount()); // run again sync = new CountDownLatch(expectedInvocationsPerCycle); myAction.reset(sync); } } finally { if (threadLoopRunner != null) { assertTrue(threadLoopRunner.shutdown(100, TimeUnit.MILLISECONDS)); } } } public void testSetDelayTimeAndMode() throws Exception { int actionWaitMillis = 1000; int delayMillis = 1500; CountDownLatch sync = new CountDownLatch(1); MyAction myAction = new MyAction(sync, actionWaitMillis, logger); ThreadLoopRunner threadLoopRunner = null; Exception mainBlockException = null; try { threadLoopRunner = new ThreadLoopRunner(myAction, delayMillis, TimeUnit.MILLISECONDS, tf, logger, "testDelayTimeMode"); // setting delay mode before running loop is fine threadLoopRunner.setDelayMode(ScheduleDelayMode.FIXED_RATE); threadLoopRunner.runLoop(); // setting delay mode while running loop must give exception try { threadLoopRunner.setDelayMode(ScheduleDelayMode.FIXED_DELAY); fail("Expected IllegalStateException"); } catch (IllegalStateException ex) { // good } // First sleep a bit to make sure task has started Thread.sleep(100); // task runs for 1 second, so it should still be running now. assertTrue(threadLoopRunner.isTaskRunning()); // changing the delay time while the task is running should stop the loop, return immediately, // and later restart the loop. StopWatch sw = new StopWatch(); delayMillis = 200; threadLoopRunner.setDelayTime(delayMillis, TimeUnit.MILLISECONDS); assertTrue("Calling setDelayTime should return very quickly, certainly in less than 100 ms.", sw.getLapTimeMillis() <= 100); // task loop should be stopped right after call to setDelayTime assertFalse(threadLoopRunner.isLoopRunning()); // when task has finished (should be the case after actionWaitMillis and all the calls above), // the task loop should be running again, with the new delay time 200 ms. Thread.sleep(actionWaitMillis); // assertTrue(threadLoopRunner.isLoopRunning()); assertEquals(delayMillis, threadLoopRunner.getDelayTimeMillis()); // verify after 10 repetitions that the new shorter delay time was actually applied sync = new CountDownLatch(10); myAction.reset(sync); logger.info("Now waiting for the loop to execute 10 times:"); assertTrue("Got timeout, after just " + myAction.getCount() + " task executions", sync.await((actionWaitMillis + delayMillis) * 10 + 100, TimeUnit.MILLISECONDS)); } catch (Exception ex1) { mainBlockException = ex1; } finally { if (threadLoopRunner != null) { try { assertTrue("Failed to shutdown thread loop runner in 2000 ms.", threadLoopRunner.shutdown(2000, TimeUnit.MILLISECONDS)); } catch (Exception ex2) { if (mainBlockException != null) { throw mainBlockException; } else { throw ex2; } } } } } /** * Tests logging of the RuntimeException or Error that can be thrown by * the user-supplied Runnable. */ public void testActionError() throws Exception { final LogRecordCollectingLogger logger2 = LogRecordCollectingLogger.getCollectingLogger("testActionError_collecting_logger"); logger2.setDelegateLogger(logger); final String errorLogMsg = "Evil error in user runnable"; Runnable action = new Runnable() { public void run() { throw new Error(errorLogMsg); } }; ThreadLoopRunner threadLoopRunner = new ThreadLoopRunner(action, 1, TimeUnit.SECONDS, tf, logger2, "testActionError"); assertEquals(ScheduleDelayMode.FIXED_RATE, threadLoopRunner.getDelayMode()); assertFalse(threadLoopRunner.isLoopRunning()); threadLoopRunner.runLoop(); assertTrue(threadLoopRunner.isLoopRunning()); assertFalse(threadLoopRunner.isDisabled()); assertEquals(ScheduleDelayMode.FIXED_RATE, threadLoopRunner.getDelayMode()); // sleep 1.5 seconds, so that the loop can run 2 times (with our 1 sec delay) at around times 0, 1 seconds int expectedNumberOfInvocations = 2; Thread.sleep((expectedNumberOfInvocations-1) * 1000 + 500); boolean shutdownRet = threadLoopRunner.shutdown(100, TimeUnit.MILLISECONDS); assertTrue("should have finished within 100 ms", shutdownRet); LogRecord[] records = logger2.getCollectedLogRecords(); List<LogRecord> errorLogRecords = new ArrayList<LogRecord>(); for (LogRecord logRecord : records) { if (logRecord.getMessage().equals("Uncaught error in scheduled Runnable.")) { errorLogRecords.add(logRecord); } } assertEquals("two error logs expected", expectedNumberOfInvocations, errorLogRecords.size()); for (LogRecord logRecord : errorLogRecords) { Throwable loggedError = logRecord.getThrown(); assertEquals(errorLogMsg, loggedError.getMessage()); } } // public void testDetailsSuspend() throws Exception { // int repeatMillis = 1000; // int sleepMillis = 500; // int allowedThreadJitterMillis = 50; // // // Test that suspend() returns without waiting for the currently executing action to finish // // CountDownLatch sync = new CountDownLatch(1); // MyAction myAction = new MyAction(sync, sleepMillis, logger); // // ThreadLoopRunner threadLoopRunner = new ThreadLoopRunner(tf, myAction, logger); // threadLoopRunner.setDelayTime(repeatMillis , TimeUnit.MILLISECONDS); // // threadLoopRunner.runLoop(ScheduleDelayMode.FIXED_DELAY); // StopWatch sw = new StopWatch(); // threadLoopRunner.suspendLoop(); // int timeToSuspendMillis = (int) sw.getLapTimeMillis(); // logger.info("Time to suspend an action running for " + sleepMillis + " ms: " + timeToSuspendMillis); // assertTrue(timeToSuspendMillis <= allowedThreadJitterMillis); // // // // // // // // Test restarting the loop before last action has finished // // threadLoopRunner.runLoop(ScheduleDelayMode.FIXED_DELAY); // } // // //// public void testLongRunningAction() throws Exception { //// int delayMillis = 1000; //// int sleepMillis = 2000; //// int allowedThreadJitterMillis = 50; //// //// // test action that takes longer than foreseen time in FIXED_RATE mode //// CountDownLatch sync = new CountDownLatch(2); //// MyActionWithCancelCheck myAction = new MyActionWithCancelCheck(sync, sleepMillis, logger); //// //// ThreadLoopRunner threadLoopRunner = new ThreadLoopRunner(tf, myAction, logger); //// threadLoopRunner.setRepeatTime(delayMillis, TimeUnit.MILLISECONDS); //// //// // call shutdown during execution of the loop action and test cancel functionality //// threadLoopRunner.runLoop(ScheduleDelayMode.FIXED_RATEFIXED_DELAY); //// StopWatch sw = new StopWatch(); //// threadLoopRunner.shutdown(timeout, unit); //// assertTrue("Got a timeout", sync.await(delayMillis / 2 + allowedThreadJitterMillis, TimeUnit.MILLISECONDS)); //// assertTrue(sw.getLapTimeMillis() > delayMillis / 2 - allowedThreadJitterMillis); //// assertEquals(1, myAction.getSleepCancelCount()); //// } //// // public void testIllegalInvocations() throws Exception { // // } /** * Action that tracks the invocations of the run() method and allows clients to wait for a certain number of such invocations */ private static class MyAction extends ThreadLoopRunner.CancelableRunnable { private volatile CountDownLatch sync; protected final int sleepMillis; protected final Logger logger; private final AtomicInteger count = new AtomicInteger(0); MyAction(CountDownLatch sync, int sleepMillis, Logger logger) { this.sleepMillis = sleepMillis; this.logger = logger; reset(sync); } void reset(CountDownLatch newSync) { this.sync = newSync; count.set(0); } public void run() { logger.info("Will sleep for " + sleepMillis + " ms."); try { sleep(); } catch (InterruptedException ex) { ex.printStackTrace(); } finally { logger.info("Woke up - " + count); count.incrementAndGet(); sync.countDown(); } } protected void sleep() throws InterruptedException { Thread.sleep(sleepMillis); } int getCount() { return count.get(); } } // // /** // * Variant that makes use of the {@link CancelableRunnable#shouldTerminate} flag // */ // private static class MyActionWithCancelCheck extends MyAction { // // private int countSleepCancel; // // MyActionWithCancelCheck(CountDownLatch sync, int sleepMillis, Logger logger) { // super(sync, sleepMillis, logger); // } // // protected void sleep() throws InterruptedException { // // first half // Thread.sleep(sleepMillis/2); // if (!shouldTerminate) { // // second half // Thread.sleep(sleepMillis / 2); // } // else { // countSleepCancel++; // logger.info("run (sleep) action cancelled, returning early."); // } // } // // int getSleepCancelCount() { // return countSleepCancel; // } // } }