Cancel context from child

Viewed 1861

I'm relatively new to Golang and am trying to incorporate Contexts into my code.

I see the benefits in terms of cancelling from the parent as well as sharing context-specific stuff (loggers, for example).

Beyond that, I might be missing something, but I can't see a way for a child to cancel the context. The example here would be if one of the child routines encounters an error that means the whole context is done.

Here's some sample code:

package main

import (
    "context"
    "fmt"
    "math/rand"
    "os"
    "os/signal"
    "sync"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // handle SIGINT (control+c)
    go func() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, os.Interrupt)
        <-c
        fmt.Println("main: interrupt received. cancelling context.")
        cancel()
    }()

    wg := sync.WaitGroup{}

    wg.Add(1)
    go func() {
        child1DoWork(ctx)
        wg.Done()
    }()
    wg.Add(1)
    go func() {
        child2DoWork(ctx)
        wg.Done()
    }()
    fmt.Println("main: waiting for children to finish")
    wg.Wait()
    fmt.Println("main: children done. exiting.")
}

func child1DoWork(ctx context.Context) {
    // pretend we're doing something useful
    tck := time.NewTicker(5 * time.Second)
    for {
        select {
        case <-tck.C:
            fmt.Println("child1: still working")
        case <-ctx.Done():
            // context cancelled
            fmt.Println("child1: context cancelled")
            return
        }
    }
}

func child2DoWork(ctx context.Context) {
    // pretend we're doing something useful
    tck := time.NewTicker(2 * time.Second)
    for {
        select {
        case <-tck.C:
            if rand.Intn(5) < 4 {
                fmt.Println("child2: did some work")
            } else {
                // pretend we encountered an error
                fmt.Println("child2: error encountered. need to cancel but how do I do it?!?")
                // PLACEHOLDER: HOW TO CANCEL FROM HERE?
                return
            }
        case <-ctx.Done():
            // context cancelled
            fmt.Println("child2: context cancelled")
            return
        }
    }
}

Here you have an example of cancelling from the parent (due to SIGINT) which works great. However, there's a placeholder in child2DoWork where an error is encountered and I want to then cancel the whole context, but I can't see a way to do that with the vanilla context capabilities.

Is this out-of-scope for contexts? Clearly I could communicate from child2 back to the parent which could then cancel, but I'm wondering if there isn't an easier way.

If communication back to the parent is the proper way, is there an idiomatic way of doing this? It does seem like a common problem.

Thanks!

3 Answers

A child can't and shouldn't cancel a context, it's the parent's call. What a child may do is return an error, and the parent should decide if the error requires cancelling the context.

Just because a "subtask" fails, it doesn't mean all other subtasks need to be cancelled. Often, a failing subtask may have a meaning that other subtasks become more important. Think of a parallel search: you may use multiple subtasks to search for the same thing in multiple sources. You may use the fastest result and may wish to cancel the slower ones. If a search fails, you do want the rest to continue.

Obviously if you pass the cancel function to the child, the child will have the power to cancel the context. But instead leave that power at the parent.

Is this out-of-scope for contexts? Clearly I could communicate from child2 back to the parent which could then cancel, but I'm wondering if there isn't an easier way.

Yes, this is exactly backwards for contexts. They are explicitly for a caller to cancel. The correct mechanism here is the simplest and most obvious: when child2DoWork encounters an error, it should return an error, and when the caller gets an error back, if the correct response is to cancel other tasks, it can then cancel the appropriate context(s).

Essentially, the child is a task, and it should be isolated from any other tasks. It shouldn't be trying to manage its siblings; the parent should be managing all of its children.

In the case that

  1. parent spawn multiple child goroutines to achieve one goal
  2. if one child failed, parent need to stop its siblings

you can use a channel to communicate, parent can listen to the channel, once there is an error, parent can cancel all children task.

I have modified your code

package main

import (
    "context"
    "fmt"
    "math/rand"
    "os"
    "os/signal"
    "sync"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    errChan := make(chan error)

    // handle SIGINT (control+c)
    go func() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, os.Interrupt)
        select {
        case <-c:
            fmt.Println("main: interrupt received. cancelling context.")
        case err := <-errChan:
            fmt.Printf("main: child goroutine returns error. cancelling context. %s\n", err)
        }
        cancel()
    }()

    wg := sync.WaitGroup{}

    wg.Add(1)
    go func() {
        child1DoWork(ctx)
        wg.Done()
    }()
    wg.Add(1)
    go func() {
        child2DoWork(ctx, errChan)
        wg.Done()
    }()
    fmt.Println("main: waiting for children to finish")
    wg.Wait()
    fmt.Println("main: children done. exiting.")
}

func child1DoWork(ctx context.Context) {
    // pretend we're doing something useful
    tck := time.NewTicker(5 * time.Second)
    for {
        select {
        case <-tck.C:
            fmt.Println("child1: still working")
        case <-ctx.Done():
            // context cancelled
            fmt.Println("child1: context cancelled")
            return
        }
    }
}

func child2DoWork(ctx context.Context, errChan chan error) {
    // pretend we're doing something useful
    tck := time.NewTicker(2 * time.Second)
    for {
        select {
        case <-tck.C:
            if rand.Intn(5) < 4 {
                fmt.Println("child2: did some work")
            } else {
                // pretend we encountered an error
                fmt.Println("child2: error encountered")
                // PLACEHOLDER: HOW TO CANCEL FROM HERE?
                errChan <- fmt.Errorf("error in child2")
                return
            }
        case <-ctx.Done():
            // context cancelled
            fmt.Println("child2: context cancelled")
            return
        }
    }
}
Related