-
-
Notifications
You must be signed in to change notification settings - Fork 735
Add rate limiter exercise #3008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
0513d96
bfe4dbb
81c6728
ae33983
4f5abf3
4f0c836
a697b81
8092b9a
87fc175
4479173
d9520b7
63ea8a3
9f6a8bf
a616a10
9f077d0
89ec426
9657afc
98d6103
43610f1
a61ef4d
50b280c
5971500
69c1ba9
6f27c12
368d7ea
87c3d3c
d451060
67baa14
aba22d5
715f181
efadb02
8dc4059
b6dea08
ae8d97e
60736bb
25a2876
d418567
baf39d2
7907757
6100e11
4d9b1bf
6ccd97a
b839de1
a8a6048
6b8e310
b3667bc
0b158f7
d5b13e9
5e4cc5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1900,6 +1900,14 @@ | |
"lists" | ||
], | ||
"difficulty": 10 | ||
}, | ||
{ | ||
"slug": "rate-limiter", | ||
"name": "rate-limiter", | ||
"uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", | ||
"practices": [], | ||
"prerequisites": [], | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"difficulty": 1 | ||
|
||
} | ||
], | ||
"foregone": [ | ||
|
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Your task is to build a fixed‑window rate limiter. | ||
andreatanky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Imagine a single server connected to one or more clients. A client sends a request, the server | ||
does some work, and returns a response. But processing takes time. If a client sends too many | ||
requests too quickly, the server can become overwhelmed — everything slows down or fails. | ||
|
||
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. Different strategies exist; in this exercise | ||
you’ll implement a fixed‑window rate limiter. | ||
|
||
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. Once the window | ||
resets, the allowance refreshes for the next window. Each client is tracked separately. | ||
|
||
Examples: | ||
- Limit: 3 requests per 10 seconds per client. | ||
- A client’s first three requests in the same window are allowed; a fourth is rejected. | ||
- After the window resets, the next request is allowed again and the counting starts fresh. | ||
andreatanky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"authors": ["andreatanky"], | ||
"files": { | ||
"solution": [ | ||
"src/main/java/RateLimiter.java", | ||
"src/main/java/TimeSource.java" | ||
], | ||
"test": [ | ||
"src/test/java/RateLimiterTest.java" | ||
], | ||
"example": [ | ||
".meta/src/reference/java/RateLimiter.java", | ||
".meta/src/reference/java/TimeSource.java" | ||
] | ||
}, | ||
andreatanky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"blurb": "Practice stateful logic and time handling by implementing a fixed-window rate limiter" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import java.time.Instant; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
public class RateLimiter<K> { | ||
|
||
private static final class WindowState { | ||
long windowStartNanos; | ||
int usedCount; | ||
|
||
WindowState(long windowStartNanos, int usedCount) { | ||
this.windowStartNanos = windowStartNanos; | ||
this.usedCount = usedCount; | ||
} | ||
} | ||
|
||
private final int limit; | ||
private final long windowSizeNanos; | ||
private final TimeSource timeSource; | ||
private final Map<K, WindowState> states = new HashMap<>(); | ||
|
||
public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { | ||
this.limit = limit; | ||
this.windowSizeNanos = windowSizeNanos; | ||
this.timeSource = timeSource; | ||
} | ||
|
||
public boolean allow(K key) { | ||
Instant nowInstant = timeSource.now(); | ||
long now = nowInstant.getEpochSecond() * 1_000_000_000L + nowInstant.getNano(); | ||
|
||
WindowState s = states.get(key); | ||
if (s == null) { | ||
s = new WindowState(now, 0); | ||
states.put(key, s); | ||
} | ||
|
||
long elapsed = now - s.windowStartNanos; | ||
if (elapsed >= windowSizeNanos) { | ||
s.windowStartNanos = now; | ||
s.usedCount = 0; | ||
} | ||
|
||
if (s.usedCount < limit) { | ||
s.usedCount += 1; | ||
return true; | ||
} | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import java.time.Duration; | ||
import java.time.Instant; | ||
|
||
public class TimeSource { | ||
private Instant now; | ||
|
||
public TimeSource(Instant start) { | ||
this.now = start; | ||
} | ||
|
||
public Instant now() { | ||
return now; | ||
} | ||
|
||
public void advance(Duration d) { | ||
this.now = this.now.plus(d); | ||
} | ||
|
||
public void advanceNanos(long nanos) { | ||
this.now = this.now.plusNanos(nanos); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
plugins { | ||
id "java" | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
dependencies { | ||
testImplementation platform("org.junit:junit-bom:5.10.0") | ||
testImplementation "org.junit.jupiter:junit-jupiter" | ||
testImplementation "org.assertj:assertj-core:3.25.1" | ||
|
||
testRuntimeOnly "org.junit.platform:junit-platform-launcher" | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
|
||
testLogging { | ||
exceptionFormat = "full" | ||
showStandardStreams = true | ||
events = ["passed", "failed", "skipped"] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import java.time.Instant; | ||
|
||
public class RateLimiter<K> { | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
andreatanky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
|
||
public boolean allow(K key) { | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
throw new UnsupportedOperationException("Delete this statement and write your own implementation."); | ||
} | ||
} |
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import java.time.Duration; | ||
import java.time.Instant; | ||
|
||
andreatanky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public class TimeSource { | ||
private Instant now; | ||
|
||
public TimeSource(Instant start) { | ||
this.now = start; | ||
} | ||
|
||
public Instant now() { | ||
return now; | ||
} | ||
|
||
public void advance(Duration d) { | ||
this.now = this.now.plus(d); | ||
} | ||
|
||
public void advanceNanos(long nanos) { | ||
this.now = this.now.plusNanos(nanos); | ||
} | ||
|
||
} |
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import org.junit.jupiter.api.Disabled; | ||
import org.junit.jupiter.api.Test; | ||
import java.time.Instant; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
class RateLimiterTest { | ||
|
||
@Test | ||
void allowsUpToLimitThenDeniesUntilBoundary() { | ||
TimeSource clock = new TimeSource(Instant.EPOCH); | ||
RateLimiter<String> limiter = new RateLimiter<>(3, 10_000L, clock); | ||
|
||
assertThat(limiter.allow("A")).isTrue(); | ||
assertThat(limiter.allow("A")).isTrue(); | ||
assertThat(limiter.allow("A")).isTrue(); | ||
assertThat(limiter.allow("A")).isFalse(); | ||
|
||
// Just before boundary: still same window | ||
clock.advanceNanos(9_999L); | ||
assertThat(limiter.allow("A")).isFalse(); | ||
|
||
// At exact boundary: new window | ||
clock.advanceNanos(1L); | ||
assertThat(limiter.allow("A")).isTrue(); | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
@Disabled("Remove to run test") | ||
@Test | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
void continuesCountingWithinWindowAfterBoundaryReset() { | ||
TimeSource clock = new TimeSource(Instant.EPOCH); | ||
RateLimiter<String> limiter = new RateLimiter<>(2, 5_000L, clock); | ||
|
||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isFalse(); | ||
|
||
// Jump to next window | ||
clock.advanceNanos(5_000L); | ||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isFalse(); | ||
} | ||
|
||
@Disabled("Remove to run test") | ||
@Test | ||
void separateKeysHaveIndependentCountersAndWindows() { | ||
TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(42L)); | ||
RateLimiter<String> limiter = new RateLimiter<>(1, 100L, clock); | ||
|
||
assertThat(limiter.allow("A")).isTrue(); | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
assertThat(limiter.allow("A")).isFalse(); | ||
assertThat(limiter.allow("B")).isTrue(); // independent key | ||
assertThat(limiter.allow("B")).isFalse(); | ||
|
||
clock.advanceNanos(100L); // new window for both at boundary | ||
assertThat(limiter.allow("A")).isTrue(); | ||
assertThat(limiter.allow("B")).isTrue(); | ||
} | ||
|
||
@Disabled("Remove to run test") | ||
@Test | ||
void longGapsResetWindowDeterministically() { | ||
andreatanky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(1_000L)); | ||
RateLimiter<String> limiter = new RateLimiter<>(2, 50L, clock); | ||
|
||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isFalse(); | ||
|
||
// Advance several windows worth | ||
clock.advanceNanos(1_000L); | ||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isFalse(); | ||
} | ||
|
||
@Disabled("Remove to run test") | ||
@Test | ||
void exactBoundaryIsNewWindowEveryTime() { | ||
TimeSource clock = new TimeSource(Instant.EPOCH); | ||
RateLimiter<String> limiter = new RateLimiter<>(1, 10L, clock); | ||
|
||
assertThat(limiter.allow("k")).isTrue(); | ||
assertThat(limiter.allow("k")).isFalse(); | ||
|
||
// Move exactly to boundary repeatedly; each time should allow once | ||
for (int i = 0; i < 5; i++) { | ||
clock.advanceNanos(10L); | ||
assertThat(limiter.allow("k")).isTrue(); | ||
assertThat(limiter.allow("k")).isFalse(); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.