DEV Community

Cover image for Go Channel Patterns - Cancellation
b0r
b0r

Posted on

Go Channel Patterns - Cancellation

To improve my Go Programming skills and become a better Go engineer, I have recently purchased an excellent on-demand education from Ardan Labs. Materials are created by an expert Go engineer, Bill Kennedy.

I have decide to record my process of learning how to write more idiomatic code, following Go best practices and design philosophies.

This series of posts will describe channel patterns used for orchestration/signaling in Go via goroutines.

Cancellation Pattern

The main idea behind the Cancellation Pattern is to have a limited amount of time to perform work. If limit is reached, the work is ignored.

We have:

  • a context with specified timeout
  • a buffered channel that provides signaling semantic
  • a worker goroutine that does the work
  • a manager goroutine that waits on (which comes first):
    • worker goroutine signal (that the work is completed)
    • context timeout signal

Example

In Cancellation Pattern we have a limited amount of time to perform some work.

Imagine we are in the ice cream making business and we have:

  • a manager (main goroutine) that get and holds a scoop of the ice cream in one hand

    • he holds out his other hand (communication channel) and waits for an employee (worker goroutine) to pass him the ice cream cone so he can sell the ice cream
  • an employee (worker goroutine) that needs some time to get the ice cream cone so he can pass it to the mangers' hand

    • if employee takes too much time to get the ice cream cone, the ice cream that manager holds will melt, so he won't need the ice cream cone anymore and the manager won't hold his hand anymore
  • an employee is now stuck with the ice cream cone in his hand and can't perform any other work (goroutine leak)

    • to fix this employee and the manager decided to use new communication channel (e.g. desk buffered channel) so that employee can complete his work, regardless of the managers' hand

Use Case

Good use case for this pattern is any request to a remote service, e.g. database request, API request or whatever request that can block. Since we don't want our request to block forever, we use timeout to cancel it.

Feel free to try the example on Go Playground

package main

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

func main() {
    // a duration that sets the max time to perform the operation

    // e.g 150 ms to get the ice cream cone
    duration := 1 * time.Millisecond

    // context.Background() returns a non-nil, empty Context.
    // It is never canceled, has no values, and has no deadline.
    // It is typically used by the main function, initialization, and tests,
    // and as the top-level Context for incoming requests.
    emptyCtx := context.Background()

    // Create new context from emptyCtx + add timeout of 150 ms
    // ctx is new context with timeout
    // cancel is a function that releases resources associated with context

    // e.g. ticker that the manager uses to check if he gets the ice cream cone fast enough
    ctx, cancel := context.WithTimeout(emptyCtx, duration)

    // Canceling this context releases resources associated with it,
    // so code should call cancel as soon as the operations running in
    // this Context complete

    // e.g. command manager uses to cancel the context (unit of work - getting ice cream cone)
    defer cancel()

    // IMPORTANT:
    // Make buffered channel of size 1, and type string which provides signaling semantics.
    // Buffered channel ensures that the worker goroutine can perform the send operation
    // and complete even if there is no-one on the receive side.
    // e.g.
    // - if worker goroutine does NOT finish in 150ms
    // -- main goroutine will continue
    // -- this will cause worker goroutine leak
    //    since there is no-one goroutine to receive the sent signal (so it blocks and waits)

    // e.g. used to prevent employee from being blocked if he doesn't complete the work in 150ms
    ch := make(chan string, 1)

    // create worker goroutine
    go func() {
        // Simulate the idea of unknown latency (do not use in production).
        // Don't forget that context timeout is 150 ms, but this can take up to 200 ms.

        // e.g. employee reaches out for the ice cream cone from the box
        time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)

        // send signal when work is done
        // e.g. employee passes ice creeam cone to the managers' hand
        ch <- "paper"
    }()

    // select-case allow us to perform multiple channel operations
    // at the same time, on the same goroutine

    // e.g. manager waits for ice cream cone, or for 150 ms timer to time out
    select {

    // best case scenario:
    // receive a result from worker goroutine in under the 150 ms

    // e.g. employee finds and passes the ice cream cone to the manager
    case d := <-ch:
        fmt.Println("work complete", d)

    // ctx.Done() call starts the 150ms duration clock ticking.
    // If 150 ms passes before the worker goroutine finishes, this println will be executed

    // e.g. manager doesn't wait for employee to get the ice cream cone anymore
    case <-ctx.Done():
        fmt.Println("work cancelled")
    }
}

Enter fullscreen mode Exit fullscreen mode

Result (1st execution)

go run main.go

work complete paper
Enter fullscreen mode Exit fullscreen mode

Result (2st execution)

go run main.go

work cancelled
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, cancel channel pattern was described. In addition, simple implementation and use case were provided.

Readers are encouraged to check out excellent Ardan Labs education materials to learn more.

Resources:

  1. Ardan Labs
  2. Cover image by Igor Mashkov from Pexels
  3. Fan out picture

Top comments (1)

Collapse
 
solarfly73 profile image
Derek Douville

Pass ctx to the go routine as well. Note here that this code is only exiting the select loop early due to the context cancellation timeout, but the go routine keeps running away in the background. The correct pattern would be (in a more complex go routine) to periodically and strategically check the context for Done then cleanup and quit early.