package org.threadly.concurrent.wrapper.limiter;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import org.threadly.concurrent.AbstractSubmitterExecutor;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.concurrent.SubmitterExecutor;
import org.threadly.util.ArgumentVerifier;
/**
* This class is designed to limit how much parallel execution happens on a provided
* {@link Executor}. This allows the user to have one thread pool for all their code, and if they
* want certain sections to have less levels of parallelism (possibly because those those sections
* would completely consume the global pool), they can wrap the executor in this class.
* <p>
* Thus providing you better control on the absolute thread count and how much parallelism can
* occur in different sections of the program.
* <p>
* This is an alternative from having to create multiple thread pools. By using this you also
* are able to accomplish more efficiently thread use than multiple thread pools would.
*
* @since 4.6.0 (since 1.0.0 at org.threadly.concurrent.limiter)
*/
public class ExecutorLimiter extends AbstractSubmitterExecutor implements SubmitterExecutor {
protected final Executor executor;
protected final Queue<LimiterRunnableWrapper> waitingTasks;
protected final int maxConcurrency;
private final AtomicInteger currentlyRunning;
/**
* Construct a new execution limiter that implements the {@link Executor} interface.
*
* @param executor {@link Executor} to submit task executions to.
* @param maxConcurrency maximum quantity of runnables to run in parallel
*/
public ExecutorLimiter(Executor executor, int maxConcurrency) {
ArgumentVerifier.assertGreaterThanZero(maxConcurrency, "maxConcurrency");
ArgumentVerifier.assertNotNull(executor, "executor");
this.executor = executor;
this.waitingTasks = new ConcurrentLinkedQueue<>();
this.maxConcurrency = maxConcurrency;
this.currentlyRunning = new AtomicInteger(0);
}
/**
* Call to check what the maximum concurrency this limiter will allow.
*
* @return maximum concurrent tasks to be run
*/
public int getMaxConcurrency() {
return maxConcurrency;
}
/**
* Returns how many tasks are currently being "limited" and thus are in queue to run from this
* limiter.
*
* @return Quantity of tasks queued in this limiter
*/
public int getUnsubmittedTaskCount() {
return waitingTasks.size();
}
/**
* Thread safe verification that the pool has space remaining to accept additional tasks.
* <p>
* If this returns {@code true} {@code currentlyRunning} has been incremented and it expects the
* task to run will invoke {@link #handleTaskFinished()} when completed.
*
* @return {@code true} if the task can be submitted to the pool
*/
private boolean canSubmitTasksToPool() {
while (true) { // loop till we have a result
int currentValue = currentlyRunning.get();
if (currentValue < maxConcurrency) {
if (currentlyRunning.compareAndSet(currentValue, currentValue + 1)) {
return true;
} // else retry in while loop
} else {
return false;
}
}
}
protected void consumeAvailable() {
/* must synchronize in queue consumer to avoid
* multiple threads from consuming tasks in parallel
* and possibly emptying after .isEmpty() check but
* before .poll()
*/
synchronized (this) {
while (! waitingTasks.isEmpty() && canSubmitTasksToPool()) {
// by entering loop we can now execute task
LimiterRunnableWrapper lrw = waitingTasks.poll();
lrw.submitToExecutor();
}
}
}
@Override
protected void doExecute(Runnable task) {
LimiterRunnableWrapper lrw = new LimiterRunnableWrapper(executor, task);
executeWrapper(lrw);
}
/**
* Check that not only are we able to submit tasks to the pool, but there are no tasks currently
* waiting to already be submitted. If only {@link #canSubmitTasksToPool()} is checked, tasks
* may be able to cut in line with tasks that are already queued in the waiting queue.
*
* @return true if the task can be submitted to the pool
*/
protected boolean canRunTask() {
return waitingTasks.isEmpty() && canSubmitTasksToPool();
}
/**
* Executes the wrapper if there is room in the limiter, otherwise it will queue the wrapper to
* be executed once the limiter has room.
*
* @param lrw Wrapper that is ready to execute once there is available slots in the limiter
*/
protected void executeWrapper(LimiterRunnableWrapper lrw) {
if (canRunTask()) {
lrw.submitToExecutor();
} else {
addToQueue(lrw);
}
}
/**
* Adds the wrapper to the queue. After adding to the queue it will be verified that there is
* not available slots in the limiter to consume (it or others) from the queue.
* <p>
* It is expected that you already attempted to run the task (by calling {@link #canRunTask()}
* before deferring to this.
*
* @param lrw {@link LimiterRunnableWrapper} to add to the queue
*/
protected void addToQueue(LimiterRunnableWrapper lrw) {
waitingTasks.add(lrw);
consumeAvailable(); // call to consume in case task finished after first check
}
/**
* Generic wrapper for runnables which are used within the limiters. This wrapper ensures that
* {@link #handleTaskFinished()} will be called after the task completes.
*
* @since 1.0.0
*/
protected class LimiterRunnableWrapper implements Runnable, RunnableContainer {
protected final Executor executor;
protected final Runnable runnable;
public LimiterRunnableWrapper(Executor executor, Runnable runnable) {
this.executor = executor;
this.runnable = runnable;
}
/**
* Called immediately after contained task finishes. That way any additional cleanup needed
* can be run.
*/
protected void doAfterRunTasks() {
// nothing in the default implementation
}
/**
* Submits this task to the executor. This can be overridden if it needs to be submitted in a
* different way.
*/
protected void submitToExecutor() {
this.executor.execute(this);
}
@Override
public void run() {
try {
runnable.run();
} finally {
try {
doAfterRunTasks();
} finally {
currentlyRunning.decrementAndGet();
consumeAvailable(); // allow any waiting tasks to run
}
}
}
@Override
public Runnable getContainedRunnable() {
return runnable;
}
}
}