12. Context

The context1 package was introduced in Go 1.7 to provide a cleaner way than using channels to manage cancellation and timeouts across goroutines.

1. https://pkg.go.dev/context

While the scope and API footprint of the package is pretty small, it was a welcome addition to the language when introduced.

The context package in Listing 12.1 defines the context.Context2 type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

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

Listing 12.1 The context Package

Context is, mostly, used for controlling concurrent subsystems in your application. In this chapter, we cover the different kinds of behavior with contexts, including canceling, timeouts, and values. We also explain how you can clean up a lot of code involving channels by using contexts.

The Context Interface

The context.Context interface, Listing 12.2, consists of four methods. These methods provide the ability to listen for cancellation and timeout events and retrieve values from the context hierarchy. They also provide a way to check what error, if any, caused the context to be canceled.

Listing 12.2 The context.Context Interface

In Listing 12.2, you can see that the context.Context interface implements several of the channel patterns we talk about in Chapter 11, such as having a Done channel that can be listened to for cancellation.

We cover each of these methods in more detail later. For now, let’s briefly look at each one of them.

Context#Deadline

You can use the context.Context.Deadline3 method, Listing 12.3, to check whether a context has a cancellation deadline set and, if so, what that deadline is.

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

Listing 12.3 The context.Context.Deadline Method

Context#Done

You can use the context.Context.Done4 method, Listing 12.4, to listen for cancellation events. This is similar to how you can listen for a channel being closed, but it is more flexible.

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

Listing 12.4 The context.Context.Done Method

Context#Err

You can use the context.Context.Err5 method, Listing 12.5, to check whether a context has been canceled.

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

Listing 12.5 The context.Context.Err Method

Context#Value

You can use the context.Context.Value6 method, Listing 12.6, to retrieve values from the context hierarchy.

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

Listing 12.6 The context.Context.Value Method

Helper Functions

The context package, Listing 12.7, provides a number of useful helper functions for wrapping a context.Context, making the need for custom implementations of the context.Context interface less common.

Listing 12.7 The context Package

The Background Context

Although often you might be given a context.Context, you might also be the one to start a context.Context. The most common way to provide a quick and easy way to start a context.Context is to use the context.Background7 function, Listing 12.8.

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

Listing 12.8 The context.Background Function

In Listing 12.9, we print the context.Context returned by context.Background. As you can see from the output, the context is empty.

Listing 12.9 The context.Background Function

Default Implementations

Although the context.Background interface is empty, it does provide default implementations of the context.Context interface, Listing 12.10. Because of this, the context.Background context is almost always used as the base of a new context.Context hierarchy.

Listing 12.10 The context.Background Function Provides Default Implementation of the context.Context Interface

Context Rules

According to the context documentation, the following rules must be adhered to when using the context package:

  • Programs that use contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation.

  • Do not store contexts inside a struct type; instead, pass a context explicitly to each function that needs it. The context should be the first parameter, typically named ctx.

  • Do not pass a nil context, even if a function permits it. Pass context.TODO if you are unsure about which context to use.

  • Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

  • The same context may be passed to functions running in different goroutines; contexts are safe for simultaneous use by multiple goroutines.

Context Nodal Hierarchy

As the context documentation states, a context.Context is not meant to be stored and held onto but should be passed at runtime.

Consider an HTTP request. An HTTP request is a runtime value that gets passed along through the application until eventually the response is returned. You would not want to store, or hold on to, the request for future use because it would be of no benefit once the response is returned.

Using context.Context in your code behaves like the HTTP request. You pass a context.Context through the application where it can be listened to for cancellation, or for other purposes, at runtime.

As a context.Context is passed through the application, a receiving method may wrap the context with its cancellation functionality or with context.WithValue to add a value, such as a request ID, before passing the context.Context along to any functions or methods that it may call.

The result is a nodal hierarchy of context.Context values that starts at the beginning of the request or the start of the application and spiders out throughout the application.

Understanding the Nodal Hierarchy

Consider Listing 12.11. We start with a context.Background context and pass it the A and B functions. Each function wraps the given context.Context, prints that new context.Context with a new one, and then either passes it along to the next function or returns.

Listing 12.11 Wrapping Context Creates Nodal Hierarchies

Wrapping with Context Values

To wrap the context.Context with a new one, we use context.WithValue. The context.WithValue function takes a context.Context and a key and value, and it returns a new context.Context with the given key and value that wraps the original context.Context. We discuss more about context.WithValue later in this chapter.

