Search code examples
javaspringrate-limitingresilience4jbucket4j

Java Spring rate limiter block specific duration when ratio reached


Currently I have a requirement: Apply rate limiter for an API. If this API get called over 100 times per 5 sec then the API will be blocked for 10 mins. I don't know if there is any java lib can fullfill this requirement. If the requirement is "Allow 100 calls per 5 sec" or "Allow 100 calls per 10 min" then I can either user Bucket4j:

Bandwidth b = Bandwidth.classic(100, Refill.intervally(100, Duration.ofSeconds(5)));
//Bandwidth b = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(10)));
Bucket bk = Bucket.builder().addLimit(b).build();

//then
if(bk.tryConsume(1)) {
    //stuff
} else {
    throw
}

or Resilence4j:

RateLimiterConfig config = RateLimiterConfig.custom()
            .limitRefreshPeriod(Duration.ofSeconds(5))
            .limitForPeriod(100)
            .timeoutDuration(Duration.ofSeconds(1))
            .build();
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);
RateLimiter rateLimiterWithCustomConfig = rateLimiterRegistry
            .rateLimiter("name2", config);
CheckedRunnable restrictedCall = RateLimiter
            .decorateCheckedRunnable(rateLimiterWithCustomConfig, this::doStuff);

//then
Try.run(restrictedCall).onFailure(throwable -> throw new RuntimeException());

But the requirement is "Allow 100 calls per 5 sec, if more, block 10 min". Is there any lib can work? Please suggest me a solution for this case. Thank you


Solution

  • There is currently this feature for Bucket4j to block a bucket for a certain period of time. (I can say the same in Resilence4j but I'm not so sure!)

    So you should to maintain blacklisting manually. (This will require more complex code improvements depending on the use case!)

    However, I think using 2 different limits as below is better in terms of code cost. (You can keep the 10-minute period somewhat flexible and maintain the 5-second condition.)

    Bandwidth bandwidth1 = Bandwidth.classic(
            1200, Refill.intervally(
                    1200, Duration.ofMinutes(10)));
    Bandwidth bandwidth2 = Bandwidth.classic(
            100, Refill.intervally(
                    100, Duration.ofSeconds(5)));
    
    Bucket bucket = Bucket.builder()
            .addLimit(bandwidth1)
            .addLimit(bandwidth2)
            .build();
    

    If we want to make manual blacklisting a simple example;

    You can keep a record of the time in which the blocked. Then, you can write another class that checks when the unblocking date is reached:

    import io.github.bucket4j.Bandwidth;
    import io.github.bucket4j.Bucket;
    import io.github.bucket4j.Refill;
    import java.time.Duration;
    import java.time.LocalDateTime;
    import static java.time.LocalDateTime.now;
    
    public class BucketWithPenaltyTest {
    
        public static void main(String[] args) throws InterruptedException {
    
            Bandwidth bandwidth = Bandwidth.classic(
                    100, Refill.intervally(
                            100, Duration.ofSeconds(5)));
    
            Bucket bucket = Bucket.builder()
                    .addLimit(bandwidth)
                    .build();
    
            BucketWithPenalty bucketWithPenalty =
                    new BucketWithPenalty(bucket, 
                            Duration.ofMinutes(10).getSeconds());
    
            while(true) {
                Thread.sleep(400);
                if (bucketWithPenalty.tryConsume(1)) {
                    System.out.println(String.format(
                            "[%s] %s", now(), 
                            "Ok."));
                } else {
                    System.err.println(String.format(
                            "[%s] %s", now(), 
                            "Rate limit error!"));
                }
            }
        }
    }
    
    import io.github.bucket4j.Bucket;
    import java.time.LocalDateTime;
    import static java.time.LocalDateTime.now;
    
    public class BucketWithPenalty {
    
        private static Bucket bucket;
        private static Long blockingTimeSeconds;
        private static LocalDateTime unblockingDate;
    
        public BucketWithPenalty(Bucket bucket, Long blockingTimeSeconds) {
            this.bucket = bucket;
            this.blockingTimeSeconds = blockingTimeSeconds;
            this.unblockingDate = now();
        }
    
        public boolean tryConsume(long numTokens) {
            if (unblockingDate.isBefore(now())
                    && bucket.tryConsume(numTokens)) {
                unblockingDate = now();
                return true;
            } else {
                if (!unblockingDate.isAfter(now())) {
                    unblockingDate = now().plusSeconds(blockingTimeSeconds);
                }
                return false;
            }
        }
    }