2424
2525public 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