package org.infinispan.test;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import javax.transaction.TransactionManager;
import org.infinispan.test.fwk.ChainMethodInterceptor;
import org.infinispan.test.fwk.NamedTestMethod;
import org.infinispan.test.fwk.TestResourceTracker;
import org.infinispan.test.fwk.TestSelector;
import org.infinispan.util.DefaultTimeService;
import org.infinispan.util.TimeService;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import org.testng.IMethodInstance;
import org.testng.IMethodInterceptor;
import org.testng.ITestContext;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Listeners;
import org.testng.internal.MethodInstance;
/**
* AbstractInfinispanTest is a superclass of all Infinispan tests.
*
* @author Vladimir Blagojevic
* @author Mircea.Markus@jboss.com
* @since 4.0
*/
@Listeners(ChainMethodInterceptor.class)
@TestSelector(interceptors = AbstractInfinispanTest.OrderByInstance.class)
public class AbstractInfinispanTest {
protected final Log log = LogFactory.getLog(getClass());
private final ThreadFactory defaultThreadFactory = getTestThreadFactory("ForkThread");
private final ThreadPoolExecutor defaultExecutorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
defaultThreadFactory);
public static final TimeService TIME_SERVICE = new DefaultTimeService();
public static class OrderByInstance implements IMethodInterceptor {
@Override
public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) {
Map<Object, List<IMethodInstance>> methodsByInstance = new IdentityHashMap<>();
for (IMethodInstance method : methods) {
methodsByInstance.computeIfAbsent(method.getInstance(), k -> new ArrayList<>()).add(method);
}
List<IMethodInstance> newOrder = new ArrayList<>(methods.size());
for (Map.Entry<Object, List<IMethodInstance>> instanceAndMethods : methodsByInstance.entrySet()) {
Object instance = instanceAndMethods.getKey();
if (instance instanceof AbstractInfinispanTest) {
String parameters = ((AbstractInfinispanTest) instance).parameters();
if (parameters != null) {
for (IMethodInstance method : instanceAndMethods.getValue()) {
// TestNG calls intercept twice (bug?) so this prevents adding the parameters two times
if (method.getMethod() instanceof NamedTestMethod) {
newOrder.add(method);
} else {
newOrder.add(new MethodInstance(new NamedTestMethod(method.getMethod(), method.getMethod().getMethodName() + parameters)));
}
}
continue;
}
}
newOrder.addAll(instanceAndMethods.getValue());
}
return newOrder;
}
}
protected String parameters() {
return null;
}
@BeforeClass(alwaysRun = true)
protected final void testClassStarted(ITestContext context) {
String fullName = context.getName();
String simpleName = fullName.substring(fullName.lastIndexOf('.') + 1);
Class testClass = context.getCurrentXmlTest().getXmlClasses().get(0).getSupportClass();
if (!simpleName.equals(testClass.getSimpleName()) && !Thread.currentThread().getName().equals("main")) {
log.warnf("Wrong test name %s for class %s", simpleName, testClass.getSimpleName());
}
TestResourceTracker.testStarted(getTestName());
}
@AfterClass(alwaysRun = true)
protected final void testClassFinished() {
killSpawnedThreads();
TestResourceTracker.testFinished(getTestName());
}
public String getTestName() {
// will qualified test name and parameters, thread names can be quite long when debugging
if (Boolean.getBoolean("test.infinispan.shortTestName")) {
return "test";
}
String parameters = parameters();
return parameters == null ? getClass().getName() : getClass().getName() + parameters;
}
protected void killSpawnedThreads() {
List<Runnable> runnables = defaultExecutorService.shutdownNow();
if (!runnables.isEmpty()) {
log.errorf("There were runnables %s left uncompleted in test %s", runnables, getClass().getSimpleName());
}
}
@AfterMethod
protected void checkThreads() {
int activeTasks = defaultExecutorService.getActiveCount();
if (activeTasks != 0) {
log.errorf("There were %d active tasks found in the test executor service for class %s", activeTasks,
getClass().getSimpleName());
}
}
protected <T> void eventuallyEquals(T expected, Supplier<T> supplier) {
eventually(() -> "expected:<" + expected + ">, got:<" + supplier.get() + ">",
() -> Objects.equals(expected, supplier.get()));
}
protected <T> void eventuallyEquals(String message, T expected, Supplier<T> supplier) {
eventually(() -> message + " expected:<" + expected + ">, got:<" + supplier.get() + ">",
() -> Objects.equals(expected, supplier.get()));
}
protected void eventually(Supplier<String> messageSupplier, BooleanSupplier condition) {
eventually(messageSupplier, condition, 30, TimeUnit.SECONDS);
}
protected void eventually(Supplier<String> messageSupplier, BooleanSupplier condition, long timeout,
TimeUnit timeUnit) {
try {
long timeoutNanos = timeUnit.toNanos(timeout);
// We want the sleep time to increase in arithmetic progression
// 30 loops with the default timeout of 30 seconds means the initial wait is ~ 65 millis
int loops = 30;
int progressionSum = loops * (loops + 1) / 2;
long initialSleepNanos = timeoutNanos / progressionSum;
long sleepNanos = initialSleepNanos;
long expectedEndTime = System.nanoTime() + timeoutNanos;
while (expectedEndTime - System.nanoTime() > 0) {
if (condition.getAsBoolean())
return;
LockSupport.parkNanos(sleepNanos);
sleepNanos += initialSleepNanos;
}
if (!condition.getAsBoolean()) {
fail(messageSupplier.get());
}
} catch (Exception e) {
throw new RuntimeException("Unexpected!", e);
}
}
protected void eventually(Condition ec, long timeoutMillis) {
eventually(ec, timeoutMillis, TimeUnit.MILLISECONDS);
}
/**
* @deprecated Use {@link #eventually(Condition, long, long, TimeUnit)} instead.
*/
@Deprecated
protected void eventually(Condition ec, long timeoutMillis, int loops) {
eventually(null, ec, timeoutMillis, loops);
}
/**
* @deprecated Use {@link #eventually(String, Condition, long, long, TimeUnit)} instead.
*/
@Deprecated
protected void eventually(String message, Condition ec, long timeoutMillis, int loops) {
if (loops <= 0) {
throw new IllegalArgumentException("Number of loops must be positive");
}
long sleepDuration = timeoutMillis / loops + 1;
eventually(message, ec, timeoutMillis, sleepDuration, TimeUnit.MILLISECONDS);
}
protected void eventually(Condition ec, long timeout, TimeUnit unit) {
eventually(null, ec, unit.toMillis(timeout), 500, TimeUnit.MILLISECONDS);
}
protected void eventually(Condition ec, long timeout, long pollInterval, TimeUnit unit) {
eventually(null, ec, timeout, pollInterval, unit);
}
protected void eventually(String message, Condition ec, long timeout, long pollInterval, TimeUnit unit) {
if (pollInterval <= 0) {
throw new IllegalArgumentException("Check interval must be positive");
}
try {
long expectedEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeout, unit);
long sleepMillis = TimeUnit.MILLISECONDS.convert(pollInterval, unit);
while (expectedEndTime - System.nanoTime() > 0) {
if (ec.isSatisfied()) return;
Thread.sleep(sleepMillis);
}
assertTrue(message, ec.isSatisfied());
} catch (Exception e) {
throw new RuntimeException("Unexpected!", e);
}
}
/**
* This method will actually spawn a fresh thread and will not use underlying pool. The
* {@link org.infinispan.test.AbstractInfinispanTest#fork(Runnable)} should be preferred
* unless you require explicit access to the thread.
* @param r The runnable to run
* @return The created thread
*/
protected Thread inNewThread(Runnable r) {
final Thread t = defaultThreadFactory.newThread(new RunnableWrapper(r));
log.tracef("About to start thread '%s' as child of thread '%s'", t.getName(), Thread.currentThread().getName());
t.start();
return t;
}
protected Future<?> fork(Runnable r) {
return defaultExecutorService.submit(new RunnableWrapper(r));
}
protected <T> Future<T> fork(Runnable r, T result) {
return defaultExecutorService.submit(new RunnableWrapper(r), result);
}
protected <T> Future<T> fork(Callable<T> c) {
return defaultExecutorService.submit(new LoggingCallable<>(c));
}
/**
* Returns an executor service that can be used by tests which has it's lifecycle handled
* by the test container itself.
* @param <V> The desired type provided by the user of the CompletionService
* @return The completion service that should be used by tests
*/
protected <V> CompletionService<V> completionService() {
return new ExecutorCompletionService<>(defaultExecutorService);
}
/**
* This should normally not be used, use the {@link AbstractInfinispanTest#completionService()}
* method when an executor is required. Although if you want a limited set of threads this could
* still be useful for something like {@link java.util.concurrent.Executors#newFixedThreadPool(int, java.util.concurrent.ThreadFactory)} or
* {@link java.util.concurrent.Executors#newSingleThreadExecutor(java.util.concurrent.ThreadFactory)}
* @param prefix The prefix starting for the thread factory
* @return A thread factory that will use the same naming schema as the other methods
*/
protected ThreadFactory getTestThreadFactory(final String prefix) {
final String className = getClass().getSimpleName();
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
String threadName = prefix + "-" + counter.incrementAndGet() + "," + className;
Thread thread = new Thread(r, threadName);
TestResourceTracker.addResource(AbstractInfinispanTest.this, new ThreadCleaner(thread));
return thread;
}
};
return threadFactory;
}
/**
* This will run the various callables at approximately the same time by ensuring they all start before
* allowing the callables to proceed. The callables may not be running yet at the offset of the return of
* the completion service.
*
* This method doesn't allow distinction between which Future applies to which Callable. If that is required
* it is suggested to use {@link AbstractInfinispanTest#completionService()} and retrieve the future
* directly when submitting the Callable while also wrapping your callables in something like
* {@link org.infinispan.test.AbstractInfinispanTest.ConcurrentCallable} to ensure your callables will be invoked
* at approximately the same time.
* @param task1 First task to add - required to ensure at least 2 callables are provided
* @param task2 Second task to add - required to ensure at least 2 callables are provided
* @param tasks The callables to run
* @param <V> The type of callables
* @return The completion service that can be used to query when a callable completes
*/
protected <V> CompletionService<V> runConcurrentlyWithCompletionService(Callable<? extends V> task1,
Callable<? extends V> task2,
Callable<? extends V>... tasks) {
if (task1 == null) {
throw new NullPointerException("Task1 was not provided!");
}
if (task2 == null) {
throw new NullPointerException("Task2 was not provided!");
}
Callable[] callables = new Callable[tasks.length + 2];
int i = 0;
callables[i++] = task1;
callables[i++] = task2;
for (Callable<? extends V> callable : tasks) {
callables[i++] = callable;
}
return runConcurrentlyWithCompletionService(callables);
}
/**
* This will run the various callables at approximately the same time by ensuring they all start before
* allowing the callables to proceed. The callables may not be running yet at the offset of the return of
* the completion service.
*
* This method doesn't allow distinction between which Future applies to which Callable. If that is required
* it is suggested to use {@link AbstractInfinispanTest#completionService()} and retrieve the future
* directly when submitting the Callable while also wrapping your callables in something like
* {@link org.infinispan.test.AbstractInfinispanTest.ConcurrentCallable} to ensure your callables will be invoked
* at approximately the same time.
* @param tasks The callables to run. Note the type isn't provided since it is difficult
* @param <V> The type of callables
* @return The completion service that can be used to query when a callable completes
*/
protected <V> CompletionService<V> runConcurrentlyWithCompletionService(Callable[] tasks) {
if (tasks.length == 0) {
throw new IllegalArgumentException("Provided tasks array was empty");
}
CompletionService<V> completionService = completionService();
CyclicBarrier barrier = new CyclicBarrier(tasks.length);
for (Callable task : tasks) {
completionService.submit(new ConcurrentCallable<>(new LoggingCallable<V>(task), barrier));
}
return completionService;
}
/**
* This will run the various callables at approximately the same time by ensuring they all start before
* allowing the callables to proceed. All callables will be ensure to completion else a TimeoutException will be thrown
* @param task1 First task to add - required to ensure at least 2 callables are provided
* @param task2 Second task to add - required to ensure at least 2 callables are provided
* @param tasks The callables to run
* @param <V> The type of callabels
* @throws InterruptedException Thrown if this thread is interrupted
* @throws ExecutionException Thrown if one of the callables throws any kind of Throwable. The
* thrown Throwable will be wrapped by this exception
* @throws TimeoutException If one of the callables doesn't complete within 10 seconds
*/
protected <V> void runConcurrently(Callable<? extends V> task1, Callable<? extends V> task2,
Callable<? extends V>... tasks) throws InterruptedException,ExecutionException,
TimeoutException {
CompletionService<V> completionService = runConcurrentlyWithCompletionService(task1, task2, tasks);
waitForCompletionServiceTasks(completionService, tasks.length + 2);
}
/**
* This will run the various callables at approximately the same time by ensuring they all start before
* allowing the callables to proceed. All callables will be ensure to completion else a TimeoutException will be thrown
* @param tasks The callables to run
* @param <V> The type of callables
* @throws InterruptedException Thrown if this thread is interrupted
* @throws ExecutionException Thrown if one of the callables throws any kind of Throwable. The
* thrown Throwable will be wrapped by this exception
* @throws TimeoutException If one of the callables doesn't complete within 10 seconds
*/
protected <V> void runConcurrently(Callable<? extends V>[] tasks) throws InterruptedException,ExecutionException,
TimeoutException {
CompletionService<V> completionService = runConcurrentlyWithCompletionService(tasks);
waitForCompletionServiceTasks(completionService, tasks.length);
}
/**
* Waits for the desired numberOfTasks in the completion service to complete one at a time polling for each up to
* 10 seconds. If one of the tasks produces an exception, the first one is rethrown wrapped in a ExecutionException
* after all tasks have completed.
* @param completionService The completion service that had tasks submitted to that total numberOfTasks
* @param numberOfTasks How many tasks have been sent to the service
* @throws InterruptedException If this thread is interruped while waiting for the tasks to complete
* @throws ExecutionException Thrown if one of the tasks produces an exception. The first encounted exception will
* be thrown
* @throws TimeoutException If a task is not found to complete within 10 seconds of the previously completed or the
* first task.
*/
protected void waitForCompletionServiceTasks(CompletionService<?> completionService, int numberOfTasks)
throws InterruptedException,ExecutionException, TimeoutException {
if (completionService == null) {
throw new NullPointerException("Provided completionService cannot be null");
}
if (numberOfTasks <= 0) {
throw new IllegalArgumentException("Provided numberOfTasks cannot be less than or equal to 0");
}
// check for any errors
ExecutionException exception = null;
for (int i = 0; i < numberOfTasks; i++) {
try {
Future<?> future = completionService.poll(10, TimeUnit.SECONDS);
if (future == null) {
throw new TimeoutException("Concurrent task didn't complete within 10 seconds!");
}
// No timeout since it is guaranteed to be done
future.get();
} catch (ExecutionException e) {
log.debug("Exception in concurrent task", e);
if (exception == null) {
exception = e;
}
}
}
// If there was an exception throw the last one
if (exception != null) {
throw exception;
}
}
/**
* This will run the callable at approximately the same time in different thrads by ensuring they all start before
* allowing the callables to proceed. All callables will be ensure to completion else a TimeoutException will be thrown
*
* The callable itself should be thread safe since it will be invoked from multiple threads.
* @param callable The callable to run multiple times
* @param <V> The type of callable
* @throws InterruptedException Thrown if this thread is interrupted
* @throws ExecutionException Thrown if one of the callables throws any kind of Throwable. The
* thrown Throwable will be wrapped by this exception
* @throws TimeoutException If one of the callables doesn't complete within 10 seconds
*/
protected <V> void runConcurrently(Callable<V> callable, int invocationCount) throws InterruptedException,
ExecutionException,
TimeoutException {
if (callable == null) {
throw new NullPointerException("Provided callable cannot be null");
}
if (invocationCount <= 0) {
throw new IllegalArgumentException("Provided invocationCount cannot be less than or equal to 0");
}
CompletionService<V> completionService = completionService();
CyclicBarrier barrier = new CyclicBarrier(invocationCount);
for (int i = 0; i < invocationCount; ++i) {
completionService.submit(new ConcurrentCallable<>(new LoggingCallable<>(callable), barrier));
}
waitForCompletionServiceTasks(completionService, invocationCount);
}
/**
* A callable that will first await on the provided barrier before calling the provided callable.
* This is useful to have a better attempt at multiple threads ran at the same time, but still is
* no guarantee since this is controlled by the thread scheduler.
* @param <V>
*/
public final class ConcurrentCallable<V> implements Callable<V> {
private final Callable<? extends V> callable;
private final CyclicBarrier barrier;
public ConcurrentCallable(Callable<? extends V> callable, CyclicBarrier barrier) {
this.callable = callable;
this.barrier = barrier;
}
@Override
public V call() throws Exception {
log.trace("About to wait on provided barrier");
try {
barrier.await(10, TimeUnit.SECONDS);
log.trace("Completed await on provided barrier");
} catch (InterruptedException | TimeoutException | BrokenBarrierException e) {
log.tracef("Barrier await was broken due to exception", e);
}
return callable.call();
}
}
public final class RunnableWrapper implements Runnable {
final Runnable realOne;
public RunnableWrapper(Runnable realOne) {
this.realOne = realOne;
}
@Override
public void run() {
try {
log.trace("Started fork runnable..");
realOne.run();
log.debug("Exiting fork runnable.");
} catch (Throwable e) {
log.debug("Exiting fork runnable due to exception", e);
throw e;
}
}
}
protected void eventually(Condition ec) {
eventually(ec, 10000);
}
protected void eventually(String message, Condition ec) {
eventually(message, ec, 10000, 500, TimeUnit.MILLISECONDS);
}
protected interface Condition {
boolean isSatisfied() throws Exception;
}
private class LoggingCallable<T> implements Callable<T> {
private final Callable<? extends T> c;
public LoggingCallable(Callable<? extends T> c) {
this.c = c;
}
@Override
public T call() throws Exception {
try {
log.trace("Started fork callable..");
T result = c.call();
log.debug("Exiting fork callable.");
return result;
} catch (Exception e) {
log.warn("Exiting fork callable due to exception", e);
throw e;
}
}
}
public void safeRollback(TransactionManager transactionManager) {
try {
transactionManager.rollback();
} catch (Exception e) {
//ignored
}
}
private class ThreadCleaner extends TestResourceTracker.Cleaner<Thread> {
public ThreadCleaner(Thread thread) {
super(thread);
}
@Override
public void close() {
if (ref.isAlive() && !ref.isInterrupted()) {
log.warnf("There was a thread %s still alive after test completion - interrupted it",
ref);
ref.interrupt();
}
}
}
}