I\'m trying to rate-limit the the number of accounts a user can create with my REST API.
I would have liked to use Guava\'s RateLimiter
to only allow an IP
You could also set it to one permit per second and acquire 120 permits for each account.
I think I came upon the same problem as in the original question, and based on Louis Wasserman's comment this is what I drew up:
import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;
public class Titrator {
private final int numDosesPerPeriod;
private final RateLimiter rateLimiter;
private long numDosesAvailable;
private transient final Object doseLock;
public Titrator(int numDosesPerPeriod, Duration period) {
this.numDosesPerPeriod = numDosesPerPeriod;
double numSeconds = period.getSeconds() + period.getNano() / 1000000000d;
rateLimiter = RateLimiter.create(1 / numSeconds);
numDosesAvailable = 0L;
doseLock = new Object();
}
/**
* Consumes a dose from this titrator, blocking until a dose is available.
*/
public void consume() {
synchronized (doseLock) {
if (numDosesAvailable == 0) { // then refill
rateLimiter.acquire();
numDosesAvailable += numDosesPerPeriod;
}
numDosesAvailable--;
}
}
}
The dose meted out by the Titrator is analogous to a permit from a RateLimiter. This implementation assumes that when you consume your first dose, the clock starts ticking on the dosage period. You can consume your max doses per period as fast as you want, but when you reach your max, you have to wait until the period elapses before you can get another dose.
For a tryConsume()
analog to RateLimiter's tryAcquire
, you would check that numDosesAvailable
is positive.
From the RateLimiter.create
javadoc:
When the incoming request rate exceeds permitsPerSecond the rate limiter will release one permit every
(1.0 / permitsPerSecond)
seconds.
So you can set permitsPerSecond
to less than 1.0
to release a permit less often than once per second.
In your specific case, five accounts in ten minutes simplifies to one account per two minutes, which is one account per 120 seconds. You'd pass 1.0/120
for permitsPerSecond
.
In your use case you probably want to accommodate bursty requests for account creations. The RateLimiter
specification doesn't seem to define what happens to unused permits, but the default implementation, SmoothRateLimiter
, seems to let permits accrue up to some maximum to satisfy bursts. This class is not public, so there's no javadoc documentation, but the SmoothRateLimiter source has a lengthy comment with a detailed discussion of the current behavior.
Just in case your miss it, the RateLimiter does specify what happened to the unused permit. The default behavior is to save the unused link up to one minute RateLimiter.
There's a class called SmoothRateLimiter.SmoothBursty
inside Guava library that implements desired behavior but it has package local access, so we can't use it directly.
There's also a Github issue to make access to that class public: https://github.com/google/guava/issues/1974
If you're not willing to wait until they release a new version of RateLimiter then you could use reflection to instantiate SmoothBursty
rate limiter. Something like this should work:
Class<?> sleepingStopwatchClass = Class.forName("com.google.common.util.concurrent.RateLimiter$SleepingStopwatch");
Method createStopwatchMethod = sleepingStopwatchClass.getDeclaredMethod("createFromSystemTimer");
createStopwatchMethod.setAccessible(true);
Object stopwatch = createStopwatchMethod.invoke(null);
Class<?> burstyRateLimiterClass = Class.forName("com.google.common.util.concurrent.SmoothRateLimiter$SmoothBursty");
Constructor<?> burstyRateLimiterConstructor = burstyRateLimiterClass.getDeclaredConstructors()[0];
burstyRateLimiterConstructor.setAccessible(true);
RateLimiter result = (RateLimiter) burstyRateLimiterConstructor.newInstance(stopwatch, maxBurstSeconds);
result.setRate(permitsPerSecond);
return result;
Yes, new version of Guava might brake your code but if you're willing to accept that risk this might be the way to go.
Our workaround for this is to create a RateLimiter class on our own and change the time units. For example, in our case, we want to make a daily rate limit.
Everything is the same as the RateLimiter except for the acquire(permits) function, where we changed the time unit in (double)TimeUnit.SECONDS.toMicros(1L) to the unit we desire. In our case, we change that into TimeUnit.Day for daily limits.
Then we create our own smooth RateLimiter and in the doSetRate(double permitsPerDay, long nowMicros) and doGetRate() function we also change the time unit.