ExerciseWarm-up
Warm-up
Break the fixed window
10 minjunior0–12 yrs
One concept, guided. Near-impossible to fail.
What this teaches
- A limiter is a function of time, so it can only be tested against a clock you control
- The fixed window's reset is the bug, and clock-aligned traffic finds it
- Token bucket has no timer — elapsed time is the refill
- Capacity and refill rate are separate dials, and capacity is the burst you chose
Starter
Starter.java
import java.util.*;
/**
* Warm-up: make a correct-looking rate limiter allow twice its limit.
*
* The one rule that makes any of this testable: the limiter reads a clock you
* pass in, never the wall clock. A limiter is a function of time, so a test
* against System.currentTimeMillis() is a test against whatever the machine
* happened to be doing that second.
*/
public class Starter {
/** A clock you advance by hand. */
static final class Ticker {
private long millis;
Ticker(long start) { this.millis = start; }
long now() { return millis; }
void advance(long ms) { millis += ms; }
}
/** Counts requests per aligned window. The reset is the bug. */
static final class FixedWindow {
private final int limit; private final long windowMs; private final Ticker clock;
private long window = Long.MIN_VALUE; private int count;
FixedWindow(int limit, long windowMs, Ticker clock) {
this.limit = limit; this.windowMs = windowMs; this.clock = clock;
}
boolean allow() {
long w = Math.floorDiv(clock.now(), windowMs);
if (w != window) { window = w; count = 0; }
if (count < limit) { count++; return true; }
return false;
}
}
public static void main(String[] args) {
// TODO 1: build a FixedWindow(100, 60_000, clock) and fire 100 requests
// at t = 59s. Count how many are allowed. Predict first.
// TODO 2: advance the clock by 1 second and fire 100 more. How many
// were allowed across those two seconds? Write down the number and
// compare it to the limit the API documentation would state.
// TODO 3: name three sources of traffic that arrive exactly on a
// minute boundary. This is why TODO 2 is not an edge case.
// TODO 4: write a SlidingLog — keep an ArrayDeque of timestamps, drop
// the ones older than the window, allow while size < limit. Run the
// same two bursts through it. It should refuse the second one.
// TODO 5: print how many timestamps your log is holding. Multiply by
// a million users and a limit of 10,000. That number is the reason
// the exact algorithm is not the default.
// TODO 6: write a TokenBucket(capacity, perSecond, clock). No timer
// and no background thread — compute the refill from elapsed time when
// a request arrives:
//
// tokens = min(capacity, tokens + (now - last) * ratePerMs)
//
// Fire 15 at once against capacity 10, rate 5/sec. Then advance one
// second and fire 15 again.
// TODO 7: advance the clock by 60 seconds and check the token count.
// It should be 10, not 300. Say what would go wrong if it were 300.
// TODO 8: your bucket uses the injected Ticker. In production it would
// read a real clock. Say why that must be System.nanoTime() and not
// System.currentTimeMillis(), and what breaks in each direction when
// NTP corrects the clock.
}
}Run it locally:
cd exercises/java/traffic-management/rate-limiting/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterDone when
- You made a 100/min fixed window allow 200 requests inside one second
- You showed a sliding log refusing the same burst
- You built a token bucket and demonstrated a bounded burst after a long idle
- You can say why System.nanoTime is the right clock source