/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.common.executors; import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.facebook.common.logging.FLog; /** * A {@link java.util.concurrent.ExecutorService} that delegates to an existing {@link Executor} * but constrains the number of concurrently executing tasks to a pre-configured value. */ public class ConstrainedExecutorService extends AbstractExecutorService { private static final Class<?> TAG = ConstrainedExecutorService.class; private final String mName; private final Executor mExecutor; private volatile int mMaxConcurrency; private final BlockingQueue<Runnable> mWorkQueue; private final Worker mTaskRunner; private final AtomicInteger mPendingWorkers; private final AtomicInteger mMaxQueueSize; /** * Creates a new {@code ConstrainedExecutorService}. * @param name Friendly name to identify the executor in logging and reporting. * @param maxConcurrency Maximum number of tasks to execute in parallel on the delegate executor. * @param executor Delegate executor for actually running tasks. * @param workQueue Queue to hold {@link Runnable}s for eventual execution. */ public ConstrainedExecutorService( String name, int maxConcurrency, Executor executor, BlockingQueue<Runnable> workQueue) { if (maxConcurrency <= 0) { throw new IllegalArgumentException("max concurrency must be > 0"); } mName = name; mExecutor = executor; mMaxConcurrency = maxConcurrency; mWorkQueue = workQueue; mTaskRunner = new Worker(); mPendingWorkers = new AtomicInteger(0); mMaxQueueSize = new AtomicInteger(0); } /** * Factory method to create a new {@code ConstrainedExecutorService} with an unbounded * {@link LinkedBlockingQueue} queue. * @param name Friendly name to identify the executor in logging and reporting. * @param maxConcurrency Maximum number of tasks to execute in parallel on the delegate executor. * @param queueSize Number of items that can be queued before new submissions are rejected. * @param executor Delegate executor for actually running tasks. * @return new {@code ConstrainedExecutorService} instance. */ public static ConstrainedExecutorService newConstrainedExecutor( String name, int maxConcurrency, int queueSize, Executor executor) { return new ConstrainedExecutorService( name, maxConcurrency, executor, new LinkedBlockingQueue<Runnable>(queueSize)); } /** * Determine whether or not the queue is idle. * @return true if there is no work being executed and the work queue is empty, false otherwise. */ public boolean isIdle() { return mWorkQueue.isEmpty() && (mPendingWorkers.get() == 0); } /** * Submit a task to be executed in the future. * @param runnable The task to be executed. */ @Override public void execute(Runnable runnable) { if (runnable == null) { throw new NullPointerException("runnable parameter is null"); } if (!mWorkQueue.offer(runnable)) { throw new RejectedExecutionException( mName + " queue is full, size=" + mWorkQueue.size()); } final int queueSize = mWorkQueue.size(); final int maxSize = mMaxQueueSize.get(); if ((queueSize > maxSize) && mMaxQueueSize.compareAndSet(maxSize, queueSize)) { FLog.v(TAG, "%s: max pending work in queue = %d", mName, queueSize); } // else, there was a race and another thread updated and logged the max queue size startWorkerIfNeeded(); } /** * Submits the single {@code Worker} instance {@code mTaskRunner} to the underlying executor an * additional time if there are fewer than {@code mMaxConcurrency} pending submissions. Does * nothing if the maximum number of workers is already pending. */ private void startWorkerIfNeeded() { // Perform a compare-and-swap retry loop for synchronization to make sure we don't start more // workers than desired. int currentCount = mPendingWorkers.get(); while (currentCount < mMaxConcurrency) { int updatedCount = currentCount + 1; if (mPendingWorkers.compareAndSet(currentCount, updatedCount)) { // Start a new worker. FLog.v(TAG, "%s: starting worker %d of %d", mName, updatedCount, mMaxConcurrency); mExecutor.execute(mTaskRunner); break; } // else: compareAndSet failed due to race; snapshot the new count and try again FLog.v(TAG, "%s: race in startWorkerIfNeeded; retrying", mName); currentCount = mPendingWorkers.get(); } } @Override public void shutdown() { throw new UnsupportedOperationException(); } @Override public List<Runnable> shutdownNow() { throw new UnsupportedOperationException(); } @Override public boolean isShutdown() { return false; } @Override public boolean isTerminated() { return false; } @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { throw new UnsupportedOperationException(); } /** * Private worker class that removes one task from the work queue and runs it. This class * maintains no state of its own, so a single instance may be submitted to an executor * multiple times. */ private class Worker implements Runnable { @Override public void run() { try { Runnable runnable = mWorkQueue.poll(); if (runnable != null) { runnable.run(); } else { // It is possible that a new worker was started before a previously started worker // de-queued its work item. FLog.v(TAG, "%s: Worker has nothing to run", mName); } } finally { int workers = mPendingWorkers.decrementAndGet(); if (!mWorkQueue.isEmpty()) { startWorkerIfNeeded(); } else { FLog.v(TAG, "%s: worker finished; %d workers left", mName, workers); } } } } }