guava icon indicating copy to clipboard operation
guava copied to clipboard

RateLimiter SmoothWarmingUp does not limit if warmup period is smaller than 1000 NANOSECONDS

Open gerabarna opened this issue 2 years ago • 7 comments

Hi!

We have encountered an issue when a Rate limiter has been set up with 1 permits per second, and a ZERO duration. This resulted in a RateLimiter, which gave out permits without any limitation. Further investigation revealed that any warmup period below 1 MICROSECOND will result in a non-limiting rate limiter.

A very simplistic reproducer:

    @Test
    void givenARateLimiterWithWarmupBelowOneMillisec_WhenRequesting_ThenRateLimitIsStillRespected() {
        RateLimiter limiter = RateLimiter.create(1, 999, TimeUnit.NANOSECONDS);
        final long start = System.currentTimeMillis();
        for (int j = 0; j < 5; j++) {
            limiter.acquire();
        }
        final long elapsed = System.currentTimeMillis() - start;
        Assertions.assertTrue(elapsed > 4000);
    }

I believe RateLimiter should either validate the input, and disallow such warmup settings, or hanle it porperly

gerabarna avatar May 08 '23 08:05 gerabarna

What is the actual elapsed time? RateLimiter interprets the warmup period in microseconds, so anything less than 1 microsecond is interpreted as zero. But that should mean you get one permit per second, so your elapsed time should be right at 4 seconds.

netdpb avatar May 09 '23 15:05 netdpb

Added some logging to the simple reproducer above:

    @Test
    void givenARateLimiterWithWarmupBelowOneMillisec_WhenRequesting_ThenRateLimitIsStillRespected() {
        RateLimiter limiter = RateLimiter.create(1, 999, TimeUnit.NANOSECONDS);
        final long start = System.currentTimeMillis();
        long last = start;
        for (int j = 0; j < 5; j++) {
            limiter.acquire();
            final long current = System.currentTimeMillis();
            System.out.println(ZonedDateTime.now() + " elapsed " + (current - last) + "ms");
            last = current;
        }
        final long elapsed = System.currentTimeMillis() - start;
        Assertions.assertTrue(elapsed > 4000);
    }

the 5 permits were given within milliseconds 2023-05-10T09:52:31.365726541Z[UTC] elapsed 0ms 2023-05-10T09:52:31.369954164Z[UTC] elapsed 4ms 2023-05-10T09:52:31.370025221Z[UTC] elapsed 1ms 2023-05-10T09:52:31.370064898Z[UTC] elapsed 0ms 2023-05-10T09:52:31.370106134Z[UTC] elapsed 0ms

So the 1 second rate limit was clearely not respected.

I understand that we went below the time resolution. In that case I suppose the input validation should reject the arguments. There was an attempt at this in RateLimiter line 194 checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod); unfortunately that does not take the resolution into account, and also allows 0. I suppose I can make a small PR, and add a check for the resolution, though the situation is a bit misleading, as the duration based factory method refers to NANOSECONDS:

public static RateLimiter create(double permitsPerSecond, Duration warmupPeriod) {
    return create(permitsPerSecond, toNanosSaturated(warmupPeriod), TimeUnit.NANOSECONDS);
}

So I am not sure what would be the correct direction for the fix

gerabarna avatar May 10 '23 09:05 gerabarna

Do you see the same behavior when calling create(1, Duration.ZERO)?

netdpb avatar May 10 '23 15:05 netdpb

yes

gerabarna avatar May 16 '23 12:05 gerabarna

How important is it to support <1µs warmup time? That sounds awfully close to no warmup time. A simple solution would be either to throw in that case or to default to a non-warmup limiter.

netdpb avatar May 16 '23 18:05 netdpb

As far as I could understand the RateLimiter classes, limiters without warmup time also have the behaviour of accumulating permits and allow short bursts from the accumulated pool. ( as I saw the SmoothBursty subclass is created ) Our use-case unfortunately cannot allow any such bursts, even if the RateLimiter was left idle for a time. the closest way to simulate such behaviour was to set the lowest possible warmup time instead. If you have a better suggestion on how to get a RateLimiter without any accumulation, I would be happy to switch.

Regardless, the point of this report was that validation should rule out an incorrectly constructed warmup limiter, which gives out permits without any limiting. Which currently happens with a low enough warmup time. Simply throwing an exception would achieve this I believe.

gerabarna avatar May 18 '23 09:05 gerabarna

I have encountered the same issue recently, and I believe you can simply use the constructor with only one param of RateLimiter to generate your limiter, and I believe that solution will meet your demand.

weizheeeeee avatar May 15 '24 07:05 weizheeeeee

(It sounds like this is probably the same as https://github.com/google/guava/issues/2730, so let's consolidate the discussion there.)

cpovirk avatar Aug 14 '24 18:08 cpovirk