Go: Context and cancellation
Introduction⌗
The context package in Go provides a mechanism to prevent the application from doing unnecessary work, hence preserving resources. The context in go it used for following purposes:
- Carry deadline information: e.g. cancel the current task after a given timestamp.
- Carry cancellation information: e.g. when a user has exited the website, stop any operations that were initiated.
- Carry request-scoped values: e.g. adding request-id at the entry point of your request and retrieving it at the database access layer.
In this post, we will discuss how to use the deadline and cancellation features to simplify use cases like:
- Cancel task or batch of tasks based on a condition.
- Finish or gracefully exit a task within a specified duration.
- Finish or gracefully exit a task within a given deadline.
The Context interface is defined as below. The first three methods in the interface provide everything we need to perform efficient context cancellations
|
|
Context Tree⌗
To understand cancellations better, we should first understand the context tree in the Go application. Most practical applications will use a derived context that is an n-th child of the parent context. These derived contexts will form a context tree that is cancellable.
To create a context tree, let us look at the below examples. Here, we first derive the contexts from the parent context and use it to call the longRunningOperation
. Our final goal is to stop the longRunningOperation
when an error happens or the client disconnects.
|
|
Mechanics⌗
The cancellation of context happens in 2 steps. First the caller emits a cancellation signal based on a a condition (for example an error). The callee of the function then receives this signal to abort the current running operation.
Emitting the cancellation event.⌗
The context.WithCancel(), context.WithTimeout() and context.WithDeadline() functions return a derived context with a second argument of the type context.CancelFunc, which is a function. You can call the CancelFunc from the caller to emit a cancellation signal.
|
|
Listening to the cancellation.⌗
The context provides ctx.Done()
method that returns <-chan struct{}
. We can listen to a close event on this channel to identify if the context is cancelled. Additionally, querying the ctx.Err()
after the ctx.Done()
is closed, will provide us the cancellation reason.
If you are dealing with the goroutines, then you can listen to the ctx.Done() channel in the callee function and abort. For sequential functions, you can just check for a not-nil value on ctx.Err() and abort when the err occurs
|
|
In the next section, we will look at some examples
Example : Simple context cancellation⌗
The code below demonstrates an example where we initiate some work in a goroutine. In our case, the work()
function reads a file “example.txt” and does an arbitrary operation that takes around 100ms, simulated by time.Sleep(time.Millisecond*100) in the work()
function. Our main function performs some more operations and encounters an error scenario. When the error occurs, we call the stop() CancelFunc
, to abort any operations that the work function was performing.
|
|
The work function is very straightforward. At the start of the function, we test if the context was canceled by checking the ctx.Err()
value. If it is not nil, then we exit. Furthermore, while processing the file line by line, we again check for the same condition and return if the error is not nil. This way, we can abort our work() function, if the caller function cancels the context. Note that the error in this case is context canceled
that indicates explicit cancellation.
Example : Simple timeout cancellation⌗
There are several scenarios where we want to abort the operation when it takes more than a specified duration. In such a scenario, we use a context with a timeout duration. In this case, the context will implicitly cancel after the specified interval.
|
|
Note that the error in this case is context deadline exceeded
that indicating that the longRunningOperation
took more time than what was expected.
Example : Simple deadline cancellation⌗
Another common scenario is to abort a task when it runs beyond a given timestamp. In such a scenario, we use a context with a deadline using context.WithDeadline() function. This function takes a timestamp as an argument and cancels when the execution continues beyond the provided timestamp.
|
|
In this case, the error is again context deadline exceeded
. It indicates that the function execution continued beyond provided timestamp.
Gotchas⌗
- Context cancellations do not kill goroutines automatically, they only propagate signals to implement graceful cancellations. The logic to abort the goroutine still needs to be implemented by the developer.
- Context can be cancelled only once.
- Passing an already cancelled context may cause unwanted results.
- Same context should be passed to all the functions and goroutines from parent.
- The heavy job in the
longRunningOperation
should be modified in such a way that it possible to checkctx.Err()
value after regular interval. This might cause problems in implementing cancellations in certain type of jobs.
Conclusion⌗
In this blog post, we discussed how cancellation happens in Go. All the examples and the corresponding code is available here. Readers are encouraged to clone the repository and try the examples. In case you have questions, please feel free to add a comment on this repository here.
References⌗
- https://stackoverflow.com/questions/53009084/parent-child-context-cancelling-order-in-go
- https://gobyexample.com/context
- https://golangbyexample.com/using-context-in-golang-complete-guide/
- https://www.sohamkamani.com/golang/context-cancellation-and-values/
- Some wonderful insights on the gopher slack channel.