/* Copyright 2013 Jonatan Jönsson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package se.softhouse.common.testlib;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.fest.assertions.Assertions.assertThat;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.concurrent.Immutable;
import com.google.common.util.concurrent.Atomics;
/**
* Helps to test how code works when it's concurrently accessed.
*/
@Immutable
public final class ConcurrencyTester
{
private ConcurrencyTester()
{
}
/**
* Can {@link #create(int) create} unique {@link Runnable}s based on a number. Used as input to
* {@link ConcurrencyTester#verify(RunnableFactory, long, TimeUnit)}.
*/
public interface RunnableFactory
{
/**
* Number of times to run each of the {@link #create(int) created} {@link Runnable}s.
*/
int iterationCount();
/**
* Creates a Runnable that should be run {@link #iterationCount()} number of times.
* This method is called {@link ConcurrencyTester#NR_OF_CONCURRENT_RUNNERS} times so the
* total number of executions will be {@code iteratationCount * NR_OF_CONCURRENT_RUNNERS}.
* Any {@link RuntimeException} or {@link Error} thrown from the created runnable will be
* caught and propagated through
* {@link ConcurrencyTester#verify(RunnableFactory, long, TimeUnit)}.
* For performance and test-harness reasons the variables created based on
* {@code uniqueNumber} should be saved and reused by the repeated runs.
*
* @param uniqueNumber
* should be used to differentiate results made from different threads to
* increase the odds of detecting thread-safety issues.
*/
Runnable create(int uniqueNumber);
}
/**
* The threads should have to fight for CPU time
*/
private static final int RUNNERS_PER_PROCESSOR = 3;
/**
* A suitable thread count to have alive at the same time to cause some intended contention.
*/
public static final int NR_OF_CONCURRENT_RUNNERS = Runtime.getRuntime().availableProcessors() * RUNNERS_PER_PROCESSOR;
/**
* Verifies that {@link Runnable}s created with {@code factory} can be run concurrently.
* Waits for the whole execution to finish for {@code timeout} in {@code unit} time.
*
* @throws Throwable if any errors occurs during the concurrent executions
*/
public static void verify(RunnableFactory factory, long timeout, TimeUnit unit) throws Throwable
{
/**
* Makes sure that all threads are alive at the same time
*/
CyclicBarrier startSignal = new CyclicBarrier(NR_OF_CONCURRENT_RUNNERS);
CountDownLatch activeWorkers = new CountDownLatch(NR_OF_CONCURRENT_RUNNERS);
/**
* Used by other threads to report failure
*/
AtomicReference<Throwable> failureReporter = Atomics.newReference();
ExecutorService executor = Executors.newFixedThreadPool(NR_OF_CONCURRENT_RUNNERS);
int iterationCount = factory.iterationCount();
for(int i = 0; i < NR_OF_CONCURRENT_RUNNERS; i++)
{
Runnable codeToTest = checkNotNull(factory.create(i));
executor.execute(new BarrieredRunnable(codeToTest, iterationCount, startSignal, activeWorkers, failureReporter));
}
InterruptedException interrupted = null;
try
{
if(!activeWorkers.await(timeout, unit))
throw new AssertionError(activeWorkers.getCount() + " of " + NR_OF_CONCURRENT_RUNNERS + " did not finish within " + timeout + " "
+ unit);
}
catch(InterruptedException e)
{
// Makes this method reentrant from the same thread
// Otherwise there could be a risk that await would throw
// InterruptedException again without actually running any code again
Thread.interrupted();
interrupted = e;
}
List<Runnable> leftoverTasks = executor.shutdownNow();
if(failureReporter.get() != null)
throw failureReporter.get();
if(interrupted != null)
// We were interrupted while verifying, propagate so that tests finish up quickly
throw interrupted;
assertThat(leftoverTasks).as("Tasks remained even though activeWorkers reached zero").isEmpty();
}
/**
* Runs a {@link Runnable} ({@code iterationCount} times) after a {@code startSignal} has been
* given. When errors are encountered they are reported to {@code failureReporter} and the
* thread that created this instance is then {@link Thread#interrupt() interrupted}.
*/
private static final class BarrieredRunnable implements Runnable
{
private final Thread originThread;
private final Runnable target;
private final int iterationCount;
private final CyclicBarrier startSignal;
private final AtomicReference<Throwable> failureReporter;
private final CountDownLatch activeWorkers;
private BarrieredRunnable(Runnable target, int iterationCount, CyclicBarrier startSignal, CountDownLatch activeWorkers,
AtomicReference<Throwable> failureReporter)
{
this.originThread = Thread.currentThread();
this.target = target;
this.iterationCount = iterationCount;
this.startSignal = startSignal;
this.activeWorkers = activeWorkers;
this.failureReporter = failureReporter;
}
@Override
public void run()
{
try
{
// Give all threads at most 10 seconds to come alive
startSignal.await(10, TimeUnit.SECONDS);
for(int i = 0; i < iterationCount; i++)
{
target.run();
startSignal.await();
}
}
catch(Throwable e)
{
// Don't report secondary failures
if(failureReporter.compareAndSet(null, e))
{
originThread.interrupt();
}
return;
}
activeWorkers.countDown();
}
}
}