42

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?

Xavi
  • 20,111
  • 14
  • 72
  • 63
  • Whoops, thanks for the heads up. – Xavi Sep 21 '15 at 23:44
  • Set ticker and other durations to smaller values in tests. – Charlie Tumahai Sep 22 '15 at 00:12
  • These "jobs" are actually running on a remote service. Though it is possible to have this program run a server locally and modify the remote service to accept subscriptions, that solution is much too heavy handed imo. – Xavi Sep 22 '15 at 07:27
  • @JimB sorry for my ignorance, but what's the race condition you're referring to around `job.State`? Is the assumption that `job` is a shared object? – Xavi Sep 22 '15 at 07:29
  • Sorry, I was on mobile, and misread the code thinking that `job` came from outside the for loop. – JimB Sep 22 '15 at 10:55
  • 2
    @Xavi : wrap your code in a function, use variables for the ticker delay and the timeout value, set those variables to smaller values when testing. – LeGEC Sep 22 '15 at 15:19

8 Answers8

67

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 loop

Given 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 loop

Given 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
    }
}

Why not just use time.Tick()?

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".

https://golang.org/pkg/time/#Tick

Bora M. Alper
  • 3,538
  • 1
  • 24
  • 35
  • 2
    Does `<- time.Tick(period)` create a Ticker on each iteration? According to document, the Ticker created by Tick cannot be garbage-collected. This terrifies me. – youfu Oct 16 '20 at 10:02
  • 1
    @youfu You are absolutely right! I have just edited my answer. – Bora M. Alper Oct 19 '20 at 16:12
25
ticker := time.NewTicker(period)
for ; true; <-ticker.C {
    ...
}

https://github.com/golang/go/issues/17601

w1100n
  • 1,460
  • 2
  • 17
  • 24
  • 7
    Just to add to this. If you don't want to lose the value you get from the chan you can do `for t := time.Now(); true; t = <-ticker.C {` – Devon Peticolas May 29 '19 at 23:59
  • This solutions is also more elegant in cases you need to call continue, inside the for block – natenho Aug 21 '23 at 05:27
6

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
}
Caleb
  • 9,272
  • 38
  • 30
  • Couldn't you just send to the ticker channel immediately ? – LenW Sep 22 '15 at 05:25
  • 2
    It's a receive only channel: `type Ticker struct { C <-chan Time // The channel on which the ticks are delivered. // contains filtered or unexported fields }` – Caleb Sep 22 '15 at 11:23
  • Why `delay` arg used here? – coanor Oct 09 '21 at 02:46
  • "delay" is not used. Also i want point out that this timer is not guaranteed to fire instantly, as the `go func(){ }` part is sheduled, but not executed for sure. But asides from that nitpicking this should be the accepted answer imho – simon Dec 08 '21 at 08:00
6

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.

Saba
  • 3,418
  • 2
  • 21
  • 34
JimB
  • 104,193
  • 13
  • 262
  • 255
  • This is the right solution imo -- also present in the stdlib, see https://github.com/golang/go/blob/master/src/net/http/server.go#L2648 – jwilner Nov 23 '18 at 18:46
2

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)
}
manpatha
  • 489
  • 4
  • 11
1

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
    }
}
rarguelloF
  • 161
  • 1
  • 5
1

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
    }
}
Rafał Krypa
  • 243
  • 2
  • 6
1

You can also drain the channel at the end of the loop:

t := time.NewTicker(period)
defer t.Stop()

for {
    ...
    
    <-t.C
}
abdusco
  • 9,700
  • 2
  • 27
  • 44