Skip to content

Commit a7a268d

Browse files
committed
슬라이딩 윈도우 처리 제한 알고리즘 구현
1 parent d0da51b commit a7a268d

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.github.gunkim.ratelimiter.window;
2+
3+
import io.github.gunkim.ratelimiter.RateLimiter;
4+
5+
import java.util.concurrent.ConcurrentHashMap;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
8+
/**
9+
* ## 처리 제한 알고리즘, 슬라이딩 윈도우 카운터
10+
* - 슬라이딩 윈도우 기법을 사용하여 고정된 간격의 윈도 내에서 요청을 제한한다.
11+
* - 현재 윈도와 이전 윈도의 요청을 샘플링하고, 가중치를 부여하여 전체 요청 수를 계산한다.
12+
* - 임계치를 넘어선 요청은 거부된다.
13+
* <p>
14+
* 이 방식을 사용하면 윈도 경계에서의 집중된 트래픽 문제를 완화할 수 있다.
15+
* 예를 들어,
16+
* - 윈도 크기가 1분이고 임계치가 100이라고 가정할 때,
17+
* - 12:00:30에서 12:01:30까지의 1분간의 요청 중 일부는 새로운 윈도로 넘어가서 가중치를 부여받는다.
18+
* - 이는 고정된 윈도우 카운터에서 발생할 수 있는 200개의 요청이 처리되는 문제를 감소시킨다.
19+
*/
20+
public class SlidingWindowRateLimiter implements RateLimiter {
21+
private final int windowDurationMillis;
22+
private final int maxRequestsPerWindow;
23+
private final TimeProvider timeProvider;
24+
25+
private final ConcurrentHashMap<Long, AtomicInteger> windowCounterMap = new ConcurrentHashMap<>();
26+
27+
public SlidingWindowRateLimiter(int windowDurationMillis, int maxRequestsPerWindow, TimeProvider timeProvider) {
28+
this.windowDurationMillis = windowDurationMillis;
29+
this.maxRequestsPerWindow = maxRequestsPerWindow;
30+
this.timeProvider = timeProvider;
31+
}
32+
33+
@Override
34+
public void handleRequest(Runnable request) {
35+
long currentMillis = timeProvider.getCurrentTimeMillis();
36+
long currentWindowKey = currentMillis / windowDurationMillis;
37+
long previousWindowKey = currentWindowKey - 1;
38+
39+
double elapsedRatio = (double) (currentMillis % windowDurationMillis) / windowDurationMillis;
40+
41+
AtomicInteger currentCounter = windowCounterMap.computeIfAbsent(currentWindowKey, k -> new AtomicInteger(0));
42+
AtomicInteger previousCounter = windowCounterMap.getOrDefault(previousWindowKey, new AtomicInteger(0));
43+
44+
// 이전 윈도우의 카운트를 가중치를 적용하여 계산
45+
double weightedPreviousCount = previousCounter.get() * (1 - elapsedRatio);
46+
long totalRequestCount = (long) (currentCounter.get() + weightedPreviousCount);
47+
48+
if (totalRequestCount < maxRequestsPerWindow) {
49+
request.run();
50+
currentCounter.incrementAndGet();
51+
}
52+
53+
// 오래된 윈도우 제거 (현재 윈도우와 이전 윈도우만 유지)
54+
windowCounterMap.entrySet().removeIf(entry -> entry.getKey() < previousWindowKey);
55+
}
56+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.github.gunkim.ratelimiter.window;
2+
3+
@FunctionalInterface
4+
public interface TimeProvider {
5+
long getCurrentTimeMillis();
6+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
import java.util.concurrent.atomic.AtomicLong;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.times;
14+
import static org.mockito.Mockito.verify;
15+
16+
@DisplayName("SlidingWindowRateLimiter는")
17+
class SlidingWindowRateLimiterTest {
18+
private static final int WINDOW_SIZE = 1_000;
19+
private static final int REQUESTS_LIMIT = 5;
20+
21+
@Test
22+
void window_size_이내_요청은_처리된다() {
23+
SlidingWindowRateLimiter rateLimiter = createRateLimiter();
24+
executeAndVerifyRequest(rateLimiter, 1);
25+
}
26+
27+
@Test
28+
void 제한_초과_요청은_처리되지_않는다() throws InterruptedException {
29+
SlidingWindowRateLimiter rateLimiter = createRateLimiter();
30+
var counter = new AtomicInteger(0);
31+
var countDownLatch = new CountDownLatch(REQUESTS_LIMIT);
32+
33+
executeBulkRequests(rateLimiter, counter, countDownLatch, REQUESTS_LIMIT + 3);
34+
35+
countDownLatch.await(1, TimeUnit.SECONDS);
36+
assertThat(counter.get()).isEqualTo(REQUESTS_LIMIT);
37+
}
38+
39+
@Test
40+
void 슬라이딩_윈도우_동작을_만족한다() {
41+
var currentTime = new AtomicLong(0);
42+
TimeProvider timeProvider = currentTime::get;
43+
var rateLimiter = new SlidingWindowRateLimiter(WINDOW_SIZE, REQUESTS_LIMIT, timeProvider);
44+
45+
int totalRequests = sendAllWindowRequests(rateLimiter, currentTime);
46+
47+
assertThat(totalRequests)
48+
.isGreaterThan(REQUESTS_LIMIT)
49+
.isLessThanOrEqualTo(REQUESTS_LIMIT * 2);
50+
}
51+
52+
private int sendAllWindowRequests(SlidingWindowRateLimiter rateLimiter, AtomicLong currentTime) {
53+
int firstWindowRequests = sendRequests(rateLimiter, REQUESTS_LIMIT + 2, currentTime);
54+
assertThat(firstWindowRequests)
55+
.as("임계치에 대한 제한이 정상적으로 구현되었는지 확인해보세요.")
56+
.isEqualTo(REQUESTS_LIMIT);
57+
58+
currentTime.addAndGet(WINDOW_SIZE / 2);
59+
int secondWindowRequests = sendRequests(rateLimiter, REQUESTS_LIMIT, currentTime);
60+
assertThat(secondWindowRequests).isZero();
61+
62+
currentTime.addAndGet(WINDOW_SIZE / 2);
63+
int thirdWindowRequests = sendRequests(rateLimiter, REQUESTS_LIMIT, currentTime);
64+
assertThat(thirdWindowRequests).isEqualTo(1);
65+
66+
return firstWindowRequests + secondWindowRequests + thirdWindowRequests;
67+
}
68+
69+
private int sendRequests(SlidingWindowRateLimiter rateLimiter, int count, AtomicLong currentTime) {
70+
var successCount = new AtomicInteger();
71+
for (int i = 0; i < count; i++) {
72+
rateLimiter.handleRequest(successCount::getAndIncrement);
73+
currentTime.addAndGet(1); // 각 요청마다 1ms 증가
74+
}
75+
return successCount.get();
76+
}
77+
78+
private void executeBulkRequests(SlidingWindowRateLimiter rateLimiter, AtomicInteger counter, CountDownLatch latch, int count) {
79+
for (int i = 0; i < count; i++) {
80+
rateLimiter.handleRequest(() -> {
81+
counter.incrementAndGet();
82+
latch.countDown();
83+
});
84+
}
85+
}
86+
87+
private SlidingWindowRateLimiter createRateLimiter() {
88+
var currentTime = new AtomicLong(0);
89+
return new SlidingWindowRateLimiter(WINDOW_SIZE, REQUESTS_LIMIT, currentTime::get);
90+
}
91+
92+
private void executeAndVerifyRequest(SlidingWindowRateLimiter rateLimiter, int times) {
93+
Runnable runnable = mock(Runnable.class);
94+
rateLimiter.handleRequest(runnable);
95+
verify(runnable, times(times)).run();
96+
}
97+
}

0 commit comments

Comments
 (0)