Skip to content

Commit fe253e4

Browse files
committed
added test coverage for app.java and fixed random to be thread safe #2973
1 parent 8afefee commit fe253e4

File tree

5 files changed

+182
-56
lines changed

5 files changed

+182
-56
lines changed

rate-limiting-pattern/pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@
2929
<scope>test</scope>
3030
</dependency>
3131

32+
<dependency>
33+
<groupId>org.mockito</groupId>
34+
<artifactId>mockito-core</artifactId>
35+
<version>5.12.0</version>
36+
<scope>test</scope>
37+
</dependency>
38+
39+
40+
<dependency>
41+
<groupId>org.slf4j</groupId>
42+
<artifactId>slf4j-api</artifactId>
43+
<version>2.0.9</version>
44+
</dependency>
45+
<dependency>
46+
<groupId>ch.qos.logback</groupId>
47+
<artifactId>logback-classic</artifactId>
48+
<version>1.4.11</version>
49+
</dependency>
50+
51+
3252
<dependency>
3353
<groupId>org.assertj</groupId>
3454
<artifactId>assertj-core</artifactId>
Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.iluwatar.rate.limiting.pattern;
22

3-
import java.util.Random;
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
45
import java.util.concurrent.*;
56
import java.util.concurrent.atomic.AtomicBoolean;
67
import java.util.concurrent.atomic.AtomicInteger;
@@ -43,66 +44,66 @@
4344
* variable load.
4445
*/
4546
public final class App {
47+
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
48+
4649
private static final int RUN_DURATION_SECONDS = 10;
4750
private static final int SHUTDOWN_TIMEOUT_SECONDS = 5;
4851

49-
private static final AtomicInteger successfulRequests = new AtomicInteger();
50-
private static final AtomicInteger throttledRequests = new AtomicInteger();
51-
private static final AtomicInteger failedRequests = new AtomicInteger();
52-
private static final AtomicBoolean running = new AtomicBoolean(true);
52+
static final AtomicInteger successfulRequests = new AtomicInteger();
53+
static final AtomicInteger throttledRequests = new AtomicInteger();
54+
static final AtomicInteger failedRequests = new AtomicInteger();
55+
static final AtomicBoolean running = new AtomicBoolean(true);
56+
private static final String DIVIDER_LINE = "====================================";
5357

5458
public static void main(String[] args) {
55-
System.out.println("\nStarting Rate Limiter Demo");
56-
System.out.println("====================================");
59+
LOGGER.info("Starting Rate Limiter Demo");
60+
LOGGER.info(DIVIDER_LINE);
5761

5862
ExecutorService executor = Executors.newFixedThreadPool(3);
5963
ScheduledExecutorService statsPrinter = Executors.newSingleThreadScheduledExecutor();
6064

6165
try {
62-
// Explicit limiter setup for demonstration clarity
63-
TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1); // capacity 2, refill 1/sec
64-
FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1); // max 3 req/sec
65-
AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6); // adaptive from 2 to 6 req/sec
66+
TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1);
67+
FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1);
68+
AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6);
6669

67-
// Print statistics every 2 seconds
6870
statsPrinter.scheduleAtFixedRate(App::printStats, 2, 2, TimeUnit.SECONDS);
6971

70-
// Launch 3 simulated clients
7172
for (int i = 1; i <= 3; i++) {
7273
executor.submit(createClientTask(i, tb, fw, ar));
7374
}
7475

75-
// Run simulation for N seconds
7676
Thread.sleep(RUN_DURATION_SECONDS * 1000L);
77-
System.out.println("\nShutting down the demo...");
78-
77+
LOGGER.info("Shutting down the demo...");
78+
} catch (InterruptedException e) {
79+
Thread.currentThread().interrupt();
80+
} finally {
7981
running.set(false);
80-
executor.shutdown();
81-
statsPrinter.shutdown();
82+
shutdownExecutor(executor, "mainExecutor");
83+
shutdownExecutor(statsPrinter, "statsPrinter");
84+
printFinalStats();
85+
LOGGER.info("Demo completed.");
86+
}
87+
}
8288

