Avatar
1
Bảo Ngô Beginner
Bảo Ngô Beginner
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(this::throwEx)

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

  • Answer
java spring bucket4j resilence4j
Remain: 5
9 Answers
Avatar
Bảo Ngô Beginner
Bảo Ngô Beginner
Sof here:

https://stackoverflow.com/questions/74064568/java-spring-rate-limiter-block-specific-duration-when-ratio-reached
  • 0
  • Reply
Avatar
monkey Beginner
monkey Beginner
You can create a class like this and then create a class like this, example:
public class EzyActionFrame5Seconds extends EzyActionFrame {

    public EzyActionFrameSecond() {
        this(Integer.MAX_VALUE);
    }

    public EzyActionFrameSecond(long maxActions) {
        super(maxActions);
    }

    public EzyActionFrameSecond(long maxActions, long startTime) {
        super(maxActions, startTime);
    }

    @Override
    protected final int getExistsTime() {
        return 5000;
    }

    @Override
    public final EzyActionFrame nextFrame() {
        return new EzyActionFrame5Seconds(maxActions, endTime);
    }
}

You can take a look here and here to see how to use

  • 1
  • Reply
Avatar
Bảo Ngô Beginner
Bảo Ngô Beginner
Thank you for your answer. If first start time and end time is 0/5, then nextFrame will have start time and end time is 5/10, is it correct?  How about 1/6, 2/7, etc?
  • 0
  • Reply
Avatar
monkey Beginner
monkey Beginner
  1. Yes, it's correct
  2. I don't understand what do exactly you mean about: 1/6, 2/7 but you can create many frame for many time frame that you want
  • 1
  • Reply
Avatar
Bảo Ngô Beginner
Bảo Ngô Beginner
I may not understand you completely. Could you help to explain more detail about your solution? For example: Firstly a request come, what next? Then second request come, what next? Thank you
  • 0
  • Reply
frame[1]> EzyActionFrame5Seconds: 2022-10-14 15-39-19.701 -> 2022-10-14 15-39-24.701, actions: 0, maxActions: 5, tooManyRequest: false
add 1 actions
frame[1]> EzyActionFrame5Seconds: 2022-10-14 15-39-19.701 -> 2022-10-14 15-39-24.701, actions: 1, maxActions: 5, tooManyRequest: false
add 2 actions
frame[1]> EzyActionFrame5Seconds: 2022-10-14 15-39-19.701 -> 2022-10-14 15-39-24.701, actions: 3, maxActions: 5, tooManyRequest: false
add 7 actions
frame[1]> EzyActionFrame5Seconds: 2022-10-14 15-39-19.701 -> 2022-10-14 15-39-24.701, actions: 10, maxActions: 5, tooManyRequest: true
add 8 actions
frame[1]> EzyActionFrame5Seconds: 2022-10-14 15-39-19.701 -> 2022-10-14 15-39-24.701, actions: 18, maxActions: 5, tooManyRequest: true
add 5 actions
frame[1]> EzyActionFrame5Seconds: 2022-10-14 15-39-19.701 -> 2022-10-14 15-39-24.701, actions: 23, maxActions: 5, tooManyRequest: true
add 6 actions
frame[2]> EzyActionFrame5Seconds: 2022-10-14 15-39-24.701 -> 2022-10-14 15-39-29.701, actions: 6, maxActions: 5, tooManyRequest: true
add 2 actions
frame[2]> EzyActionFrame5Seconds: 2022-10-14 15-39-24.701 -> 2022-10-14 15-39-29.701, actions: 8, maxActions: 5, tooManyRequest: true
add 4 actions
frame[2]> EzyActionFrame5Seconds: 2022-10-14 15-39-24.701 -> 2022-10-14 15-39-29.701, actions: 12, maxActions: 5, tooManyRequest: true
add 2 actions
frame[2]> EzyActionFrame5Seconds: 2022-10-14 15-39-24.701 -> 2022-10-14 15-39-29.701, actions: 14, maxActions: 5, tooManyRequest: true
add 4 actions
frame[2]> EzyActionFrame5Seconds: 2022-10-14 15-39-24.701 -> 2022-10-14 15-39-29.701, actions: 18, maxActions: 5, tooManyRequest: true
add 8 actions
frame[3]> EzyActionFrame5Seconds: 2022-10-14 15-39-29.701 -> 2022-10-14 15-39-34.701, actions: 8, maxActions: 5, tooManyRequest: true
 –  monkey 1665711592000
