Go Package context: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 81: Line 81:
* Context instances cannot be mutated or cancelled directly, as explained [[#Context_Cannot_Be_Canceled_Directly|above]].
* Context instances cannot be mutated or cancelled directly, as explained [[#Context_Cannot_Be_Canceled_Directly|above]].
* To allow a parent goroutine to cancel its children, create a new Context instance with one of the <code>WithCancel()</code>, <code>WithTimeout()</code> and <code>WithDeadline()</code> methods and pass '''that''' instance to the function executing on the child goroutine. See [[#Programmatic_Preemption|Programmatic Preemption]], [[#Programmatic_Timeout|Programmatic Timeout]] and [[#Programmatic_Deadline|Programmatic Deadline]] below.
* To allow a parent goroutine to cancel its children, create a new Context instance with one of the <code>WithCancel()</code>, <code>WithTimeout()</code> and <code>WithDeadline()</code> methods and pass '''that''' instance to the function executing on the child goroutine. See [[#Programmatic_Preemption|Programmatic Preemption]], [[#Programmatic_Timeout|Programmatic Timeout]] and [[#Programmatic_Deadline|Programmatic Deadline]] below.
* If the parent goroutine does not need to modify the cancellation behavior of the functions running on children goroutines, it will pass the unmodified Context. In this way, successive layers of the call graph can create Context instances that adhere to their needs, without affecting the parents.
* If the parent goroutine does not need to modify the cancellation behavior of the functions running on children goroutines, it will pass the unmodified Context. In this way, successive layers of the call graph can create Context instances that adhere to their needs, without affecting the parents. This composability enables writing large system without mixing concerns through the call graph.
* Only pass Context instances as parameters to functions. Do not store references to Context instances. These instances may look equivalent from the outside, but internally may change at every stack frame.  
* Only pass Context instances as parameters to functions. Do not store references to Context instances. These instances may look equivalent from the outside, but internally may change at every stack frame.  
==<span id='Preemption'></span><span id='Cancellation'></span><span id='Programmatic_Preemption'></span>Programmatic Preemption (Cancellation)==
==<span id='Preemption'></span><span id='Cancellation'></span><span id='Programmatic_Preemption'></span>Programmatic Preemption (Cancellation)==

Revision as of 18:37, 9 February 2024

External

Internal

Overview

The context package provides two, somewhat unrelated, features.

One is the capability to preempt, or cancel, blocking code executed by goroutines running downstream in the call graph. The context package provides an API for cancelling branches of the call graph. Cancellation can be programmatic, on timeout or on deadline. This is an idiomatic preemption pattern, equivalent, but preferred to the Done channel pattern. It is preferred because its implementation is available in the standard library, making it a standard Go idiom to consider when working with concurrent code. The implementation is also more expressive.

The second feature is the capability to store request-scoped data and propagate this state through the call graph.

Explicit context propagation, as an argument of functions implementing the pattern, is a key difference between Go and other languages.

Concepts

Context

https://pkg.go.dev/context#Context

context.Context is an interface exposed by the context package. The Context instances flow across API boundaries and through the system in the same way "done" channels do. The role of the Context instances is to carry cancellation signals, deadlines and request-scoped values. To implement the pattern, each function downstream from the top level concurrent call must take a Context as parameter. Conventionally, is the first parameter of the function. For more rules of using Contexts, see Programming Model. The implementations must ensure that they are concurrent-safe, so their methods may be called by multiple goroutines simultaneously.

type Context interface {

	// Deadline returns the time when work done on behalf of this context should be canceled. 
	Deadline() (deadline time.Time, ok bool)

	// Done returns a channel that's closed when work done on behalf of this context should be canceled. 
	Done() <-chan struct{}

	// If Done is not yet closed, Err returns nil. If Done is closed, Err returns a non-nil error 
	// explaining why: Canceled if the context was canceled or DeadlineExceeded if the context's deadline passed.
	Err() error

	// Value returns the value associated with this context for key, or nil if no value is associated with key. 
	Value(key any) any
}

The Context interface does not expose any method that allows cancelling the Context. This protects functions up the call stack from children cancelling the context - only the parent can cancel the children functions, as shown in the Programmatic Preemption (Cancellation) section.

The Context interface does not expose any method that can mutate the state of the underlying structure, either. The context is immutable, so when new values are stored, the context is actually cloned, with extra metadata.

Err()

ctx.Err() returns a standard response, as follows:

  • nil is the Done channel is not closed yet.
  • the context.Canceled error, if the context was canceled.
  • the context.DeadlineExceeded error, if the context's deadline has passed.

The Background Context

https://pkg.go.dev/context#Background

The background Context is a non-null, empty, no-deadline and no-values Context returned by the context.Background() function. This context is never canceled, and it is typically used by the main function, initialization and tests, and as the top-level Context for incoming requests.

The TODO Context

TODO is intended to serve as a placeholder for when you don't know which Context to utilize, or if you expect your code to be provided with a Context, but the upstream code hasn't yet furnished one. TODO is not meant for use in production.

Derived Context and the Context Hierarchy

Preemption (Cancellation)

Preemption, or cancellation of a function executing a blocking operation on a channel, or any other blocking operation (disk, network IO, user), consists in the immediate return from the blocking operation, and usually return from the function with a cancellation error. Programmatic cancelation is useful in stopping processing or handling application shutdown.

Cancellation is initiated by a parent goroutine in the call graph.

Cancellation Signal

The "cancellation signal" carried by the context is a Done channel that is closed when the function associated with the context needs to be prepped. The channel is returned by the context's Done() method.

Cancel Function

The cancel function is a function returned by the invocation of the WithCancel function, which then can be used to cancel the context returned by the same invocation of the WithCancel function. For an example, see Programmatic Preemption below.

Deadline

A deadline is communicated to the user by the result of the Context's Deadline() method. The method returns the time when work done on behalf of this Context should be cancelled, or a false value if no deadline is set. Also see Programmatic Deadline section, below.

Timeout

See Programmatic Timeout section, below.

Call Graph

Requests and Request-Scoped Data

One of the primary use of goroutines is to service requests. Usually, in these programs, request-specific information needs to be. passed along in addition to information about preemption. The Context exposes the Value() method, which returns a request-scoped value for a given key.

Using the value propagation feature of the Context is dubious. Don't use Context values for stuff that should be regular dependencies between components. Use Context values for data that can't be passed to your program in any other way: only data that is request-scoped, stuff that is created at the beginning of a request lifecycle, like request ID, etc. If the information is available when the program starts or at any point prior to when the request starts, do not use Context values. This is the case for database handles, loggers, etc.

Programming Model

  • Each function downstream from the top level concurrent call must take a Context as the first parameter.
  • Context instances cannot be mutated or cancelled directly, as explained above.
  • To allow a parent goroutine to cancel its children, create a new Context instance with one of the WithCancel(), WithTimeout() and WithDeadline() methods and pass that instance to the function executing on the child goroutine. See Programmatic Preemption, Programmatic Timeout and Programmatic Deadline below.
  • If the parent goroutine does not need to modify the cancellation behavior of the functions running on children goroutines, it will pass the unmodified Context. In this way, successive layers of the call graph can create Context instances that adhere to their needs, without affecting the parents. This composability enables writing large system without mixing concerns through the call graph.
  • Only pass Context instances as parameters to functions. Do not store references to Context instances. These instances may look equivalent from the outside, but internally may change at every stack frame.

Programmatic Preemption (Cancellation)

This section documents the idiomatic pattern to preempt, or cancel, a blocking function.

It consists in a someFunc() function that gets a Context instance as its first argument. The function will select reading the Done channel returned by the Context's Done() method. When the context is externally cancelled, the Done channel is closed, reading operation on the Done channel unblocks and allows the function to return. The recommended return value in this case is the error returned by ctx.Err().

// someFunc will return a context.Canceled error if the context was externally canceled
func someFunc(ctx context.Context, c <-chan interface{}) error {
	for {
		select {
		case <-ctx.Done():
			return ctx.Err() // context.Canceled is the context was externally canceled 
		case <-c: // this will ensure the goroutine will block, as we will never write on that channel
		}
	}
}

The parent goroutine must use WithCancel() function to create a new derived context to be passed to functions executed on children goroutines. The function also returns a cancel function. To externally cancel the context, and implicitly all functions listening on its Done channel, invoke the cancel function:

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

// spin off someFunc() on its own thread and get it to block by reading on the channel we will never write on
go func() {
	err := someFunc(ctx, make(chan interface{}))
	fmt.Printf("someFunc() errored out because %v\n", err)
}()

// spin off the anonymous function that will cancel someFunc() after 5 seconds
go func() {
	time.Sleep(5 * time.Second)
	cancel() // Calling the cancel() function cancels the context
}()

Programmatic Timeout

The WithTimeout() function creates a new derived context that closes its Done channel after the given timeout. It also returns a cancel function that can be used independently of the timeout.

Programmatic Deadline

The WithDeadline() function creates a new derived context that closes its Done channel when the machine's clock advances past the given deadline.