codewarrior
codewarrior

Reputation: 981

Can I specify a timeout value when cancelling a context?

ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()

go publishMetrics(ctx)

In my understanding, when the cancelFunc() is invoked, context is cancelled and the go routine receives the signal, but is it allowed for the routine to do some cleanup work? For example, there might be unpublished metrics in memory, the background routine might try to upload them before exiting, I want to give the background go routine sometime to cleanup when context is cancelled, for example, wait for 100ms and if the go routine can upload within 100ms, that is good, if not, just exit, is there a way to achieve the goal? Thanks.

Upvotes: 1

Views: 1136

Answers (3)

Brits
Brits

Reputation: 18290

when the cancelFunc() is invoked, context is cancelled and the go routine receives the signal, but is it allowed for the routine to do some cleanup work?

The simple answer is yes. Calling cancelFunc() provides a cancellation signal to the goroutine via the Context. The goroutine can:

  • Totally ignore the signal and continue doing whatever it was doing.
  • Process the signal and exit
  • Process the signal, do some cleanup, and then exit.
  • Something else...

This means that if your goroutine is performing a standalone task (e.g. publishing metrics!) you can just cancel the context (call cancelFunc()) and continue doing whatever you need to (if the goroutine is well behaved it will terminate in a timely manner).

However often you do need to know when the the goroutine has completed it's processing (cleanup etc). One common situation is handling program termination. When main completes the program exits (the runtime will not wait for goroutines to complete), so if you want to allow a goroutine to perform cleanup work, then you need to wait for this.

It's important to note that this is two separate operations:

  • Signal (via Context or another means) that the goroutine should terminate.
  • Wait for the goroutine to actually terminate (perhaps with a timeout)

An example of this follows (playground); the gracePeriod configures how long the application will wait for the goroutine to complete before exiting:

func main() {
    gracePeriod := 800 * time.Millisecond // decrease to, say 200, to observe exit without waiting for completion

    // ctx, cancel = signal.NotifyContext(context.Background(), os.Interrupt) // real app would wait for an interupt signal from the operating system
    ctx, cancel := context.WithCancel(context.Background()) // we simulate this by valling cancel() below
    defer cancel()                                          // not really needed here but good practice (easy to forget to cancel a context in some paths)

    done := make(chan struct{}) // This will be used to signal when the goroutine is complete
    go func() {
        defer close(done)
        publishMetrics(ctx)
    }()

    // At some point we receive a cancellation signal - lets simulate that
    time.Sleep(time.Millisecond)
    cancel()

    select {
    case <-done:
        fmt.Println("Terminating regularly")
    case <-time.After(gracePeriod): // after gracePeriod we exit regardless of the goroutine state
        fmt.Println("Timed out waiting; exiting without waiting any longer")
    }
}

func publishMetrics(ctx context.Context) {
    // Lets assume that this function runs until it receives a signal (at which point it tidies up and exits)
    for {
        // Do work
        time.Sleep(10 * time.Millisecond)

        // Using ctx.Err to detect when the context has been cancelled; often you would use `ctx.Done()` in a `select` whilst waiting for something else
        if ctx.Err() != nil {
            break
        }
    }

    // Simulate cleanup
    fmt.Println("goroutine - cancel signalled")
    time.Sleep(500 * time.Millisecond)
    fmt.Println("goroutine - cleanup done")
}

Note: The other answers discuss context.WithTimeout; this is generally used to signal that an operation should be cancelled if it does not complete within the specified time. In other words it sends a delayed signal; this is different from waiting for the goroutine to process the signal and perform whatever cleanup is needed.

Upvotes: 0

eik
eik

Reputation: 4610

Goroutines do not receive signals. The only thing that cancelling a context does is - cancelling this context. See an example of a cancellable goroutine - it has to either do

    select {
    case <-ctx.Done():
        // handle cacellation
    // ... do work

on the passed context or check ctx.Err(), see also this blog post about cancellation. Even in Java threads have to check Thread.interrupted() and Thread.stop is deprecated.

If you want to delay the cancellation signal for derived contexts you have to break the cancellation chain with WithoutCancel and build your own. I would advise against it, because the semantics would be unclear - something timed out, but you want to add 100mS to this timeout - and the next routine might add 100mS too, and so on...

When you exit a program all goroutines are terminated immediately, like daemon threads in Java. So you might want to signal “upload unpublished metrics” immediately and be willing to wait 100mS for completion before exiting (Go Playground):

package main

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

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

    exitCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()

    uploadDone := make(chan struct{})
    go func() {
        defer close(uploadDone)
        publishMetrics(exitCtx)
    }()

    select {
    case <-uploadDone:
        fmt.Println("Terminating regularly")

    case <-exitCtx.Done():
        fmt.Println("Timed out")
    }
}

func publishMetrics(ctx context.Context) {
    timer := time.NewTimer(500 * time.Millisecond) // Simulate upload
    select {
    case <-timer.C:
        fmt.Println("Upload successful")

    case <-ctx.Done():
        timer.Stop()
        fmt.Println("Upload aborted")
    }
}

Upvotes: 1

koustav_ch
koustav_ch

Reputation: 9

For doing some cleanup work, you can use

context.WithTimeout(context.Background(), N * time.Second)

where N = time (in seconds) after which the created context will be timed out instead of using context.WithCancel(context.Background()).

For example, if you want to wait for 100 ms (= 0.1 s), then you may write

ctx, cancelFunc := context.WithTimeout(context.Background(), 0.1 * time.Second)

The context created in the above method will be automatically cancelled after the time-out period (in seconds).

Another way is modifying your code in a way like this, where you create a new sub-routine:

    ctx, cancelFunc := context.WithCancel(context.Background())

    go func() {

        time.Sleep(0.1 * time.Second)

        cancelFunc()

    }

Upvotes: 0

Related Questions