Avatar
Bảo Ngô Beginner
Bảo Ngô Beginner
Let change the rule to smaller number: if more than 4 requests per 3 sec, then block 10min

Firstly, I will create an instance of EzyActionFrame3Seconds (start:0/end:0+3/capacity:4). A request comes (second 0.5), add to the frame. Next request comes (second 1), add to the frame. 3rd request comes (second 2), add to he frame. 4th request comes (second 3 == endTime), add to the frame. This point, the first frame will end its life without violate the rule.

5th request comes (second 4), create second EzyActionFrame3Seconds(start:3/end:3+3), add to this frame. 6th request comes (second 4.1), add to the frame. 7th request comes (second 4.2), add to the frame. 8th request comes (second 4.3), add to the frame.

The second frame does not break the rule, right?

But the rule actually are break! From second 2 to second 4.3, it take 4.3 - 2 = 2.3 seconds, within 3 seconds; and from second 2 to second 4.3, there are 5 requests (4,5,6,7,8) (>4).

Am I wrong?

  • 0
  • Reply
Avatar
monkey Beginner
monkey Beginner
Yes, you're right. So, maybe it's what do you want:
public class EzyCompositeActionFrame {
    private final int seconds;
    private long totalActions;
    private final long maxActions;
    private final List inSecondFrames;

    public EzyCompositeActionFrame(int seconds, long maxActions) {
        this.seconds = seconds;
        this.maxActions = maxActions;
        this.inSecondFrames = new ArrayList<>();
    }

    public synchronized boolean addActions(int actions) {
        final long currentTime = System.currentTimeMillis();
        final long minStartTime = currentTime - seconds;
        final Iterator iterator = inSecondFrames.iterator();
        while (iterator.hasNext()) {
            final EzyActionFrame frame = iterator.next();
            if (frame.getStartTime() < minStartTime) {
                iterator.remove();
            } else {
                break
            }
        } EzyActionFrame currentFrame = null for (EzyActionFrame frame : inSecondFrames) {
            if (frame.isValid()) {
                currentFrame = frame;
                break
            }
        } if (currentFrame == null) {
            currentFrame = new EzyActionFrameSecond(maxActions, currentTime);
            inSecondFrames.add(currentFrame);
        }
        currentFrame.addActions(actions);
        totalActions = inSecondFrames.stream().mapToLong(EzyActionFrame::getActions).sum();
        return totalActions > maxActions;
    }

    public synchronized boolean isInvalid() {return totalActions > maxActions;}

    @Override
    public String toString() {
        final long startTime = inSecondFrames.isEmpty() ? System.currentTimeMillis() : inSecondFrames.get(0).startTime;
        final long endTime = startTime + seconds;
        final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS");
        return df.format(startTime) + " -> " + df.format(endTime) + ", actions: " + totalActions + ", maxActions: " + maxActions + ", tooManyRequest: " + isInvalid();
    }

    public static void main(String[] args) throws Exception {
        final EzyCompositeActionFrame frame = new EzyCompositeActionFrame(3, 4);
        System.out.println(frame);
        while (true) {
            final int actions = ThreadLocalRandom.current().nextInt(1, 10);
            System.out.println("add " + actions + " actions");
            frame.addActions(actions);
            System.out.println(frame);
            Thread.sleep(1000);
        }
    }
}
  • 0
  • Reply
Avatar
Bảo Ngô Beginner
Bảo Ngô Beginner

if (frame.isInvalid()) it should be if (frame.isValid()), right?

But even so, the result is not my expectation.

This bellow code is my test case for the above example

package com.example.ratelimiting;

