16

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 to create, let's say, 5 accounts within 10 minutes, but the RateLimiter.create method only takes a double specifying the number of permits "per second".

Is there a way to configure RateLimiter to release permits at a granularity greater than one second?

Jeffrey Bosboom
  • 13,313
  • 16
  • 79
  • 92
Johny19
  • 5,364
  • 14
  • 61
  • 99

6 Answers6

17

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.

Jeffrey Bosboom
  • 13,313
  • 16
  • 79
  • 92
  • Thank you for your reply @Jeffrey. But as you said using 1/120 doesn't really is "5 permit per 10 minutes (per users)". because the user will have to wait 2 minutes before creating a new account. Really he shouldn't have to wait and should be able to create 5 account whenever he wants "within" 10 minutes. But even looking at the SmoothRateLimiter it doesn't really allow you to do this no? Or maybe I didn't understand how it works ? – Johny19 Dec 31 '14 at 11:18
  • 3
    It is correct that `RateLimiter` doesn't release more than one permit at a time, but you might build some sort of wrapper where one permit from the `RateLimiter` is translated into five accounts that can be created, and set the `RateLimiter` to release a permit every ten minutes. – Louis Wasserman Dec 31 '14 at 11:22
  • @LouisWasserman Could you please elaborate the wrapper approach bit more in detail? – Vishal John Apr 23 '18 at 09:01
5

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.

4

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.

user4851
  • 765
  • 1
  • 8
  • 19
3

You could also set it to one permit per second and acquire 120 permits for each account.

Dean Hiller
  • 19,235
  • 25
  • 129
  • 212
  • I believe this will not work - if you set it to 1 permit per second, and try to acquire 120, than you will never succeed acquiring, cause the limit is checked per second. – Opster Elasticsearch Expert Aug 13 '15 at 15:17
  • The limit is not 'checked' per second. Read the guava code. If you have 10 permits per second, guave actually lets you do 1 permet every 100 ms. We tested that all out and it seemed to work great. – Dean Hiller Aug 13 '15 at 21:13
  • and if I remember, I believe when you call first guava records t1 and gives you a permit. Then when you call in the future, it records t2 and does t2-t1 and figure out how many more permits the user can consume. Anyways, it's best to just read and understand the code for your use case. What burned us was there was no initial bursting. – Dean Hiller Aug 13 '15 at 21:15
  • you are right. I've got it to work. in case the permits per second is lower than 1, the guava's RateLimiter rate should be set to 1 permit per second, and the acquire should be for (1/rate) which fits the above example where the rate is 1/120: 1 / rate = 1 / (1/120) = 120 permits to acquire each 'check'. How did you solve the burst support? I still didn't get how to do that. – Opster Elasticsearch Expert Aug 13 '15 at 23:59
  • I create a pool of N on startup so by the time a customer uses it, they have a certain burst they can use up and then use as normal. I add to the pool as soon as a customer takes one in the hopes that it is not used and has long enough to fill up on burst(a hack basically). – Dean Hiller Aug 17 '15 at 22:11
0

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.

Martin Z
  • 171
  • 1
  • 6
-1

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.

Pupusa
  • 167
  • 1
  • 3
  • 14