83-
if (!executor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
84-
executor.shutdownNow();
85-
}
86-
if (!statsPrinter.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
87-
statsPrinter.shutdownNow();
89+
private static void shutdownExecutor(ExecutorService service, String name) {
90+
service.shutdown();
91+
try {
92+
if (!service.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
93+
service.shutdownNow();
94+
LOGGER.warn("Forced shutdown of {}", name);
8895
}
89-
9096
} catch (InterruptedException e) {
97+
service.shutdownNow();
9198
Thread.currentThread().interrupt();
92-
} finally {
93-
printFinalStats();
94-
System.out.println("Demo completed.");
9599
}
96100
}
97101

98-
private static Runnable createClientTask(
99-
int clientId, RateLimiter s3Limiter, RateLimiter dynamoDbLimiter, RateLimiter lambdaLimiter) {
102+
static Runnable createClientTask(int clientId, RateLimiter s3Limiter, RateLimiter dynamoDbLimiter, RateLimiter lambdaLimiter) {
100103
return () -> {
101104
String[] services = {"s3", "dynamodb", "lambda"};
102-
String[] operations = {
103-
"GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"
104-
};
105-
Random random = new Random();
105+
String[] operations = {"GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"};
106+
ThreadLocalRandom random = ThreadLocalRandom.current();
106107

107108
while (running.get() && !Thread.currentThread().isInterrupted()) {
108109
try {
@@ -113,52 +114,55 @@ private static Runnable createClientTask(
113114
case "s3" -> makeRequest(clientId, s3Limiter, service, operation);
114115
case "dynamodb" -> makeRequest(clientId, dynamoDbLimiter, service, operation);
115116
case "lambda" -> makeRequest(clientId, lambdaLimiter, service, operation);
117+
default -> LOGGER.warn("Unknown service: {}", service);
116118
}
117119

118-
Thread.sleep(30 + random.nextInt(50));
120+
Thread.sleep(30L + random.nextInt(50));
119121
} catch (InterruptedException e) {
120122
Thread.currentThread().interrupt();
121123
}
122124
}
123125
};
124126
}
125127

126-
private static void makeRequest(
127-
int clientId, RateLimiter limiter, String service, String operation) {
128+
static void makeRequest(int clientId, RateLimiter limiter, String service, String operation) {
128129
try {
129130
limiter.check(service, operation);
130131
successfulRequests.incrementAndGet();
131-
System.out.printf("Client %d: %s.%s - ALLOWED%n", clientId, service, operation);
132+
LOGGER.info("Client {}: {}.{} - ALLOWED", clientId, service, operation);
132133
} catch (ThrottlingException e) {
133134
throttledRequests.incrementAndGet();
134-
System.out.printf(
135-
"Client %d: %s.%s - THROTTLED (Retry in %dms)%n",
136-
clientId, service, operation, e.getRetryAfterMillis());
135+
LOGGER.warn("Client {}: {}.{} - THROTTLED (Retry in {}ms)", clientId, service, operation, e.getRetryAfterMillis());
137136
} catch (ServiceUnavailableException e) {
138137
failedRequests.incrementAndGet();
139-
System.out.printf("Client %d: %s.%s - SERVICE UNAVAILABLE%n", clientId, service, operation);
138+
LOGGER.warn("Client {}: {}.{} - SERVICE UNAVAILABLE", clientId, service, operation);
140139
} catch (Exception e) {
141140
failedRequests.incrementAndGet();
142-
System.out.printf(
143-
"Client %d: %s.%s - ERROR: %s%n", clientId, service, operation, e.getMessage());
141+
LOGGER.error("Client {}: {}.{} - ERROR: {}", clientId, service, operation, e.getMessage());
144142
}
145143
}
146144

147-
private static void printStats() {
145+
static void printStats() {
148146
if (!running.get()) return;
149-
System.out.println("\n=== Current Statistics ===");
150-
System.out.printf("Successful Requests: %d%n", successfulRequests.get());
151-
System.out.printf("Throttled Requests : %d%n", throttledRequests.get());
152-
System.out.printf("Failed Requests : %d%n", failedRequests.get());
153-
System.out.println("==========================\n");
147+
LOGGER.info("=== Current Statistics ===");
148+
LOGGER.info("Successful Requests: {}", successfulRequests.get());
149+
LOGGER.info("Throttled Requests : {}", throttledRequests.get());
150+
LOGGER.info("Failed Requests : {}", failedRequests.get());
151+
LOGGER.info(DIVIDER_LINE);
152+
}
153+
154+
static void printFinalStats() {
155+
LOGGER.info("Final Statistics");
156+
LOGGER.info(DIVIDER_LINE);
157+
LOGGER.info("Successful Requests: {}", successfulRequests.get());
158+
LOGGER.info("Throttled Requests : {}", throttledRequests.get());
159+
LOGGER.info("Failed Requests : {}", failedRequests.get());
160+
LOGGER.info(DIVIDER_LINE);
154161
}
155162

156-
private static void printFinalStats() {
157-
System.out.println("\nFinal Statistics");
158-
System.out.println("==========================");
159-
System.out.printf("Successful Requests: %d%n", successfulRequests.get());
160-
System.out.printf("Throttled Requests : %d%n", throttledRequests.get());
161-
System.out.printf("Failed Requests : %d%n", failedRequests.get());
162-
System.out.println("==========================");
163+
static void resetCountersForTesting() {
164+
successfulRequests.set(0);
165+
throttledRequests.set(0);
166+
failedRequests.set(0);
163167
}
164168
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
8+
9+
/**
10+
* Unit tests for {@link App}.
11+
*/
12+
class AppTest {
13+
14+
private RateLimiter mockLimiter;
15+
16+
@BeforeEach
17+
void setUp() {
18+
mockLimiter = mock(RateLimiter.class);
19+
AppTestUtils.resetCounters(); // Ensures counters are clean before every test
20+
}
21+
22+
@Test
23+
void shouldAllowRequest() {
24+
AppTestUtils.invokeMakeRequest(1, mockLimiter, "s3", "GetObject");
25+
assertEquals(1, AppTestUtils.getSuccessfulRequests().get(), "Successful count should be 1");
26+
assertEquals(0, AppTestUtils.getThrottledRequests().get(), "Throttled count should be 0");
27+
assertEquals(0, AppTestUtils.getFailedRequests().get(), "Failed count should be 0");
28+
}
29+
30+
@Test
31+
void shouldHandleThrottlingException() throws Exception {
32+
doThrow(new ThrottlingException("s3", "PutObject", 1000)).when(mockLimiter).check(any(), any());
33+
AppTestUtils.invokeMakeRequest(2, mockLimiter, "s3", "PutObject");
34+
assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
35+
assertEquals(1, AppTestUtils.getThrottledRequests().get());
36+
assertEquals(0, AppTestUtils.getFailedRequests().get());
37+
}
38+
39+
@Test
40+
void shouldHandleServiceUnavailableException() throws Exception {
41+
doThrow(new ServiceUnavailableException("lambda", 500)).when(mockLimiter).check(any(), any());
42+
AppTestUtils.invokeMakeRequest(3, mockLimiter, "lambda", "Invoke");
43+
assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
44+
assertEquals(0, AppTestUtils.getThrottledRequests().get());
45+
assertEquals(1, AppTestUtils.getFailedRequests().get());
46+
}
47+
48+
@Test
49+
void shouldHandleGenericException() throws Exception {
50+
doThrow(new RuntimeException("Unexpected")).when(mockLimiter).check(any(), any());
51+
AppTestUtils.invokeMakeRequest(4, mockLimiter, "dynamodb", "Query");
52+
assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
53+
assertEquals(0, AppTestUtils.getThrottledRequests().get());
54+
assertEquals(1, AppTestUtils.getFailedRequests().get());
55+
}
56+
57+
@Test
58+
void shouldRunMainMethodWithoutException() {
59+
assertDoesNotThrow(() -> App.main(new String[] {}));
60+
}
61+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.iluwatar.rate.limiting.pattern;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
public class AppTestUtils {
6+
7+
public static void invokeMakeRequest(int clientId, RateLimiter limiter, String service, String operation) {
8+
App.makeRequest(clientId, limiter, service, operation);
9+
}
10+
11+
public static void resetCounters() {
12+
App.resetCountersForTesting();
13+
}
14+
15+
public static AtomicInteger getSuccessfulRequests() {
16+
return App.successfulRequests;
17+
}
18+
19+
public static AtomicInteger getThrottledRequests() {
20+
return App.throttledRequests;
21+
}
22+
23+
public static AtomicInteger getFailedRequests() {
24+
return App.failedRequests;
25+
}
26+
}

rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,19 @@ void shouldExecuteUsingDefaultHelper() throws Exception {
4343
RateLimiter limiter = new TokenBucketRateLimiter(5, 5);
4444
shouldExecuteWhenUnderLimit(createOperation(limiter));
4545
}
46+
47+
@Test
48+
void shouldThrowServiceUnavailableOnInterruptedException() {
49+
RateLimiter noOpLimiter = (service, operation) -> {}; // no throttling
50+
51+
FindCustomerRequest request = new FindCustomerRequest("999", noOpLimiter) {
52+
@Override
53+
public String execute() throws RateLimitException {
54+
Thread.currentThread().interrupt(); // Simulate thread interruption
55+
return super.execute(); // Should throw ServiceUnavailableException
56+
}
57+
};
58+
59+
assertThrows(ServiceUnavailableException.class, request::execute);
60+
}
4661
}

0 commit comments

Comments
 (0)