import lombok.Getter;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class EzyCompositeActionFrame {

    private final int seconds;
    private long totalActions;
    private final long maxActions;
    private final List<EzyActionFrame> inSecondFrames;

    public EzyCompositeActionFrame(int seconds, long maxActions) {
        this.seconds = seconds;
        this.maxActions = maxActions;
        this.inSecondFrames = new ArrayList<>();

    }

    public synchronized boolean addActions(int actions) {
        final long currentTime = System.currentTimeMillis();
        final long minStartTime = currentTime - seconds;
        final Iterator<EzyActionFrame> iterator = inSecondFrames.iterator();
        while (iterator.hasNext()) {
            final EzyActionFrame frame = iterator.next();
            if (frame.getStartTime() < minStartTime) {
                iterator.remove();
            } else {
                break;
            }
        }
        EzyActionFrame currentFrame = null;
        for (EzyActionFrame frame : inSecondFrames) {
            if (frame.isValid()) {
                currentFrame = frame;
                break;
            }
        }
        if (currentFrame == null) {
            currentFrame = new EzyActionFrameSecond(
                    maxActions,
                    currentTime
            );
            inSecondFrames.add(currentFrame);
        }
        currentFrame.addActions(actions);
        totalActions = inSecondFrames
                .stream()
                .mapToLong(EzyActionFrame::getActions)
                .sum();
        return totalActions > maxActions;
    }

    public synchronized boolean isInvalid() {
        return totalActions > maxActions;
    }

    @Override
    public String toString() {
        final long startTime = inSecondFrames.isEmpty()
                ? System.currentTimeMillis()
                : inSecondFrames.get(0).startTime;
        final long endTime = startTime + seconds;
        final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS");
        return df.format(startTime) +
                " -> " +
                df.format(endTime) +
                ", actions: " + totalActions +
                ", maxActions: " + maxActions +
                ", tooManyRequest: " + isInvalid();
    }

    @Getter
    static abstract class EzyActionFrame {

        protected final long endTime;
        protected final long startTime;
        protected final long maxActions;
        protected volatile long actions;

        public EzyActionFrame(long maxActions) {
            this(maxActions, System.currentTimeMillis());
        }

        public EzyActionFrame(long maxActions, long startTime) {
            this.maxActions = maxActions;
            this.startTime = startTime;
            this.endTime = startTime + getExistsTime();
        }

        protected abstract int getExistsTime();

        public boolean addActions(long actions) {
            return (this.actions += actions) > maxActions;
        }

        public boolean isExpired() {
            return System.currentTimeMillis() > endTime;
        }

        public boolean isInvalid() {
            return actions > maxActions;
        }

        public boolean isValid() {
            return !isInvalid();
        }

        public abstract EzyActionFrame nextFrame();

        @Override
        public String toString() {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS");
            return getClass().getSimpleName() +
                    ": " +
                    df.format(startTime) +
                    " -> " +
                    df.format(endTime);
        }
    }

    static class EzyActionFrameSecond extends EzyActionFrame {

        public EzyActionFrameSecond() {
            this(Integer.MAX_VALUE);
        }

        public EzyActionFrameSecond(long maxActions) {
            super(maxActions);
        }

        public EzyActionFrameSecond(long maxActions, long startTime) {
            super(maxActions, startTime);
        }

        @Override
        protected final int getExistsTime() {
            return 1000;
        }

        @Override
        public final EzyActionFrame nextFrame() {
            return new EzyActionFrameSecond(maxActions, endTime);
        }
    }

    public static void main(String[] args) throws Exception {
        final EzyCompositeActionFrame frame = new EzyCompositeActionFrame(3, 4);
        // 1st request comes (second ~0.5)
        Thread.sleep(500);
        System.out.println("add 1st actions");
        frame.addActions(1);
        System.out.println(frame);

        // 2nd request comes (second ~1)
        Thread.sleep(450);
        System.out.println("add 2nd actions");
        frame.addActions(2);
        System.out.println(frame);

        // 3th request comes (second ~2)
        Thread.sleep(1000);
        System.out.println("add 3th actions");
        frame.addActions(3);
        System.out.println(frame);

        // 4th request comes (second ~3)
        Thread.sleep(1000);
        System.out.println("add 4th actions");
        frame.addActions(4);
        System.out.println(frame);

        // 5th request comes (second ~4)
        Thread.sleep(1000);
        System.out.println("add 5th actions");
        frame.addActions(5);
        System.out.println(frame);

        // 6th request comes (second ~4.1)
        Thread.sleep(100);
        System.out.println("add 6th actions");
        frame.addActions(6);
        System.out.println(frame);

        // 7th request comes (second ~4.2)
        Thread.sleep(100);
        System.out.println("add 7th actions");
        frame.addActions(7);
        System.out.println(frame);

        // 8th request comes (second ~4.3)
        Thread.sleep(100);
        System.out.println("add 8th actions");
        frame.addActions(8);
        System.out.println(frame);
    }
}

