// lesson: clocks-and-token-buckets
Clocks You Can Control
time.After in a hot loop allocates a timer per iteration; time.Ticker
must be stopped or it lives forever. You know this. The deeper drill: code
that reads the wall clock directly is untestable, and rate limiters are
where people learn that the hard way โ the naive test for "2 requests per
second" is a test that takes seconds and flakes in CI.
The fix is dependency injection at its smallest: take now func() time.Time
as a parameter and never call time.Now yourself. Tests hand you a fake
clock they advance by hand; production hands you time.Now.
The token bucket itself is two lines of math. A bucket holds up to burst
tokens and starts full. Tokens drip in continuously at rate per second:
elapsed := now.Sub(last)
tokens = min(tokens+elapsed.Seconds()*float64(rate), float64(burst))
Each admitted request spends one token. The subtle bugs: forgetting to cap
at burst, doing integer math so half-tokens vanish, discarding fractional
progress on a denied call, and racing unsynchronized state under concurrent
callers.
โบ Deterministic Token Bucket
25 ptsImplement a Limiter with:
func New(rate, burst int, now func() time.Time) *Limiter
func (l *Limiter) Allow() bool
Required semantics:
- The bucket starts full (
bursttokens). EachAllowthat returns true spends exactly one token; with no whole token available it returns false and spends nothing. - Tokens accrue continuously at
rateper second according to the injected clock: two 500ms waits at rate 1 add up to one whole token. A deniedAllowmust not discard fractional progress. - Token count never exceeds
burst, no matter how long the limiter sits idle. Allownever blocks and must be safe to call from many goroutines. With a frozen clock and burst 100, exactly 100 of 1000 concurrent calls may succeed.- Only the injected
nowmay be consulted โ notime.Now, no sleeping. The tests drive a fake clock and never wait on real time.
Log in to submit a solution and earn points.