// 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 pts

Implement a Limiter with:

func New(rate, burst int, now func() time.Time) *Limiter
func (l *Limiter) Allow() bool

Required semantics:

  • The bucket starts full (burst tokens). Each Allow that returns true spends exactly one token; with no whole token available it returns false and spends nothing.
  • Tokens accrue continuously at rate per second according to the injected clock: two 500ms waits at rate 1 add up to one whole token. A denied Allow must not discard fractional progress.
  • Token count never exceeds burst, no matter how long the limiter sits idle.
  • Allow never 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 now may be consulted โ€” no time.Now, no sleeping. The tests drive a fake clock and never wait on real time.

Log in to submit a solution and earn points.