Guava’s RateLimiter per minutes instead of seconds?

こ雲淡風輕ζ 提交于 2020-01-01 08:27:18

问题


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?


回答1:


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.




回答2:


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.




回答3:


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




回答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.




回答5:


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.




回答6:


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.



来源:https://stackoverflow.com/questions/27711962/guava-s-ratelimiter-per-minutes-instead-of-seconds

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!