Following the Context Nodes

In Listing 12.12, we define the functions used in Listing 12.11. Each of these functions takes a context.Context as an argument. They then wrap the context.Context with a new one, print the new context.Context with a new one, and pass it along to the next function.

Listing 12.12 The Example Application

When you look at the output of the program, Listing 12.13, you can see that when we print out any given context.Context, we see that it is at the bottom of the node tree, and the context.Background context is at the top of the node tree hierarchy.

Listing 12.13 Printing the Node Tree

In Figure 12.1, you see that the B1a context.Context is a child of the B1 context.Context, the B1 context.Context is a child of the B context.Context, and the B context.Context is a child of the original background context.Context.

images

Figure 12.1 Visualizing the node tree

Context Values

As we have seen, one feature of the context package is that it allows you to pass request specific values to the next function in the chain.

This provide a lot of useful benefits, such as passing request or session specific values, such as the request id, user id of the requestor, etc. to the next function in the chain.

Using values, however, has its disadvantages, as we will see shortly.

Understanding Context Values

You can use the context.WithValue8 function to wrap a given context.Context with a new context.Context that contains the given key/value pair (see Listing 12.14).

8. https://pkg.go.dev/context#WithValue

Listing 12.14 The context.WithValue Function

The context.WithValue function takes a context.Context as its first argument and a key and a value as its second and third arguments.

Both the key and value are any values. Although this may seem like you can use any type for the key, this is not the case. Like maps, keys must be comparable, so complex types like maps or functions are not allowed. In Listing 12.15, for example, when trying to use a map as a key for the context, the application crashes with a panic.

Listing 12.15 A Compilation Panic because of a Noncomparable Key Type in a Map

Key Resolution

When you ask for a key through the context.Context.Value function, the context.Context first checks whether the key is present in the current context.Context. If the key is present, the value is returned. If the key is not present, the context.Context then checks whether the key is present in the parent context.Context. If the key is present, the value is returned. If the key is not present, the context.Context then checks whether the key is present in the context.Context’s parent’s parent, and so on.

Consider the example in Listing 12.16. We wrap a context.Context multiple times with different key/values.

Listing 12.16 Nesting and Printing Different Contextual Nodes

From the output in Listing 12.17, you can see that the final context.Context has a parentage that includes all of the values added with context.WithValue. You can also see that we are able to find all of the keys, including the very first one that we set.

Listing 12.17 Output Demonstrating Context Nodal Ancestry

Problems with String Keys

As is mentioned in the context documentation, shown in Listing 12.18, using string keys is not recommended. As you just saw when context.Context.Value tries to resolve a key, it finds the first, if any, context.Context that contains the key and returns that value.

Listing 12.18 Using Strings as Context Keys Is Not Recommended per the Documentation

When you use the context.Context.Value function, you get the last value that was set for the given key. Each time you use context.WithValue to wrap a context.Context with a new context.Context, the new context.Context essentially will have replaced the previous value for the given key. For example, in Figure 12.2, Context B is setting the key request_id with the value B-123. Context B1 also sets a value, B1-abc, using the request_id key. Now, any child nodes of Context B1, such as Context B1a, will have a different request_id from other direct descendants of Context B1.

images

Figure 12.2 Visualizing context nodal hierarchies

Key Collisions

Consider the example in Listing 12.19. We wrap a context.Context multiple times, each time with a different value but the same key, request_id, which is of type string.

Listing 12.19 Overwriting Context Values by Using Identical Keys

When we try to log the request_id for both A and B, we see that they are both set to the same value (as in Listing 12.20).

Listing 12.20 The Value Set in Function A Is Overridden by Function B

One way to solve this problem is to try to “namespace” your string keys, myapp.request_id. Although you may never get into a collision scenario, the possibility of someone else using the same key does exist.

Custom String Key Types

Because Go is a typed language, you can leverage the type system to solve the problem of key collisions. You can create a new type based on string that you can use as the key, as in Listing 12.21.

Listing 12.21 Using Different Types for Keys Can Prevent Collisions

The Logger is now properly able to retrieve the two different request_id values because they are no longer of the same type (see Listing 12.22).

Listing 12.22 The Value Set in Function A Is No Longer Overridden by Function B

This code can be further cleaned up by using constants for the keys that our package, or application, uses. This allows for cleaner code and makes it easier to document the potential keys that may be in a context.Context. For example, in Listing 12.23, new constants, such as A_RequestID, are defined and exported by the package along with documentation for the constants’ usage. These constants can now be safely used to set and retrieve context values.

