Reputation: 20439
I have a loop that iterates until a job is up and running:
ticker := time.NewTicker(time.Second * 2)
defer ticker.Stop()
started := time.Now()
for now := range ticker.C {
job, err := client.Job(jobID)
switch err.(type) {
case DoesNotExistError:
continue
case InternalError:
return err
}
if job.State == "running" {
break
}
if now.Sub(started) > time.Minute*2 {
return fmt.Errorf("timed out waiting for job")
}
}
Works great in production. The only problem is that it makes my tests slow. They all wait at least 2 seconds before completing. Is there anyway to get time.Tick
to tick immediately?
Upvotes: 51
Views: 32896
Reputation: 3844
Unfortunately, it seems that Go developers will not add such functionality in any foreseeable future, so we have to cope...
There are two common ways to use tickers:
for
loopGiven something like this:
ticker := time.NewTicker(period)
defer ticker.Stop()
for <- ticker.C {
...
}
Use:
ticker := time.NewTicker(period)
defer ticker.Stop()
for ; true; <- ticker.C {
...
}
for
-select
loopGiven something like this:
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
ticker := time.NewTicker(period)
defer ticker.Stop()
loop:
for {
select {
case <- ticker.C:
f()
case <- interrupt:
break loop
}
}
Use:
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
ticker := time.NewTicker(period)
defer ticker.Stop()
loop:
for {
f()
select {
case <- ticker.C:
continue
case <- interrupt:
break loop
}
}
time.Tick()
?If you're using Go 1.23+, you can safely use time.Tick()
instead.
Before Go 1.23:
While Tick is useful for clients that have no need to shut down the Ticker, be aware that without a way to shut it down the underlying Ticker cannot be recovered by the garbage collector; it "leaks".
After Go 1.23:
Before Go 1.23, this documentation warned that the underlying
Ticker
would never be recovered by the garbage collector, and that if efficiency was a concern, code should useNewTicker
instead and callTicker.Stop
when the ticker is no longer needed. As of Go 1.23, the garbage collector can recover unreferenced tickers, even if they haven't been stopped. TheStop
method is no longer necessary to help the garbage collector. There is no longer any reason to preferNewTicker
whenTick
will do.
https://golang.org/pkg/time/#Tick
Upvotes: 80
Reputation: 31
I will expand on @Bora M. Alper's already good suggestions. If you are using for
-select
loop, it's better to refactor this:
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
ticker := time.NewTicker(period)
defer ticker.Stop()
loop:
for {
select {
case <- ticker.C:
f()
case <- interrupt:
break loop
}
}
to this:
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
ticker := time.NewTicker(period)
defer ticker.Stop()
loop:
for ; true; <- ticker.C {
select {
case <- interrupt:
break loop
default:
f()
}
}
This way you do not skip the initial check for interruption and you also gain the first instant tick advantage.
Upvotes: 2
Reputation: 11091
You can also drain the channel at the end of the loop:
t := time.NewTicker(period)
defer t.Stop()
for {
...
<-t.C
}
Upvotes: 1
Reputation: 353
How about using Timer instead of Ticker? Timer can be started with zero duration and then reset to the desired duration value:
timer := time.NewTimer(0)
defer timer.Stop()
for {
select {
case <-timer.C:
timer.Reset(interval)
job()
case <-ctx.Done():
break
}
}
Upvotes: 4
Reputation: 161
I think this might be an interesting alternative for the for-select
loop, specially if the contents of the case
are not a simple function:
Having:
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
ticker := time.NewTicker(period)
defer ticker.Stop()
loop:
for {
select {
case <- ticker.C:
f()
case <- interrupt:
break loop
}
}
Use:
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
ticker := time.NewTicker(period)
defer ticker.Stop()
firstTick := false
// create a wrapper of the ticker that ticks the first time immediately
tickerChan := func() <-chan time.Time {
if !firstTick {
firstTick = true
c := make(chan time.Time, 1)
c <- time.Now()
return c
}
return ticker.C
}
loop:
for {
select {
case <- tickerChan():
f()
case <- interrupt:
break loop
}
}
Upvotes: 1
Reputation: 529
I cooked up something like this
func main() {
t := time.Now()
callme := func() {
// do somethign more
fmt.Println("callme", time.Since(t))
}
ticker := time.NewTicker(10 * time.Second)
first := make(chan bool, 1)
first <- true
for {
select {
case <-ticker.C:
callme()
case <-first:
callme()
}
t = time.Now()
}
close(first)
}
Upvotes: 3
Reputation: 109347
If you want to check the job right away, don't use the ticker as the condition in the for loop. For example:
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
started := time.Now()
for {
job, err := client.Job(jobID)
if err == InternalError {
return err
}
if job.State == "running" {
break
}
now := <-ticker.C
if now.Sub(started) > 2*time.Minute {
return fmt.Errorf("timed out waiting for job")
}
}
If you do still need to check for DoesNotExistError
, you want to make sure you do it after the ticker so you don't have a busy-wait.
Upvotes: 6
Reputation: 1668
ticker := time.NewTicker(period)
for ; true; <-ticker.C {
...
}
https://github.com/golang/go/issues/17601
Upvotes: 31
Reputation: 9458
The actual implementation of Ticker
internally is pretty complicated. But you can wrap it with a goroutine:
func NewTicker(delay, repeat time.Duration) *time.Ticker {
ticker := time.NewTicker(repeat)
oc := ticker.C
nc := make(chan time.Time, 1)
go func() {
nc <- time.Now()
for tm := range oc {
nc <- tm
}
}()
ticker.C = nc
return ticker
}
Upvotes: 6