All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.softhouse.common.testlib.ConcurrencyTester Maven / Gradle / Ivy

There is a newer version: 0.4.14
Show newest version
/* 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 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 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 failureReporter;
		private final CountDownLatch activeWorkers;

		private BarrieredRunnable(Runnable target, int iterationCount, CyclicBarrier startSignal, CountDownLatch activeWorkers,
				AtomicReference 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();
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy