naive bursty rate limiters in go
About
I've read a few articles that describe a simple implementation of a bursty rate limiter.
package main
import (
"fmt"
"time"
)
const (
limitPerSecond = 3
)
func hit(guard chan time.Time) {
<-guard
now := time.Now().Format(time.StampMicro)
fmt.Println(now)
}
func main() {
ticker := time.NewTicker(time.Second / time.Duration(limitPerSecond))
guard := make(chan time.Time, limitPerSecond)
go func() {
for {
t := <-ticker.C
guard <- t
}
}()
for {
hit(guard)
}
}Although we set a limit of three, it can burst to up to six per second.
Explanation
When the channel is full, its length is three and the goroutine is held at the
guard <- t line.
This means that as the channel will be back back to full, giving an effective buffered burst capacity of of four.
Additionally, the ticker's channel is overdue so t := <- ticker.C is
immediately unblocked on the next iteration, giving an effective buffered burst
capacity of five.
Above all this, we must also account for the refill rate, which gives an additional burst capacity of three within a second.
All this said, we will experience an initial burst capacity of 8 even though our naive rate limiter is set to 3.
Experiment
We can confirm the description above by adding a bit of sleep so that the buffer capacity fills up.
func main () {
// ...
time.Sleep(3 * time.Second)
for {
hit(guard)
}
} --- initial buffer capacity (transient)
May 6 23:58:21.920075
May 6 23:58:21.920364
May 6 23:58:21.920367
May 6 23:58:21.920368
May 6 23:58:21.920369
--- continuous refill within the first second (transient)
May 6 23:58:22.253359
May 6 23:58:22.585897
May 6 23:58:22.920059
--- continuous refill (steady state)
May 6 23:58:23.252562
May 6 23:58:23.586682
May 6 23:58:23.920012
May 6 23:58:24.252565
May 6 23:58:24.586673
May 6 23:58:24.919287
May 6 23:58:25.253333
May 6 23:58:25.586672
May 6 23:58:25.919988
May 6 23:58:26.252630A better solution
The golang.org/x/time/rate package implements a token bucket that can deploy
the burst limiter we intended.
package main
import (
"context"
"fmt"
"golang.org/x/time/rate"
"time"
)
const (
r = rate.Limit(10)
b = 3
)
func hit() {
now := time.Now().Format(time.StampMicro)
fmt.Println(now)
}
func main() {
ctx := context.Background()
limiter := rate.NewLimiter(r, b)
time.Sleep(5 * time.Second)
for {
if err := limiter.Wait(ctx); err != nil {
continue
}
hit()
}
}May 7 22:17:33.161845
May 7 22:17:33.162648
May 7 22:17:33.162662
May 7 22:17:33.262158
May 7 22:17:33.362813
May 7 22:17:33.462167
May 7 22:17:33.562831
May 7 22:17:33.662231
May 7 22:17:33.762879
May 7 22:17:33.862244
May 7 22:17:33.962163
May 7 22:17:34.062833