package org.threadly.concurrent.wrapper.limiter;
import java.util.concurrent.Callable;
import org.threadly.concurrent.AbstractSubmitterExecutor;
import org.threadly.concurrent.DoNothingRunnable;
import org.threadly.concurrent.SubmitterScheduler;
import org.threadly.concurrent.future.ImmediateResultListenableFuture;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.Clock;
/**
* Another way to limit executions on a scheduler. Unlike the {@link ExecutorLimiter} this does
* not attempt to limit concurrency. Instead it schedules tasks on a scheduler so that given
* permits are only used at a rate per second. This can be used for limiting the rate of data
* that you want to put on hardware resource (in a non-blocking way).
* <p>
* It is important to note that if something is executed and it exceeds the rate, it will be
* future tasks which are delayed longer.
* <p>
* It is also important to note that it is the responsibility of the application to not be
* providing more tasks into this limiter than can be consumed at the rate. Since this limiter
* will not block, if provided tasks too fast they could continue to be scheduled out further and
* further. This should be used to flatten out possible bursts that could be used in the
* application, it is not designed to be a push back mechanism for the application.
*
* @since 4.6.0 (since 2.0.0 at org.threadly.concurrent.limiter)
*/
public class RateLimiterExecutor extends AbstractSubmitterExecutor {
protected final SubmitterScheduler scheduler;
protected final RejectedExecutionHandler rejectedExecutionHandler;
protected final Object permitLock;
protected volatile double permitsPerSecond;
protected volatile long maxScheduleDelayMillis;
private double lastScheduleTime;
/**
* Constructs a new {@link RateLimiterExecutor}. Tasks will be scheduled on the provided
* scheduler, so it is assumed that the scheduler will have enough threads to handle the
* average permit amount per task, per second.
* <p>
* This will schedule tasks out infinitely far in order to maintain rate. If you want tasks to
* be rejected at a certain point consider using
* {@link #RateLimiterExecutor(SubmitterScheduler, double, long)}.
*
* @param scheduler scheduler to schedule/execute tasks on
* @param permitsPerSecond how many permits should be allowed per second
*/
public RateLimiterExecutor(SubmitterScheduler scheduler, double permitsPerSecond) {
this(scheduler, permitsPerSecond, Long.MAX_VALUE);
}
/**
* Constructs a new {@link RateLimiterExecutor}. Tasks will be scheduled on the provided
* scheduler, so it is assumed that the scheduler will have enough threads to handle the
* average permit amount per task, per second.
* <p>
* This constructor accepts a maximum schedule delay. If a task requires being scheduled out
* beyond this delay, then a {@link java.util.concurrent.RejectedExecutionException} will be
* thrown instead of scheduling the task.
*
* @since 4.8.0
* @param scheduler scheduler to schedule/execute tasks on
* @param permitsPerSecond how many permits should be allowed per second
* @param maxScheduleDelayMillis Maximum amount of time delay tasks in order to maintain rate
*/
public RateLimiterExecutor(SubmitterScheduler scheduler, double permitsPerSecond,
long maxScheduleDelayMillis) {
this(scheduler, permitsPerSecond, maxScheduleDelayMillis, null);
}
/**
* Constructs a new {@link RateLimiterExecutor}. Tasks will be scheduled on the provided
* scheduler, so it is assumed that the scheduler will have enough threads to handle the
* average permit amount per task, per second.
* <p>
* This constructor accepts a maximum schedule delay. If a task requires being scheduled out
* beyond this delay, then the provided {@link RejectedExecutionHandler} will be invoked.
*
* @since 4.8.0
* @param scheduler scheduler to schedule/execute tasks on
* @param permitsPerSecond how many permits should be allowed per second
* @param maxScheduleDelayMillis Maximum amount of time delay tasks in order to maintain rate
* @param rejectedExecutionHandler Handler to accept tasks which could not be executed
*/
public RateLimiterExecutor(SubmitterScheduler scheduler, double permitsPerSecond,
long maxScheduleDelayMillis,
RejectedExecutionHandler rejectedExecutionHandler) {
ArgumentVerifier.assertNotNull(scheduler, "scheduler");
this.scheduler = scheduler;
if (rejectedExecutionHandler == null) {
rejectedExecutionHandler = RejectedExecutionHandler.THROW_REJECTED_EXECUTION_EXCEPTION;
}
this.rejectedExecutionHandler = rejectedExecutionHandler;
this.permitLock = new Object();
this.lastScheduleTime = Clock.lastKnownForwardProgressingMillis();
setPermitsPerSecond(permitsPerSecond);
setMaxScheduleDelayMillis(maxScheduleDelayMillis);
}
/**
* Sets the allowed permits per second. When this rate is updated, it only applies to future
* submitted task. In addition if the rate has already been exceeded (and thus there is a delay
* in scheduling future items), that delay based off the previous permit rate will not be
* adjusted. For example if the rate was {@code 1/sec} and a 10 permit task was just submitted,
* and thus we have a delay of 10 seconds for a future task. Adjusting this higher will NOT
* reduce the delay time for the next task, it will only effect schedule rates after currently
* scheduled tasks have been satisfied.
*
* @since 4.6.3
* @param permitsPerSecond how many permits should be allowed per second
*/
public void setPermitsPerSecond(double permitsPerSecond) {
ArgumentVerifier.assertGreaterThanZero(permitsPerSecond, "permitsPerSecond");
this.permitsPerSecond = permitsPerSecond;
}
/**
* At runtime adjust the maximum amount that this rate limiter will be willing to schedule out
* tasks in order to maintain the rate. This value must be greater than zero.
*
* @since 4.8.0
* @param maxScheduleDelayMillis Maximum task delay in milliseconds
*/
public void setMaxScheduleDelayMillis(long maxScheduleDelayMillis) {
ArgumentVerifier.assertGreaterThanZero(maxScheduleDelayMillis, "maxScheduleDelayMillis");
this.maxScheduleDelayMillis = maxScheduleDelayMillis;
}
/**
* This call will check how far out we have already scheduled tasks to be run. Because it is
* the applications responsibility to not provide tasks too fast for the limiter to run them,
* this can give an idea of how backed up tasks provided through this limiter actually are.
*
* @return minimum delay in milliseconds for the next task to be provided
*/
public int getMinimumDelay() {
double accurateDelayMillis;
synchronized (permitLock) {
accurateDelayMillis = lastScheduleTime - Clock.lastKnownForwardProgressingMillis();
}
return (int)Math.max(0, Math.ceil(accurateDelayMillis));
}
/**
* In order to help assist with avoiding to schedule too much on the scheduler at any given
* time, this call returns a future that will block until the delay for the next task falls
* below the maximum delay provided into this call. If you want to ensure that the next task
* will execute immediately, you should provide a zero to this function. If more tasks are
* added to the limiter after this call, it will NOT impact when this future will unblock. So
* this future is assuming that nothing else is added to the limiter after requested.
*
* @param maximumDelay maximum delay in milliseconds until returned Future should unblock
* @return Future that will unblock {@code get()} calls once delay has been reduced below the provided maximum
*/
public ListenableFuture<?> getFutureTillDelay(long maximumDelay) {
int currentMinimumDelay = getMinimumDelay();
if (currentMinimumDelay == 0) {
return ImmediateResultListenableFuture.NULL_RESULT;
} else {
ListenableFutureTask<?> lft = new ListenableFutureTask<>(false, DoNothingRunnable.instance());
long futureDelay;
if (maximumDelay > 0 && currentMinimumDelay > maximumDelay) {
futureDelay = maximumDelay;
} else {
futureDelay = currentMinimumDelay;
}
scheduler.schedule(lft, futureDelay);
return lft;
}
}
/**
* Exact same as execute counter part, except you can specify how many permits this task will
* require/use (instead of defaulting to 1). The task will be scheduled out as far as necessary
* to ensure it conforms to the set rate.
*
* @param permits resource permits for this task
* @param task Runnable to execute when ready
* @return Time in milliseconds task was delayed to maintain rate, or {@code -1} if rejected but handler did not throw
*/
public long execute(double permits, Runnable task) {
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(permits, "permits");
return doExecute(permits, task);
}
/**
* Exact same as the submit counter part, except you can specify how many permits this task will
* require/use (instead of defaulting to 1). The task will be scheduled out as far as necessary
* to ensure it conforms to the set rate.
*
* @param permits resource permits for this task
* @param task Runnable to execute when ready
* @return Future that will indicate when the execution of this task has completed
*/
public ListenableFuture<?> submit(double permits, Runnable task) {
return submit(permits, task, null);
}
/**
* Exact same as the submit counter part, except you can specify how many permits this task will
* require/use (instead of defaulting to 1). The task will be scheduled out as far as necessary
* to ensure it conforms to the set rate.
*
* @param <T> type of result returned from the future
* @param permits resource permits for this task
* @param task Runnable to execute when ready
* @param result result to return from future when task completes
* @return Future that will return provided result when the execution has completed
*/
public <T> ListenableFuture<T> submit(double permits, Runnable task, T result) {
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(permits, "permits");
ListenableFutureTask<T> lft = new ListenableFutureTask<>(false, task, result);
doExecute(permits, lft);
return lft;
}
/**
* Exact same as the submit counter part, except you can specify how many permits this task will
* require/use (instead of defaulting to 1). The task will be scheduled out as far as necessary
* to ensure it conforms to the set rate.
*
* @param <T> type of result returned from the future
* @param permits resource permits for this task
* @param task Callable to execute when ready
* @return Future that will return the callables provided result when the execution has completed
*/
public <T> ListenableFuture<T> submit(double permits, Callable<T> task) {
ArgumentVerifier.assertNotNull(task, "task");
ArgumentVerifier.assertNotNegative(permits, "permits");
ListenableFutureTask<T> lft = new ListenableFutureTask<>(false, task);
doExecute(permits, lft);
return lft;
}
@Override
protected void doExecute(Runnable task) {
doExecute(1, task);
}
/**
* Performs the execution by scheduling the task out as necessary. The provided permits will
* impact the next execution's schedule time to ensure the given rate.
*
* @param permits number of permits for this task
* @param task Runnable to be executed once rate can be maintained
* @return Time in milliseconds task was delayed to maintain rate, or {@code -1} if rejected but handler did not throw
*/
protected long doExecute(double permits, Runnable task) {
double effectiveDelay = (permits / permitsPerSecond) * 1000;
synchronized (permitLock) {
if (permits == 0 && lastScheduleTime < Clock.lastKnownForwardProgressingMillis()) {
// shortcut
scheduler.execute(task);
return 0;
} else {
double scheduleDelay = lastScheduleTime - Clock.accurateForwardProgressingMillis();
if (scheduleDelay > maxScheduleDelayMillis) {
// let rejection handler invoke outside of lock
} else if (scheduleDelay < 1) {
if (scheduleDelay < 0) {
lastScheduleTime = Clock.lastKnownForwardProgressingMillis() + effectiveDelay;
} else {
lastScheduleTime += effectiveDelay;
}
scheduler.execute(task);
return 0;
} else {
lastScheduleTime += effectiveDelay;
long lScheduleDelay = (long)scheduleDelay;
scheduler.schedule(task, lScheduleDelay);
return lScheduleDelay;
}
}
}
// if not returned, then task is rejected
rejectedExecutionHandler.handleRejectedTask(task);
return -1;
}
}