Listing 12.23 Using Constants Can Make Working with Context Values Easier

Securing Context Keys and Values

If we export or make public the types and names of the context.Context keys our package or application uses, we run the risk of a malicious agent stealing or modifying our values. For example, in a web request, we might set a request_id at the beginning of the request, but a piece of middleware later in the chain might modify that value to something else. In Listing 12.24, the WithBar function is replacing the value set in the WithFoo function by using the exported foo.RequestID key.

Listing 12.24 Exporting Context Keys Can Lead to Malicious Use

Securing by Not Exporting

The best way to ensure that your key/value pairs aren’t maliciously overwritten or accessed is by not exporting the types, and any constants, used for keys. In Listing 12.25, the constant requestID is not exported and can only be used within its defining package.

Listing 12.25 Unexported Context Keys with Custom Types Provide the Best Security

Now you are in control of what values from the context you want to make public. For example, you can add a helper function to allow others to get access to the request_id value.

Because the return value from context.Context.Value is an empty interface, interface{}, you can use these helper functions to not just retrieve access to the value but also type assert the value to the type you want or return an error if it doesn’t. In Listing 12.26, the RequestIDFrom function takes a given context.Context and tries to extract a value using the nonexported custom type context key. In addition to hiding details about the key, it also hides details of how the value is stored. In the future, if the value is no longer stored as a string but rather as a struct, this function prevents external API breakage.

Listing 12.26 Future-Proofing Implementation Details with a Helper Function

Our application can be updated to use the new helper function to print the request_id or exit if there was a problem getting the value (see Listing 12.27).

Listing 12.27 Using the RequestIDFrom Function to Properly Retrieve a Context Value

The malicious bar package can no longer set or retrieve the request_id value set by the foo package (Listing 12.28). The bar package does not have the ability to create a new type of value foo.ctxKey because the type is unexported and cannot be accessed outside of the foo package.

Listing 12.28 The WithBar Function Can No Longer Maliciously Access Values Set by WithFoo

As a result of securing our context.Context values, the application now correctly retrieves the request_id value set by the foo package (see Listing 12.29).

Listing 12.29 The Request ID Set by WithFoo Is Now Safe-Guarded

Cancellation Propagation with Contexts

Although having the ability to pass contextual information via a context.Context is useful, the real benefit, and design of the context package, is that it can be used to propagate cancellation events to those listening to the context. When a parent context.Context is canceled, all its children are also canceled.

Creating a Cancelable Context

To cancel a context.Context, you must have a way of cancelling it. The context.WithCancel function, Listing 12.30, wraps a given context.Context with a context.Context that can be canceled.

Listing 12.30 The context.WithCancel Function

The context.WithCancel9 function returns a second argument—that of a context.CancelFunc10 function, which can be used to cancel the context.Context.

9. https://pkg.go.dev/context#WithCancel

10. https://pkg.go.dev/context#CancelFunc

The Cancel Function

There a few things that need to be noted about the context.CancelFunc function, Listing 12.31. So let’s examine each in more detail.

Listing 12.31 The context.CancelFunc Function

Idempotent Behavior

“After the first call, subsequent calls to a CancelFunc do nothing.”

context.CancelFunc documentation

According to the context.CancelFunc documentation, the context.CancelFunc function is idempotent, Listing 12.32. That is, calling it multiple times has no effect beyond the first call.

Listing 12.32 The Idempotent Behavior of the context.CancelFunc Function

Leaking Resources

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

context.WithCancel documentation

Often you will want to defer execution of the context.CancelFunc function, as in Listing 12.33, until the function or application exits. This ensures proper shutdown of the context.Context and prevents the context.Context from leaking resources.

Listing 12.33 Prevent Leaking Goroutines by Calling the context.CancelFunc

Canceling a Context

Consider Listing 12.34. The listener function takes a context.Context as its first argument and an int representing the goroutine id as its second argument.

The listener function blocks until the context.Context is canceled, which closes the channel behind context.Context.Done method. This unblocks the listener function and allows it to exit.

Listing 12.34 Blocking on context.Context.Done

The application creates a context.Background context and then wraps it with a cancellable context.Context. The context.CancelFunc returned is immediately deferred to ensure the application doesn’t leak any resources.

In Listing 12.35, we create several goroutines that listen for the context.Context to be canceled.

Listing 12.35 Using Context Cancellation

As you can see from the output in Listing 12.36, the listener function unblocks and exits when the context.CancelFunc is called, cancel().

