'How can I sleep with responsive context cancelation?

In Go, I want to time.Sleep for some time (e.g. waiting between retries), but want to return quickly if the context gets canceled (not just from a deadline, but also manually).

What is the right or best way to do that? Thanks!

go


Solution 1:[1]

You can use select to acheive this:

package main

import (
    "fmt"
    "time"
    "context"
)

func main() {
    fmt.Println("Hello, playground")
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go func(){
        t := time.Now()
        select{
        case <-ctx.Done(): //context cancelled
        case <-time.After(2 * time.Second): //timeout
        }
        fmt.Printf("here after: %v\n", time.Since(t))
    }()

    cancel() //cancel manually, comment out to see timeout kick in
    time.Sleep(3 * time.Second)
    fmt.Println("done")

}

Here is the Go-playground link

Solution 2:[2]

Here is a sleepContext function that you can use instead of time.Sleep:

func sleepContext(ctx context.Context, delay time.Duration) {
    select {
    case <-ctx.Done():
    case <-time.After(delay):
    }
}

And some sample usage (full runnable code on the Go Playground):

func main() {
    ctx := context.Background()

    fmt.Println(time.Now())
    sleepContext(ctx, 1*time.Second)
    fmt.Println(time.Now())

    ctxTimeout, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
    sleepContext(ctxTimeout, 1*time.Second)
    cancel()
    fmt.Println(time.Now())
}

Solution 3:[3]

You can use select as others have mentioned; however, the other answers have a bug since timer.After() will leak memory if not cleaned up.

func SleepWithContext(ctx context.Context, d time.Duration) {
    timer := time.NewTimer(d)
    select {
    case <-ctx.Done():
        if !timer.Stop() {
            <-timer.C
        }
    case <-timer.C:
    }
}

Solution 4:[4]

I managed to do something similar by combining a CancelContext with a TimeoutContext...

Here is the sample code:

cancelCtx, cancel := context.WithCancel(context.Background())
defer cancel()
// The program "sleeps" for 5 seconds.
timeoutCtx, _ := context.WithTimeout(cancelCtx, 5*time.Second)
select {
case <-timeoutCtx.Done():
    if cancelCtx.Err() != nil {
        log.Printf("Context cancelled")
    }
}

In this repo you can find the complete usage of the above code. Sorry for my short answer, I didn't turned on the computer yet, and is not that easy to answer from the phone...

Solution 5:[5]

The time.After() function has this problem:

The underlying Timer is not recovered by the garbage collector until the timer fires. If efficiency is a concern, use NewTimer instead and call Timer.Stop if the timer is no longer needed.

It is better to use a Timer object and call Stop():

// Delay returns nil after the specified duration or error if interrupted.
func Delay(ctx context.Context, d time.Duration) error {
    t := time.NewTimer(d)
    select {
    case <-ctx.Done():
        t.Stop()
        return fmt.Errorf("Interrupted")
    case <-t.C:
    }
    return nil
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Hunter Ford
Solution 4 Sebastián Rodriguez
Solution 5 lgekman