Stopping running function using context timeout in Golang

Issue

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?

Solution

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.

Answered By – icza

Answer Checked By – David Marino (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.