-
-
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 5 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,12 @@ | ||
What this exercise is about | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
You will build a fixed‑window rate limiter: a small component that restricts how many | ||
operations each key may perform within a fixed period of time (a “window”). | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
What you should implement | ||
|
||
- Implement a single method in `src/main/java/FixedWindowRateLimiter.java`: | ||
- `public boolean allow(K key)` — decide whether to allow the operation for the key | ||
and update the limiter’s internal state accordingly. | ||
|
||
Everything else (interfaces and constructor) is provided for you. | ||
kahgoh 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,19 @@ | ||
{ | ||
"authors": [], | ||
|
||
"files": { | ||
"solution": [ | ||
"src/main/java/RateLimiter.java", | ||
"src/main/java/TimeSource.java", | ||
"src/main/java/FixedWindowRateLimiter.java" | ||
], | ||
"test": [ | ||
"src/test/java/RateLimiterTest.java" | ||
], | ||
"example": [ | ||
".meta/src/reference/java/RateLimiter.java", | ||
".meta/src/reference/java/TimeSource.java", | ||
".meta/src/reference/java/FixedWindowRateLimiter.java" | ||
] | ||
}, | ||
"blurb": "" | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
public class FixedWindowRateLimiter<K> implements 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 FixedWindowRateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { | ||
this.limit = limit; | ||
this.windowSizeNanos = windowSizeNanos; | ||
this.timeSource = timeSource; | ||
} | ||
|
||
@Override | ||
public boolean allow(K key) { | ||
long now = timeSource.nowNanos(); | ||
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,3 @@ | ||
public interface RateLimiter<K> { | ||
boolean allow(K key); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
public interface TimeSource { | ||
long nowNanos(); | ||
|
||
final class Fake implements TimeSource { | ||
private long now; | ||
|
||
public Fake(long startNanos) { | ||
this.now = startNanos; | ||
} | ||
|
||
@Override | ||
public long nowNanos() { | ||
return now; | ||
} | ||
|
||
public void advance(long nanos) { | ||
this.now += 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,32 @@ | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
public class FixedWindowRateLimiter<K> implements 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<>(); | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
public FixedWindowRateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { | ||
this.limit = limit; | ||
this.windowSizeNanos = windowSizeNanos; | ||
this.timeSource = timeSource; | ||
} | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
@Override | ||
public boolean allow(K key) { | ||
throw new UnsupportedOperationException("Delete this statement and write your own implementation."); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
public interface RateLimiter<K> { | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
boolean allow(K key); | ||
} |
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
public interface TimeSource { | ||
long nowNanos(); | ||
kahgoh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
andreatanky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* Deterministic fake clock for tests. | ||
*/ | ||
final class Fake implements TimeSource { | ||
private long now; | ||
|
||
public Fake(long startNanos) { | ||
this.now = startNanos; | ||
} | ||
|
||
@Override | ||
public long nowNanos() { | ||
return now; | ||
} | ||
|
||
public void advance(long nanos) { | ||
this.now += nanos; | ||
} | ||
|
||
} | ||
} | ||
|
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import org.junit.jupiter.api.Test; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
class RateLimiterTest { | ||
|
||
@Test | ||
void allowsUpToLimitThenDeniesUntilBoundary() { | ||
TimeSource.Fake clock = new TimeSource.Fake(0L); | ||
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(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.advance(9_999L); | ||
assertThat(limiter.allow("A")).isFalse(); | ||
|
||
// At exact boundary: new window | ||
clock.advance(1L); | ||
assertThat(limiter.allow("A")).isTrue(); | ||
} | ||
|
||
|
||
@Test | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
void continuesCountingWithinWindowAfterBoundaryReset() { | ||
TimeSource.Fake clock = new TimeSource.Fake(0L); | ||
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(2, 5_000L, clock); | ||
|
||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isFalse(); | ||
|
||
// Jump to next window | ||
clock.advance(5_000L); | ||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isTrue(); | ||
assertThat(limiter.allow("key")).isFalse(); | ||
} | ||
|
||
@Test | ||
void separateKeysHaveIndependentCountersAndWindows() { | ||
TimeSource.Fake clock = new TimeSource.Fake(42L); | ||
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(1, 100L, clock); | ||
|
||
assertThat(limiter.allow("A")).isTrue(); | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
assertThat(limiter.allow("B")).isTrue(); // independent key | ||
assertThat(limiter.allow("A")).isFalse(); | ||
kahgoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
assertThat(limiter.allow("B")).isFalse(); | ||
|
||
clock.advance(100L); // new window for both at boundary | ||
assertThat(limiter.allow("A")).isTrue(); | ||
assertThat(limiter.allow("B")).isTrue(); | ||
} | ||
|
||
@Test | ||
void longGapsResetWindowDeterministically() { | ||
andreatanky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
TimeSource.Fake clock = new TimeSource.Fake(1_000L); | ||
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(2, 50L, clock); | ||
|
||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isFalse(); | ||
|
||
// Advance several windows worth | ||
clock.advance(1_000L); | ||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isTrue(); | ||
assertThat(limiter.allow("X")).isFalse(); | ||
} | ||
|
||
@Test | ||
void exactBoundaryIsNewWindowEveryTime() { | ||
TimeSource.Fake clock = new TimeSource.Fake(0L); | ||
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(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.advance(10L); | ||
assertThat(limiter.allow("k")).isTrue(); | ||
assertThat(limiter.allow("k")).isFalse(); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.