Goroutine cancellation with recovery

325 Views Asked by At

Without frameworks like RxGo, how could I accomplish the following in Go?:

  • Three goroutines of different running times are in a context: short, medium, and, long. These goroutines are long-running jobs like reading or uploading a large file.
  • If any one of the three goroutines cancel (on error), the other two will cancel.
  • Run code on all three goroutines when cancelled (like panic & recover). These are actions like, closing the large file or notifying the upload has failed.

In a diagram, the desired outcome:

enter image description here

Updated attempt:

package main

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

func randInt(min int, max int) int {
    return min + rand.Intn(max-min)
}

func main() {

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    rand.Seed(time.Now().UnixNano())
    wg := &sync.WaitGroup{}
    errworker := make(chan int, 4)

    work := func(i int, ec chan int, c context.Context, cx context.CancelFunc) {
        defer wg.Done()

        workchan := make(chan struct{})

        go func() {
            interval := time.Duration(randInt(1, 10000)) * time.Millisecond
            fmt.Printf("Operation %d started: will take %s\n", i, interval)
            time.Sleep(interval)

            if randInt(0, 100) > 80 {
                fmt.Printf("Operation %d failed!\n", i)
                ec <- i
                cx()
            }
            workchan <- struct{}{}
        }()

        select {
        case <-workchan:
            fmt.Printf("Operation %d done\n", i)
        case <-c.Done():
            fmt.Printf("Operation %d halted\n", i)
        }
    }

    for i := 0; i < 4; i++ {
        wg.Add(1)
        go work(i, errworker, ctx, cancel)
    }

    wg.Wait()
    close(errworker)
    for e := range errworker {
        fmt.Printf("Error in worker %d\n", e)
    }
}

(Edit) Playground: https://go.dev/play/p/CNACYe43Dh3

Updated attempt playground: https://go.dev/play/p/5lzdERwqG8o

Is there a simple, elegant ELI5 solution to this problem? I can't help but notice with channels, I may need child contexts with child goroutines to listen for the cancellation signal, since when a goroutine enters a long running job, there was no way (from what I've tried) to stop the goroutine other than cancelling the context altogether. A controlled way to terminate the goroutines early is desired.

(Edit) In my updated attempt I simply placed the actual work in an inner goroutine while the outer goroutine listens for context completion. In this attempt I decided to use an error channel (although in theory only one error will come through) to catch and process errors at the end.

Are there any caveats or blind spots to this approach? Feel free to correct me on implementation and approach, the goal is to have a controlled way to terminate goroutines early.

Thanks!

0

There are 0 best solutions below