Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions exercises/rate-limiter/canonical-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{
"exercise": "rate-limiter",
"comments": [
"This exercise specifies a fixed-window, per-key rate limiter.",
"Tests assume an injected time source that can advance deterministically,",
"avoiding sleeps. Each \"allow\" operation occurs at the current time;",
"an \"advance\" operation moves the current time forward by a duration in milliseconds.",
"",
"Property: rateLimiter",
"Input: limit (integer), windowMillis (integer), operations (array)",
"Operations: { op: 'allow', key: string } or { op: 'advance', millis: integer }",
"Expected: an array of booleans corresponding to each 'allow' operation in order."
],
"cases": [
{
"uuid": "3f7d0b0a-9a7a-4f2b-9b5b-8a2c3d4e5f6a",
"description": "allows up to the limit within a window",
"property": "rateLimiter",
"input": {
"limit": 3,
"windowMillis": 200,
"operations": [
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" }
]
},
"expected": [true, true, true, false]
},
{
"uuid": "d2b4c1e0-1c0a-4d8e-87a1-9b4fbc8ad9e2",
"description": "resets after window passes",
"property": "rateLimiter",
"input": {
"limit": 3,
"windowMillis": 200,
"operations": [
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "advance", "millis": 200 },
{ "op": "allow", "key": "alice" }
]
},
"expected": [true, true, true, false, true]
},
{
"uuid": "a8b2f6c4-2d2b-4f0d-9a8c-bf2f6e1c3a5d",
"description": "separate keys have independent limits and windows",
"property": "rateLimiter",
"input": {
"limit": 2,
"windowMillis": 100,
"operations": [
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "bob" },
{ "op": "allow", "key": "bob" },
{ "op": "allow", "key": "alice" },
{ "op": "advance", "millis": 100 },
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "bob" }
]
},
"expected": [true, true, true, true, false, true, true]
},
{
"uuid": "4e1f2a3b-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"description": "exactly at window boundary the counter resets",
"property": "rateLimiter",
"input": {
"limit": 2,
"windowMillis": 100,
"operations": [
{ "op": "allow", "key": "alice" },
{ "op": "allow", "key": "alice" },
{ "op": "advance", "millis": 100 },
{ "op": "allow", "key": "alice" }
]
},
"expected": [true, true, true]
},
{
"uuid": "9b7c6d5e-4f3a-4b2c-8d1e-2f3a4b5c6d7e",
"description": "multiple windows do not carry over counts",
"property": "rateLimiter",
"input": {
"limit": 1,
"windowMillis": 50,
"operations": [
{ "op": "allow", "key": "k" },
{ "op": "allow", "key": "k" },
{ "op": "advance", "millis": 50 },
{ "op": "allow", "key": "k" },
{ "op": "allow", "key": "k" },
{ "op": "advance", "millis": 50 },
{ "op": "allow", "key": "k" }
]
},
"expected": [true, false, true, false, true]
},
{
"uuid": "c3d2e1f0-4a5b-4c6d-8e9f-a0b1c2d3e4f5",
"description": "invalid configuration: non-positive limit",
"property": "rateLimiter",
"input": {
"limit": 0,
"windowMillis": 100,
"operations": []
},
"expected": { "error": "invalid limit" }
},
{
"uuid": "e1f2a3b4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"description": "invalid configuration: non-positive window duration",
"property": "rateLimiter",
"input": {
"limit": 2,
"windowMillis": 0,
"operations": []
},
"expected": { "error": "invalid window" }
}
]
}

56 changes: 56 additions & 0 deletions exercises/rate-limiter/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Instructions

Implement a fixed-window, per-key rate limiter.

- Each key has its own independent window and counter.
- At most `limit` requests are allowed within a single window.
- When the window duration passes, the counter resets for that key.
- Use an injected, controllable time source for determining the current time so tests don't require sleeping.

Suggested public API (example):

```

Check failure on line 12 in exercises/rate-limiter/instructions.md

View workflow job for this annotation

GitHub Actions / Lint markdown files

Fenced code blocks should have a language specified

exercises/rate-limiter/instructions.md:12 MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md040.md
interface RateLimiter {
// Returns true if the request for this key is allowed in the current window.
boolean allow(String key);

static RateLimiter fixedWindow(int limit, Duration window, TimeSource timeSource) {
return new FixedWindowRateLimiter(limit, window, timeSource);
}
}

/**
* Abstraction over a monotonic time source used by the rate limiter.
* Units: nanoseconds.
*/
interface TimeSource {
/**
* Returns the current monotonic time in nanoseconds.
* Implementations should use a monotonic source (e.g., System.nanoTime()).
*/
long nowNanos();

final class Fake implements TimeSource {
private long currentNanos;
public Fake(long initialNanos) { this.currentNanos = initialNanos; }
public long nowNanos() { return currentNanos; }
public void advanceMillis(long millis) { this.currentNanos += millis * 1_000_000L; }
public void advanceSeconds(long seconds) { this.currentNanos += seconds * 1_000_000_000L; }
}
}
```

Notes:

- The exact language and types may differ per track; the key idea is a function that, given a `key`, answers whether it is allowed right now, based on a per-key counter and a fixed-size time window.
- Prefer a monotonic time source when available (e.g., `System.nanoTime`), or a provided/injected clock/time source in tracks that support it.
- Handle window boundaries carefully: if the current time is at or beyond the start of the next window, reset the counter for that key and start the next window.

Example timeline (limit = 3, window = 200 ms):

- t=0: allow("alice") → true
- t=0: allow("alice") → true
- t=0: allow("alice") → true
- t=0: allow("alice") → false (limit reached)
- advance time by 200 ms
- t=200 ms: allow("alice") → true (counter reset)
6 changes: 6 additions & 0 deletions exercises/rate-limiter/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Introduction

You're building a public API and need to prevent abuse.
To do that, you'll implement a fixed-window rate limiter: for each client (identified by a key, e.g. username or API token), only a limited number of requests should be allowed within a fixed time window.
Requests beyond that limit are rejected until the next window starts.

3 changes: 3 additions & 0 deletions exercises/rate-limiter/metadata.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
title = "Rate Limiter"
blurb = "Implement a fixed-window per-key rate limiter with an injectable time source."

Loading