From 0513d96b4d9b4b4477df8f5ae5456242661eed79 Mon Sep 17 00:00:00 2001 From: andrea Date: Mon, 8 Sep 2025 21:08:22 +0800 Subject: [PATCH 01/39] Run configlet create --- config.json | 8 ++++++++ .../practice/rate-limiter/.docs/instructions.md | 0 exercises/practice/rate-limiter/.meta/config.json | 15 +++++++++++++++ .../.meta/src/reference/java/RateLimiter.java | 0 exercises/practice/rate-limiter/build.gradle | 0 .../rate-limiter/src/main/java/RateLimiter.java | 0 .../src/test/java/RateLimiterTest.java | 0 7 files changed, 23 insertions(+) create mode 100644 exercises/practice/rate-limiter/.docs/instructions.md create mode 100644 exercises/practice/rate-limiter/.meta/config.json create mode 100644 exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java create mode 100644 exercises/practice/rate-limiter/build.gradle create mode 100644 exercises/practice/rate-limiter/src/main/java/RateLimiter.java create mode 100644 exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java diff --git a/config.json b/config.json index fa08384ff..77750b8b8 100644 --- a/config.json +++ b/config.json @@ -1900,6 +1900,14 @@ "lists" ], "difficulty": 10 + }, + { + "slug": "rate-limiter", + "name": "rate-limiter", + "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ], "foregone": [ diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json new file mode 100644 index 000000000..b9c0e8170 --- /dev/null +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -0,0 +1,15 @@ +{ + "authors": [], + "files": { + "solution": [ + "src/main/java/RateLimiter.java" + ], + "test": [ + "src/test/java/RateLimiterTest.java" + ], + "example": [ + ".meta/src/reference/java/RateLimiter.java" + ] + }, + "blurb": "" +} diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/practice/rate-limiter/build.gradle b/exercises/practice/rate-limiter/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java new file mode 100644 index 000000000..e69de29bb From bfe4dbb5f5b212142becf9f538e3b34c5660f60d Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Sep 2025 21:00:15 +0800 Subject: [PATCH 02/39] Update rate limiter implementation --- .../practice/rate-limiter/.meta/config.json | 8 +- .../java/FixedWindowRateLimiter.java | 49 +++++++++++ .../.meta/src/reference/java/RateLimiter.java | 3 + .../.meta/src/reference/java/TimeSource.java | 21 +++++ exercises/practice/rate-limiter/build.gradle | 25 ++++++ .../src/main/java/FixedWindowRateLimiter.java | 53 +++++++++++ .../src/main/java/RateLimiter.java | 3 + .../src/main/java/TimeSource.java | 24 +++++ .../src/test/java/RateLimiterTest.java | 88 +++++++++++++++++++ exercises/settings.gradle | 1 + 10 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java create mode 100644 exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java create mode 100644 exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java create mode 100644 exercises/practice/rate-limiter/src/main/java/TimeSource.java diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json index b9c0e8170..e122adaeb 100644 --- a/exercises/practice/rate-limiter/.meta/config.json +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -2,13 +2,17 @@ "authors": [], "files": { "solution": [ - "src/main/java/RateLimiter.java" + "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/RateLimiter.java", + ".meta/src/reference/java/TimeSource.java", + ".meta/src/reference/java/FixedWindowRateLimiter.java" ] }, "blurb": "" diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java new file mode 100644 index 000000000..349288ecf --- /dev/null +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java @@ -0,0 +1,49 @@ +import java.util.HashMap; +import java.util.Map; + +public class FixedWindowRateLimiter implements RateLimiter { + + 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 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; + } +} + diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java index e69de29bb..c9d96f9ff 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java @@ -0,0 +1,3 @@ +public interface RateLimiter { + boolean allow(K key); +} diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java b/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java new file mode 100644 index 000000000..df52d04e6 --- /dev/null +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java @@ -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; + } + } +} + diff --git a/exercises/practice/rate-limiter/build.gradle b/exercises/practice/rate-limiter/build.gradle index e69de29bb..d28f35dee 100644 --- a/exercises/practice/rate-limiter/build.gradle +++ b/exercises/practice/rate-limiter/build.gradle @@ -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"] + } +} diff --git a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java new file mode 100644 index 000000000..e6653e344 --- /dev/null +++ b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java @@ -0,0 +1,53 @@ +import java.util.HashMap; +import java.util.Map; + +/** + * Deterministic fixed-window rate limiter. + * Single-threaded; per-key counters/windows; boundary rollover inclusive. + */ +public class FixedWindowRateLimiter implements RateLimiter { + + 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 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; + } +} + diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java index e69de29bb..c9d96f9ff 100644 --- a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java @@ -0,0 +1,3 @@ +public interface RateLimiter { + boolean allow(K key); +} diff --git a/exercises/practice/rate-limiter/src/main/java/TimeSource.java b/exercises/practice/rate-limiter/src/main/java/TimeSource.java new file mode 100644 index 000000000..dd7e15231 --- /dev/null +++ b/exercises/practice/rate-limiter/src/main/java/TimeSource.java @@ -0,0 +1,24 @@ +public interface TimeSource { + long nowNanos(); + + /** + * 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; + } + } +} + diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index e69de29bb..f0f6321de 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -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 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 + void continuesCountingWithinWindowAfterBoundaryReset() { + TimeSource.Fake clock = new TimeSource.Fake(0L); + RateLimiter 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 limiter = new FixedWindowRateLimiter<>(1, 100L, clock); + + assertThat(limiter.allow("A")).isTrue(); + assertThat(limiter.allow("B")).isTrue(); // independent key + assertThat(limiter.allow("A")).isFalse(); + 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() { + TimeSource.Fake clock = new TimeSource.Fake(1_000L); + RateLimiter 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 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(); + } + } +} diff --git a/exercises/settings.gradle b/exercises/settings.gradle index e17e28e81..1119f4b98 100644 --- a/exercises/settings.gradle +++ b/exercises/settings.gradle @@ -117,6 +117,7 @@ include 'practice:proverb' include 'practice:pythagorean-triplet' include 'practice:queen-attack' include 'practice:rail-fence-cipher' +include 'practice:rate-limiter' include 'practice:raindrops' include 'practice:rational-numbers' include 'practice:react' From 81c67283e79e365fd384d59894a4f842a1d4aaa9 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Sep 2025 22:09:23 +0800 Subject: [PATCH 03/39] Add instructions --- .../rate-limiter/.docs/instructions.md | 12 ++++++++++++ .../src/main/java/FixedWindowRateLimiter.java | 19 +------------------ 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index e69de29bb..608e3b172 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -0,0 +1,12 @@ +What this exercise is about + +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”). + +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. diff --git a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java index e6653e344..930f4fddd 100644 --- a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java @@ -30,24 +30,7 @@ public FixedWindowRateLimiter(int limit, long windowSizeNanos, TimeSource timeSo @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; + throw new UnsupportedOperationException("Delete this statement and write your own implementation."); } } From ae33983f847488cb0b132666ad95d5e67d39d3c1 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Sep 2025 22:12:51 +0800 Subject: [PATCH 04/39] Add instructions --- .../rate-limiter/src/main/java/FixedWindowRateLimiter.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java index 930f4fddd..9467bb468 100644 --- a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java @@ -1,10 +1,6 @@ import java.util.HashMap; import java.util.Map; -/** - * Deterministic fixed-window rate limiter. - * Single-threaded; per-key counters/windows; boundary rollover inclusive. - */ public class FixedWindowRateLimiter implements RateLimiter { private static final class WindowState { From 4f0c836ba331967d436838e29b29c0315f760b7c Mon Sep 17 00:00:00 2001 From: Andrea Date: Sun, 14 Sep 2025 10:37:54 +0800 Subject: [PATCH 05/39] Remove interfaces --- .../rate-limiter/.docs/instructions.md | 14 +---- .../practice/rate-limiter/.meta/config.json | 6 +-- .../java/FixedWindowRateLimiter.java | 49 ------------------ .../.meta/src/reference/java/RateLimiter.java | 51 ++++++++++++++++++- .../.meta/src/reference/java/TimeSource.java | 31 +++++------ .../src/main/java/FixedWindowRateLimiter.java | 32 ------------ .../src/main/java/RateLimiter.java | 16 +++++- .../src/main/java/TimeSource.java | 34 ++++++------- .../src/test/java/RateLimiterTest.java | 40 ++++++++------- 9 files changed, 122 insertions(+), 151 deletions(-) delete mode 100644 exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java delete mode 100644 exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index 608e3b172..8cb1624fd 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -1,12 +1,2 @@ -What this exercise is about - -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”). - -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. +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”). diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json index e122adaeb..87fe135a0 100644 --- a/exercises/practice/rate-limiter/.meta/config.json +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -3,16 +3,14 @@ "files": { "solution": [ "src/main/java/RateLimiter.java", - "src/main/java/TimeSource.java", - "src/main/java/FixedWindowRateLimiter.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", - ".meta/src/reference/java/FixedWindowRateLimiter.java" + ".meta/src/reference/java/TimeSource.java" ] }, "blurb": "" diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java deleted file mode 100644 index 349288ecf..000000000 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/FixedWindowRateLimiter.java +++ /dev/null @@ -1,49 +0,0 @@ -import java.util.HashMap; -import java.util.Map; - -public class FixedWindowRateLimiter implements RateLimiter { - - 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 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; - } -} - diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java index c9d96f9ff..7bc3d8863 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java @@ -1,3 +1,50 @@ -public interface RateLimiter { - boolean allow(K key); +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +public class RateLimiter { + + 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 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; + } } diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java b/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java index df52d04e6..0f5fb28f7 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java @@ -1,21 +1,22 @@ -public interface TimeSource { - long nowNanos(); +import java.time.Duration; +import java.time.Instant; - final class Fake implements TimeSource { - private long now; +public class TimeSource { + private Instant now; - public Fake(long startNanos) { - this.now = startNanos; - } + public TimeSource(Instant start) { + this.now = start; + } - @Override - public long nowNanos() { - return now; - } + public Instant now() { + return now; + } - public void advance(long nanos) { - this.now += nanos; - } + public void advance(Duration d) { + this.now = this.now.plus(d); } -} + public void advanceNanos(long nanos) { + this.now = this.now.plusNanos(nanos); + } +} diff --git a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java b/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java deleted file mode 100644 index 9467bb468..000000000 --- a/exercises/practice/rate-limiter/src/main/java/FixedWindowRateLimiter.java +++ /dev/null @@ -1,32 +0,0 @@ -import java.util.HashMap; -import java.util.Map; - -public class FixedWindowRateLimiter implements RateLimiter { - - 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 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) { - throw new UnsupportedOperationException("Delete this statement and write your own implementation."); - } -} - diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java index c9d96f9ff..0d99d6e64 100644 --- a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java @@ -1,3 +1,15 @@ -public interface RateLimiter { - boolean allow(K key); +import java.time.Instant; + +// Students: implement a fixed-window rate limiter here. +// Intentionally minimal starter: you will choose state and logic. +public class RateLimiter { + + // Provide a blank constructor so the class compiles. + public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { + // students implement + } + + public boolean allow(K key) { + throw new UnsupportedOperationException("Please implement RateLimiter.allow()"); + } } diff --git a/exercises/practice/rate-limiter/src/main/java/TimeSource.java b/exercises/practice/rate-limiter/src/main/java/TimeSource.java index dd7e15231..0f5fb28f7 100644 --- a/exercises/practice/rate-limiter/src/main/java/TimeSource.java +++ b/exercises/practice/rate-limiter/src/main/java/TimeSource.java @@ -1,24 +1,22 @@ -public interface TimeSource { - long nowNanos(); +import java.time.Duration; +import java.time.Instant; - /** - * Deterministic fake clock for tests. - */ - final class Fake implements TimeSource { - private long now; +public class TimeSource { + private Instant now; - public Fake(long startNanos) { - this.now = startNanos; - } + public TimeSource(Instant start) { + this.now = start; + } - @Override - public long nowNanos() { - return now; - } + public Instant now() { + return now; + } - public void advance(long nanos) { - this.now += nanos; - } + public void advance(Duration d) { + this.now = this.now.plus(d); } -} + public void advanceNanos(long nanos) { + this.now = this.now.plusNanos(nanos); + } +} diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index f0f6321de..4dc8950ee 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -1,4 +1,6 @@ +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; @@ -6,8 +8,8 @@ class RateLimiterTest { @Test void allowsUpToLimitThenDeniesUntilBoundary() { - TimeSource.Fake clock = new TimeSource.Fake(0L); - RateLimiter limiter = new FixedWindowRateLimiter<>(3, 10_000L, clock); + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(3, 10_000L, clock); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isTrue(); @@ -15,72 +17,76 @@ void allowsUpToLimitThenDeniesUntilBoundary() { assertThat(limiter.allow("A")).isFalse(); // Just before boundary: still same window - clock.advance(9_999L); + clock.advanceNanos(9_999L); assertThat(limiter.allow("A")).isFalse(); // At exact boundary: new window - clock.advance(1L); + clock.advanceNanos(1L); assertThat(limiter.allow("A")).isTrue(); } + @Disabled("Remove to run test") @Test void continuesCountingWithinWindowAfterBoundaryReset() { - TimeSource.Fake clock = new TimeSource.Fake(0L); - RateLimiter limiter = new FixedWindowRateLimiter<>(2, 5_000L, clock); + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter 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.advance(5_000L); + 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.Fake clock = new TimeSource.Fake(42L); - RateLimiter limiter = new FixedWindowRateLimiter<>(1, 100L, clock); + TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(42L)); + RateLimiter limiter = new RateLimiter<>(1, 100L, clock); assertThat(limiter.allow("A")).isTrue(); - assertThat(limiter.allow("B")).isTrue(); // independent key assertThat(limiter.allow("A")).isFalse(); + assertThat(limiter.allow("B")).isTrue(); // independent key assertThat(limiter.allow("B")).isFalse(); - clock.advance(100L); // new window for both at boundary + 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() { - TimeSource.Fake clock = new TimeSource.Fake(1_000L); - RateLimiter limiter = new FixedWindowRateLimiter<>(2, 50L, clock); + TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(1_000L)); + RateLimiter 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.advance(1_000L); + 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.Fake clock = new TimeSource.Fake(0L); - RateLimiter limiter = new FixedWindowRateLimiter<>(1, 10L, clock); + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter 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.advance(10L); + clock.advanceNanos(10L); assertThat(limiter.allow("k")).isTrue(); assertThat(limiter.allow("k")).isFalse(); } From a697b8102cd90cc7c6e0372f85bd223ef42af3b7 Mon Sep 17 00:00:00 2001 From: Andrea Date: Sun, 14 Sep 2025 11:06:59 +0800 Subject: [PATCH 06/39] Update instructions --- .../rate-limiter/.docs/instructions.md | 21 +++++++++++++++++-- .../src/main/java/RateLimiter.java | 7 ++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index 8cb1624fd..4c8b3e17e 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -1,2 +1,19 @@ -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”). +Your task is to build a fixed‑window rate limiter. + +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. + diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java index 0d99d6e64..e72cf592f 100644 --- a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java @@ -1,15 +1,12 @@ import java.time.Instant; -// Students: implement a fixed-window rate limiter here. -// Intentionally minimal starter: you will choose state and logic. public class RateLimiter { - // Provide a blank constructor so the class compiles. public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { - // students implement + } public boolean allow(K key) { - throw new UnsupportedOperationException("Please implement RateLimiter.allow()"); + throw new UnsupportedOperationException("Delete this statement and write your own implementation."); } } From 87fc17500f3745c9a892cd82bebb33ae640601e7 Mon Sep 17 00:00:00 2001 From: Andrea Date: Sun, 14 Sep 2025 12:04:40 +0800 Subject: [PATCH 07/39] Add github handle to authors array --- exercises/practice/rate-limiter/.meta/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json index 87fe135a0..567eadac5 100644 --- a/exercises/practice/rate-limiter/.meta/config.json +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -1,5 +1,5 @@ { - "authors": [], + "authors": ["andreatanky"], "files": { "solution": [ "src/main/java/RateLimiter.java", From 44791734aff6a1d7e2af6b49a19e6c63eb635a22 Mon Sep 17 00:00:00 2001 From: Andrea Date: Sun, 14 Sep 2025 12:30:43 +0800 Subject: [PATCH 08/39] Add blurb --- exercises/practice/rate-limiter/.meta/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json index 567eadac5..7e83dae71 100644 --- a/exercises/practice/rate-limiter/.meta/config.json +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -13,5 +13,5 @@ ".meta/src/reference/java/TimeSource.java" ] }, - "blurb": "" + "blurb": "Practice stateful logic and time handling by implementing a fixed-window rate limiter" } From 63ea8a31f3c5bc7b35683b4adf8cde04ac7fc45c Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 18 Sep 2025 17:44:37 +0800 Subject: [PATCH 09/39] Update long to use Duration --- .../.meta/src/reference/java/RateLimiter.java | 9 +++++---- .../rate-limiter/src/main/java/RateLimiter.java | 3 ++- .../rate-limiter/src/test/java/RateLimiterTest.java | 11 ++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java index 7bc3d8863..9cb9e8e63 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java @@ -1,3 +1,4 @@ +import java.time.Duration; import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -15,13 +16,13 @@ private static final class WindowState { } private final int limit; - private final long windowSizeNanos; + private final Duration windowSize; private final TimeSource timeSource; private final Map states = new HashMap<>(); - public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { + public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) { this.limit = limit; - this.windowSizeNanos = windowSizeNanos; + this.windowSize = windowSize; this.timeSource = timeSource; } @@ -36,7 +37,7 @@ public boolean allow(K key) { } long elapsed = now - s.windowStartNanos; - if (elapsed >= windowSizeNanos) { + if (elapsed >= windowSize.toNanos()) { s.windowStartNanos = now; s.usedCount = 0; } diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java index e72cf592f..5b50fddde 100644 --- a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java @@ -1,8 +1,9 @@ +import java.time.Duration; import java.time.Instant; public class RateLimiter { - public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) { + public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) { } diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 4dc8950ee..b36e48fd4 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -1,5 +1,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.time.Duration; import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; @@ -9,7 +10,7 @@ class RateLimiterTest { @Test void allowsUpToLimitThenDeniesUntilBoundary() { TimeSource clock = new TimeSource(Instant.EPOCH); - RateLimiter limiter = new RateLimiter<>(3, 10_000L, clock); + RateLimiter limiter = new RateLimiter<>(3, Duration.ofNanos(10_000L), clock); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isTrue(); @@ -29,7 +30,7 @@ void allowsUpToLimitThenDeniesUntilBoundary() { @Test void continuesCountingWithinWindowAfterBoundaryReset() { TimeSource clock = new TimeSource(Instant.EPOCH); - RateLimiter limiter = new RateLimiter<>(2, 5_000L, clock); + RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(5_000L), clock); assertThat(limiter.allow("key")).isTrue(); assertThat(limiter.allow("key")).isTrue(); @@ -46,7 +47,7 @@ void continuesCountingWithinWindowAfterBoundaryReset() { @Test void separateKeysHaveIndependentCountersAndWindows() { TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(42L)); - RateLimiter limiter = new RateLimiter<>(1, 100L, clock); + RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isFalse(); @@ -62,7 +63,7 @@ void separateKeysHaveIndependentCountersAndWindows() { @Test void longGapsResetWindowDeterministically() { TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(1_000L)); - RateLimiter limiter = new RateLimiter<>(2, 50L, clock); + RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(50L), clock); assertThat(limiter.allow("X")).isTrue(); assertThat(limiter.allow("X")).isTrue(); @@ -79,7 +80,7 @@ void longGapsResetWindowDeterministically() { @Test void exactBoundaryIsNewWindowEveryTime() { TimeSource clock = new TimeSource(Instant.EPOCH); - RateLimiter limiter = new RateLimiter<>(1, 10L, clock); + RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(10L), clock); assertThat(limiter.allow("k")).isTrue(); assertThat(limiter.allow("k")).isFalse(); From 9f6a8bf7683b10185b8ee7ae3fbcfcf42df5dd45 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 19 Sep 2025 09:01:20 +0800 Subject: [PATCH 10/39] Update advance method to use Duration in tests --- .../rate-limiter/src/test/java/RateLimiterTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index b36e48fd4..1a74d2eb2 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -18,11 +18,11 @@ void allowsUpToLimitThenDeniesUntilBoundary() { assertThat(limiter.allow("A")).isFalse(); // Just before boundary: still same window - clock.advanceNanos(9_999L); + clock.advance(Duration.ofNanos(9_999L)); assertThat(limiter.allow("A")).isFalse(); // At exact boundary: new window - clock.advanceNanos(1L); + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow("A")).isTrue(); } @@ -37,7 +37,7 @@ void continuesCountingWithinWindowAfterBoundaryReset() { assertThat(limiter.allow("key")).isFalse(); // Jump to next window - clock.advanceNanos(5_000L); + clock.advance(Duration.ofNanos(5_000L)); assertThat(limiter.allow("key")).isTrue(); assertThat(limiter.allow("key")).isTrue(); assertThat(limiter.allow("key")).isFalse(); @@ -54,7 +54,7 @@ void separateKeysHaveIndependentCountersAndWindows() { assertThat(limiter.allow("B")).isTrue(); // independent key assertThat(limiter.allow("B")).isFalse(); - clock.advanceNanos(100L); // new window for both at boundary + clock.advance(Duration.ofNanos(100L)); // new window for both at boundary assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("B")).isTrue(); } @@ -70,7 +70,7 @@ void longGapsResetWindowDeterministically() { assertThat(limiter.allow("X")).isFalse(); // Advance several windows worth - clock.advanceNanos(1_000L); + clock.advance(Duration.ofNanos(1_000L)); assertThat(limiter.allow("X")).isTrue(); assertThat(limiter.allow("X")).isTrue(); assertThat(limiter.allow("X")).isFalse(); @@ -87,7 +87,7 @@ void exactBoundaryIsNewWindowEveryTime() { // Move exactly to boundary repeatedly; each time should allow once for (int i = 0; i < 5; i++) { - clock.advanceNanos(10L); + clock.advance(Duration.ofNanos(10L)); assertThat(limiter.allow("k")).isTrue(); assertThat(limiter.allow("k")).isFalse(); } From a616a10599bfb59007049d52c7e93ff49bccb9af Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 19 Sep 2025 09:04:52 +0800 Subject: [PATCH 11/39] Remove unused advanceNanos method --- exercises/practice/rate-limiter/src/main/java/TimeSource.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/exercises/practice/rate-limiter/src/main/java/TimeSource.java b/exercises/practice/rate-limiter/src/main/java/TimeSource.java index 0f5fb28f7..db3bc2a86 100644 --- a/exercises/practice/rate-limiter/src/main/java/TimeSource.java +++ b/exercises/practice/rate-limiter/src/main/java/TimeSource.java @@ -15,8 +15,4 @@ public Instant now() { public void advance(Duration d) { this.now = this.now.plus(d); } - - public void advanceNanos(long nanos) { - this.now = this.now.plusNanos(nanos); - } } From 9f077d0bfe3f6e787d6becf243772dd16de14aa6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 19 Sep 2025 09:20:47 +0800 Subject: [PATCH 12/39] Add non string tests --- .../src/test/java/RateLimiterTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 1a74d2eb2..afc93337d 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -92,4 +93,96 @@ void exactBoundaryIsNewWindowEveryTime() { assertThat(limiter.allow("k")).isFalse(); } } + + + @Disabled("Remove to run test") + @Test + void supportsUuidKeys() { + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); + + UUID a = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID b = UUID.fromString("00000000-0000-0000-0000-000000000002"); + + assertThat(limiter.allow(a)).isTrue(); + assertThat(limiter.allow(a)).isFalse(); + assertThat(limiter.allow(b)).isTrue(); + assertThat(limiter.allow(b)).isFalse(); + + clock.advance(Duration.ofNanos(100L)); + assertThat(limiter.allow(a)).isTrue(); + assertThat(limiter.allow(b)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + void supportsIntegerKeys() { + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); + + assertThat(limiter.allow(42)).isTrue(); + assertThat(limiter.allow(42)).isFalse(); + assertThat(limiter.allow(84)).isTrue(); // independent key of different type + assertThat(limiter.allow(84)).isFalse(); + + clock.advance(Duration.ofNanos(100L)); // boundary resets both + assertThat(limiter.allow(42)).isTrue(); + assertThat(limiter.allow(84)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + void supportsLongKeys() { + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(50L), clock); + + assertThat(limiter.allow(1L)).isTrue(); + assertThat(limiter.allow(1L)).isTrue(); + assertThat(limiter.allow(1L)).isFalse(); + + assertThat(limiter.allow(2L)).isTrue(); // independent key + assertThat(limiter.allow(2L)).isTrue(); + assertThat(limiter.allow(2L)).isFalse(); + + clock.advance(Duration.ofNanos(50L)); + assertThat(limiter.allow(1L)).isTrue(); + assertThat(limiter.allow(2L)).isTrue(); + } + + @Disabled("Remove to run test") + @Test + void supportsCustomObjectKeys() { + final class ClientId { + final String id; + + ClientId(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + return (o instanceof ClientId) && ((ClientId) o).id.equals(this.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + } + + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(10L), clock); + + ClientId a = new ClientId("A"); + ClientId b = new ClientId("B"); + + assertThat(limiter.allow(a)).isTrue(); + assertThat(limiter.allow(a)).isFalse(); + assertThat(limiter.allow(b)).isTrue(); + assertThat(limiter.allow(b)).isFalse(); + + clock.advance(Duration.ofNanos(10L)); + assertThat(limiter.allow(a)).isTrue(); + assertThat(limiter.allow(b)).isTrue(); + } } From 89ec42668202bf825a97e9a4a6fbb904f92f99a1 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 19 Sep 2025 09:33:30 +0800 Subject: [PATCH 13/39] Break down test cases --- .../src/test/java/RateLimiterTest.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index afc93337d..17bb2a6ba 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -9,7 +9,7 @@ class RateLimiterTest { @Test - void allowsUpToLimitThenDeniesUntilBoundary() { + void allowsUpToLimit() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(3, Duration.ofNanos(10_000L), clock); @@ -17,13 +17,35 @@ void allowsUpToLimitThenDeniesUntilBoundary() { assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isFalse(); + } + + @Disabled("Remove to run test") + @Test + void denyCloseToBoundary() { + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(10_000L), clock); + + assertThat(limiter.allow("A")).isTrue(); + assertThat(limiter.allow("A")).isTrue(); + assertThat(limiter.allow("A")).isFalse(); // Just before boundary: still same window clock.advance(Duration.ofNanos(9_999L)); assertThat(limiter.allow("A")).isFalse(); + } + + @Disabled("Remove to run test") + @Test + void allowsNewBoundary() { + TimeSource clock = new TimeSource(Instant.EPOCH); + RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(10_000L), clock); + + assertThat(limiter.allow("A")).isTrue(); + assertThat(limiter.allow("A")).isTrue(); + assertThat(limiter.allow("A")).isFalse(); // At exact boundary: new window - clock.advance(Duration.ofNanos(1L)); + clock.advance(Duration.ofNanos(10_000L)); assertThat(limiter.allow("A")).isTrue(); } From 9657afcd9957097cebeef8e850e206c2856a34ab Mon Sep 17 00:00:00 2001 From: Andrea Date: Sat, 20 Sep 2025 11:41:41 +0800 Subject: [PATCH 14/39] Update to one sentence per line --- .../rate-limiter/.docs/instructions.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index 4c8b3e17e..c05b2177f 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -1,19 +1,18 @@ Your task is to build a fixed‑window rate limiter. -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. +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. +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. +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. - From 98d61037bf1f1f14440b30ea9fa63c2ebb8e3ffc Mon Sep 17 00:00:00 2001 From: Andrea Date: Sat, 20 Sep 2025 11:54:35 +0800 Subject: [PATCH 15/39] Update WindowState long type to use Instant --- .../.meta/src/reference/java/RateLimiter.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java index 9cb9e8e63..133ab5b2c 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java @@ -6,11 +6,11 @@ public class RateLimiter { private static final class WindowState { - long windowStartNanos; + Instant windowStart; int usedCount; - WindowState(long windowStartNanos, int usedCount) { - this.windowStartNanos = windowStartNanos; + WindowState(Instant windowStart, int usedCount) { + this.windowStart = windowStart; this.usedCount = usedCount; } } @@ -27,8 +27,7 @@ public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) { } public boolean allow(K key) { - Instant nowInstant = timeSource.now(); - long now = nowInstant.getEpochSecond() * 1_000_000_000L + nowInstant.getNano(); + Instant now = timeSource.now(); WindowState s = states.get(key); if (s == null) { @@ -36,9 +35,8 @@ public boolean allow(K key) { states.put(key, s); } - long elapsed = now - s.windowStartNanos; - if (elapsed >= windowSize.toNanos()) { - s.windowStartNanos = now; + if (!now.isBefore(s.windowStart.plus(windowSize))) { + s.windowStart = now; s.usedCount = 0; } From 43610f182419ca56153692a140ebc6d642e3849d Mon Sep 17 00:00:00 2001 From: Andrea Date: Sat, 20 Sep 2025 11:58:36 +0800 Subject: [PATCH 16/39] Remove advanceNanos --- .../rate-limiter/.meta/src/reference/java/TimeSource.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java b/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java index 0f5fb28f7..db3bc2a86 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/TimeSource.java @@ -15,8 +15,4 @@ public Instant now() { public void advance(Duration d) { this.now = this.now.plus(d); } - - public void advanceNanos(long nanos) { - this.now = this.now.plusNanos(nanos); - } } From 50b280c5c9f99c92edb3dad3b9c3a5f7af57a853 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 08:45:25 +0800 Subject: [PATCH 17/39] Update naming of key to clientId --- .../rate-limiter/.meta/src/reference/java/RateLimiter.java | 6 +++--- .../practice/rate-limiter/src/main/java/RateLimiter.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java index 133ab5b2c..a3cebe23c 100644 --- a/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/.meta/src/reference/java/RateLimiter.java @@ -26,13 +26,13 @@ public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) { this.timeSource = timeSource; } - public boolean allow(K key) { + public boolean allow(K clientId) { Instant now = timeSource.now(); - WindowState s = states.get(key); + WindowState s = states.get(clientId); if (s == null) { s = new WindowState(now, 0); - states.put(key, s); + states.put(clientId, s); } if (!now.isBefore(s.windowStart.plus(windowSize))) { diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java index 5b50fddde..48b07e405 100644 --- a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java @@ -7,7 +7,7 @@ public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) { } - public boolean allow(K key) { + public boolean allow(K clientId) { throw new UnsupportedOperationException("Delete this statement and write your own implementation."); } } From 69c1ba99c58009d3617eb5f156c9cc30df1b3b7b Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 08:57:46 +0800 Subject: [PATCH 18/39] Update exercises/practice/rate-limiter/.docs/instructions.md Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/.docs/instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index c05b2177f..f8bd7d60b 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -10,7 +10,7 @@ Different strategies exist; in this exercise you’ll implement a fixed‑window 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. +Each client is tracked separately, so another client can make requests within that same period. Examples: - Limit: 3 requests per 10 seconds per client. From 6f27c128d4d6d47705b14d28d842ad3e9f644673 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 08:57:56 +0800 Subject: [PATCH 19/39] Update exercises/practice/rate-limiter/.docs/instructions.md Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/.docs/instructions.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index f8bd7d60b..80a3899a0 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -12,7 +12,9 @@ Fixed‑window rate limiting groups time into equal‑length windows (for exampl Once the window resets, the allowance refreshes for the next window. Each client is tracked separately, so another client can make requests within that same period. -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. +For example, consider a rate limiter configured to limit 2 requests per 10 seconds per client. +Lets say client A sends a request. +- Being its first request, the request is permitted. +- A second request within 10 seconds after the first one is also permitted. +- However, further requests after that would be denied _until_ at least 10 seconds has elapsed since the first request. +- 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. From 368d7ea461e37e40ee0411683e4a5c5dbeec5746 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 09:37:12 +0800 Subject: [PATCH 20/39] Remove custom object test --- .../src/test/java/RateLimiterTest.java | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 17bb2a6ba..dc08a4f2b 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -170,41 +170,4 @@ void supportsLongKeys() { assertThat(limiter.allow(1L)).isTrue(); assertThat(limiter.allow(2L)).isTrue(); } - - @Disabled("Remove to run test") - @Test - void supportsCustomObjectKeys() { - final class ClientId { - final String id; - - ClientId(String id) { - this.id = id; - } - - @Override - public boolean equals(Object o) { - return (o instanceof ClientId) && ((ClientId) o).id.equals(this.id); - } - - @Override - public int hashCode() { - return id.hashCode(); - } - } - - TimeSource clock = new TimeSource(Instant.EPOCH); - RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(10L), clock); - - ClientId a = new ClientId("A"); - ClientId b = new ClientId("B"); - - assertThat(limiter.allow(a)).isTrue(); - assertThat(limiter.allow(a)).isFalse(); - assertThat(limiter.allow(b)).isTrue(); - assertThat(limiter.allow(b)).isFalse(); - - clock.advance(Duration.ofNanos(10L)); - assertThat(limiter.allow(a)).isTrue(); - assertThat(limiter.allow(b)).isTrue(); - } } From 87c3d3cea2f28388e54d85ff380e41249a39c243 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 09:40:44 +0800 Subject: [PATCH 21/39] Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java Co-authored-by: Kah Goh --- .../practice/rate-limiter/src/test/java/RateLimiterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index dc08a4f2b..fcde44d11 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -84,7 +84,7 @@ void separateKeysHaveIndependentCountersAndWindows() { @Disabled("Remove to run test") @Test - void longGapsResetWindowDeterministically() { + void longGapsResetWindow() { TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(1_000L)); RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(50L), clock); From d45106008c38f9c1ee8209f118fcf8f66aff053b Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 12:29:06 +0800 Subject: [PATCH 22/39] Add tiny clock advances in tests --- .../rate-limiter/src/test/java/RateLimiterTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index fcde44d11..8657bed06 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -14,6 +14,8 @@ void allowsUpToLimit() { RateLimiter limiter = new RateLimiter<>(3, Duration.ofNanos(10_000L), clock); assertThat(limiter.allow("A")).isTrue(); + // Advance minimally to model time passage between requests + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isFalse(); @@ -56,6 +58,7 @@ void continuesCountingWithinWindowAfterBoundaryReset() { RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(5_000L), clock); assertThat(limiter.allow("key")).isTrue(); + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow("key")).isTrue(); assertThat(limiter.allow("key")).isFalse(); @@ -73,6 +76,8 @@ void separateKeysHaveIndependentCountersAndWindows() { RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); assertThat(limiter.allow("A")).isTrue(); + // Advance a tiny amount to model time moving between requests + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow("A")).isFalse(); assertThat(limiter.allow("B")).isTrue(); // independent key assertThat(limiter.allow("B")).isFalse(); @@ -89,6 +94,7 @@ void longGapsResetWindow() { RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(50L), clock); assertThat(limiter.allow("X")).isTrue(); + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow("X")).isTrue(); assertThat(limiter.allow("X")).isFalse(); @@ -128,6 +134,8 @@ void supportsUuidKeys() { assertThat(limiter.allow(a)).isTrue(); assertThat(limiter.allow(a)).isFalse(); + // Advance slightly so the second client's window anchors later + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow(b)).isTrue(); assertThat(limiter.allow(b)).isFalse(); @@ -144,6 +152,7 @@ void supportsIntegerKeys() { assertThat(limiter.allow(42)).isTrue(); assertThat(limiter.allow(42)).isFalse(); + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow(84)).isTrue(); // independent key of different type assertThat(limiter.allow(84)).isFalse(); @@ -162,6 +171,7 @@ void supportsLongKeys() { assertThat(limiter.allow(1L)).isTrue(); assertThat(limiter.allow(1L)).isFalse(); + clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow(2L)).isTrue(); // independent key assertThat(limiter.allow(2L)).isTrue(); assertThat(limiter.allow(2L)).isFalse(); From 67baa142aebda0036e9eb9c4a8edc71041b10d5d Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 16:15:51 +0800 Subject: [PATCH 23/39] Update difficulty to 4 --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index d9588387d..d9022a23d 100644 --- a/config.json +++ b/config.json @@ -1908,7 +1908,7 @@ "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", "practices": [], "prerequisites": [], - "difficulty": 1 + "difficulty": 4 } ], "foregone": [ From aba22d5e272d43c8dc328447a104fc37e084037b Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 16:18:52 +0800 Subject: [PATCH 24/39] Add generic types to prereq --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index d9022a23d..302017953 100644 --- a/config.json +++ b/config.json @@ -1907,7 +1907,7 @@ "name": "rate-limiter", "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", "practices": [], - "prerequisites": [], + "prerequisites": ["generic-types"], "difficulty": 4 } ], From 715f18161af55188b0fe59f794b79dec3724e919 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 16:19:27 +0800 Subject: [PATCH 25/39] Update exercises/practice/rate-limiter/src/main/java/TimeSource.java Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/src/main/java/TimeSource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exercises/practice/rate-limiter/src/main/java/TimeSource.java b/exercises/practice/rate-limiter/src/main/java/TimeSource.java index db3bc2a86..bc2c998d3 100644 --- a/exercises/practice/rate-limiter/src/main/java/TimeSource.java +++ b/exercises/practice/rate-limiter/src/main/java/TimeSource.java @@ -1,6 +1,9 @@ import java.time.Duration; import java.time.Instant; +/** + * NOTE: There is no need to change this file and is treated as read only by the Exercism test runners. + */ public class TimeSource { private Instant now; From efadb02fd4498b2f2b903f7b4f3490e7989aa819 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 16:20:42 +0800 Subject: [PATCH 26/39] Update exercises/practice/rate-limiter/.meta/config.json Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/.meta/config.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json index 7e83dae71..7b358e52e 100644 --- a/exercises/practice/rate-limiter/.meta/config.json +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -2,8 +2,7 @@ "authors": ["andreatanky"], "files": { "solution": [ - "src/main/java/RateLimiter.java", - "src/main/java/TimeSource.java" + "src/main/java/RateLimiter.java" ], "test": [ "src/test/java/RateLimiterTest.java" @@ -11,6 +10,9 @@ "example": [ ".meta/src/reference/java/RateLimiter.java", ".meta/src/reference/java/TimeSource.java" + ], + "editor": [ + "src/main/java/TimeSource.java" ] }, "blurb": "Practice stateful logic and time handling by implementing a fixed-window rate limiter" From 8dc4059184a6071d9e11f5954982aacddfbd74e0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 22 Sep 2025 16:24:21 +0800 Subject: [PATCH 27/39] Update UUID test to include mixture of clock advance duration --- .../rate-limiter/src/test/java/RateLimiterTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 8657bed06..edb477375 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -127,7 +127,8 @@ void exactBoundaryIsNewWindowEveryTime() { @Test void supportsUuidKeys() { TimeSource clock = new TimeSource(Instant.EPOCH); - RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); + // Use a seconds-long window and advance in smaller units to mix Duration usage + RateLimiter limiter = new RateLimiter<>(1, Duration.ofSeconds(1L), clock); UUID a = UUID.fromString("00000000-0000-0000-0000-000000000001"); UUID b = UUID.fromString("00000000-0000-0000-0000-000000000002"); @@ -135,11 +136,12 @@ void supportsUuidKeys() { assertThat(limiter.allow(a)).isTrue(); assertThat(limiter.allow(a)).isFalse(); // Advance slightly so the second client's window anchors later - clock.advance(Duration.ofNanos(1L)); + clock.advance(Duration.ofMillis(1L)); assertThat(limiter.allow(b)).isTrue(); assertThat(limiter.allow(b)).isFalse(); - clock.advance(Duration.ofNanos(100L)); + // Advance exactly one second to hit the window boundary + clock.advance(Duration.ofSeconds(1L)); assertThat(limiter.allow(a)).isTrue(); assertThat(limiter.allow(b)).isTrue(); } From 60736bb5f11c5e364d39f9ef3aecea071276c3f6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Sep 2025 08:42:28 +0800 Subject: [PATCH 28/39] Shift exercise in config.json --- config.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.json b/config.json index 302017953..08c6e857e 100644 --- a/config.json +++ b/config.json @@ -810,6 +810,14 @@ ], "difficulty": 4 }, + { + "slug": "rate-limiter", + "name": "rate-limiter", + "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", + "practices": [], + "prerequisites": ["generic-types"], + "difficulty": 4 + }, { "slug": "rotational-cipher", "name": "Rotational Cipher", @@ -1901,14 +1909,6 @@ "lists" ], "difficulty": 10 - }, - { - "slug": "rate-limiter", - "name": "rate-limiter", - "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", - "practices": [], - "prerequisites": ["generic-types"], - "difficulty": 4 } ], "foregone": [ From d4185677e16d172aa780dfe91d7de4e52c83c1f0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Sep 2025 08:45:44 +0800 Subject: [PATCH 29/39] Copy gradle wrapper --- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + exercises/practice/rate-limiter/gradlew | 249 ++++++++++++++++++ exercises/practice/rate-limiter/gradlew.bat | 92 +++++++ 4 files changed, 348 insertions(+) create mode 100644 exercises/practice/rate-limiter/gradle/wrapper/gradle-wrapper.jar create mode 100644 exercises/practice/rate-limiter/gradle/wrapper/gradle-wrapper.properties create mode 100644 exercises/practice/rate-limiter/gradlew create mode 100644 exercises/practice/rate-limiter/gradlew.bat diff --git a/exercises/practice/rate-limiter/gradle/wrapper/gradle-wrapper.jar b/exercises/practice/rate-limiter/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vSTxF-Vi3+ZOI=Thq2} zyQgjYY1_7^ZQHh{?P))4+qUiQJLi1&{yE>h?~jU%tjdV0h|FENbM3X(KnJdPKc?~k zh=^Ixv*+smUll!DTWH!jrV*wSh*(mx0o6}1@JExzF(#9FXgmTXVoU+>kDe68N)dkQ zH#_98Zv$}lQwjKL@yBd;U(UD0UCl322=pav<=6g>03{O_3oKTq;9bLFX1ia*lw;#K zOiYDcBJf)82->83N_Y(J7Kr_3lE)hAu;)Q(nUVydv+l+nQ$?|%MWTy`t>{havFSQloHwiIkGK9YZ79^9?AZo0ZyQlVR#}lF%dn5n%xYksXf8gnBm=wO7g_^! zauQ-bH1Dc@3ItZ-9D_*pH}p!IG7j8A_o94#~>$LR|TFq zZ-b00*nuw|-5C2lJDCw&8p5N~Z1J&TrcyErds&!l3$eSz%`(*izc;-?HAFD9AHb-| z>)id`QCrzRws^9(#&=pIx9OEf2rmlob8sK&xPCWS+nD~qzU|qG6KwA{zbikcfQrdH z+ zQg>O<`K4L8rN7`GJB0*3<3`z({lWe#K!4AZLsI{%z#ja^OpfjU{!{)x0ZH~RB0W5X zTwN^w=|nA!4PEU2=LR05x~}|B&ZP?#pNgDMwD*ajI6oJqv!L81gu=KpqH22avXf0w zX3HjbCI!n9>l046)5rr5&v5ja!xkKK42zmqHzPx$9Nn_MZk`gLeSLgC=LFf;H1O#B zn=8|^1iRrujHfbgA+8i<9jaXc;CQBAmQvMGQPhFec2H1knCK2x!T`e6soyrqCamX% zTQ4dX_E*8so)E*TB$*io{$c6X)~{aWfaqdTh=xEeGvOAN9H&-t5tEE-qso<+C!2>+ zskX51H-H}#X{A75wqFe-J{?o8Bx|>fTBtl&tcbdR|132Ztqu5X0i-pisB-z8n71%q%>EF}yy5?z=Ve`}hVh{Drv1YWL zW=%ug_&chF11gDv3D6B)Tz5g54H0mDHNjuKZ+)CKFk4Z|$RD zfRuKLW`1B>B?*RUfVd0+u8h3r-{@fZ{k)c!93t1b0+Q9vOaRnEn1*IL>5Z4E4dZ!7 ztp4GP-^1d>8~LMeb}bW!(aAnB1tM_*la=Xx)q(I0Y@__Zd$!KYb8T2VBRw%e$iSdZ zkwdMwd}eV9q*;YvrBFTv1>1+}{H!JK2M*C|TNe$ZSA>UHKk);wz$(F$rXVc|sI^lD zV^?_J!3cLM;GJuBMbftbaRUs$;F}HDEDtIeHQ)^EJJ1F9FKJTGH<(Jj`phE6OuvE) zqK^K`;3S{Y#1M@8yRQwH`?kHMq4tHX#rJ>5lY3DM#o@or4&^_xtBC(|JpGTfrbGkA z2Tu+AyT^pHannww!4^!$5?@5v`LYy~T`qs7SYt$JgrY(w%C+IWA;ZkwEF)u5sDvOK zGk;G>Mh&elvXDcV69J_h02l&O;!{$({fng9Rlc3ID#tmB^FIG^w{HLUpF+iB`|
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/exercises/practice/rate-limiter/gradlew.bat b/exercises/practice/rate-limiter/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/exercises/practice/rate-limiter/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From baf39d2690f06eef63ab3631c98015bbc0a6421f Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Sep 2025 08:46:14 +0800 Subject: [PATCH 30/39] Update exercises/practice/rate-limiter/.docs/instructions.md Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/.docs/instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index 80a3899a0..0bf7e4991 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -1,3 +1,5 @@ +# Instructions + Your task is to build a fixed‑window rate limiter. Imagine a single server connected to one or more clients. From 7907757b8b87d79678940b9ca164ff2488d31491 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Sep 2025 08:46:26 +0800 Subject: [PATCH 31/39] Update exercises/practice/rate-limiter/.docs/instructions.md Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/.docs/instructions.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index 0bf7e4991..42a212376 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -15,7 +15,8 @@ Once the window resets, the allowance refreshes for the next window. Each client is tracked separately, so another client can make requests within that same period. For example, consider a rate limiter configured to limit 2 requests per 10 seconds per client. -Lets say client A sends a request. +Lets say a client sends a request: + - Being its first request, the request is permitted. - A second request within 10 seconds after the first one is also permitted. - However, further requests after that would be denied _until_ at least 10 seconds has elapsed since the first request. From 6100e1142c4652e5bccffe989ad97af0a87bc8ca Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Sep 2025 08:50:10 +0800 Subject: [PATCH 32/39] Add display name annotation --- .../rate-limiter/src/test/java/RateLimiterTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index edb477375..2a398c0ef 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -1,4 +1,5 @@ import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; @@ -9,6 +10,7 @@ class RateLimiterTest { @Test + @DisplayName("Allows up to window limit") void allowsUpToLimit() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(3, Duration.ofNanos(10_000L), clock); @@ -23,6 +25,7 @@ void allowsUpToLimit() { @Disabled("Remove to run test") @Test + @DisplayName("Denies requests just before boundary") void denyCloseToBoundary() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(10_000L), clock); @@ -38,6 +41,7 @@ void denyCloseToBoundary() { @Disabled("Remove to run test") @Test + @DisplayName("Allows first request at exact boundary") void allowsNewBoundary() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(10_000L), clock); @@ -53,6 +57,7 @@ void allowsNewBoundary() { @Disabled("Remove to run test") @Test + @DisplayName("Resets at boundary, then counts within window") void continuesCountingWithinWindowAfterBoundaryReset() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(5_000L), clock); @@ -71,6 +76,7 @@ void continuesCountingWithinWindowAfterBoundaryReset() { @Disabled("Remove to run test") @Test + @DisplayName("Independent counters/windows per key") void separateKeysHaveIndependentCountersAndWindows() { TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(42L)); RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); @@ -89,6 +95,7 @@ void separateKeysHaveIndependentCountersAndWindows() { @Disabled("Remove to run test") @Test + @DisplayName("Long gaps reset window") void longGapsResetWindow() { TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(1_000L)); RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(50L), clock); @@ -107,6 +114,7 @@ void longGapsResetWindow() { @Disabled("Remove to run test") @Test + @DisplayName("Exact boundary starts a new window each time") void exactBoundaryIsNewWindowEveryTime() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(10L), clock); @@ -125,6 +133,7 @@ void exactBoundaryIsNewWindowEveryTime() { @Disabled("Remove to run test") @Test + @DisplayName("Supports UUID keys with mixed time units") void supportsUuidKeys() { TimeSource clock = new TimeSource(Instant.EPOCH); // Use a seconds-long window and advance in smaller units to mix Duration usage @@ -148,6 +157,7 @@ void supportsUuidKeys() { @Disabled("Remove to run test") @Test + @DisplayName("Supports Integer keys") void supportsIntegerKeys() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(1, Duration.ofNanos(100L), clock); @@ -165,6 +175,7 @@ void supportsIntegerKeys() { @Disabled("Remove to run test") @Test + @DisplayName("Supports Long keys") void supportsLongKeys() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(2, Duration.ofNanos(50L), clock); From 6ccd97a99c731f351ac29c4f0e274faa7ae0273f Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 23 Sep 2025 08:56:44 +0800 Subject: [PATCH 33/39] Run configlet to format config.json --- exercises/practice/rate-limiter/.meta/config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/.meta/config.json b/exercises/practice/rate-limiter/.meta/config.json index 7b358e52e..0f7b8ebea 100644 --- a/exercises/practice/rate-limiter/.meta/config.json +++ b/exercises/practice/rate-limiter/.meta/config.json @@ -1,5 +1,7 @@ { - "authors": ["andreatanky"], + "authors": [ + "andreatanky" + ], "files": { "solution": [ "src/main/java/RateLimiter.java" From b839de1c5aa3c64a09e1dbe19b76b65801314d35 Mon Sep 17 00:00:00 2001 From: andrea Date: Tue, 23 Sep 2025 22:05:44 +0800 Subject: [PATCH 34/39] Reformat files --- config.json | 4 +++- exercises/practice/rate-limiter/.docs/instructions.md | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 08c6e857e..7905a88ec 100644 --- a/config.json +++ b/config.json @@ -815,7 +815,9 @@ "name": "rate-limiter", "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", "practices": [], - "prerequisites": ["generic-types"], + "prerequisites": [ + "generic-types" + ], "difficulty": 4 }, { diff --git a/exercises/practice/rate-limiter/.docs/instructions.md b/exercises/practice/rate-limiter/.docs/instructions.md index 42a212376..1e229cd6e 100644 --- a/exercises/practice/rate-limiter/.docs/instructions.md +++ b/exercises/practice/rate-limiter/.docs/instructions.md @@ -15,9 +15,9 @@ Once the window resets, the allowance refreshes for the next window. Each client is tracked separately, so another client can make requests within that same period. For example, consider a rate limiter configured to limit 2 requests per 10 seconds per client. -Lets say a client sends a request: +Lets say a client sends a request: - Being its first request, the request is permitted. - A second request within 10 seconds after the first one is also permitted. - However, further requests after that would be denied _until_ at least 10 seconds has elapsed since the first request. -- 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. +- 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. From a8a6048faa53f734edda988a8f7d6cc53004946c Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 24 Sep 2025 08:29:28 +0800 Subject: [PATCH 35/39] Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java Co-authored-by: Kah Goh --- .../practice/rate-limiter/src/test/java/RateLimiterTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 2a398c0ef..5a875c28c 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -136,7 +136,6 @@ void exactBoundaryIsNewWindowEveryTime() { @DisplayName("Supports UUID keys with mixed time units") void supportsUuidKeys() { TimeSource clock = new TimeSource(Instant.EPOCH); - // Use a seconds-long window and advance in smaller units to mix Duration usage RateLimiter limiter = new RateLimiter<>(1, Duration.ofSeconds(1L), clock); UUID a = UUID.fromString("00000000-0000-0000-0000-000000000001"); From 6b8e31082361c994ab754e9cf09cfab21593f925 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 24 Sep 2025 08:29:53 +0800 Subject: [PATCH 36/39] Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java Co-authored-by: Kah Goh --- .../practice/rate-limiter/src/test/java/RateLimiterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 5a875c28c..9b428befa 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -133,7 +133,7 @@ void exactBoundaryIsNewWindowEveryTime() { @Disabled("Remove to run test") @Test - @DisplayName("Supports UUID keys with mixed time units") + @DisplayName("Supports UUID keys") void supportsUuidKeys() { TimeSource clock = new TimeSource(Instant.EPOCH); RateLimiter limiter = new RateLimiter<>(1, Duration.ofSeconds(1L), clock); From b3667bc1bb93cbd8b084dc309ae5de16f8cddbbf Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 24 Sep 2025 08:30:03 +0800 Subject: [PATCH 37/39] Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java Co-authored-by: Kah Goh --- .../practice/rate-limiter/src/test/java/RateLimiterTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java index 9b428befa..c740b4bf3 100644 --- a/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java +++ b/exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java @@ -16,7 +16,6 @@ void allowsUpToLimit() { RateLimiter limiter = new RateLimiter<>(3, Duration.ofNanos(10_000L), clock); assertThat(limiter.allow("A")).isTrue(); - // Advance minimally to model time passage between requests clock.advance(Duration.ofNanos(1L)); assertThat(limiter.allow("A")).isTrue(); assertThat(limiter.allow("A")).isTrue(); From 0b158f779ff91bb5c7985402c148a29db75385c5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 24 Sep 2025 08:30:15 +0800 Subject: [PATCH 38/39] Update exercises/practice/rate-limiter/src/main/java/RateLimiter.java Co-authored-by: Kah Goh --- exercises/practice/rate-limiter/src/main/java/RateLimiter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java index 48b07e405..8a28ed946 100644 --- a/exercises/practice/rate-limiter/src/main/java/RateLimiter.java +++ b/exercises/practice/rate-limiter/src/main/java/RateLimiter.java @@ -4,7 +4,7 @@ public class RateLimiter { public RateLimiter(int limit, Duration windowSize, TimeSource timeSource) { - + throw new UnsupportedOperationException("Delete this statement and write your own implementation."); } public boolean allow(K clientId) { From 5e4cc5c7a2a336d7d684fe320ce3bc0fbdb17919 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 24 Sep 2025 17:41:26 +0800 Subject: [PATCH 39/39] Update config.json Co-authored-by: Kah Goh --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 7905a88ec..484d807d0 100644 --- a/config.json +++ b/config.json @@ -812,7 +812,7 @@ }, { "slug": "rate-limiter", - "name": "rate-limiter", + "name": "Rate Limiter", "uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c", "practices": [], "prerequisites": [