mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-09-03 04:18:19 +00:00
* need to fix one test case shouldGraduallyIncreaseLimitWhenHealthy failing for AdaptiveRateLimiter.java * Added Class Diagram and Flow Diagrams for Adaptive, Fixed Window and Token Bucket Rate Limiter * Updated README.md. All test case passed. Updated with Google Java Guidelines * Updated parent pom #2973 * Updated parent pom #2973 * fixed shouldResetCounterAfterWindow() test #2973 * formatting fixed #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * added test coverage for app.java and fixed random to be thread safe #2973 * fixed random to be thread safe #2973 * fixed random to be thread safe #2973 * fixed random to be thread safe #2973 * fixed spacing in pom.xml #2973 --------- Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
This commit is contained in:
co-authored by
Ilkka Seppälä
parent
a3fcc63167
commit
265e3d0bda
@@ -248,6 +248,7 @@
|
||||
<module>visitor</module>
|
||||
<module>backpressure</module>
|
||||
<module>actor-model</module>
|
||||
<module>rate-limiting-pattern</module>
|
||||
</modules>
|
||||
<repositories>
|
||||
<repository>
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
---
|
||||
title: "Rate Limiting Pattern in Java: Controlling System Overload Gracefully"
|
||||
shortTitle: Rate Limiting
|
||||
description: "Explore multiple rate limiting strategies in Java—Token Bucket, Fixed Window, and Adaptive Limiting. Learn with diagrams, programmatic examples, and real-world simulation."
|
||||
category: Behavioral
|
||||
language: en
|
||||
tag:
|
||||
- Resilience
|
||||
- System Overload Protection
|
||||
- API Throttling
|
||||
- Concurrency
|
||||
- Cloud Patterns
|
||||
---
|
||||
|
||||
## Also known as
|
||||
|
||||
- Throttling
|
||||
- Request Limiting
|
||||
- API Rate Limiting
|
||||
|
||||
---
|
||||
|
||||
## Intent of Rate Limiting Design Pattern
|
||||
|
||||
To regulate the number of requests sent to a service in a specific time window, avoiding resource exhaustion and ensuring system stability. This is especially useful in distributed and cloud-native architectures.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Explanation of Rate Limiting with Real-World Examples
|
||||
|
||||
### Real-world example
|
||||
|
||||
Imagine you're entering a concert hall that only allows 50 people per minute. If too many fans arrive at once, the gate staff slows down entry, allowing only a few at a time. This prevents overcrowding and ensures safety. Similarly, the rate limiter controls how many requests are processed to avoid overloading a server.
|
||||
|
||||
### In plain words
|
||||
|
||||
Regulate the number of requests a system handles within a time frame to protect availability and performance.
|
||||
|
||||
|
||||
### AWS says
|
||||
|
||||
> "API Gateway limits the steady-state rate and burst rate of requests that it allows for each method in your REST APIs. When request rates exceed these limits, API Gateway begins to throttle requests."
|
||||
|
||||
— [API Gateway quotas and important notes - AWS Documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||

|
||||
|
||||
This UML shows the key components:
|
||||
- `RateLimiter` interface
|
||||
- `TokenBucketRateLimiter`, `FixedWindowRateLimiter`, `AdaptiveRateLimiter`
|
||||
- Supporting exception classes
|
||||
- `FindCustomerRequest` as a rate-limited operation
|
||||
|
||||
---
|
||||
|
||||
## Flowcharts
|
||||
|
||||
### 1. Token Bucket Strategy
|
||||
|
||||

|
||||
|
||||
### 2. Fixed Window Strategy
|
||||
|
||||

|
||||
|
||||
### 3. Adaptive Rate Limiting Strategy
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Programmatic Example of Rate Limiter Pattern in Java
|
||||
|
||||
The **Rate Limiter** design pattern helps protect systems from overload by restricting the number of operations that can be performed in a given time window. It is especially useful when accessing shared resources, APIs, or services that are sensitive to spikes in traffic.
|
||||
|
||||
This implementation demonstrates three strategies for rate limiting:
|
||||
|
||||
- **Token Bucket Rate Limiter**
|
||||
- **Fixed Window Rate Limiter**
|
||||
- **Adaptive Rate Limiter**
|
||||
|
||||
Let’s walk through the key components.
|
||||
|
||||
---
|
||||
|
||||
#### 1. Token Bucket Rate Limiter
|
||||
|
||||
The token bucket allows short bursts followed by a steady rate. Tokens are added periodically and requests are only allowed if a token is available.
|
||||
|
||||
```java
|
||||
public class TokenBucketRateLimiter implements RateLimiter {
|
||||
private final int capacity;
|
||||
private final int refillRate;
|
||||
private final ConcurrentHashMap<String, TokenBucket> buckets = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public TokenBucketRateLimiter(int capacity, int refillRate) {
|
||||
this.capacity = capacity;
|
||||
this.refillRate = refillRate;
|
||||
scheduler.scheduleAtFixedRate(this::refillBuckets, 1, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(String serviceName, String operationName) throws RateLimitException {
|
||||
String key = serviceName + ":" + operationName;
|
||||
TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(capacity));
|
||||
if (!bucket.tryConsume()) {
|
||||
throw new ThrottlingException(serviceName, operationName, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private void refillBuckets() {
|
||||
buckets.forEach((k, b) -> b.refill(refillRate));
|
||||
}
|
||||
|
||||
private static class TokenBucket {
|
||||
private final int capacity;
|
||||
private final AtomicInteger tokens;
|
||||
|
||||
TokenBucket(int capacity) {
|
||||
this.capacity = capacity;
|
||||
this.tokens = new AtomicInteger(capacity);
|
||||
}
|
||||
|
||||
boolean tryConsume() {
|
||||
while (true) {
|
||||
int current = tokens.get();
|
||||
if (current <= 0) return false;
|
||||
if (tokens.compareAndSet(current, current - 1)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
void refill(int amount) {
|
||||
tokens.getAndUpdate(current -> Math.min(current + amount, capacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. Fixed Window Rate Limiter
|
||||
|
||||
This strategy uses a simple counter within a fixed time window.
|
||||
|
||||
```java
|
||||
public class FixedWindowRateLimiter implements RateLimiter {
|
||||
private final int limit;
|
||||
private final long windowMillis;
|
||||
private final ConcurrentHashMap<String, WindowCounter> counters = new ConcurrentHashMap<>();
|
||||
|
||||
public FixedWindowRateLimiter(int limit, long windowSeconds) {
|
||||
this.limit = limit;
|
||||
this.windowMillis = TimeUnit.SECONDS.toMillis(windowSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void check(String serviceName, String operationName) throws RateLimitException {
|
||||
String key = serviceName + ":" + operationName;
|
||||
WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter());
|
||||
|
||||
if (!counter.tryIncrement()) {
|
||||
throw new RateLimitException("Rate limit exceeded for " + key, windowMillis);
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowCounter {
|
||||
private AtomicInteger count = new AtomicInteger(0);
|
||||
private volatile long windowStart = System.currentTimeMillis();
|
||||
|
||||
synchronized boolean tryIncrement() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - windowStart > windowMillis) {
|
||||
count.set(0);
|
||||
windowStart = now;
|
||||
}
|
||||
return count.incrementAndGet() <= limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. Adaptive Rate Limiter
|
||||
|
||||
This version adjusts the rate based on system health, reducing the rate when throttling occurs and recovering periodically.
|
||||
|
||||
```java
|
||||
public class AdaptiveRateLimiter implements RateLimiter {
|
||||
private final int initialLimit;
|
||||
private final int maxLimit;
|
||||
private final AtomicInteger currentLimit;
|
||||
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public AdaptiveRateLimiter(int initialLimit, int maxLimit) {
|
||||
this.initialLimit = initialLimit;
|
||||
this.maxLimit = maxLimit;
|
||||
this.currentLimit = new AtomicInteger(initialLimit);
|
||||
healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(String serviceName, String operationName) throws RateLimitException {
|
||||
String key = serviceName + ":" + operationName;
|
||||
int current = currentLimit.get();
|
||||
RateLimiter limiter = limiters.computeIfAbsent(key, k -> new TokenBucketRateLimiter(current, current));
|
||||
|
||||
try {
|
||||
limiter.check(serviceName, operationName);
|
||||
} catch (RateLimitException e) {
|
||||
currentLimit.updateAndGet(curr -> Math.max(initialLimit, curr / 2));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustLimits() {
|
||||
currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4. Simulated Demo Using All Limiters
|
||||
|
||||
```java
|
||||
public final class App {
|
||||
public static void main(String[] args) {
|
||||
TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1);
|
||||
FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1);
|
||||
AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6);
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(3);
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
executor.submit(createClientTask(i, tb, fw, ar));
|
||||
}
|
||||
}
|
||||
|
||||
private static Runnable createClientTask(int clientId, RateLimiter tb, RateLimiter fw, RateLimiter ar) {
|
||||
return () -> {
|
||||
String[] services = {"s3", "dynamodb", "lambda"};
|
||||
String[] operations = {"GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"};
|
||||
Random random = new Random();
|
||||
|
||||
while (true) {
|
||||
String service = services[random.nextInt(services.length)];
|
||||
String operation = operations[random.nextInt(operations.length)];
|
||||
try {
|
||||
switch (service) {
|
||||
case "s3" -> tb.check(service, operation);
|
||||
case "dynamodb" -> fw.check(service, operation);
|
||||
case "lambda" -> ar.check(service, operation);
|
||||
}
|
||||
System.out.printf("Client %d: %s.%s - ALLOWED%n", clientId, service, operation);
|
||||
} catch (RateLimitException e) {
|
||||
System.out.printf("Client %d: %s.%s - THROTTLED%n", clientId, service, operation);
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(30 + random.nextInt(50));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This example highlights how the Rate Limiter pattern supports various throttling techniques and how they respond under simulated traffic pressure, making it invaluable for building scalable, resilient systems.
|
||||
|
||||
## When to Use Rate Limiting
|
||||
|
||||
- APIs receiving unpredictable traffic
|
||||
- Shared cloud resources (e.g., DB, compute)
|
||||
- Services requiring fair client usage
|
||||
- Preventing DoS or abuse
|
||||
|
||||
---
|
||||
|
||||
## Real-World Applications
|
||||
|
||||
- **AWS API Gateway**
|
||||
- **Google Cloud Functions**
|
||||
- **Netflix Zuul API Gateway**
|
||||
- **Stripe API Throttling**
|
||||
|
||||
---
|
||||
|
||||
## Benefits and Trade-offs
|
||||
|
||||
### Benefits
|
||||
|
||||
- Protects backend from overload
|
||||
- Fair distribution of resources
|
||||
- Better user experience under load
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- May delay valid requests
|
||||
- Requires tuning of limits
|
||||
- Could create bottlenecks if misused
|
||||
|
||||
---
|
||||
|
||||
## Related Java Design Patterns
|
||||
|
||||
- [Circuit Breaker](https://java-design-patterns.com/patterns/circuit-breaker/)
|
||||
- [Retry](https://java-design-patterns.com/patterns/retry/)
|
||||
- [Throttling Queue](https://java-design-patterns.com/patterns/throttling/)
|
||||
|
||||
---
|
||||
|
||||
## References and Credits
|
||||
|
||||
- [Microsoft Cloud Design Patterns](https://learn.microsoft.com/en-us/azure/architecture/patterns/throttling)
|
||||
- [AWS API Gateway Throttling](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html)
|
||||
- *Designing Data-Intensive Applications* by Martin Kleppmann
|
||||
- [Resilience4j](https://resilience4j.readme.io/)
|
||||
- Java Design Patterns Project: [java-design-patterns](https://github.com/iluwatar/java-design-patterns)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>rate-limiter</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>22</maven.compiler.source>
|
||||
<maven.compiler.target>22</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.jupiter.version>5.11.1</junit.jupiter.version>
|
||||
<junit.platform.version>1.11.1</junit.platform.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- JUnit 5 API and Engine -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.12.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.4.11</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.24.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>2.44.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>check</goal> <!-- Fails the build if formatting is off -->
|
||||
<goal>apply</goal> <!-- Automatically formats code -->
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<java>
|
||||
<googleJavaFormat />
|
||||
</java>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<useModulePath>false</useModulePath> <!-- for Java 17+ compatibility -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Adaptive rate limiter that adjusts limits based on system health. */
|
||||
public class AdaptiveRateLimiter implements RateLimiter {
|
||||
private final int initialLimit;
|
||||
private final int maxLimit;
|
||||
private final AtomicInteger currentLimit;
|
||||
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public AdaptiveRateLimiter(int initialLimit, int maxLimit) {
|
||||
this.initialLimit = initialLimit;
|
||||
this.maxLimit = maxLimit;
|
||||
this.currentLimit = new AtomicInteger(initialLimit);
|
||||
// Periodically increase limit to recover if system appears healthy
|
||||
healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(String serviceName, String operationName) throws RateLimitException {
|
||||
String key = serviceName + ":" + operationName;
|
||||
int current = currentLimit.get();
|
||||
|
||||
// Reuse or create TokenBucket for this key using currentLimit
|
||||
RateLimiter limiter =
|
||||
limiters.computeIfAbsent(key, k -> new TokenBucketRateLimiter(current, current));
|
||||
|
||||
try {
|
||||
limiter.check(serviceName, operationName);
|
||||
System.out.printf(
|
||||
"[Adaptive] Allowed %s.%s - CurrentLimit: %d%n", serviceName, operationName, current);
|
||||
} catch (RateLimitException e) {
|
||||
// On throttling, reduce system limit to reduce load
|
||||
currentLimit.updateAndGet(curr -> Math.max(initialLimit, curr / 2));
|
||||
System.out.printf(
|
||||
"[Adaptive] Throttled %s.%s - Decreasing limit to %d%n",
|
||||
serviceName, operationName, currentLimit.get());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic recovery mechanism to raise limits when the system is under control
|
||||
private void adjustLimits() {
|
||||
int updated = currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2)));
|
||||
System.out.printf("[Adaptive] Health check passed - Increasing limit to %d%n", updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The <em>Rate Limiter</em> pattern is a key defensive strategy used to prevent system overload and
|
||||
* ensure fair usage of shared services. This demo showcases how different rate limiting techniques
|
||||
* can regulate traffic in distributed systems.
|
||||
*
|
||||
* <p>Specifically, this simulation implements three rate limiter strategies:
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>Token Bucket</b> – Allows short bursts followed by steady request rates.
|
||||
* <li><b>Fixed Window</b> – Enforces a strict limit per discrete time window (e.g., 3
|
||||
* requests/sec).
|
||||
* <li><b>Adaptive</b> – Dynamically scales limits based on system health, simulating elastic
|
||||
* backoff.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Each simulated service (e.g., S3, DynamoDB, Lambda) is governed by one of these limiters.
|
||||
* Multiple concurrent client threads issue randomized requests to these services over a fixed
|
||||
* duration. Each request is either:
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>ALLOWED</b> – Permitted under the current rate limit
|
||||
* <li><b>THROTTLED</b> – Rejected due to quota exhaustion
|
||||
* <li><b>FAILED</b> – Dropped due to transient service failure
|
||||
* </ul>
|
||||
*
|
||||
* <p>Statistics are printed every few seconds, and the simulation exits gracefully after a fixed
|
||||
* runtime, offering a clear view into how each limiter behaves under pressure.
|
||||
*
|
||||
* <p><b>Relation to AWS API Gateway:</b><br>
|
||||
* This implementation mirrors the throttling behavior described in the <a
|
||||
* href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html">
|
||||
* AWS API Gateway Request Throttling documentation</a>, where limits are applied per second and
|
||||
* over longer durations (burst and rate limits). The <code>TokenBucketRateLimiter</code> mimics
|
||||
* burst capacity, the <code>FixedWindowRateLimiter</code> models steady rate enforcement, and the
|
||||
* <code>AdaptiveRateLimiter</code> reflects elasticity in real-world systems like AWS Lambda under
|
||||
* variable load.
|
||||
*/
|
||||
public final class App {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||
|
||||
private static final int RUN_DURATION_SECONDS = 10;
|
||||
private static final int SHUTDOWN_TIMEOUT_SECONDS = 5;
|
||||
|
||||
static final AtomicInteger successfulRequests = new AtomicInteger();
|
||||
static final AtomicInteger throttledRequests = new AtomicInteger();
|
||||
static final AtomicInteger failedRequests = new AtomicInteger();
|
||||
static final AtomicBoolean running = new AtomicBoolean(true);
|
||||
private static final String DIVIDER_LINE = "====================================";
|
||||
|
||||
public static void main(String[] args) {
|
||||
LOGGER.info("Starting Rate Limiter Demo");
|
||||
LOGGER.info(DIVIDER_LINE);
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(3);
|
||||
ScheduledExecutorService statsPrinter = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
try {
|
||||
TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1);
|
||||
FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1);
|
||||
AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6);
|
||||
|
||||
statsPrinter.scheduleAtFixedRate(App::printStats, 2, 2, TimeUnit.SECONDS);
|
||||
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
executor.submit(createClientTask(i, tb, fw, ar));
|
||||
}
|
||||
|
||||
Thread.sleep(RUN_DURATION_SECONDS * 1000L);
|
||||
LOGGER.info("Shutting down the demo...");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
running.set(false);
|
||||
shutdownExecutor(executor, "mainExecutor");
|
||||
shutdownExecutor(statsPrinter, "statsPrinter");
|
||||
printFinalStats();
|
||||
LOGGER.info("Demo completed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void shutdownExecutor(ExecutorService service, String name) {
|
||||
service.shutdown();
|
||||
try {
|
||||
if (!service.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
service.shutdownNow();
|
||||
LOGGER.warn("Forced shutdown of {}", name);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
service.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
static Runnable createClientTask(
|
||||
int clientId, RateLimiter s3Limiter, RateLimiter dynamoDbLimiter, RateLimiter lambdaLimiter) {
|
||||
|
||||
return () -> {
|
||||
String[] services = {"s3", "dynamodb", "lambda"};
|
||||
String[] operations = {
|
||||
"GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"
|
||||
};
|
||||
SecureRandom random = new SecureRandom(); // ✅ Safe & compliant for SonarCloud
|
||||
|
||||
while (running.get() && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
String service = services[random.nextInt(services.length)];
|
||||
String operation = operations[random.nextInt(operations.length)];
|
||||
|
||||
switch (service) {
|
||||
case "s3" -> makeRequest(clientId, s3Limiter, service, operation);
|
||||
case "dynamodb" -> makeRequest(clientId, dynamoDbLimiter, service, operation);
|
||||
case "lambda" -> makeRequest(clientId, lambdaLimiter, service, operation);
|
||||
default -> LOGGER.warn("Unknown service: {}", service);
|
||||
}
|
||||
|
||||
Thread.sleep(30L + random.nextInt(50));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void makeRequest(int clientId, RateLimiter limiter, String service, String operation) {
|
||||
try {
|
||||
limiter.check(service, operation);
|
||||
successfulRequests.incrementAndGet();
|
||||
LOGGER.info("Client {}: {}.{} - ALLOWED", clientId, service, operation);
|
||||
} catch (ThrottlingException e) {
|
||||
throttledRequests.incrementAndGet();
|
||||
LOGGER.warn(
|
||||
"Client {}: {}.{} - THROTTLED (Retry in {}ms)",
|
||||
clientId,
|
||||
service,
|
||||
operation,
|
||||
e.getRetryAfterMillis());
|
||||
} catch (ServiceUnavailableException e) {
|
||||
failedRequests.incrementAndGet();
|
||||
LOGGER.warn("Client {}: {}.{} - SERVICE UNAVAILABLE", clientId, service, operation);
|
||||
} catch (Exception e) {
|
||||
failedRequests.incrementAndGet();
|
||||
LOGGER.error("Client {}: {}.{} - ERROR: {}", clientId, service, operation, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static void printStats() {
|
||||
if (!running.get()) return;
|
||||
LOGGER.info("=== Current Statistics ===");
|
||||
LOGGER.info("Successful Requests: {}", successfulRequests.get());
|
||||
LOGGER.info("Throttled Requests : {}", throttledRequests.get());
|
||||
LOGGER.info("Failed Requests : {}", failedRequests.get());
|
||||
LOGGER.info(DIVIDER_LINE);
|
||||
}
|
||||
|
||||
static void printFinalStats() {
|
||||
LOGGER.info("Final Statistics");
|
||||
LOGGER.info(DIVIDER_LINE);
|
||||
LOGGER.info("Successful Requests: {}", successfulRequests.get());
|
||||
LOGGER.info("Throttled Requests : {}", throttledRequests.get());
|
||||
LOGGER.info("Failed Requests : {}", failedRequests.get());
|
||||
LOGGER.info(DIVIDER_LINE);
|
||||
}
|
||||
|
||||
static void resetCountersForTesting() {
|
||||
successfulRequests.set(0);
|
||||
throttledRequests.set(0);
|
||||
failedRequests.set(0);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
/**
|
||||
* A rate-limited customer lookup operation. This class wraps the rate limiting logic and represents
|
||||
* an executable business request.
|
||||
*/
|
||||
public class FindCustomerRequest implements RateLimitOperation<String> {
|
||||
private final String customerId;
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
public FindCustomerRequest(String customerId, RateLimiter rateLimiter) {
|
||||
this.customerId = customerId;
|
||||
this.rateLimiter = rateLimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "CustomerService";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOperationName() {
|
||||
return "FindCustomer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute() throws RateLimitException {
|
||||
// Ensure the operation respects the assigned rate limiter
|
||||
rateLimiter.check(getServiceName(), getOperationName());
|
||||
|
||||
// Simulate actual operation
|
||||
try {
|
||||
Thread.sleep(50); // Simulate processing time
|
||||
return "Customer-" + customerId;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceUnavailableException(getServiceName(), 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Implements a fixed window rate limiter. It allows up to 'limit' number of requests within a time
|
||||
* window of fixed size.
|
||||
*/
|
||||
public class FixedWindowRateLimiter implements RateLimiter {
|
||||
private final int limit;
|
||||
private final long windowMillis;
|
||||
private final ConcurrentHashMap<String, WindowCounter> counters = new ConcurrentHashMap<>();
|
||||
|
||||
public FixedWindowRateLimiter(int limit, long windowSeconds) {
|
||||
this.limit = limit;
|
||||
this.windowMillis = TimeUnit.SECONDS.toMillis(windowSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void check(String serviceName, String operationName)
|
||||
throws RateLimitException {
|
||||
String key = serviceName + ":" + operationName;
|
||||
WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter());
|
||||
|
||||
if (!counter.tryIncrement()) {
|
||||
System.out.printf(
|
||||
"[FixedWindow] Throttled %s.%s - Limit %d reached in window%n",
|
||||
serviceName, operationName, limit);
|
||||
throw new RateLimitException("Rate limit exceeded for " + key, windowMillis);
|
||||
} else {
|
||||
System.out.printf(
|
||||
"[FixedWindow] Allowed %s.%s - Count within window%n", serviceName, operationName);
|
||||
}
|
||||
}
|
||||
|
||||
/** Tracks the count of requests within the current window. */
|
||||
private class WindowCounter {
|
||||
private AtomicInteger count = new AtomicInteger(0);
|
||||
private volatile long windowStart = System.currentTimeMillis();
|
||||
|
||||
synchronized boolean tryIncrement() {
|
||||
long now = System.currentTimeMillis();
|
||||
// Reset window if expired
|
||||
if (now - windowStart > windowMillis) {
|
||||
count.set(0);
|
||||
windowStart = now;
|
||||
}
|
||||
// Enforce the request limit within window
|
||||
return count.incrementAndGet() <= limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
/** Base exception for rate limiting errors. */
|
||||
public class RateLimitException extends Exception {
|
||||
private final long retryAfterMillis;
|
||||
|
||||
public RateLimitException(String message, long retryAfterMillis) {
|
||||
super(message);
|
||||
this.retryAfterMillis = retryAfterMillis;
|
||||
}
|
||||
|
||||
public long getRetryAfterMillis() {
|
||||
return retryAfterMillis;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
/** Represents a business operation that needs rate limiting. Supports type-safe return values. */
|
||||
public interface RateLimitOperation<T> {
|
||||
String getServiceName();
|
||||
|
||||
String getOperationName();
|
||||
|
||||
T execute() throws RateLimitException;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
/** Base interface for all rate limiter strategies. */
|
||||
public interface RateLimiter {
|
||||
/**
|
||||
* Checks if a request is allowed under current rate limits
|
||||
*
|
||||
* @param serviceName Service being called (e.g., "dynamodb")
|
||||
* @param operationName Operation being performed (e.g., "Query")
|
||||
* @throws RateLimitException if request exceeds limits
|
||||
*/
|
||||
void check(String serviceName, String operationName) throws RateLimitException;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
/** Exception for when a service is temporarily unavailable. */
|
||||
public class ServiceUnavailableException extends RateLimitException {
|
||||
private final String serviceName;
|
||||
|
||||
public ServiceUnavailableException(String serviceName, long retryAfterMillis) {
|
||||
super("Service temporarily unavailable: " + serviceName, retryAfterMillis);
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
/** Exception thrown when AWS-style throttling occurs. */
|
||||
public class ThrottlingException extends RateLimitException {
|
||||
private final String serviceName;
|
||||
private final String errorCode;
|
||||
|
||||
public ThrottlingException(String serviceName, String operationName, long retryAfterMillis) {
|
||||
super("AWS throttling error for " + serviceName + "/" + operationName, retryAfterMillis);
|
||||
this.serviceName = serviceName;
|
||||
this.errorCode = "ThrottlingException";
|
||||
}
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Token Bucket rate limiter implementation. Allows requests to proceed as long as there are tokens
|
||||
* available in the bucket. Tokens are added at a fixed interval up to a defined capacity.
|
||||
*/
|
||||
public class TokenBucketRateLimiter implements RateLimiter {
|
||||
private final int capacity;
|
||||
private final int refillRate;
|
||||
private final ConcurrentHashMap<String, TokenBucket> buckets = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public TokenBucketRateLimiter(int capacity, int refillRate) {
|
||||
this.capacity = capacity;
|
||||
this.refillRate = refillRate;
|
||||
// Refill tokens in all buckets every second
|
||||
scheduler.scheduleAtFixedRate(this::refillBuckets, 1, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(String serviceName, String operationName) throws RateLimitException {
|
||||
String key = serviceName + ":" + operationName;
|
||||
TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(capacity));
|
||||
|
||||
if (!bucket.tryConsume()) {
|
||||
System.out.printf(
|
||||
"[TokenBucket] Throttled %s.%s - No tokens available%n", serviceName, operationName);
|
||||
throw new ThrottlingException(serviceName, operationName, 1000);
|
||||
} else {
|
||||
System.out.printf(
|
||||
"[TokenBucket] Allowed %s.%s - Tokens remaining%n", serviceName, operationName);
|
||||
}
|
||||
}
|
||||
|
||||
private void refillBuckets() {
|
||||
buckets.forEach((k, b) -> b.refill(refillRate));
|
||||
}
|
||||
|
||||
/** Inner class that represents the bucket holding tokens for each service-operation. */
|
||||
private static class TokenBucket {
|
||||
private final int capacity;
|
||||
private final AtomicInteger tokens;
|
||||
|
||||
TokenBucket(int capacity) {
|
||||
this.capacity = capacity;
|
||||
this.tokens = new AtomicInteger(capacity);
|
||||
}
|
||||
|
||||
boolean tryConsume() {
|
||||
while (true) {
|
||||
int current = tokens.get();
|
||||
if (current <= 0) return false;
|
||||
if (tokens.compareAndSet(current, current - 1)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
void refill(int amount) {
|
||||
tokens.getAndUpdate(current -> Math.min(current + amount, capacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AdaptiveRateLimiterTest {
|
||||
@Test
|
||||
void shouldDecreaseLimitWhenThrottled() throws Exception {
|
||||
AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
|
||||
|
||||
// Exceed initial limit
|
||||
for (int i = 0; i < 11; i++) {
|
||||
try {
|
||||
limiter.check("test", "op");
|
||||
} catch (RateLimitException e) {
|
||||
// Expected after 10 requests
|
||||
}
|
||||
}
|
||||
|
||||
// Verify limit was reduced
|
||||
assertThrows(
|
||||
RateLimitException.class,
|
||||
() -> {
|
||||
for (int i = 0; i < 6; i++) { // New limit should be 5 (10/2)
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGraduallyIncreaseLimitWhenHealthy() throws Exception {
|
||||
AdaptiveRateLimiter limiter =
|
||||
new AdaptiveRateLimiter(4, 10); // Start from 4 → expect 2 → expect increase to 4
|
||||
|
||||
// Force throttling to reduce limit
|
||||
for (int i = 0; i < 5; i++) {
|
||||
try {
|
||||
limiter.check("test", "op");
|
||||
} catch (RateLimitException e) {
|
||||
// Expected to throttle and reduce limit
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for health check to increase limit
|
||||
Thread.sleep(11000); // Wait slightly more than 10 seconds
|
||||
|
||||
// Allow up to 4 requests again (limit should've increased to 4)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
|
||||
// 5th should throw exception again
|
||||
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link App}. */
|
||||
class AppTest {
|
||||
|
||||
private RateLimiter mockLimiter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockLimiter = mock(RateLimiter.class);
|
||||
AppTestUtils.resetCounters(); // Ensures counters are clean before every test
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowRequest() {
|
||||
AppTestUtils.invokeMakeRequest(1, mockLimiter, "s3", "GetObject");
|
||||
assertEquals(1, AppTestUtils.getSuccessfulRequests().get(), "Successful count should be 1");
|
||||
assertEquals(0, AppTestUtils.getThrottledRequests().get(), "Throttled count should be 0");
|
||||
assertEquals(0, AppTestUtils.getFailedRequests().get(), "Failed count should be 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleThrottlingException() throws Exception {
|
||||
doThrow(new ThrottlingException("s3", "PutObject", 1000)).when(mockLimiter).check(any(), any());
|
||||
AppTestUtils.invokeMakeRequest(2, mockLimiter, "s3", "PutObject");
|
||||
assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
|
||||
assertEquals(1, AppTestUtils.getThrottledRequests().get());
|
||||
assertEquals(0, AppTestUtils.getFailedRequests().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleServiceUnavailableException() throws Exception {
|
||||
doThrow(new ServiceUnavailableException("lambda", 500)).when(mockLimiter).check(any(), any());
|
||||
AppTestUtils.invokeMakeRequest(3, mockLimiter, "lambda", "Invoke");
|
||||
assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
|
||||
assertEquals(0, AppTestUtils.getThrottledRequests().get());
|
||||
assertEquals(1, AppTestUtils.getFailedRequests().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleGenericException() throws Exception {
|
||||
doThrow(new RuntimeException("Unexpected")).when(mockLimiter).check(any(), any());
|
||||
AppTestUtils.invokeMakeRequest(4, mockLimiter, "dynamodb", "Query");
|
||||
assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
|
||||
assertEquals(0, AppTestUtils.getThrottledRequests().get());
|
||||
assertEquals(1, AppTestUtils.getFailedRequests().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunMainMethodWithoutException() {
|
||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AppTestUtils {
|
||||
|
||||
public static void invokeMakeRequest(
|
||||
int clientId, RateLimiter limiter, String service, String operation) {
|
||||
App.makeRequest(clientId, limiter, service, operation);
|
||||
}
|
||||
|
||||
public static void resetCounters() {
|
||||
App.resetCountersForTesting();
|
||||
}
|
||||
|
||||
public static AtomicInteger getSuccessfulRequests() {
|
||||
return App.successfulRequests;
|
||||
}
|
||||
|
||||
public static AtomicInteger getThrottledRequests() {
|
||||
return App.throttledRequests;
|
||||
}
|
||||
|
||||
public static AtomicInteger getFailedRequests() {
|
||||
return App.failedRequests;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ConcurrencyTests {
|
||||
@Test
|
||||
void tokenBucketShouldHandleConcurrentRequests() throws Exception {
|
||||
int threadCount = 10;
|
||||
int requestLimit = 5;
|
||||
RateLimiter limiter = new TokenBucketRateLimiter(requestLimit, requestLimit);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||
|
||||
AtomicInteger successCount = new AtomicInteger();
|
||||
AtomicInteger failureCount = new AtomicInteger();
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
executor.submit(
|
||||
() -> {
|
||||
try {
|
||||
limiter.check("test", "op");
|
||||
successCount.incrementAndGet();
|
||||
} catch (RateLimitException e) {
|
||||
failureCount.incrementAndGet();
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
latch.await();
|
||||
assertEquals(requestLimit, successCount.get());
|
||||
assertEquals(threadCount - requestLimit, failureCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adaptiveLimiterShouldAdjustUnderLoad() throws Exception {
|
||||
AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(20);
|
||||
|
||||
// Flood with requests to trigger throttling
|
||||
for (int i = 0; i < 30; i++) {
|
||||
executor.submit(
|
||||
() -> {
|
||||
try {
|
||||
limiter.check("test", "op");
|
||||
} catch (RateLimitException ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Thread.sleep(15000); // Wait for adjustment
|
||||
|
||||
// Verify new limit is in effect
|
||||
int allowed = 0;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
try {
|
||||
limiter.check("test", "op");
|
||||
allowed++;
|
||||
} catch (RateLimitException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(allowed > 5 && allowed < 15); // Should be between initial and max
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ExceptionTests {
|
||||
@Test
|
||||
void rateLimitExceptionShouldContainRetryInfo() {
|
||||
RateLimitException exception = new RateLimitException("Test", 1000);
|
||||
assertEquals(1000, exception.getRetryAfterMillis());
|
||||
assertEquals("Test", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void throttlingExceptionShouldContainServiceInfo() {
|
||||
ThrottlingException exception = new ThrottlingException("dynamodb", "Query", 500);
|
||||
assertEquals("dynamodb", exception.getServiceName());
|
||||
assertEquals("ThrottlingException", exception.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void serviceUnavailableExceptionShouldContainRetryInfo() {
|
||||
ServiceUnavailableException exception = new ServiceUnavailableException("s3", 2000);
|
||||
assertEquals("s3", exception.getServiceName());
|
||||
assertEquals(2000, exception.getRetryAfterMillis());
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FindCustomerRequestTest implements RateLimitOperationTest<String> {
|
||||
|
||||
@Override
|
||||
public RateLimitOperation<String> createOperation(RateLimiter limiter) {
|
||||
return new FindCustomerRequest("123", limiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteWhenUnderRateLimit() throws Exception {
|
||||
RateLimiter limiter = new TokenBucketRateLimiter(10, 10);
|
||||
RateLimitOperation<String> request = createOperation(limiter);
|
||||
|
||||
String result = request.execute();
|
||||
assertEquals("Customer-123", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenRateLimitExceeded() {
|
||||
RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled
|
||||
RateLimitOperation<String> request = createOperation(limiter);
|
||||
|
||||
assertThrows(RateLimitException.class, request::execute);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCorrectServiceAndOperationNames() {
|
||||
RateLimiter limiter = new TokenBucketRateLimiter(10, 10);
|
||||
FindCustomerRequest request = new FindCustomerRequest("123", limiter);
|
||||
|
||||
assertEquals("CustomerService", request.getServiceName());
|
||||
assertEquals("FindCustomer", request.getOperationName());
|
||||
}
|
||||
|
||||
// Reuse helper logic from the interface for coverage
|
||||
@Test
|
||||
void shouldExecuteUsingDefaultHelper() throws Exception {
|
||||
RateLimiter limiter = new TokenBucketRateLimiter(5, 5);
|
||||
shouldExecuteWhenUnderLimit(createOperation(limiter));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowServiceUnavailableOnInterruptedException() {
|
||||
RateLimiter noOpLimiter = (service, operation) -> {}; // no throttling
|
||||
|
||||
FindCustomerRequest request =
|
||||
new FindCustomerRequest("999", noOpLimiter) {
|
||||
@Override
|
||||
public String execute() throws RateLimitException {
|
||||
Thread.currentThread().interrupt(); // Simulate thread interruption
|
||||
return super.execute(); // Should throw ServiceUnavailableException
|
||||
}
|
||||
};
|
||||
|
||||
assertThrows(ServiceUnavailableException.class, request::execute);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FixedWindowRateLimiterTest extends RateLimiterTest {
|
||||
@Override
|
||||
protected RateLimiter createRateLimiter(int limit, long windowMillis) {
|
||||
return new FixedWindowRateLimiter(limit, windowMillis / 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResetCounterAfterWindow() throws Exception {
|
||||
FixedWindowRateLimiter limiter =
|
||||
new FixedWindowRateLimiter(1, 1); // 1 request per 1 second window
|
||||
|
||||
// First request should pass
|
||||
limiter.check("test", "op");
|
||||
|
||||
// Second request in same window should be throttled
|
||||
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
||||
|
||||
// Wait a bit more than 1 second to ensure window resets
|
||||
TimeUnit.MILLISECONDS.sleep(1100);
|
||||
|
||||
// After window reset, this should pass again
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAllowMoreThanLimitInWindow() throws Exception {
|
||||
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(3, 1);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
interface RateLimitOperationTest<T> {
|
||||
|
||||
RateLimitOperation<T> createOperation(RateLimiter limiter);
|
||||
|
||||
@Test
|
||||
default void shouldThrowWhenRateLimited() {
|
||||
RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled
|
||||
RateLimitOperation<T> operation = createOperation(limiter);
|
||||
assertThrows(RateLimitException.class, operation::execute);
|
||||
}
|
||||
|
||||
// ✅ No @Test here, just a helper method
|
||||
default void shouldExecuteWhenUnderLimit(RateLimitOperation<T> operation) throws Exception {
|
||||
assertNotNull(operation.execute());
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public abstract class RateLimiterTest {
|
||||
protected abstract RateLimiter createRateLimiter(int limit, long windowMillis);
|
||||
|
||||
@Test
|
||||
void shouldAllowRequestsWithinLimit() throws Exception {
|
||||
RateLimiter limiter = createRateLimiter(5, 1000);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenLimitExceeded() throws Exception {
|
||||
RateLimiter limiter = createRateLimiter(2, 1000);
|
||||
limiter.check("test", "op");
|
||||
limiter.check("test", "op");
|
||||
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.iluwatar.rate.limiting.pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TokenBucketRateLimiterTest extends RateLimiterTest {
|
||||
@Override
|
||||
protected RateLimiter createRateLimiter(int limit, long windowMillis) {
|
||||
return new TokenBucketRateLimiter(limit, (int) (limit * 1000 / windowMillis));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowBurstRequests() throws Exception {
|
||||
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 5);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRefillTokensAfterTime() throws Exception {
|
||||
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1, 1);
|
||||
limiter.check("test", "op");
|
||||
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
||||
|
||||
TimeUnit.SECONDS.sleep(1);
|
||||
limiter.check("test", "op");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMultipleServicesSeparately() throws Exception {
|
||||
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1, 1);
|
||||
limiter.check("service1", "op");
|
||||
limiter.check("service2", "op");
|
||||
assertThrows(RateLimitException.class, () -> limiter.check("service1", "op"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user