Rate Limiter
Build a rate limiter that decides, for each request, whether it may go through. There are three well-known algorithms, and the point of the exercise is that they all sit behind one method, allow, and the caller picks one by name. The caller must never contain an if (strategy === ...) ladder, and adding a fourth algorithm later must not mean editing the three that already work. This is the Strategy pattern.
Implement the class RateLimiter:
RateLimiter(strategy, config): starts with the named strategy. The three names and their configs are below.allow(key, nowSec): returnstrueif the request fromkeyat timenowSec(seconds) is allowed, otherwisefalse. A request is counted only when it is allowed; a rejected request uses up nothing. Keys are fully independent of each other, so every key has its own budget.useStrategy(strategy, config): switches to another strategy while the limiter is running and returnstrue. Every key starts fresh under the new strategy, as if it had never been seen. If the name is unknown it returnsfalseand changes nothing: the old strategy and all its state stay as they were.
The three strategies
"fixed-window"with{limit, windowSec}. Time is cut into windows[k × windowSec, (k + 1) × windowSec)fork = 0, 1, 2, .... A key may have at mostlimitallowed requests in each window, and its count goes back to zero when a new window starts."sliding-window-log"with{limit, windowSec}. A request at timetis allowed only if the key has fewer thanlimitallowed requests in the window(t - windowSec, t]. The start is exclusive: a request made exactlywindowSecseconds ago no longer counts."token-bucket"with{capacity, refillPerSec}. Each key owns a bucket that holds up tocapacitytokens and is full the first time the key is seen. Tokens flow back continuously atrefillPerSecper second, never abovecapacity, so afterdquiet seconds the bucket holdsmin(capacity, tokens + d × refillPerSec), fractions included. A request needs at least one whole token (tokens >= 1) and takes exactly one when it is allowed.
Notes
- Time is passed in, never read from a clock.
nowSecnever decreases for the same limiter and may be fractional such as0.25. - Configs are always valid positive numbers, and the constructor is always given a known strategy name.
- The fixed window is the cheap one but it has a flaw the others do not: a burst at the end of one window followed by a burst at the start of the next lets nearly
2 × limitrequests through in a short time. The tests check that difference. - Return values must be exactly
trueorfalse.
Examples
Example 1
Input: ["RateLimiter","allow","allow","allow","allow","allow","allow","allow"]
[["fixed-window",{"limit":2,"windowSec":10}],["a",0],["a",1],["a",2],["a",9],["a",10],["a",11],["a",12]]
Output: [null,true,true,false,false,true,true,false]Windows are [0,10), [10,20), and so on. The first two requests fill window 0, so the next two (at 2 and 9) are rejected. At t=10 a new window starts with a count of zero, so two more get through.
Example 2
Input: ["RateLimiter","allow","allow","allow","allow","allow","allow"]
[["sliding-window-log",{"limit":2,"windowSec":10}],["a",0],["a",1],["a",2],["a",10],["a",10.5],["a",11]]
Output: [null,true,true,false,true,false,true]The window is (t-10, t]. At t=2 the log holds 0 and 1, so the request is rejected (and not logged). At t=10 the request from t=0 is exactly 10 seconds old and no longer counts, so one slot is free. At 10.5 the log holds 1 and 10 and is full; at 11 the entry from t=1 has expired.
Example 3
Input: ["RateLimiter","allow","allow","allow","allow","allow","allow"]
[["token-bucket",{"capacity":3,"refillPerSec":1}],["a",0],["a",0],["a",0],["a",0],["a",1],["a",1]]
Output: [null,true,true,true,false,true,false]The bucket starts full, so the burst of three at t=0 passes and the fourth is rejected. One second later exactly one token has flowed back: it lets one request through and the next one is rejected.
Each test creates the class with the first operation, then calls the methods in order. Output lists what each call returns; null marks the constructor and methods that return nothing, and their return values aren't checked.
class RateLimiter {
/** `strategy` is "fixed-window", "sliding-window-log" or "token-bucket"; `config` holds that strategy's numbers. */
constructor(strategy, config) {
}
/** Switch algorithm at runtime and start every key fresh. @return {boolean} false (and change nothing) for an unknown strategy name */
useStrategy(strategy, config) {
}
/** Decide one request for `key` at time `nowSec`; count it only if allowed. @return {boolean} */
allow(key, nowSec) {
}
}
Ctrl/⌘ + ' runs the examples, Ctrl/⌘ + Enter submits. Use console.log freely: output shows under Console. In the editor, press Esc then Tab to move focus out.