/*
* 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);
}
}
}