Skip to content

Commit cd30e4a

Browse files
committed
고정 윈도우 카운터 처리율 제한 구현
1 parent 236b210 commit cd30e4a

File tree

2 files changed

+105
-0
lines changed

2 files changed

+105
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package io.github.gunkim.ratelimiter.window;
2+
3+
import io.github.gunkim.ratelimiter.RateLimiter;
4+
5+
import java.time.Instant;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
public class FixedWindowRateLimiter implements RateLimiter {
10+
private final int maxRequestsPerWindow;
11+
private final int windowDurationMillis;
12+
private final ConcurrentHashMap<Long, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
13+
14+
public FixedWindowRateLimiter(int maxRequestsPerWindow, int windowDurationMillis) {
15+
this.maxRequestsPerWindow = maxRequestsPerWindow;
16+
this.windowDurationMillis = windowDurationMillis;
17+
}
18+
19+
@Override
20+
public void request(Runnable request) {
21+
long currentTimeMillis = Instant.now().toEpochMilli();
22+
long currentWindowKey = currentTimeMillis / windowDurationMillis;
23+
24+
cleanupOldWindows(currentWindowKey);
25+
26+
AtomicInteger currentWindowRequestCount = getOrCreateRequestCounterForWindow(currentWindowKey);
27+
currentWindowRequestCount.incrementAndGet();
28+
29+
if (currentWindowRequestCount.get() <= maxRequestsPerWindow) {
30+
request.run();
31+
} else {
32+
currentWindowRequestCount.decrementAndGet();
33+
}
34+
}
35+
36+
private AtomicInteger getOrCreateRequestCounterForWindow(long currentWindowKey) {
37+
return requestCounts.computeIfAbsent(currentWindowKey, k -> new AtomicInteger(0));
38+
}
39+
40+
private void cleanupOldWindows(long currentWindowKey) {
41+
requestCounts.forEach((windowKey, count) -> {
42+
if (windowKey < currentWindowKey) {
43+
requestCounts.remove(windowKey);
44+
}
45+
});
46+
}
47+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.github.gunkim.ratelimiter.window;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.concurrent.CountDownLatch;
7+
import java.util.concurrent.TimeUnit;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.mockito.Mockito.mock;
12+
import static org.mockito.Mockito.times;
13+
import static org.mockito.Mockito.verify;
14+
15+
@DisplayName("FixedWindowRateLimiter는")
16+
class FixedWindowRateLimiterTest {
17+
private static final int WINDOW_SIZE = 10_000;
18+
private static final int REQUESTS_LIMIT = 2;
19+
20+
@Test
21+
void window_size_이내_요청은_처리된다() {
22+
Runnable runnable = mock(Runnable.class);
23+
FixedWindowRateLimiter rateLimiter = createRateLimiter(10);
24+
rateLimiter.request(runnable);
25+
verify(runnable, times(1)).run();
26+
}
27+
28+
@Test
29+
void window_size가_2이고_요청이_3회라면_2회만_처리된다() throws InterruptedException {
30+
int requestCount = 3;
31+
AtomicInteger counter = new AtomicInteger(0);
32+
CountDownLatch countDownLatch = new CountDownLatch(REQUESTS_LIMIT);
33+
Runnable request = createRunnable(counter, countDownLatch);
34+
35+
FixedWindowRateLimiter rateLimiter = createRateLimiter(REQUESTS_LIMIT);
36+
sendRequests(rateLimiter, request, requestCount);
37+
38+
countDownLatch.await(1, TimeUnit.SECONDS);
39+
assertThat(counter.get()).isEqualTo(REQUESTS_LIMIT);
40+
}
41+
42+
private FixedWindowRateLimiter createRateLimiter(int limit) {
43+
return new FixedWindowRateLimiter(limit, WINDOW_SIZE);
44+
}
45+
46+
private Runnable createRunnable(AtomicInteger counter, CountDownLatch countDownLatch) {
47+
return () -> {
48+
counter.incrementAndGet();
49+
countDownLatch.countDown();
50+
};
51+
}
52+
53+
private void sendRequests(FixedWindowRateLimiter rateLimiter, Runnable request, int requestCount) {
54+
for (int i = 0; i < requestCount; i++) {
55+
rateLimiter.request(request);
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)