Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Context confusion regarding cancellation

Tags:

go

package main

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

func myfunc(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Ctx is kicking in with error:%+v\n", ctx.Err())
            return
        default:
            time.Sleep(15 * time.Second)
            fmt.Printf("I was not canceled\n")
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(
        context.Background(),
        time.Duration(3*time.Second))
    defer cancel()

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        myfunc(ctx)
    }()

    wg.Wait()
    fmt.Printf("In main, ctx err is %+v\n", ctx.Err())
}

I have the above snippet that does print the output like this

I was not canceled
In main, ctx err is context deadline exceeded

Process finished with exit code 0

I understand that context times-out after 3 seconds and hence it does give me the expected error when I call ctx.Err() in the end. I also get the fact that in my myfunc once select matches on the case for default, it won't match on the done. What I do not understand is that how do I make my go func myfunc get aborted in 3 seconds using the context logic. Basically, it won't terminate in 3 seconds so I am trying to understand how can golang's ctx help me with this?

like image 587
curiousengineer Avatar asked Oct 14 '18 03:10

curiousengineer


1 Answers

If you want to use the timeout and cancellation feature from the context, then in your case the ctx.Done() need to be handled synchronously.

Explanation from https://golang.org/pkg/context/#Context

Done returns a channel that's closed when work is done on behalf of this context should be canceled. Done may return nil if this context can never be canceled. Successive calls to Done return the same value.

So basically the <-ctx.Done() will be called on two conditions:

  1. when context timeout exceeds
  2. when context canceled by force

And when that happens, the ctx.Err() will never be nil.

We can perform some checking on the error object to see whether the context is canceled by force or exceeding the timeout.

Context package provides two error objects, context.DeadlineExceeded and context.Timeout, this two will help us to identify why <-ctx.Done() is called.


Example #1 scenario: context cancelled by force (via cancel())

In the test, we'll try to make the context to be canceled before the timeout exceeds, so the <-ctx.Done() will be executed.

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))

go func(ctx context.Context) {
    // simulate a process that takes 2 second to complete
    time.Sleep(2 * time.Second)

    // cancel context by force, assuming the whole process is complete
    cancel()
}(ctx)

select {
case <-ctx.Done():
    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Println("context timeout exceeded")
    case context.Canceled:
        fmt.Println("context cancelled by force. whole process is complete")
    }
}

Output:

$ go run test.go 
context cancelled by force

Example #2 scenario: context timeout exceeded

In this scenario, we make the process takes longer than context timeout, so ideally the <-ctx.Done() will also be executed.

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))

go func(ctx context.Context) {
    // simulate a process that takes 4 second to complete
    time.Sleep(4 * time.Second)

    // cancel context by force, assuming the whole process is complete
    cancel()
}(ctx)

select {
case <-ctx.Done():
    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Println("context timeout exceeded")
    case context.Canceled:
        fmt.Println("context cancelled by force. whole process is complete")
    }
}

Output:

$ go run test.go 
context timeout exceeded

Example #3 scenario: context canceled by force due to error occurred

There might be a situation where we need to stop the goroutine in the middle of the process because error occurred. And sometimes, we might need to retrieve that error object on the main routine.

To achieve that, we need an additional channel to transport the error object from goroutine into main routine.

In the below example, I've prepared a channel called chErr. Whenever error happens in the middle of (goroutine) process, then we will send that error object through the channel and then stop process immediately from.

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))

chErr := make(chan error)

go func(ctx context.Context) {
    // ... some process ...

    if err != nil {
        // cancel context by force, an error occurred
        chErr <- err
        return
    }

    // ... some other process ...

    // cancel context by force, assuming the whole process is complete
    cancel()
}(ctx)

select {
case <-ctx.Done():
    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Println("context timeout exceeded")
    case context.Canceled:
        fmt.Println("context cancelled by force. whole process is complete")
    }
case err := <-chErr:
    fmt.Println("process fail causing by some error:", err.Error())
}

Additional info #1: calling cancel() right after context initialized

As per context documentation regarding the cancel() function:

Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.

It's good to always call cancel() function right after the context declaration. doesn't matter whether it's also called within the goroutine. This is due to ensure context is always cancelled when the whole process within the block are fully complete.

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))
defer cancel()

// ...

Additional info #2: defer cancel() call within goroutine

You can use defer on the cancel() statement within the goroutine (if you want).

// ...

go func(ctx context.Context) {
    defer cancel()

    // ...
}(ctx)

// ...
like image 54
novalagung Avatar answered Oct 07 '22 07:10

novalagung