diff --git a/pom.xml b/pom.xml index 8337c97966da..e967691baa65 100644 --- a/pom.xml +++ b/pom.xml @@ -246,6 +246,7 @@ visitor backpressure actor-model + rate-limiting-pattern diff --git a/rate-limiting-pattern/README.md b/rate-limiting-pattern/README.md new file mode 100644 index 000000000000..de7742a5d13f --- /dev/null +++ b/rate-limiting-pattern/README.md @@ -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 + +![UML Class Diagram](etc/UMLClassDiagram.png) + +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 + +![Token Bucket Rate Limiter](etc/TokenBucketRateLimiter.png) + +### 2. Fixed Window Strategy + +![Fixed Window Rate Limiter](etc/FixedWindowRateLimiter.png) + +### 3. Adaptive Rate Limiting Strategy + +![Adaptive Rate Limiter](etc/AdaptiveRateLimiter.png) + +--- + +### 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 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 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 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) diff --git a/rate-limiting-pattern/etc/AdaptiveRateLimiter.png b/rate-limiting-pattern/etc/AdaptiveRateLimiter.png new file mode 100644 index 000000000000..2d849ee8d057 Binary files /dev/null and b/rate-limiting-pattern/etc/AdaptiveRateLimiter.png differ diff --git a/rate-limiting-pattern/etc/FixedWindowRateLimiter.png b/rate-limiting-pattern/etc/FixedWindowRateLimiter.png new file mode 100644 index 000000000000..a81f61d7fa95 Binary files /dev/null and b/rate-limiting-pattern/etc/FixedWindowRateLimiter.png differ diff --git a/rate-limiting-pattern/etc/TokenBucketRateLimiter.png b/rate-limiting-pattern/etc/TokenBucketRateLimiter.png new file mode 100644 index 000000000000..d41701781e27 Binary files /dev/null and b/rate-limiting-pattern/etc/TokenBucketRateLimiter.png differ diff --git a/rate-limiting-pattern/etc/UMLClassDiagram.png b/rate-limiting-pattern/etc/UMLClassDiagram.png new file mode 100644 index 000000000000..9292880244e0 Binary files /dev/null and b/rate-limiting-pattern/etc/UMLClassDiagram.png differ diff --git a/rate-limiting-pattern/pom.xml b/rate-limiting-pattern/pom.xml new file mode 100644 index 000000000000..672ab73eb7b2 --- /dev/null +++ b/rate-limiting-pattern/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + rate-limiter + + + 22 + 22 + UTF-8 + 5.11.1 + 1.11.1 + + + + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + org.mockito + mockito-core + 5.12.0 + test + + + + + org.slf4j + slf4j-api + 2.0.9 + + + ch.qos.logback + logback-classic + 1.4.11 + + + + + org.assertj + assertj-core + 3.24.2 + test + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.44.2 + + + + check + apply + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + false + + + + + diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiter.java new file mode 100644 index 000000000000..1b18a8c941b9 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiter.java @@ -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 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); + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java new file mode 100644 index 000000000000..e76ed5254345 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java @@ -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 Rate Limiter 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. + * + *

Specifically, this simulation implements three rate limiter strategies: + * + *

    + *
  • Token Bucket – Allows short bursts followed by steady request rates. + *
  • Fixed Window – Enforces a strict limit per discrete time window (e.g., 3 + * requests/sec). + *
  • Adaptive – Dynamically scales limits based on system health, simulating elastic + * backoff. + *
+ * + *

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: + * + *

    + *
  • ALLOWED – Permitted under the current rate limit + *
  • THROTTLED – Rejected due to quota exhaustion + *
  • FAILED – Dropped due to transient service failure + *
+ * + *

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. + * + *

Relation to AWS API Gateway:
+ * This implementation mirrors the throttling behavior described in the + * AWS API Gateway Request Throttling documentation, where limits are applied per second and + * over longer durations (burst and rate limits). The TokenBucketRateLimiter mimics + * burst capacity, the FixedWindowRateLimiter models steady rate enforcement, and the + * AdaptiveRateLimiter 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); + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java new file mode 100644 index 000000000000..a61fc35f496a --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java @@ -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 { + 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); + } + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java new file mode 100644 index 000000000000..3a2b52888ca7 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java @@ -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 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; + } + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java new file mode 100644 index 000000000000..2b3cc1f3006b --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java @@ -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; + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java new file mode 100644 index 000000000000..59191e81fc04 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java @@ -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 { + String getServiceName(); + + String getOperationName(); + + T execute() throws RateLimitException; +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java new file mode 100644 index 000000000000..19495b401ddb --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java @@ -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; +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java new file mode 100644 index 000000000000..f9fc55f15d20 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java @@ -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; + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java new file mode 100644 index 000000000000..e07087dfee6c --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java @@ -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; + } +} diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java new file mode 100644 index 000000000000..3001c880aad3 --- /dev/null +++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java @@ -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 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)); + } + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java new file mode 100644 index 000000000000..042d606490d0 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java @@ -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")); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java new file mode 100644 index 000000000000..11815a75de84 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java @@ -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[] {})); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java new file mode 100644 index 000000000000..9d652ec96c63 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java @@ -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; + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java new file mode 100644 index 000000000000..35a1294a8ad3 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java @@ -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 + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java new file mode 100644 index 000000000000..a7b037fbe73f --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java @@ -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()); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java new file mode 100644 index 000000000000..d0c3197289cd --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java @@ -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 { + + @Override + public RateLimitOperation createOperation(RateLimiter limiter) { + return new FindCustomerRequest("123", limiter); + } + + @Test + void shouldExecuteWhenUnderRateLimit() throws Exception { + RateLimiter limiter = new TokenBucketRateLimiter(10, 10); + RateLimitOperation request = createOperation(limiter); + + String result = request.execute(); + assertEquals("Customer-123", result); + } + + @Test + void shouldThrowWhenRateLimitExceeded() { + RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled + RateLimitOperation 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); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java new file mode 100644 index 000000000000..656185b7e68e --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java @@ -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")); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java new file mode 100644 index 000000000000..d4922bdfc073 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java @@ -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 { + + RateLimitOperation createOperation(RateLimiter limiter); + + @Test + default void shouldThrowWhenRateLimited() { + RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled + RateLimitOperation operation = createOperation(limiter); + assertThrows(RateLimitException.class, operation::execute); + } + + // ✅ No @Test here, just a helper method + default void shouldExecuteWhenUnderLimit(RateLimitOperation operation) throws Exception { + assertNotNull(operation.execute()); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java new file mode 100644 index 000000000000..7f1e6b4a2806 --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java @@ -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")); + } +} diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java new file mode 100644 index 000000000000..5696299512fb --- /dev/null +++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java @@ -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")); + } +}