Skip to content

Commit ebdbb1a

Browse files
committed
토큰 버킷 알고리즘 기반 처리율 제한기 구현
1 parent aca6066 commit ebdbb1a

File tree

7 files changed

+167
-0
lines changed

7 files changed

+167
-0
lines changed

build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,8 @@ subprojects {
4646
testImplementation("org.junit.jupiter:junit-jupiter")
4747
testImplementation("org.mockito:mockito-core:5.14.2")
4848
testImplementation("org.assertj:assertj-core:3.26.3")
49+
50+
implementation("org.slf4j:slf4j-api:2.0.16")
51+
implementation("org.slf4j:slf4j-simple:2.0.16")
4952
}
5053
}

rate-limiter/build.gradle.kts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.github.gunkim.ratelimiter;
2+
3+
import io.github.gunkim.ratelimiter.tokenbucket.TokenBucket;
4+
5+
public class Application {
6+
public static void main(String[] args) {
7+
//4초에 2개 요청에 대한 처리율 제한 설정
8+
final int bucketSize = 20;
9+
final long refillRate = 60_000L;
10+
var bucket = new TokenBucket(bucketSize, refillRate);
11+
12+
while (true) {
13+
processRequest(bucket);
14+
sleep(1_000);
15+
}
16+
}
17+
18+
private static void processRequest(TokenBucket bucket) {
19+
bucket.request(() -> System.out.println("Request"));
20+
}
21+
22+
private static void sleep(long millis) {
23+
try {
24+
Thread.sleep(millis);
25+
} catch (InterruptedException ex) {
26+
Thread.currentThread().interrupt();
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.github.gunkim.ratelimiter.tokenbucket;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.util.concurrent.Executors;
7+
import java.util.concurrent.ScheduledExecutorService;
8+
import java.util.concurrent.TimeUnit;
9+
import java.util.concurrent.atomic.AtomicInteger;
10+
11+
public class TokenBucket implements AutoCloseable {
12+
private static final Logger logger = LoggerFactory.getLogger(TokenBucket.class);
13+
14+
private final ScheduledExecutorService executorService;
15+
private final AtomicInteger tokens;
16+
17+
public TokenBucket(int bucketSize, long refillRate) {
18+
this.executorService = Executors.newSingleThreadScheduledExecutor();
19+
this.tokens = new AtomicInteger(bucketSize);
20+
21+
this.executorService.scheduleAtFixedRate(() -> refillTokens(bucketSize), refillRate, refillRate, TimeUnit.MILLISECONDS);
22+
23+
logger.info("Token bucket created with size: {} and refill rate: {}", bucketSize, refillRate);
24+
}
25+
26+
public void request(Runnable request) {
27+
int currentTokens;
28+
do {
29+
currentTokens = tokens.get();
30+
if (currentTokens <= 0) {
31+
logger.debug("Dropped request");
32+
return;
33+
}
34+
} while (!tokens.compareAndSet(currentTokens, currentTokens - 1));
35+
36+
request.run();
37+
logger.debug("Request processed. Remaining tokens: {}", tokens.get());
38+
}
39+
40+
public void shutdown() {
41+
executorService.shutdown();
42+
try {
43+
if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
44+
executorService.shutdownNow();
45+
}
46+
} catch (InterruptedException ex) {
47+
executorService.shutdownNow();
48+
Thread.currentThread().interrupt();
49+
}
50+
}
51+
52+
@Override
53+
public void close() {
54+
shutdown();
55+
}
56+
57+
private void refillTokens(int bucketSize) {
58+
int currentTokens;
59+
do {
60+
currentTokens = tokens.get();
61+
if (currentTokens >= bucketSize) {
62+
return;
63+
}
64+
} while (!tokens.compareAndSet(currentTokens, bucketSize));
65+
logger.debug("Refilled tokens: {}", tokens.get());
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.slf4j.simpleLogger.defaultLogLevel=debug
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.github.gunkim.ratelimiter.tokenbucket;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
import org.mockito.Mockito;
6+
7+
import java.util.concurrent.ExecutorService;
8+
import java.util.concurrent.Executors;
9+
import java.util.concurrent.TimeUnit;
10+
11+
import static org.mockito.Mockito.never;
12+
import static org.mockito.Mockito.times;
13+
14+
@DisplayName("TokenBucket은")
15+
class TokenBucketTest {
16+
@Test
17+
void 토큰_수가_0이면_요청_메서드가_요청을_삭제한다() {
18+
var tokenBucket = new TokenBucket(0, 20_000);
19+
Runnable request = Mockito.mock(Runnable.class);
20+
tokenBucket.request(request);
21+
Mockito.verify(request, never()).run();
22+
}
23+
24+
@Test
25+
void 토큰_수가_최대치일_때_요청_메소드는_요청을_처리한다() {
26+
var tokenBucket = new TokenBucket(10, 20_000);
27+
Runnable request = Mockito.mock(Runnable.class);
28+
for (int i = 0; i < 10; i++) {
29+
tokenBucket.request(request);
30+
}
31+
Mockito.verify(request, times(10)).run();
32+
}
33+
34+
@Test
35+
void 토큰_수가_3이고_요청이_6개_들어오면_3개만_처리한다() {
36+
var tokenBucket = new TokenBucket(3, 20_000);
37+
Runnable request = Mockito.mock(Runnable.class);
38+
for (int i = 0; i < 6; i++) {
39+
tokenBucket.request(request);
40+
}
41+
Mockito.verify(request, times(3)).run();
42+
}
43+
44+
@Test
45+
void 단일_스레드_환경에서_토큰_버킷이_올바르게_작동한다() {
46+
var tokenBucket = new TokenBucket(5, 1000);
47+
Runnable request = Mockito.mock(Runnable.class);
48+
for (int i = 0; i < 7; i++) {
49+
tokenBucket.request(request);
50+
}
51+
Mockito.verify(request, times(5)).run();
52+
}
53+
54+
@Test
55+
void 다중_스레드_환경에서_토큰_버킷이_올바르게_작동한다() throws InterruptedException {
56+
var tokenBucket = new TokenBucket(100, 5_000);
57+
Runnable request = Mockito.mock(Runnable.class);
58+
ExecutorService taskExecutor = Executors.newFixedThreadPool(10);
59+
for (int i = 0; i < 150; i++) {
60+
taskExecutor.execute(() -> tokenBucket.request(request));
61+
}
62+
taskExecutor.shutdown();
63+
taskExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS);
64+
Mockito.verify(request, times(100)).run();
65+
}
66+
}

settings.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ include("string-calculator")
2323
include("blackjack")
2424
include("banking")
2525
include("number-baseball")
26+
include("rate-limiter")

0 commit comments

Comments
 (0)