package jitter import ( "context" "time" "go.unistack.org/micro/v3/util/rand" ) // Ticker is similar to time.Ticker but ticks at random intervals between // the min and max duration values (stored internally as int64 nanosecond // counts). type Ticker struct { ctx context.Context done chan chan struct{} C chan time.Time min int64 max int64 exit bool rng rand.Rand } // NewTickerContext returns a pointer to an initialized instance of the Ticker. // It works like NewTicker except that it has ability to close via context. // Also it works fine with context.WithTimeout to handle max time to run ticker. func NewTickerContext(ctx context.Context, min, max time.Duration) *Ticker { ticker := &Ticker{ C: make(chan time.Time), done: make(chan chan struct{}), min: min.Nanoseconds(), max: max.Nanoseconds(), ctx: ctx, } go ticker.run() return ticker } // NewTicker returns a pointer to an initialized instance of the Ticker. // Min and max are durations of the shortest and longest allowed // ticks. Ticker will run in a goroutine until explicitly stopped. func NewTicker(min, max time.Duration) *Ticker { ticker := &Ticker{ C: make(chan time.Time), done: make(chan chan struct{}), min: min.Nanoseconds(), max: max.Nanoseconds(), ctx: context.Background(), } go ticker.run() return ticker } // Stop terminates the ticker goroutine and closes the C channel. func (ticker *Ticker) Stop() { if ticker.exit { return } c := make(chan struct{}) ticker.done <- c <-c // close(ticker.C) ticker.exit = true } func (ticker *Ticker) run() { defer close(ticker.C) t := time.NewTimer(ticker.nextInterval()) for { // either a stop signal or a timeout select { case <-ticker.ctx.Done(): t.Stop() case c := <-ticker.done: t.Stop() close(c) return case <-t.C: select { case ticker.C <- time.Now(): t.Stop() t = time.NewTimer(ticker.nextInterval()) default: // there could be noone receiving... } } } } func (ticker *Ticker) nextInterval() time.Duration { return time.Duration(ticker.rng.Int63n(ticker.max-ticker.min)+ticker.min) * time.Nanosecond }