package org.threadly.concurrent.wrapper.limiter;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import org.threadly.concurrent.AbstractSubmitterExecutor;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.util.ArgumentVerifier;
/**
* A simple way to limit any {@link Executor} so that queues are managed. In addition this queue
* is tracked completely independent of the {@link Executor}'s actual queue, so these can be
* distributed in code to limit queues differently to different parts of the system, while letting
* them all back the same {@link Executor}.
* <p>
* Once the limit has been reached, if additional tasks are supplied a
* {@link RejectedExecutionException} will be thrown. This is the threadly equivalent of
* supplying a limited sized blocking queue to a java.util.concurrent thread pool.
* <p>
* See {@link SubmitterSchedulerQueueLimitRejector}, {@link SchedulerServiceQueueLimitRejector}
* and {@link PrioritySchedulerServiceQueueLimitRejector} as other possible implementations.
*
* @since 4.6.0 (since 4.3.0 at org.threadly.concurrent.limiter)
*/
public class ExecutorQueueLimitRejector extends AbstractSubmitterExecutor {
protected final Executor parentExecutor;
protected final RejectedExecutionHandler rejectedExecutionHandler;
protected final AtomicInteger queuedTaskCount;
private volatile int queuedTaskLimit;
/**
* Constructs a new {@link ExecutorQueueLimitRejector} with the provided scheduler and limit.
*
* @param parentExecutor Executor to execute tasks on to
* @param queuedTaskLimit Maximum number of queued tasks before executions should be rejected
*/
public ExecutorQueueLimitRejector(Executor parentExecutor, int queuedTaskLimit) {
this(parentExecutor, queuedTaskLimit, null);
}
/**
* Constructs a new {@link ExecutorQueueLimitRejector} with the provided scheduler and limit.
*
* @since 4.8.0
* @param parentExecutor Executor to execute tasks on to
* @param queuedTaskLimit Maximum number of queued tasks before executions should be rejected
* @param rejectedExecutionHandler Handler to accept tasks which could not be executed due to queue size
*/
public ExecutorQueueLimitRejector(Executor parentExecutor, int queuedTaskLimit,
RejectedExecutionHandler rejectedExecutionHandler) {
ArgumentVerifier.assertNotNull(parentExecutor, "parentExecutor");
ArgumentVerifier.assertGreaterThanZero(queuedTaskLimit, "queuedTaskLimit");
this.parentExecutor = parentExecutor;
if (rejectedExecutionHandler == null) {
rejectedExecutionHandler = RejectedExecutionHandler.THROW_REJECTED_EXECUTION_EXCEPTION;
}
this.rejectedExecutionHandler = rejectedExecutionHandler;
this.queuedTaskCount = new AtomicInteger();
this.queuedTaskLimit = queuedTaskLimit;
}
/**
* Invoked to check how many tasks are currently being tracked as queued by this limiter.
*
* @return Number of tracked tasks waiting for execution to start
*/
public int getQueuedTaskCount() {
return queuedTaskCount.get();
}
/**
* Invoked to check the currently set queue limit.
*
* @return Maximum number of tasks allowed to queue in the parent executor
*/
public int getQueueLimit() {
return queuedTaskLimit;
}
/**
* Invoked to change the set limit. Negative and zero limits are allowed, but they will cause
* all executions to be rejected. If set below the current queue size, those tasks will still
* remain queued for execution.
*
* @param newLimit New limit to avoid executions
*/
public void setQueueLimit(int newLimit) {
this.queuedTaskLimit = newLimit;
}
@Override
protected void doExecute(Runnable task) {
while (true) {
int casValue = queuedTaskCount.get();
if (casValue >= queuedTaskLimit) {
rejectedExecutionHandler.handleRejectedTask(task);
return; // in case handler did not throw exception
} else if (queuedTaskCount.compareAndSet(casValue, casValue + 1)) {
try {
parentExecutor.execute(new DecrementingRunnable(task, queuedTaskCount));
} catch (RejectedExecutionException e) {
queuedTaskCount.decrementAndGet();
throw e;
}
break;
} // else loop and retry
}
}
/**
* This runnable decrements a provided AtomicInteger at the START of execution.
*
* @since 4.3.0
*/
protected static class DecrementingRunnable implements Runnable, RunnableContainer {
private final Runnable task;
private final AtomicInteger queuedTaskCount;
public DecrementingRunnable(Runnable task, AtomicInteger queuedTaskCount) {
this.task = task;
this.queuedTaskCount = queuedTaskCount;
}
@Override
public Runnable getContainedRunnable() {
return task;
}
@Override
public void run() {
queuedTaskCount.decrementAndGet();
task.run();
}
}
}