Listing 12.36 The Output of the Application

Only Child Nodes of the Context Are Canceled

Figure 12.3 shows that by canceling a node in the hierarchy, all its child nodes are also canceled. Other notes in the hierarchy, such as parent and sibling nodes, are unaffected.

images

Figure 12.3 Cancellation propagation

Listening for Cancellation Confirmation

Previously, we have used time.Sleep11 to block the execution of the program. This is not a good practice because it can lead to deadlocks and other problems. Instead, the application should receive a context.Context cancellation confirmation.

11. https://pkg.go.dev/time#Sleep

Starting a Concurrent Monitor

Consider Listing 12.37. To start a Monitor, we must use the Start method, giving it a context.Context. In return, the Start method returns a context.Context that can be listened to by the application to confirm the shutdown of the Monitor later on.

Listing 12.37 Accepting a context.Context and Returning a New One

To prevent the application from blocking, we launch the listen method in a goroutine with the given context.Context. Unless this context.Context is canceled, the listen method never stops and continues to leak resources until the application exits.

The context.CancelFunc is held onto by the Manager so when the Manager is told to cancel by the client, it also cancels the Monitor context. This tells the client that the Monitor has been shut down, confirming the cancellation of the Monitor.

Monitor Checking

The listen method blocks until the context.Context, given by the application, is canceled, Listing 12.38. We first make sure to defer the context.CancelFunc in the Monitor to ensure that if the listen method exits for any reason, clients will be notified that the Monitor has been shut down.

Listing 12.38 The Monitor Calls Its context.CancelFunc if External context.Context Is Canceled

Using the Cancellation Confirmation

In Listing 12.39, the application starts with a context.Background context and then wraps that with a cancellable context.Context. The context.CancelFunc returned is immediately deferred to ensure the application doesn’t leak any resources. After a short while, in a goroutine, the cancel function is called, and the context.Context is canceled.

The Monitor is then started with our cancellable context.Context. The context.Context returned by the Start method is listened to by the application. When the Monitor is canceled, the application is unblocked and can exit. Alternatively, if the application is still running after a couple of seconds, the application is forcibly terminated.

Listing 12.39 Using the Cancellation Confirmation

As you can see from the output in Listing 12.40, the application waits for the Monitor to properly shut down before exiting. We were also able to remove the use of time.Sleep to allow the monitor to finish.

Listing 12.40 The Output of the Application

Timeouts and Deadlines

In addition to enabling you to manually cancel a context.Context, the context package also provides mechanisms for creating a context.Context that self-cancels after or at a given time. Using these mechanics allows you to control how long to run some before you give up and assume that the operation has failed.

Canceling at a Specific Time

The context package provides two functions for creating a time-based self-canceling context.Context: context.WithTimeout12 and context.WithDeadline.13

12. https://pkg.go.dev/context#WithTimeout

13. https://pkg.go.dev/context#WithDeadline

When using context.WithDeadline, Listing 12.41, we need to provide an absolute time at which the context.Context should be canceled. That means we need an exact date/time we want this context.Context to be canceled—for example, March 14, 2029 3:45pm.

Listing 12.41 The context.WithDeadline Function

Consider Listing 12.42. In it, we create a new time.Time14 for January 1, 2030 00:00:00 and use it to create a context.Context that self-cancels at that date and time.

14. https://pkg.go.dev/time#Time

Listing 12.42 Using context.WithDeadline

Canceling after a Duration

Although being able to cancel a context.Context at a particular time is useful, more often than not you want to cancel a context.Context after a certain amount of time has passed.

When using context.WithTimeout, Listing 12.43, we need to provide a relative time.Duration15 at which the context.Context should be canceled.

15. https://pkg.go.dev/time#Duration

Listing 12.43 The context.WithTimeout Function

Consider Listing 12.44. In it, we create a new self-canceling context.Context that uses context.WithTimeout to self-cancel after 10 milliseconds.

Listing 12.44 Using context.WithTimeout

Functionally, we could have used context.WithDeadline instead, but context.WithTimeout is more convenient when we want to cancel a context.Context after a certain amount of time has passed.

Context Errors

In a complex system, or even in a small one, when a context.Context is canceled, you need a way to know what caused the cancellation. It is possible that context.Context was canceled by a context.CancelFunc successfully, was canceled because it timed out, or was canceled for some other reason.

The context.Context.Err method in Listing 12.45 returns the error that caused the context to be canceled.

Listing 12.45 The context.Context.Err Method