Result:

add 1st actions
2022-10-14 19-39-13.346 -> 2022-10-14 19-39-13.349, actions: 1, maxActions: 4, tooManyRequest: false
add 2nd actions
2022-10-14 19-39-13.914 -> 2022-10-14 19-39-13.917, actions: 2, maxActions: 4, tooManyRequest: false
add 3th actions
2022-10-14 19-39-14.927 -> 2022-10-14 19-39-14.930, actions: 3, maxActions: 4, tooManyRequest: false
add 4th actions
2022-10-14 19-39-15.940 -> 2022-10-14 19-39-15.943, actions: 4, maxActions: 4, tooManyRequest: false
add 5th actions
2022-10-14 19-39-16.953 -> 2022-10-14 19-39-16.956, actions: 5, maxActions: 4, tooManyRequest: true
add 6th actions
2022-10-14 19-39-17.063 -> 2022-10-14 19-39-17.066, actions: 6, maxActions: 4, tooManyRequest: true
add 7th actions
2022-10-14 19-39-17.172 -> 2022-10-14 19-39-17.175, actions: 7, maxActions: 4, tooManyRequest: true
add 8th actions
2022-10-14 19-39-17.280 -> 2022-10-14 19-39-17.283, actions: 8, maxActions: 4, tooManyRequest: true

Expected result:

add 1st actions
2022-10-14 19-39-13.346 -> 2022-10-14 19-39-13.349, actions: 1, maxActions: 4, tooManyRequest: false
add 2nd actions
2022-10-14 19-39-13.914 -> 2022-10-14 19-39-13.917, actions: 2, maxActions: 4, tooManyRequest: false
add 3th actions
2022-10-14 19-39-14.927 -> 2022-10-14 19-39-14.930, actions: 3, maxActions: 4, tooManyRequest: false
add 4th actions
2022-10-14 19-39-15.940 -> 2022-10-14 19-39-15.943, actions: 4, maxActions: 4, tooManyRequest: false
add 5th actions
2022-10-14 19-39-16.953 -> 2022-10-14 19-39-16.956, actions: 5, maxActions: 4, tooManyRequest: false
add 6th actions
2022-10-14 19-39-17.063 -> 2022-10-14 19-39-17.066, actions: 6, maxActions: 4, tooManyRequest: false
add 7th actions
2022-10-14 19-39-17.172 -> 2022-10-14 19-39-17.175, actions: 7, maxActions: 4, tooManyRequest: false
add 8th actions
2022-10-14 19-39-17.280 -> 2022-10-14 19-39-17.283, actions: 8, maxActions: 4, tooManyRequest: true

  • 0
  • Reply
