package org.cryptocoinpartners.util;
import java.util.Collection;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.joda.time.Duration;
/**
* Implements an Executor which delays execution until a rate limit is fulfilled.
*
* @author Tim Olson
*/
@SuppressWarnings("NullableProblems")
public class RateLimiter implements Executor {
/**
* Constructs a RateLimiter with a single-threaded executor
* @see #RateLimiter(java.util.concurrent.Executor, int, org.joda.time.Duration)
*/
public RateLimiter(final int invocations, final Duration per) {
this(null, invocations, per);
}
/**
* Implements the Token Bucket algorithm to provide a maximum number of invocations within each fixed time window.
* Useful for rate-limiting. If given a non-null executor, the scheduled runnables are passed to that executor
* for execution at the rate limit. If executor is null, a single-threaded executor is used
* @param executor the Executor which executes the Runnables. the executor is not called with the runnable until
* the rate limit has been fulfilled
* @param invocations number of queries allowed during each time window
* @param per the duration of each time window
*/
public RateLimiter(Executor executor, final int invocations, final Duration per) {
if (executor != null) {
this.executor = executor;
} else {
this.executor = Executors.newSingleThreadExecutor();
}
// This thread fills the TokenBucket with available requests every time window
ScheduledThreadPoolExecutor replenisher = new ScheduledThreadPoolExecutor(1);
replenisher.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
int permitsToCreate = invocations - requestsAvailable.availablePermits();
if (permitsToCreate > 0) {
synchronized (requestsAvailable) {
// bring the number of requests up to the maximum size per time window
requestsAvailable.release(permitsToCreate);
}
}
}
}, 0, per.getMillis(), TimeUnit.MILLISECONDS);
pump = new RunnablePump();
pump.start();
}
@Override
public void execute(Runnable runnable) {
waitingRunnables.add(runnable);
runnableCount.release();
}
public boolean remove(Runnable runnable) throws Throwable {
if (waitingRunnables.remove(runnable)) {
finalize();
// runnableCount.release();
// requestsAvailable.release();
return true;
}
return false;
}
public Collection<Runnable> getRunnables() {
return waitingRunnables;
}
public void stopRunnablePump() {
pump.interrupt();
}
//}
// This thread waits for a Runnable to be put on the waitingRunnables queue, then waits for the rate limit
// to be available, then pushes the Runnable to the executor for execution.
private class RunnablePump extends Thread {
private RunnablePump() {
setDaemon(true);
}
@Override
public void run() {
boolean running = true;
while (running && !isInterrupted()) {
try {
runnableCount.acquire(); // wait until there are any scheduled runnables available
Runnable runnable;
runnable = waitingRunnables.poll();
requestsAvailable.acquire(); // wait until there is request limit available, issue here with acquring the lock
executor.execute(runnable);
} catch (InterruptedException e) {
running = false;
break;
}
}
}
}
@Override
public void finalize() throws Throwable {
pump.interrupt();
super.finalize();
}
private final RunnablePump pump;
private final Semaphore runnableCount = new Semaphore(0);
private final Semaphore requestsAvailable = new Semaphore(0);
private final Queue<Runnable> waitingRunnables = new ConcurrentLinkedQueue<>();
private final Executor executor;
}