0

Let's say I have a long-running function...

func longRunningThing(data string) {
    for i := 0; i < 100; i++ {
        fmt.Printf("%v %v\n", data, i)
        time.Sleep(1 * time.Second)
    }
}

... and I would like it to support context cancellation. Am I right to assume that it's the function's responsibility to periodically check if the passed in context has been cancelled?

For example, like this:

func longRunningThing(ctx context.Context, data string) {
    for i := 0; i < 100; i++ {
        select {
        case <-ctx.Done():
            return
        default:
            fmt.Printf("%v %v\n", data, i)
            time.Sleep(1 * time.Second)
        }
    }
}

Reason I'm asking is, in the O’Reilly book Learning Go, there's a section Handling Context Cancellation in Your Own Code (in chapter 12) which suggests the following pattern:

func longRunningThingManager(ctx context.Context, data string) (string, error) {
    type wrapper struct {
        result string
        error  error
    }
    ch := make(chan wrapper, 1)
    go func() {
        result, err := longRunningThing(ctx, data)
        ch <- wrapper{result, err}
    }()
    select {
    case result := <-ch:
        return result.result, result.error
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

The idea here appears to be that the longRunningThingManager wrapper function keeps an eye on the context and instantly returns in the event of a cancellation, not longRunningThing itself.

Here's where I'm confused: while that'll immediately unblock a blocked caller, I would assume that it doesn't actually stop the running goroutine. I.e. if longRunningThing doesn't periodically check for context cancellation as well as outlined above, it'll actually keep running until the end, potentially wasting resources, causing side-effects etc.

Is my assumption correct, or am I missing something here?

Max
  • 9,220
  • 10
  • 51
  • 83
  • 3
    Your assumption is correct; you're not missing anything. – jub0bs Feb 23 '23 at 21:45
  • Excellent, thanks! I'll see if I can let the author of the book know. – Max Feb 23 '23 at 21:48
  • 1
    Since `longRunningThing()` takes a `context.Context`, _"you did the best you could"_. Meaning you passed the context that carries cancellation or deadline signal, but there's nothing more you could do, there's no way to forcibly terminate the function. If the function wants to, it can monitor the context and terminate early. The above construct just ensures immediate return in case `longRunningThing()` doesn't do what it should (or if it can't react to cancellation fast enough). – icza Feb 23 '23 at 22:08
  • Right! So, `longRunningThingManager` returns immediately, `longRunningThing` exits shortly after (if implemented properly). I wondering now if that short latency could be a source of errors? The caller thinks it's been cancelled, but really it's around for another few ms.. – Max Feb 23 '23 at 22:15
  • 1
    Actually the outcome is not determinisic. It's a possible outcome that once the context is cancelled, `longRunningThing()` may monitor it and return, and its return values may be sent over `ch`, and if this happens before `longRunningThingManager()` could advance, you have a situation where both `<-ch` and `<-ctx.Done()` can proceed, so one is chosen pseudo-randomly. – icza Feb 23 '23 at 22:21

0 Answers0