/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.toolkit.modules.concurrency.internal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.After; import org.junit.Before; import org.junit.Test; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService; import de.rcenvironment.toolkit.modules.concurrency.api.CallablesGroup; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; import de.rcenvironment.toolkit.modules.concurrency.api.ThreadPoolManagementAccess; import de.rcenvironment.toolkit.modules.concurrency.api.threadcontext.ThreadContext; import de.rcenvironment.toolkit.modules.concurrency.api.threadcontext.ThreadContextBuilder; import de.rcenvironment.toolkit.modules.concurrency.api.threadcontext.ThreadContextHolder; /** * Tests for the shared thread pool represented by {@link AsyncTaskService}. * * @author Robert Mischke */ public class AsyncTaskServiceTest extends AbstractConcurrencyModuleTest { private static final int CALLABLES_TEST_WAIT_MSEC = 100; private static final int NUM_CALLABLES_FOR_GROUP_TEST = 50; private AsyncTaskService threadPool; private ThreadPoolManagementAccess threadPoolManagement; private final Log log = LogFactory.getLog(getClass()); /** * Resets the {@link AsyncTaskServiceImpl} before each test. */ @Before public void resetBefore() { threadPool = getAsyncTaskService(); threadPoolManagement = getThreadPoolManagement(); threadPoolManagement.reset(); } /** * Resets the {@link AsyncTaskServiceImpl} after each test. */ @After public void resetAfter() { log.debug(threadPoolManagement.getFormattedStatistics(false)); threadPoolManagement.reset(); } /** * getCurrentThreadCount() test. * * @throws InterruptedException on interruption */ @Test public void threadCount() throws InterruptedException { assertEquals(0, threadPoolManagement.getCurrentThreadCount()); final CountDownLatch latchIn = new CountDownLatch(1); final Semaphore semOut = new Semaphore(0); Runnable runnable = new Runnable() { @Override @TaskDescription("testThreadCount() Runnable") public void run() { try { latchIn.await(); semOut.release(); } catch (InterruptedException e) { log.warn(e); // only log compact exception on test interruption } } }; threadPool.execute(runnable); assertEquals(1, threadPoolManagement.getCurrentThreadCount()); threadPool.execute(runnable); assertEquals(2, threadPoolManagement.getCurrentThreadCount()); // wait for Runnables latchIn.countDown(); semOut.acquire(2); } /** * createCallablesGroup() test. */ @Test public void callablesGroup() { int numTasks = NUM_CALLABLES_FOR_GROUP_TEST; CallablesGroup<String> callablesGroup = getConcurrencyUtilsFactory().createCallablesGroup(String.class); final Random random = new Random(); for (int i = 1; i <= numTasks; i++) { final String result = Integer.toString(i); callablesGroup.add(new Callable<String>() { @Override @TaskDescription("testCallablesGroup() Callable") public String call() throws Exception { // minimum wait prevents thread reuse; causes n threads to be spawned // random wait causes tasks to finish out of sequence Thread.sleep(CALLABLES_TEST_WAIT_MSEC + random.nextInt(CALLABLES_TEST_WAIT_MSEC)); return result; } }); } assertEquals("Premature start of tasks", 0, threadPoolManagement.getCurrentThreadCount()); List<String> results = callablesGroup.executeParallel(null); assertEquals("Incomplete parallelization?", numTasks, threadPoolManagement.getCurrentThreadCount()); assertEquals(numTasks, results.size()); for (int i = 1; i <= numTasks; i++) { assertEquals(results.get(i - 1), Integer.toString(i)); } } // TODO add test for exception-throwing Callables // TODO add test for uncaught exceptions from Runnables and Callables // TODO add test for task canceling /** * Verifies that there is sufficient task throughput (and therefore, no unusual congestion) in the common thread pool. * * @throws InterruptedException on test interruption */ @Test public void commonPoolTaskThroughput() throws InterruptedException { final int taskCount = 10000; // must be significantly greater than the max thread pool size final int waitTimeForCompletion = 5000; final CountDownLatch counter = new CountDownLatch(taskCount); for (int i = 0; i < taskCount; i++) { threadPool.execute(new Runnable() { @Override public void run() { sleep(10); counter.countDown(); } }); } assertTrue(counter.await(waitTimeForCompletion, TimeUnit.MILLISECONDS)); final int waitTimeBeforeShutdown = 500; sleep(waitTimeBeforeShutdown); // prevent irrelevant shutdown warnings } /** * Verifies that scheduled tasks are actually executed concurrently, ie in more than one thread. */ @Test public void scheduledTasksRunConcurrently() { final AtomicInteger counter = new AtomicInteger(); final AtomicBoolean success = new AtomicBoolean(); // test time scale; adjust if test fails on slow machines final int waitTimeSlice = 300; threadPool.scheduleAfterDelay(new Runnable() { @Override public void run() { log.debug("Running in thread " + Thread.currentThread().getName()); counter.incrementAndGet(); sleep(2 * waitTimeSlice); if (counter.get() == 2) { // only reached if the second task ran in parallel success.set(true); } } }, 0); threadPool.scheduleAfterDelay(new Runnable() { @Override public void run() { log.debug("Running in thread " + Thread.currentThread().getName()); sleep(1 * waitTimeSlice); counter.incrementAndGet(); } }, 0); sleep(3 * waitTimeSlice); assertEquals(Boolean.TRUE, success.get()); } /** * Verifies that the calling thread's {@link ThreadContext} is transfered to the spawned tasks' threads. * * @throws Exception none expected */ @Test public void threadContextTransfer() throws Exception { final String testValue1 = "test1"; final ThreadContext initialContext = ThreadContextBuilder.empty().setAspect(String.class, testValue1).build(); ThreadContextHolder.setCurrentContext(initialContext); verifyContextStateInsideTasks(initialContext, testValue1); // intentionally testing null context after a custom context was already set ThreadContextHolder.setCurrentContext(null); verifyContextStateInsideTasks(null, null); } private void verifyContextStateInsideTasks(final ThreadContext expectedContext, final String expectedValue) throws InterruptedException, ExecutionException, TimeoutException { // test Runnable final CountDownLatch test1CDL = new CountDownLatch(1); threadPool.execute(new Runnable() { @Override @TaskDescription("Runnable context test") public void run() { assertEquals(expectedContext, ThreadContextHolder.getCurrentContext()); assertEquals(expectedValue, ThreadContextHolder.getCurrentContextAspect(String.class)); test1CDL.countDown(); } }); test1CDL.await(1, TimeUnit.SECONDS); // test Callable final Future<String> test2Future = threadPool.submit(new Callable<String>() { @Override @TaskDescription("Callable context test") public String call() throws Exception { assertEquals(expectedContext, ThreadContextHolder.getCurrentContext()); return ThreadContextHolder.getCurrentContextAspect(String.class); } }); test2Future.get(1, TimeUnit.SECONDS); } private void sleep(int msec) { try { Thread.sleep(msec); } catch (InterruptedException e) { throw new RuntimeException(e); } } }