Search code examples
javalimitguavarate-limiting

Why doesn't Guava RateLimiter limit for too large permits per second anymore?


I'm uploading some data using some HTTP lib to some web service and need to limit the amount of data uploaded per second. The used limiter is Guava RateLimiter, pretty much following the second example limiting some data stream. Two differences for me: I'm providing some InputStream to the consumer and am using aquire in the read-method providing one byte at a time only. No packets of different size or such ever, which from my understanding should make things easier. Additionally, my permits per second are larger than the example.

Things seemed to work pretty well for low numbers, but doesn't for higher ones and there's exactly one number at which things start to not work anymore. I'm limiting in terms of KiB/s and everything up to and including 1 * 1024 * 976 works, but with 977 things start to fail and the limit doesn't seem to be applied anymore. This can easily be seen using some network monitor, the first config limits upload ~7-8 MBit/s, while the latter raises up to 60 or more, depending on how the outgoing interface is used otherwise and such. From my understanding the increase in upload should be much smaller, 1 KiB/s only and therefore not notable at all.

I'm able to reproduce the problem using the following code:

import com.google.common.util.concurrent.RateLimiter;

public class Test
{
    public static void main(String[] args)
    {
        RateLimiter rateLimiter = RateLimiter.create(1 * 1024 * 976);
        RateLimiter msgLimiter  = RateLimiter.create(1);
        long        aquired     = 0L;

        while (true)
        {
            rateLimiter.acquire();
            ++aquired;

            if (msgLimiter.tryAcquire())
            {
                System.out.println(
                    String.format(  "Aquired: %d MBit/s",
                                    (aquired * 8) / (1024 * 1024)));
                aquired = 0;
            }
        }
    }
}

Results with 976:

Aquired: 0 MBit/s
Aquired: 7 MBit/s
Aquired: 7 MBit/s
Aquired: 7 MBit/s
Aquired: 7 MBit/s

Results with 977:

Aquired: 0 MBit/s
Aquired: 77 MBit/s
Aquired: 85 MBit/s
Aquired: 82 MBit/s
Aquired: 83 MBit/s

Do you have any idea why this is happening? Thanks!

I've already read about the "downsides" of RateLimiter regarding bursts and such, but don't see if and how this explains my problem.

The problem doesn't occur using a TimedSemaphore:

import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.concurrent.TimedSemaphore;

import com.google.common.util.concurrent.RateLimiter;

public class Test
{
    public static void main(String[] args) throws InterruptedException
    {
        int             rateLimit   = 1 * 1024 * 2000;
        //RateLimiter   rateLimiter = RateLimiter.create(rateLimit);
        TimedSemaphore  rateLimiter = new TimedSemaphore(1, TimeUnit.SECONDS, rateLimit);
        RateLimiter     msgLimiter  = RateLimiter.create(1);
        long            aquired     = 0L;

        while (true)
        {
            rateLimiter.acquire();
            ++aquired;

            if (msgLimiter.tryAcquire())
            {
                System.out.println(
                    String.format(  "Aquired: %d MBit/s",
                                    (aquired * 8) / (1024 * 1024)));
                aquired = 0;
            }
        }
    }
}

Results with 976 are the same as before, so higher values are of more interest:

Aquired: 0 MBit/s
Aquired: 15 MBit/s
Aquired: 15 MBit/s
Aquired: 15 MBit/s
Aquired: 15 MBit/s

Solution

  • It seems like Guava RateLimiter can only work up to a limit of 1,000,000 aquire calls per second. If you try to do 1,000,001 aquire calls per second the call to aquire will not wait at all (return value of aquire() is always 0.0) -> no throttling occurs.

    Therefore the following scenarios work as expected:

    RateLimiter.create(1000000l); // this will work when using aquire() / aquire(1)

    RateLimiter.create(100000000l); // this will work when using aquire(100) or higher.

    Assuming that typical network traffic is never received byte-by-byte but in blocks of 100-1000 bytes the Guava RateLimiter would work up to a limit of 100,000,000 (~762 MBit) to 1,000,000,000 (7.45 GBit/s) bytes per second.

    If those values would be sufficient for your project you can stick with Guava RateLimiter. If not I would recommend to use the Apache TimedSemaphore instead.