Xavi
Xavi

Reputation: 20439

How to get time.Tick to tick immediately

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

Answers (9)

Bora M. Alper
Bora M. Alper

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 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()?

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 use NewTicker instead and call Ticker.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. The Stop method is no longer necessary to help the garbage collector. There is no longer any reason to prefer NewTicker when Tick will do.

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

Upvotes: 80

Kaloyan Tanev
Kaloyan Tanev

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

abdusco
abdusco

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

Rafał Krypa
Rafał Krypa

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

rarguelloF
rarguelloF

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

manpatha
manpatha

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

Mr_Pink
Mr_Pink

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

w1100n
w1100n

Reputation: 1668

ticker := time.NewTicker(period)
for ; true; <-ticker.C {
    ...
}

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

Upvotes: 31

Caleb
Caleb

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

Related Questions