Skip to content

Commit 6583833

Browse files
committed
Add method for prevent unbounded growth of buckets per‑IP
1 parent ff5e13e commit 6583833

1 file changed

Lines changed: 55 additions & 10 deletions

File tree

src/main/java/org/example/filter/RateLimitingFilter.java

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@
2424

2525
public class RateLimitingFilter implements Filter {
2626
private static final Logger logger = Logger.getLogger(RateLimitingFilter.class.getName());
27-
private static final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
28-
private static final long capacity = 10;
29-
private static final long refillTokens = 1;
30-
private final Duration refillPeriod = Duration.ofSeconds(10);
27+
private static final Map<String, BucketWrapper> buckets = new ConcurrentHashMap<>();
28+
private static final long CAPACITY = 10;
29+
private static final long REFILL_TOKENS = 1;
30+
private final Duration REFILL_PERIOD = Duration.ofSeconds(10);
31+
private static final int MAX_BUCKETS_THRESHOLD = 1000;
3132

3233
@Override
3334
public void init() {
34-
logger.info("RateLimitingFilter initialized with capacity: " + capacity);
35+
logger.info("RateLimitingFilter initialized with capacity: " + CAPACITY);
36+
startCleanupThread();
3537
}
3638

3739
/**
3840
* Intercepts the request and checks if the client has enough tokens.
3941
*/
40-
4142
@Override
4243
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
4344

@@ -49,9 +50,12 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterCh
4950
}
5051

5152
String clientIp = (String) clientIpAttr;
52-
Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createNewBucket());
5353

54-
if (bucket.tryConsume(1)) {
54+
BucketWrapper wrapper = buckets.computeIfAbsent(clientIp, k -> new BucketWrapper(createNewBucket()));
55+
56+
wrapper.updateAccess();
57+
58+
if (wrapper.bucket.tryConsume(1)) {
5559
chain.doFilter(request, response);
5660
} else {
5761
logger.warning("Limit exceeded per IP: " + clientIp);
@@ -71,13 +75,54 @@ public void destroy() {
7175
private Bucket createNewBucket() {
7276
return Bucket.builder()
7377
.addLimit(Bandwidth.builder()
74-
.capacity(capacity)
75-
.refillGreedy(refillTokens, refillPeriod)
78+
.capacity(CAPACITY)
79+
.refillGreedy(REFILL_TOKENS, REFILL_PERIOD)
7680
.build())
7781
.build();
7882
}
7983

8084
public void clearBuckets(){
8185
buckets.clear();
8286
}
87+
88+
/**
89+
* Track the last access time of every bucket
90+
*/
91+
private static class BucketWrapper {
92+
private final Bucket bucket;
93+
private volatile long lastAccessTime;
94+
95+
BucketWrapper(Bucket bucket) {
96+
this.bucket = bucket;
97+
this.lastAccessTime = System.currentTimeMillis();
98+
}
99+
100+
void updateAccess() {
101+
this.lastAccessTime = System.currentTimeMillis();
102+
}
103+
}
104+
105+
private void startCleanupThread() {
106+
Thread cleanupThread = new Thread(() -> {
107+
while (!Thread.currentThread().isInterrupted()) {
108+
try {
109+
//it checks every 10 minutes
110+
Thread.sleep(Duration.ofMinutes(10).toMillis());
111+
112+
//it will only clean when the size of the buckets is more than 1000
113+
if (buckets.size() > MAX_BUCKETS_THRESHOLD) {
114+
long idleThreshold = System.currentTimeMillis() - Duration.ofMinutes(30).toMillis();
115+
buckets.entrySet().removeIf(entry -> entry.getValue().lastAccessTime < idleThreshold);
116+
}
117+
118+
119+
} catch (InterruptedException e) {
120+
Thread.currentThread().interrupt();
121+
}
122+
}
123+
});
124+
cleanupThread.setDaemon(true);
125+
cleanupThread.start();
126+
}
127+
83128
}

0 commit comments

Comments
 (0)