A ticker with jitter, cancelled by context
time.NewTicker is fine for a single process, but when you have 30 replicas of a service all doing a 1-minute cache refresh, they will all fire at the same millisecond and DDoS the upstream. A little jitter spreads them out.
I also dislike remembering to defer ticker.Stop() at every call site. Wrapping it in a function that takes a context lets me skip that.
// TickWithJitter sends on the returned channel at roughly interval,
// with +/- jitter added to each tick, until ctx is done.
func TickWithJitter(ctx context.Context, interval, jitter time.Duration) <-chan time.Time {
ch := make(chan time.Time, 1)
go func() {
defer close(ch)
for {
d := interval + time.Duration(rand.Int63n(int64(2*jitter))) - jitter
if d < 0 {
d = interval
}
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
return
case now := <-t.C:
select {
case ch <- now:
case <-ctx.Done():
return
}
}
}
}()
return ch
}
Usage is the same as a normal ticker. No .Stop() needed, just cancel the context. See also /posts/the-goroutine-leak-i-didnt-notice-for-six-weeks/.