/* * Copyright (C) 2012 Facebook, Inc. * * 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 com.facebook.concurrency; import com.google.common.collect.Iterators; import java.util.Iterator; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import com.facebook.logging.Logger; import com.facebook.logging.LoggerImpl; import com.facebook.util.ExtRunnable; import com.facebook.util.exceptions.ExceptionHandler; /** * Utility class in order to execute tasks in parallel on top of an executor, but bound the * number of concurrent tasks used in that executor. Note, if the executor itself has a bound * lower than specified, that bound will of course be used. */ public class ParallelRunner { private static final Logger LOG = LoggerImpl.getLogger(ParallelRunner.class); private static final String DEFAULT_NAME_PREFIX = "ParallelRun-"; private final AtomicLong instanceNumber = new AtomicLong(0); private final ExecutorService executor; private final String defaultNamePrefix; /** * Create an instance on top of an underlying executor * * @param executor executor to wrap * @param defaultNamePrefix borrowed threads will use this name prefix */ public ParallelRunner(ExecutorService executor, String defaultNamePrefix) { this.executor = executor; this.defaultNamePrefix = defaultNamePrefix; } /** * use a default naming prefix, ParallelRunner.DEFAULT_NAME_PREFIX * * @param executor */ public ParallelRunner(ExecutorService executor) { this(executor, DEFAULT_NAME_PREFIX); } /** * Helper using default name ParallelRunner.DEFAULT_NAME_PREFIX and Iterables of ExtRunnables * * @param tasks * @param numThreads * @param exceptionHandler * @param <E> * @throws E */ public <E extends Exception> void parallelRunExt( Iterable<? extends ExtRunnable<E>> tasks, int numThreads, final ExceptionHandler<E> exceptionHandler ) throws E { parallelRunExt(tasks.iterator(), numThreads, exceptionHandler); } /** * Helper using default name ParallelRunner.DEFAULT_NAME_PREFIX * * @param tasksIter * @param numThreads * @param exceptionHandler * @param <E> * @throws E */ public <E extends Exception> void parallelRunExt( Iterator<? extends ExtRunnable<E>> tasksIter, int numThreads, final ExceptionHandler<E> exceptionHandler ) throws E { parallelRunExt( tasksIter, numThreads, exceptionHandler, defaultNamePrefix + instanceNumber.getAndIncrement() ); } /** * Helper function for Iterables and ExtRunnables * * @param tasks * @param numThreads * @param exceptionHandler * @param baseName * @param <E> * @throws E */ public <E extends Exception> void parallelRunExt( Iterable<? extends ExtRunnable<E>> tasks, int numThreads, final ExceptionHandler<E> exceptionHandler, String baseName ) throws E { parallelRunExt(tasks.iterator(), numThreads, exceptionHandler, baseName); } /** * Adapter methods for ExtRunnable<E> to convert to native Runnable format. An ExceptionHandler * will be used to guarantee type E is thrown, and only one, the "first" exception will be * thrown. The system is fail-fast in that once a task execution observes an exception has * occurred, it does not run additional tasks. * * It has the same contract as far as executing tasks as they are extracted from the Iterator * * @param tasksIter * @param numThreads * @param exceptionHandler * @param baseName * @param <E> * @throws E */ public <E extends Exception> void parallelRunExt( Iterator<? extends ExtRunnable<E>> tasksIter, int numThreads, final ExceptionHandler<E> exceptionHandler, String baseName ) throws E { final AtomicReference<E> exception = new AtomicReference<E>(); Iterator<Runnable> wrappedIterator = Iterators.transform(tasksIter, new ShortCircuitRunnable<>(exception, exceptionHandler)); parallelRun(wrappedIterator, numThreads, baseName); if (exception.get() != null) { throw exception.get(); } } /** * helper method with default name prefix ParallelRunner.DEFAULT_NAME_PREFIX for Iterabless * * @param tasks * @param numThreads */ public void parallelRun(Iterable<? extends Runnable> tasks, int numThreads) { parallelRun(tasks.iterator(), numThreads); } /** * helper method with default name prefix ParallelRunner.DEFAULT_NAME_PREFIX * @param tasksIter * @param numThreads */ public void parallelRun(Iterator<? extends Runnable> tasksIter, int numThreads) { parallelRun( tasksIter, numThreads, defaultNamePrefix + instanceNumber.getAndIncrement() ); } /** * adapter method for Iterables * * @param tasks * @param numThreads * @param baseName */ public void parallelRun( Iterable<? extends Runnable> tasks, int numThreads, String baseName ) { parallelRun(tasks.iterator(), numThreads, baseName); } /** * This is the core method of ParallelRunner , which takes an iterator of tasks. It is ideal as * often it is desirable to begin execution of tasks before the entire set has been created. In * this way, task are started immediately as they are pulled off of the iterator than than * draining the iterator and then executing them. * * Clients may use this fact and create Iterators that are more of a "queue" and take advantage * of this fact. Another way to look at this is as this is a consumer of tasks that come from a * producer (iterator). The expectation is that eventually, most use cases will eventually quit * producing tasks, and hence taskIter.hasNext() return false. * * There is nothing in the implementation that requires this, however, and if a client * constructs an unbounded Iterator, this will function correctly. * * @param tasksIter * @param numThreads * @param baseName */ public void parallelRun( Iterator<? extends Runnable> tasksIter, int numThreads, String baseName ) { ExecutorService executorForInvocation; // create a virtual executor that bounds the # of threads we can use // for this run executorForInvocation = new UnstoppableExecutorService( new ExecutorServiceFront( new LinkedBlockingQueue<Runnable>(), executor, baseName, numThreads ) ); int totalTasks = 0; while (tasksIter.hasNext()) { executorForInvocation.execute(tasksIter.next()); totalTasks++; } // now wait for everything to finish executorForInvocation.shutdown(); try { while (!executorForInvocation.awaitTermination(10, TimeUnit.SECONDS)) { LOG.info( "(%d) %s waited 10s for %d tasks, waiting some more", Thread.currentThread().getId(), baseName, totalTasks ); } LOG.info( "(%d) tasksIter for %s completed", Thread.currentThread().getId(), baseName ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.warn("interrupted waiting for tasks to complete", e); } } }