Avatar
tvd12 Beginner
tvd12 Beginner
Sorry, could you try this one:
import lombok.Getter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class EzyCompositeActionFrame {
    private final int seconds;
    private volatile long totalActions;
    private final long maxActions;
    private final List inSecondFrames;

    public EzyCompositeActionFrame(int seconds, long maxActions) {
        this.seconds = seconds;
        this.maxActions = maxActions;
        this.inSecondFrames = new ArrayList<>();
    }

    public synchronized boolean addActions(int actions) {
        final long currentTime = System.currentTimeMillis();
        final long minStartTime = currentTime - seconds * 1000L final Iterator iterator = inSecondFrames.iterator();
        while (iterator.hasNext()) {
            final EzyActionFrame frame = iterator.next();
            if (frame.getStartTime() < minStartTime) {
                iterator.remove();
            } else {
                break
            }
        } EzyActionFrame currentFrame = null for (EzyActionFrame frame : inSecondFrames) {
            if (frame.isValid()) {
                currentFrame = frame;
                break
            }
        } if (currentFrame == null) {
            currentFrame = new EzyActionFrameSecond(maxActions, currentTime);
            inSecondFrames.add(currentFrame);
        }
        currentFrame.addActions(actions);
        totalActions = inSecondFrames.stream().mapToLong(EzyActionFrame::getActions).sum();
        return totalActions > maxActions;
    }

    public synchronized boolean isInvalid() {return totalActions > maxActions;}

    @Override
    public String toString() {
        final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS");
        return df.format(System.currentTimeMillis()) + ", frames: " + inSecondFrames.size() + ", actions: " + totalActions + ", maxActions: " + maxActions + ", tooManyRequest: " + isInvalid();
    }

    @Getter
    static abstract class EzyActionFrame {
        protected final long endTime;
        protected final long startTime;
        protected final long maxActions;
        protected volatile long actions;

        public EzyActionFrame(long maxActions) {this(maxActions, System.currentTimeMillis());}

        public EzyActionFrame(long maxActions, long startTime) {
            this.maxActions = maxActions;
            this.startTime = startTime;
            this.endTime = startTime + getExistsTime();
        }

        protected abstract int getExistsTime()

        public boolean addActions(long actions) {return (this.actions += actions) > maxActions;}

        public boolean isExpired() {return System.currentTimeMillis() > endTime;}

        public boolean isInvalid() {return actions > maxActions;}

        public boolean isValid() {return !isInvalid();}

        public abstract EzyActionFrame nextFrame()

        @Override
        public String toString() {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSS");
            return getClass().getSimpleName() + ": " + df.format(startTime) + " -> " + df.format(endTime);
        }
    }

    static class EzyActionFrameSecond extends EzyActionFrame {
        public EzyActionFrameSecond() {this(Integer.MAX_VALUE);}

        public EzyActionFrameSecond(long maxActions) {super(maxActions);}

        public EzyActionFrameSecond(long maxActions, long startTime) {super(maxActions, startTime);}

        @Override
        protected final int getExistsTime() {return 1000}

        @Override
        public final EzyActionFrame nextFrame() {return new EzyActionFrameSecond(maxActions, endTime);}
    }

    public static void main(String[] args) throws Exception {
        final EzyCompositeActionFrame frame = new EzyCompositeActionFrame(3, 4); // 1st request comes (second ~0.5)
        Thread.sleep(500);
        System.out.println("add 1st actions");
        frame.addActions(1);
        System.out.println(frame);
        // 2nd request comes (second ~1)
        Thread.sleep(450);
        System.out.println("add 2nd actions");
        frame.addActions(2);
        System.out.println(frame); // 3th request comes (second ~2)
        Thread.sleep(1000);
        System.out.println("add 3th actions");
        frame.addActions(3);
        System.out.println(frame); // 4th request comes (second ~3)
        Thread.sleep(1000);
        System.out.println("add 4th actions");
        frame.addActions(4);
        System.out.println(frame); // 5th request comes (second ~4)
        Thread.sleep(1000);
        System.out.println("add 5th actions");
        frame.addActions(5);
        System.out.println(frame); // 6th request comes (second ~4.1)
        Thread.sleep(100);
        System.out.println("add 6th actions");
        frame.addActions(6); System.out.println(frame); // 7th request comes (second ~4.2)
        Thread.sleep(100);
        System.out.println("add 7th actions"); frame.addActions(7);
        System.out.println(frame); // 8th request comes (second ~4.3) Thread.sleep(100);
        System.out.println("add 8th actions");
        frame.addActions(8);
        System.out.println(frame);
    }
}
  • 0
  • Reply