1

I want to utilize context in golang to be used for cancellation when timeout reached.

The code:

package main

import "fmt"
import "time"
import "context"

func F(ctx context.Context) error {
  ctx, cancel := context.WithTimeout(ctx,3*time.Second)
  defer cancel()
  for i:=0;i<10;i++ {
    time.Sleep(1 * time.Second)
    fmt.Println("No: ",i)
  }
  select {
    case <-ctx.Done():
      fmt.Println("TIME OUT")
      cancel()
      return ctx.Err()
    default:
      fmt.Println("ALL DONE")
      return nil
  }
}

func main() {
  ctx := context.Background()
  err := F(ctx)
  if err != nil {
    fmt.Println(err)
  }else {
    fmt.Println("Success")
  }
}

Expectation: code above should stop running the loop at counter 2, because the timeout is 3 second and looping run 1 second each. So I expect someting like this:

No:  0
No:  1
No:  2
TIME OUT
context deadline exceeded

Actual: What actually happen is the loop keep running until finish even though the context meet timeout and the select listener catch that on <-ctx.Done(). This code prints this:

No:  0
No:  1
No:  2
No:  3
No:  4
No:  5
No:  6
No:  7
No:  8
No:  9
TIME OUT
context deadline exceeded

How to stop the function execution after timeout meet?

Jake Muller
  • 925
  • 4
  • 18
  • 25

1 Answers1

8

context.Context can only relay the message that timeout or cancellation happened. It does not have the power to actually stop any goroutines (for details, see cancel a blocking operation in Go). The goroutine itself is responsible for checking the timeout and cancellation, and abort early.

You have a loop which unconditionally iterates 10 times and prints something. And you only check the timeout after the loop.

You have to move the context checking into the loop:

func F(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("TIME OUT")
            return ctx.Err()
        default:
            time.Sleep(1 * time.Second)
            fmt.Println("No: ", i)
        }
    }
    fmt.Println("ALL DONE")
    return nil
}

With this change, output will be (try it on the Go Playground):

No:  0
No:  1
No:  2
No:  3
TIME OUT
context deadline exceeded

Note: whether you see "No: 3" printed may or may not happen, as any iteration takes 1 second, and timeout is 3 seconds = 3 * iteration delay, so whether timeout happens first or the 4th iteration begins first is "racy". If you decrease the timeout to like 2900 ms, "No: 3" will not be printed.

icza
  • 389,944
  • 63
  • 907
  • 827
  • I know the `Sleep` was probably illustrative in the OP, but if one is in a polling loop that requires a wait, it's better to add a `<-time.After(...)` to the select block. That way the cancellation will be instant, instead of (worse case) 1 second late. – colm.anseo Apr 05 '20 at 12:11
  • Looks like it depends on the compiler/version or the machine where it compiled or running. I tried running it in https://repl.it using `ctx.Done()` it prints until `No: 2` consistently. When I tried it on golang playground using `ctx.Done()` it always print `No: 3`, but if I used `time.After` it consistently prints until `No: 2`. – Jake Muller Apr 05 '20 at 12:33
  • Is the `cancel()` call inside the `case <-ctx.Done():` block necessary? Won't the defer after the context initialization call it anyways? – Sif Jul 03 '21 at 23:07
  • 1
    @Sif Yes, you're right, it's not needed. I left it there because I copied the `select` code form the question, but removed it to avoid confusion. – icza Jul 04 '21 at 07:06