Context Canceled Error

The context package defines two different error variables that can be used to check an error that was returned from context.Context.Err method.

The first is context.Canceled, Listing 12.46, which is returned when the context is canceled through the use of a context.CancelFunc function. This error is considered to indicate a “successful” cancellation.

Listing 12.46 The context.Canceled Error

Consider Listing 12.47. When we first check the context.Context.Err method, it returns nil. After we call the context.CancelFunc function provided by context.WithCancel, the context.Context.Err method returns a context.Canceled error.

Listing 12.47 Checking for Cancellation Errors

As you can see from the output in Listing 12.47, repeated calls to the context.Context.Err method return the same context.Canceled error.

Context Deadline Exceeded Error

When a context.Context is canceled due to a deadline or timeout being exceeded, the context.Context.Err method returns a context.DeadlineExceeded16 error, as in Listing 12.48.

16. https://pkg.go.dev/context#DeadlineExceeded

Listing 12.48 The context.DeadlineExceeded Error

Consider Listing 12.49. We create a context.Context that self-cancels after 1 second. When we check the context.Context.Err method before the context.Context times out, it returns nil.

Listing 12.49 Checking for Deadline Exceeded Errors

As you can see from the output, the context.Context times out after the specified time, and the context.Context.Err method returns a context.DeadlineExceeded error.

Listening for System Signals with Context

Previously, when we discussed channels in Chapter 11, we showed you how to capture system signals, such as ctrl-c, using signal.Notify.17 The signal.NotifyContext18 function, Listing 12.50, is a variant of signal.Notify that takes a context.Context as an argument. In return, we are given a context.Context that will be canceled when the signal is received.

17. https://pkg.go.dev/os/signal#Notify

18. https://pkg.go.dev/os/signal#NotifyContext

Listing 12.50 The signal.NotifyContext Function

Consider Listing 12.51. We use signal.NotifyContext to listen for ctrl-c. This function returns a wrapped context.Context that will cancel when the signal is received. It also returns a context.CancelFunc that can be used to cancel context.Context when needed.

Listing 12.51 Listening for System Signals

Testing Signals

Testing system signals is tricky, and you must take care not to accidentally exit your running tests. Unfortunately, the syscall19 package does not provide a “test” signal or a way to implement a test signal.

19. https://pkg.go.dev/syscall

You can use syscall.SIGUSR120 or syscall.SIGUSR221 in your tests because these are allocated to the developer to use for their own purposes.

20. https://pkg.go.dev/syscall#SIGUSR1

21. https://pkg.go.dev/syscall#SIGUSR2

When you are testing signals, you are testing a global signal that will be caught by anyone else who is listening to that signal. We want to make that when testing signals, we aren’t running the tests in parallel and that you don’t have other tests also listening to the same signal.

Consider Listing 12.52. How do we test that the Listener function will respond properly to a signal? We don’t want to make that the responsibility of the Listener function; it already has a context.Context that it can listen to for cancellation. The Listener function doesn’t care why it was told to stop listening; it just needs to stop listening. This could be because we receive an interrupt signal because a deadline has passed or because the application no longer needs the Listener function to keep running.

Listing 12.52 The Listener Function

In Listing 12.53, before we call the Listener function, we first create a context.Context that self-cancels after 5 seconds if nothing else happens. We then wrap that context.Context with one received from the signal.NotifyContext function that self-cancels when the system receives a TEST_SIGNAL signal.

Our test blocks with a select waiting for either context.Context to be canceled and then respond accordingly.

Listing 12.53 Testing the Listener Function

Inside the test, in a goroutine, we can trigger the TEST_SIGNAL signal by sending it to the current process, syscall.Getpid,22 with the syscall.Kill23 function, as shown in Listing 12.54. We can also see in Listing 12.54 the test successfully exits after 1s.

22. https://pkg.go.dev/syscall#Getpid

23. https://pkg.go.dev/syscall#Kill

Listing 12.54 Sending a TEST_SIGNAL Signal

Summary

In this chapter, we explored the concept of contexts in Go. We explained that contexts are a way to manage cancellation, timeouts, and other request-scoped values across API boundaries and between processes. We also discussed how to use contexts to clean up a lot of code involving channels, such as listening for system signals. We discussed the nodal hierarchy of how the context package wraps a new context.Context around a parent context.Context. We explained the difference was to cancel a context.Context and how to use multiple context.Contexts to confirm shutdown behavior. The context package, while small, is a very powerful tool for managing concurrency in your application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset