/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* 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 org.pentaho.di.concurrency;
import com.google.common.base.Throwables;
import org.junit.Assert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class is aimed to be a general runner for concurrency tests. You need to follow a convention while using it. By
* it, there are two types of actors in multithreaded environment: monitored and background. The formers are active and
* are considered to do some mutations of class undergoing testing. The latter group is all about accessors, that
* normally should not change the state, but just ask for some information, e.g. invoke getters.
* <p/>
* There is a special condition flag, shared among all actors. Each of them must stop when it has found out the flag has
* been cleared. Also, in most cases it makes sense to clear the flag after any exception has raised (see {@linkplain
* StopOnErrorCallable}, because any actor can face with it in concurrency environment.
* <p/>
* The runner stores results of all actors, though in most cases this information is needless - what is important that
* is the fact the execution has completed with no errors.
*
* @author Andrey Khayrutdinov
*/
class ConcurrencyTestRunner<M, B> {
/**
* Runs all tasks and simply checks no exceptions were thrown during the execution. The timeout is 5 minutes.
*
* @param monitoredTasks active actors
* @param backgroundTasks background actors
* @param condition stop condition
* @throws Exception exception
*/
@SuppressWarnings( "unchecked" )
static void runAndCheckNoExceptionRaised( List<? extends Callable<?>> monitoredTasks,
List<? extends Callable<?>> backgroundTasks,
AtomicBoolean condition ) throws Exception {
ConcurrencyTestRunner<?, ?> runner = new ConcurrencyTestRunner( monitoredTasks, backgroundTasks, condition );
runner.runConcurrentTest();
runner.checkNoExceptionRaised();
}
private final List<? extends Callable<? extends M>> monitoredTasks;
private final List<? extends Callable<? extends B>> backgroundTasks;
private final AtomicBoolean condition;
private final long timeout;
private final Map<Callable<? extends M>, ExecutionResult<M>> monitoredResults;
private final Map<Callable<? extends B>, ExecutionResult<B>> backgroundResults;
private Exception exception;
ConcurrencyTestRunner( List<? extends Callable<? extends M>> monitoredTasks,
List<? extends Callable<? extends B>> backgroundTasks,
AtomicBoolean condition ) {
this( monitoredTasks, backgroundTasks, condition, TimeUnit.MINUTES.toMillis( 5 ) );
}
ConcurrencyTestRunner( List<? extends Callable<? extends M>> monitoredTasks,
List<? extends Callable<? extends B>> backgroundTasks,
AtomicBoolean condition,
long timeout ) {
this.monitoredTasks = monitoredTasks;
this.backgroundTasks = backgroundTasks;
this.condition = condition;
this.timeout = timeout;
this.monitoredResults = new HashMap<Callable<? extends M>, ExecutionResult<M>>( monitoredTasks.size() );
this.backgroundResults = new HashMap<Callable<? extends B>, ExecutionResult<B>>( backgroundTasks.size() );
}
void runConcurrentTest() throws Exception {
this.exception = null;
final int tasksAmount = monitoredTasks.size() + backgroundTasks.size();
final ExecutorService executors = Executors.newFixedThreadPool( tasksAmount );
try {
List<Future<? extends B>> background = new ArrayList<Future<? extends B>>( backgroundTasks.size() );
for ( Callable<? extends B> task : backgroundTasks ) {
background.add( executors.submit( task ) );
}
List<Future<? extends M>> monitored = new ArrayList<Future<? extends M>>( monitoredTasks.size() );
for ( Callable<? extends M> task : monitoredTasks ) {
monitored.add( executors.submit( task ) );
}
try {
final long start = System.currentTimeMillis();
while ( condition.get() && !isDone( monitored ) && checkTimeout( start ) ) {
Thread.sleep( 200 );
}
} catch ( Exception e ) {
exception = e;
}
condition.set( false );
for ( int i = 0; i < monitored.size(); i++ ) {
Future<? extends M> future = monitored.get( i );
monitoredResults.put( monitoredTasks.get( i ), ExecutionResult.from( future ) );
}
for ( int i = 0; i < background.size(); i++ ) {
Future<? extends B> future = background.get( i );
while ( !future.isDone() ) {
// wait: condition flag is cleared, thus background tasks must complete by convention
}
backgroundResults.put( backgroundTasks.get( i ), ExecutionResult.from( future ) );
}
} finally {
executors.shutdown();
}
}
private boolean isDone( List<? extends Future<?>> futures ) {
for ( Future<?> future : futures ) {
if ( !future.isDone() ) {
return false;
}
}
return true;
}
private boolean checkTimeout( long start ) throws TimeoutException {
if ( this.timeout > 0 ) {
if ( System.currentTimeMillis() - start > timeout ) {
throw new TimeoutException( "Execution time limit is exceeded: " + timeout + " ms." );
}
}
return true;
}
Exception getException() {
return exception;
}
List<Throwable> getTasksErrors() {
List<Throwable> errors = new ArrayList<Throwable>();
errors.addAll( pickupErrors( monitoredResults.values() ) );
errors.addAll( pickupErrors( backgroundResults.values() ) );
return errors;
}
private List<Throwable> pickupErrors( Collection<? extends ExecutionResult<?>> collection ) {
List<Throwable> errors = new ArrayList<Throwable>( collection.size() );
for ( ExecutionResult<?> result : collection ) {
if ( result.isError() ) {
errors.add( result.getThrowable() );
}
}
return errors;
}
void checkNoExceptionRaised() {
List<Throwable> errors = getTasksErrors();
if ( !errors.isEmpty() ) {
StringBuilder message = new StringBuilder( 1024 );
message.append( "There are expected no exceptions during the test, but " )
.append( errors.size() ).append( " raised:" );
for ( Throwable throwable : errors ) {
String stacktrace = Throwables.getStackTraceAsString( throwable );
message.append( '\n' ).append( stacktrace );
}
Assert.fail( message.toString() );
}
}
List<M> getMonitoredTasksResults() {
return pickupResults( monitoredResults.values() );
}
private <T> List<T> pickupResults( Collection<? extends ExecutionResult<T>> collection ) {
List<T> errors = new ArrayList<T>( collection.size() );
for ( ExecutionResult<T> result : collection ) {
if ( !result.isError() ) {
errors.add( result.getResult() );
}
}
return errors;
}
Map<Callable<? extends M>, ExecutionResult<M>> getMonitoredResults() {
return Collections.unmodifiableMap( monitoredResults );
}
}