Skip to content

Commit 85eaced

Browse files
authored
Add rate limiter exercise (#3008)
1 parent c2165cb commit 85eaced

File tree

14 files changed

+723
-0
lines changed

14 files changed

+723
-0
lines changed

config.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,16 @@
810810
],
811811
"difficulty": 4
812812
},
813+
{
814+
"slug": "rate-limiter",
815+
"name": "Rate Limiter",
816+
"uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c",
817+
"practices": [],
818+
"prerequisites": [
819+
"generic-types"
820+
],
821+
"difficulty": 4
822+
},
813823
{
814824
"slug": "rotational-cipher",
815825
"name": "Rotational Cipher",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Instructions
2+
3+
Your task is to build a fixed‑window rate limiter.
4+
5+
Imagine a single server connected to one or more clients.
6+
A client sends a request, the server does some work, and returns a response.
7+
But processing takes time.
8+
If a client sends too many requests too quickly, the server can become overwhelmed — everything slows down or fails.
9+
10+
A rate limiter is a small component that decides whether to allow or reject a request based on how frequently that client has been making requests.
11+
Different strategies exist; in this exercise you’ll implement a fixed‑window rate limiter.
12+
13+
Fixed‑window rate limiting groups time into equal‑length windows (for example, every 10 seconds) and allows up to a certain number of requests within each window for each client.
14+
Once the window resets, the allowance refreshes for the next window.
15+
Each client is tracked separately, so another client can make requests within that same period.
16+
17+
For example, consider a rate limiter configured to limit 2 requests per 10 seconds per client.
18+
Lets say a client sends a request:
19+
20+
- Being its first request, the request is permitted.
21+
- A second request within 10 seconds after the first one is also permitted.
22+
- However, further requests after that would be denied _until_ at least 10 seconds has elapsed since the first request.
23+
- If a second client sends its first request within 10 seconds with of the first client's first request, it would also be permitted, _regardless_ of whether the first client has sent a second request.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"authors": [
3+
"andreatanky"
4+
],
5+
"files": {
6+
"solution": [
7+
"src/main/java/RateLimiter.java"
8+
],
9+
"test": [
10+
"src/test/java/RateLimiterTest.java"
11+
],
12+
"example": [
13+
".meta/src/reference/java/RateLimiter.java",
14+
".meta/src/reference/java/TimeSource.java"
15+
],
16+
"editor": [
17+
"src/main/java/TimeSource.java"
18+
]
19+
},
20+
"blurb": "Practice stateful logic and time handling by implementing a fixed-window rate limiter"
21+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import java.time.Duration;
2+
import java.time.Instant;
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class RateLimiter<K> {
7+
8+
private static final class WindowState {
9+
Instant windowStart;
10+
int usedCount;
11+
12+
WindowState(Instant windowStart, int usedCount) {
13+
this.windowStart = windowStart;
14+
this.usedCount = usedCount;
15+
}
16+
}
17+
18+
private final int limit;
19+
private final Duration windowSize;
20+
private final TimeSource timeSource;
21+
private final Map<K, WindowState> states = new HashMap<>();
22+
23+
public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) {
24+
this.limit = limit;
25+
this.windowSize = windowSize;
26+
this.timeSource = timeSource;
27+
}
28+
29+
public boolean allow(K clientId) {
30+
Instant now = timeSource.now();
31+
32+
WindowState s = states.get(clientId);
33+
if (s == null) {
34+
s = new WindowState(now, 0);
35+
states.put(clientId, s);
36+
}
37+
38+
if (!now.isBefore(s.windowStart.plus(windowSize))) {
39+
s.windowStart = now;
40+
s.usedCount = 0;
41+
}
42+
43+
if (s.usedCount < limit) {
44+
s.usedCount += 1;
45+
return true;
46+
}
47+
return false;
48+
}
49+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import java.time.Duration;
2+
import java.time.Instant;
3+
4+
public class TimeSource {
5+
private Instant now;
6+
7+
public TimeSource(Instant start) {
8+
this.now = start;
9+
}
10+
11+
public Instant now() {
12+
return now;
13+
}
14+
15+
public void advance(Duration d) {
16+
this.now = this.now.plus(d);
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
plugins {
2+
id "java"
3+
}
4+
5+
repositories {
6+
mavenCentral()
7+
}
8+
9+
dependencies {
10+
testImplementation platform("org.junit:junit-bom:5.10.0")
11+
testImplementation "org.junit.jupiter:junit-jupiter"
12+
testImplementation "org.assertj:assertj-core:3.25.1"
13+
14+
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
15+
}
16+
17+
test {
18+
useJUnitPlatform()
19+
20+
testLogging {
21+
exceptionFormat = "full"
22+
showStandardStreams = true
23+
events = ["passed", "failed", "skipped"]
24+
}
25+
}
42.4 KB
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)