At the core of Slide, there is a ticker which runs at a user specified interval. The ticker is used to perform operations on a loop at the given interval. For example, Slide’s ticker runs every x interval and finds a new background from the database and applies it as the wallpaper. Another ticker is used to check for new images and download them at a user defined interval.
The normal use case for a time.Ticker is as follows:
package main
import "time"
func main() {
interval := time.Second * 5
ticker := time.NewTicker(interval)
for range ticker.C {
// do something every 5 seconds
}
}
In Slide, we allow users to modify the interval at which backgrounds are changed. If the interval changes, the ticker needs to be updated (or replaced?) to reflect these changes, without requiring a full application restart. We need a way to change the interval with as minimal an impact to the application as possible.
One attempt to solve the problem is to replace the running ticker with a new ticker at a new interval, however this does not work.
interval := time.Second * 5
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
fmt.Printf("Tick!: %s\n", time.Now().String())
}
}()
// wait for 2 ticks, then change interval
time.Sleep(interval * 2)
ticker = time.NewTicker(time.Second)
// wait for another 10 seconds and end the ticker
time.Sleep(10 * time.Second)
ticker.Stop()
However, this doesn’t work. ticker.C
still refers to the channel of
the
original ticker, so it will continue running at the original interval.
Tick!: 2009-11-10 23:00:05 +0000 UTC m=+5.000000001
Tick!: 2009-11-10 23:00:10 +0000 UTC m=+10.000000001
Tick!: 2009-11-10 23:00:15 +0000 UTC m=+15.000000001
Attempts to dereference the ticker and replace its value with a new ticker (e.g.
*ticker
= NewTicker(newInterval)
) also fail, causing the old ticker to stop
running.
What if there was a way to extend the functionality of time.Ticker and made it possible to update this interval?
The following implementation allows just that. A ticker is created with an interval
just
as before, but a channel is used
to
send a time.Duration
to update the ticker.
In the loop()
method of the ticker, a select
handles
messages
from 3 different channels:
time.Ticker
must be stopped and replaced with a new instance which
has
the new interval (the value passed down the channel)
time.Ticker
and pushes it into the mutable ticker’s channel. This
passes the tick on to the familiar ticker.C
channel which is
accessible
by applications using the mutable ticker.
MutableTicker.Stop()
. This returns
from
the loop and ends the goroutine.
// NewMutableTicker provides a ticker with an interval which can be changed
func NewMutableTicker(t time.Duration) *MutableTicker {
ticker := time.NewTicker(t)
c := make(chan time.Time)
d := make(chan time.Duration)
ctx, cfn := context.WithCancel(context.Background())
tk := &MutableTicker{
ctx: ctx,
cfn: cfn,
ticker: ticker,
updateCh: d,
c: c,
C: c,
}
go tk.loop()
return tk
}
// MutableTicker is a time.Ticker which can change update intervals
type MutableTicker struct {
ctx context.Context
cfn context.CancelFunc
ticker *time.Ticker
updateCh chan time.Duration
// c is a read/write private channel
c chan time.Time
// C is a read only channel
C <-chan time.Time
}
func (m *MutableTicker) loop() {
for {
select {
// the interval of the ticker has been updated.
case d := <-m.updateCh:
// stop the old ticker and replace it with a new one
m.ticker.Stop()
m.ticker = time.NewTicker(d)
case t := <-m.ticker.C:
// updates from the underlying ticker are passed into the writable version of C
m.c <- t
case <-m.ctx.Done():
// use context.Done() to quit the loop when the CancelFunc is called.
return
}
}
}
// UpdateInterval modifies the interval of the Ticker.
func (m *MutableTicker) UpdateInterval(t time.Duration) {
m.updateCh <- t
}
// Stop the Ticker.
func (m *MutableTicker) Stop() {
m.cfn()
if m.ticker != nil {
m.ticker.Stop()
}
close(m.c)
close(m.updateCh)
}
Simple!
interval := time.Second * 5
ticker := NewMutableTicker(interval)
defer ticker.Stop()
go func() {
for range ticker.C {
fmt.Printf("Tick!: %s\n", time.Now().String())
}
}()
// wait for 2 ticks, then change interval
time.Sleep(interval * 2)
ticker.UpdateInterval(time.Second * 2)
time.Sleep(10 * time.Second)
And you get the following output:
Tick!: 2009-11-10 23:00:05 +0000 UTC m=+5.000000001
Tick!: 2009-11-10 23:00:10 +0000 UTC m=+10.000000001
Tick!: 2009-11-10 23:00:12 +0000 UTC m=+12.000000001
Tick!: 2009-11-10 23:00:14 +0000 UTC m=+14.000000001
Tick!: 2009-11-10 23:00:16 +0000 UTC m=+16.000000001
Tick!: 2009-11-10 23:00:18 +0000 UTC m=+18.000000001