Skip to content

Commit dd0fc2b

Browse files
committed
need to fix one test case shouldGraduallyIncreaseLimitWhenHealthy failing for AdaptiveRateLimiter.java
1 parent ede37bd commit dd0fc2b

20 files changed

+789
-0
lines changed

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@
246246
<module>visitor</module>
247247
<module>backpressure</module>
248248
<module>actor-model</module>
249+
<module>rate-limiting-pattern</module>
250+
<module>rate-limiting-pattern</module>
249251

250252
</modules>
251253
<repositories>

rate-limiting-pattern/pom.xml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.iluwatar</groupId>
9+
<artifactId>java-design-patterns</artifactId>
10+
<version>1.26.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>rate-limiter</artifactId>
14+
15+
<properties>
16+
<maven.compiler.source>22</maven.compiler.source>
17+
<maven.compiler.target>22</maven.compiler.target>
18+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
19+
<junit.jupiter.version>5.11.1</junit.jupiter.version>
20+
<junit.platform.version>1.11.1</junit.platform.version>
21+
</properties>
22+
23+
<dependencies>
24+
<!-- JUnit 5 API and Engine -->
25+
<dependency>
26+
<groupId>org.junit.jupiter</groupId>
27+
<artifactId>junit-jupiter</artifactId>
28+
<version>${junit.jupiter.version}</version>
29+
<scope>test</scope>
30+
</dependency>
31+
32+
<dependency>
33+
<groupId>org.assertj</groupId>
34+
<artifactId>assertj-core</artifactId>
35+
<version>3.24.2</version>
36+
<scope>test</scope>
37+
</dependency>
38+
</dependencies>
39+
40+
<build>
41+
<plugins>
42+
<plugin>
43+
<groupId>org.apache.maven.plugins</groupId>
44+
<artifactId>maven-surefire-plugin</artifactId>
45+
<version>3.1.2</version>
46+
<configuration>
47+
<useModulePath>false</useModulePath> <!-- for Java 17+ compatibility -->
48+
</configuration>
49+
</plugin>
50+
</plugins>
51+
</build>
52+
</project>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
import java.util.concurrent.*;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
6+
/**
7+
* Adaptive rate limiter that adjusts limits based on system health.
8+
*/
9+
public class AdaptiveRateLimiter implements RateLimiter {
10+
private final int initialLimit;
11+
private final int maxLimit;
12+
private final AtomicInteger currentLimit;
13+
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
14+
private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1);
15+
16+
public AdaptiveRateLimiter(int initialLimit, int maxLimit) {
17+
this.initialLimit = initialLimit;
18+
this.maxLimit = maxLimit;
19+
this.currentLimit = new AtomicInteger(initialLimit);
20+
healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS);
21+
}
22+
23+
@Override
24+
public void check(String serviceName, String operationName) throws RateLimitException {
25+
String key = serviceName + ":" + operationName;
26+
int current = currentLimit.get();
27+
28+
RateLimiter limiter = limiters.computeIfAbsent(key,
29+
k -> new TokenBucketRateLimiter(current, current));
30+
31+
try {
32+
limiter.check(serviceName, operationName);
33+
System.out.printf("[Adaptive] Allowed %s.%s - CurrentLimit: %d%n", serviceName, operationName, current);
34+
} catch (RateLimitException e) {
35+
currentLimit.updateAndGet(curr -> Math.max(initialLimit, curr / 2));
36+
System.out.printf("[Adaptive] Throttled %s.%s - Decreasing limit to %d%n",
37+
serviceName, operationName, currentLimit.get());
38+
throw e;
39+
}
40+
}
41+
42+
private void adjustLimits() {
43+
int updated = currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2)));
44+
System.out.printf("[Adaptive] Health check passed - Increasing limit to %d%n", updated);
45+
}
46+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
import java.util.Random;
4+
import java.util.concurrent.*;
5+
import java.util.concurrent.atomic.AtomicBoolean;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
8+
/**
9+
* The <em>Rate Limiter</em> pattern is a key defensive strategy used to prevent system overload
10+
* and ensure fair usage of shared services. This demo showcases how different rate limiting techniques
11+
* can regulate traffic in distributed systems.
12+
*
13+
* <p>Specifically, this simulation implements three rate limiter strategies:
14+
*
15+
* <ul>
16+
* <li><b>Token Bucket</b> – Allows short bursts followed by steady request rates.</li>
17+
* <li><b>Fixed Window</b> – Enforces a strict limit per discrete time window (e.g., 3 requests/sec).</li>
18+
* <li><b>Adaptive</b> – Dynamically scales limits based on system health, simulating elastic backoff.</li>
19+
* </ul>
20+
*
21+
* <p>Each simulated service (e.g., S3, DynamoDB, Lambda) is governed by one of these limiters. Multiple
22+
* concurrent client threads issue randomized requests to these services over a fixed duration. Each
23+
* request is either:
24+
*
25+
* <ul>
26+
* <li><b>ALLOWED</b> – Permitted under the current rate limit</li>
27+
* <li><b>THROTTLED</b> – Rejected due to quota exhaustion</li>
28+
* <li><b>FAILED</b> – Dropped due to transient service failure</li>
29+
* </ul>
30+
*
31+
* <p>Statistics are printed every few seconds, and the simulation exits gracefully after a fixed runtime,
32+
* offering a clear view into how each limiter behaves under pressure.
33+
*
34+
* <p><b>Relation to AWS API Gateway:</b><br>
35+
* This implementation mirrors the throttling behavior described in the
36+
* <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html">
37+
* AWS API Gateway Request Throttling documentation</a>, where limits are applied per second and over
38+
* longer durations (burst and rate limits). The <code>TokenBucketRateLimiter</code> mimics burst capacity,
39+
* the <code>FixedWindowRateLimiter</code> models steady rate enforcement, and the <code>AdaptiveRateLimiter</code>
40+
* reflects elasticity in real-world systems like AWS Lambda under variable load.
41+
*
42+
* */
43+
public final class App {
44+
private static final int RUN_DURATION_SECONDS = 10;
45+
private static final int SHUTDOWN_TIMEOUT_SECONDS = 5;
46+
47+
private static final AtomicInteger successfulRequests = new AtomicInteger();
48+
private static final AtomicInteger throttledRequests = new AtomicInteger();
49+
private static final AtomicInteger failedRequests = new AtomicInteger();
50+
private static final AtomicBoolean running = new AtomicBoolean(true);
51+
52+
public static void main(String[] args) {
53+
System.out.println("\nStarting Rate Limiter Demo");
54+
System.out.println("====================================");
55+
56+
ExecutorService executor = Executors.newFixedThreadPool(3);
57+
ScheduledExecutorService statsPrinter = Executors.newSingleThreadScheduledExecutor();
58+
59+
try {
60+
// Explicit limiter setup for demonstration clarity
61+
TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1); // capacity 2, refill 1/sec
62+
FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1); // max 3 req/sec
63+
AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6); // adaptive from 2 to 6 req/sec
64+
65+
// Print statistics every 2 seconds
66+
statsPrinter.scheduleAtFixedRate(App::printStats, 2, 2, TimeUnit.SECONDS);
67+
68+
// Launch 3 simulated clients
69+
for (int i = 1; i <= 3; i++) {
70+
executor.submit(createClientTask(i, tb, fw, ar));
71+
}
72+
73+
// Run simulation for N seconds
74+
Thread.sleep(RUN_DURATION_SECONDS * 1000L);
75+
System.out.println("\nShutting down the demo...");
76+
77+
running.set(false);
78+
executor.shutdown();
79+
statsPrinter.shutdown();
80+
81+
if (!executor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
82+
executor.shutdownNow();
83+
}
84+
if (!statsPrinter.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
85+
statsPrinter.shutdownNow();
86+
}
87+
88+
} catch (InterruptedException e) {
89+
Thread.currentThread().interrupt();
90+
} finally {
91+
printFinalStats();
92+
System.out.println("Demo completed.");
93+
}
94+
}
95+
96+
private static Runnable createClientTask(int clientId,
97+
RateLimiter s3Limiter,
98+
RateLimiter dynamoDbLimiter,
99+
RateLimiter lambdaLimiter) {
100+
return () -> {
101+
String[] services = {"s3", "dynamodb", "lambda"};
102+
String[] operations = {
103+
"GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"
104+
};
105+
Random random = new Random();
106+
107+
while (running.get() && !Thread.currentThread().isInterrupted()) {
108+
try {
109+
String service = services[random.nextInt(services.length)];
110+
String operation = operations[random.nextInt(operations.length)];
111+
112+
switch (service) {
113+
case "s3" -> makeRequest(clientId, s3Limiter, service, operation);
114+
case "dynamodb" -> makeRequest(clientId, dynamoDbLimiter, service, operation);
115+
case "lambda" -> makeRequest(clientId, lambdaLimiter, service, operation);
116+
}
117+
118+
Thread.sleep(30 + random.nextInt(50));
119+
} catch (InterruptedException e) {
120+
Thread.currentThread().interrupt();
121+
}
122+
}
123+
};
124+
}
125+
126+
private static void makeRequest(int clientId, RateLimiter limiter,
127+
String service, String operation) {
128+
try {
129+
limiter.check(service, operation);
130+
successfulRequests.incrementAndGet();
131+
System.out.printf("Client %d: %s.%s - ALLOWED%n", clientId, service, operation);
132+
} catch (ThrottlingException e) {
133+
throttledRequests.incrementAndGet();
134+
System.out.printf("Client %d: %s.%s - THROTTLED (Retry in %dms)%n",
135+
clientId, service, operation, e.getRetryAfterMillis());
136+
} catch (ServiceUnavailableException e) {
137+
failedRequests.incrementAndGet();
138+
System.out.printf("Client %d: %s.%s - SERVICE UNAVAILABLE%n",
139+
clientId, service, operation);
140+
} catch (Exception e) {
141+
failedRequests.incrementAndGet();
142+
System.out.printf("Client %d: %s.%s - ERROR: %s%n",
143+
clientId, service, operation, e.getMessage());
144+
}
145+
}
146+
147+
private static void printStats() {
148+
if (!running.get()) return;
149+
System.out.println("\n=== Current Statistics ===");
150+
System.out.printf("Successful Requests: %d%n", successfulRequests.get());
151+
System.out.printf("Throttled Requests : %d%n", throttledRequests.get());
152+
System.out.printf("Failed Requests : %d%n", failedRequests.get());
153+
System.out.println("==========================\n");
154+
}
155+
156+
private static void printFinalStats() {
157+
System.out.println("\nFinal Statistics");
158+
System.out.println("==========================");
159+
System.out.printf("Successful Requests: %d%n", successfulRequests.get());
160+
System.out.printf("Throttled Requests : %d%n", throttledRequests.get());
161+
System.out.printf("Failed Requests : %d%n", failedRequests.get());
162+
System.out.println("==========================");
163+
}
164+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
/**
4+
* Example operation implementation for finding customers.
5+
*/
6+
public class FindCustomerRequest implements RateLimitOperation<String> {
7+
private final String customerId;
8+
private final RateLimiter rateLimiter;
9+
10+
public FindCustomerRequest(String customerId, RateLimiter rateLimiter) {
11+
this.customerId = customerId;
12+
this.rateLimiter = rateLimiter;
13+
}
14+
15+
@Override
16+
public String getServiceName() {
17+
return "CustomerService";
18+
}
19+
20+
@Override
21+
public String getOperationName() {
22+
return "FindCustomer";
23+
}
24+
25+
@Override
26+
public String execute() throws RateLimitException {
27+
rateLimiter.check(getServiceName(), getOperationName());
28+
29+
// Simulate actual operation
30+
try {
31+
Thread.sleep(50); // Simulate processing time
32+
return "Customer-" + customerId;
33+
} catch (InterruptedException e) {
34+
Thread.currentThread().interrupt();
35+
throw new ServiceUnavailableException(getServiceName(), 1000);
36+
}
37+
}
38+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
import java.util.concurrent.*;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
6+
/**
7+
* Fixed window rate limiter implementation.
8+
*/
9+
public class FixedWindowRateLimiter implements RateLimiter {
10+
private final int limit;
11+
private final long windowMillis;
12+
private final ConcurrentHashMap<String, WindowCounter> counters = new ConcurrentHashMap<>();
13+
14+
public FixedWindowRateLimiter(int limit, long windowSeconds) {
15+
this.limit = limit;
16+
this.windowMillis = TimeUnit.SECONDS.toMillis(windowSeconds);
17+
}
18+
19+
@Override
20+
public synchronized void check(String serviceName, String operationName) throws RateLimitException {
21+
String key = serviceName + ":" + operationName;
22+
WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter());
23+
24+
if (!counter.tryIncrement()) {
25+
System.out.printf("[FixedWindow] Throttled %s.%s - Limit %d reached in window%n",
26+
serviceName, operationName, limit);
27+
throw new RateLimitException("Rate limit exceeded for " + key, windowMillis);
28+
} else {
29+
System.out.printf("[FixedWindow] Allowed %s.%s - Count within window%n", serviceName, operationName);
30+
}
31+
}
32+
33+
private class WindowCounter {
34+
private AtomicInteger count = new AtomicInteger(0);
35+
private volatile long windowStart = System.currentTimeMillis();
36+
37+
synchronized boolean tryIncrement() {
38+
long now = System.currentTimeMillis();
39+
if (now - windowStart > windowMillis) {
40+
count.set(0);
41+
windowStart = now;
42+
}
43+
return count.incrementAndGet() <= limit;
44+
}
45+
}
46+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
/**
4+
* Base exception for rate limiting errors.
5+
*/
6+
public class RateLimitException extends Exception {
7+
private final long retryAfterMillis;
8+
9+
public RateLimitException(String message, long retryAfterMillis) {
10+
super(message);
11+
this.retryAfterMillis = retryAfterMillis;
12+
}
13+
14+
public long getRetryAfterMillis() {
15+
return retryAfterMillis;
16+
}
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
/**
4+
* Interface representing an operation that needs rate limiting.
5+
*/
6+
public interface RateLimitOperation<T> {
7+
String getServiceName();
8+
String getOperationName();
9+
T execute() throws RateLimitException;
10+
}

0 commit comments

Comments
 (0)