There is tonne of information available on Go Concurrency and context usage like the context package, this blog and this but it can be a bit overwhelming. This article attempts to explain Context via a simple but perhaps the most common use case you would find in Microservices architecture.
Pre-Requisite
Article assumes that Reader is familiar with Goroutines, channels and HTTP
Why Context?
The primary idea behind context is the ability to cancel unneeded work.
- A calls B
- B starts to do work
- While B is working, A no longer needs the result either because B is slow, or A’s caller no longer needs the result (say because of a user action or expiry of some deadline)
How does A walk away and let B know to stop? B needs the advice to stop so that it gets a chance to cleanup any resources in use.
That’s where context comes in. In the rest of the article we will see an example, which I think is the most common usage of Context
HTTP GET call to a Slow Server
The picture below depicts a simple REST client calling a GET on HTTP Server that takes time to respond.
Here is the sample code
- A Sample slow server is started . The handler sleeps for 10 second before responding to any request.
TIP: Uses httptest package, an excellent tool for writing HTTP Tests)
- Client makes a request and then waits for 10 seconds before it gets the response from Server
What if the Client wants to bail out if the response doesn’t return within 5 seconds?
In Microservices architecture, cutting the chord on a slow response is a common practice (fail-fast). It also reduces pressure on the overloaded server which may be the reason behind the higher response latency. Ideas like Circuit Breaker are built around this concept.
A Timeout handling Pattern with Context
For the impatient, here is the full code. In the sections below we will go through it step by step
Step 1
For better code organization we define a type called ResponseHandler
. This is a function that can process the result of http request. Implementation of this handler can hence contain any custom logic as required
Here is the func
type and our super simple handler that just prints response to stdout
//Func Type to handle response
type ResponseHandler func(resp *http.Response, err error) error//Implementation of Response Handler
func stdOutHandler(resp * http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()
//Handle the response
//In this case we just print the body
body, _: = ioutil.ReadAll(resp.Body)
fmt.Printf("Body from response %s\n", string(body))
return nil
}
Step 2
- Create Request with a Timeout context. This is the key
ctx, cancel := context.WithTimeout(context.Background(), timeout)request, _ := http.NewRequestWithContext(ctx, "GET", getURL, nil)
Things to note here
- Context is a Tree i.e a Parent with many descendants
Background
is the root of anyContext
tree (Represented by the codecontext.Background()
) .
From Go Doc : Background returns an empty Context. It is never canceled, has no deadline, and has no values. Background is typically used in main, init, and tests, and as the top-level Context for incoming requests.
context.WithTimeout(context.Background(), timeout)
creates a copy of context with a deadline set tocurrent time + specified timeout
Step 3
- Do the
request with context
in a Goroutine.
fResult: = make(chan error, 0)
go func() {
fResult < -respHandler(http.DefaultClient.Do(request))
}()
Why separate Goroutine? Well, what we want is to make this call and then move on to monitor the “status” of the call via the Context mechanism. So the request shouldn’t be blocking us to proceed further and do the Select
Step 4
*️⃣ The Select
- Recall that our objective is to process the response if it happens before the specified timeout, else give up on the request
select {
case <-ctx.Done():
< -fResult //let the go routine end too
return ctx.Err()
case err:= < -fResult:
return err //any other errors in response
}
- When we create a derived Context, in this case with timeout, the returned context has a
Done
channel.Done
channel is closed when the deadline expires and hence works as a perfect broadcast mechanism
So in the above code snippet, what we are doing is waiting for one of the two things to happen
- The deadline expires (Done channel is closed)
OR
- The GET is successful and the ResponseHandler processes the response. Any errors in processing are returned in the error channel.
cancel
function : Creating the derived context with timeout also returns a CancelFunc. It’s important that the function be called when the operation running in the context completes, so as to release any resources associated with it and prevent leaks. In the code above we do adefer cancel()
immediately after creating the context
Example Run
Running the gist, would report the following error (your port may differ since the httptest
server picks a random port)
Error in GET request to server http://127.0.0.1:60171, error = context deadline exceeded
This is because in the test code we create a context with timeout of 6 seconds whereas the Slow Server takes 10 seconds to respond
Context is Propagated
We saw that handling timeout
on the HTTP Client allowed the caller to walk way from a slow response, but what about the Server? Wouldn’t it be nice if the Server could also know that the Client no longer cares about the result and stops processing it.
Turns out that golang Context is indeed transitive and propagated to the Server.
Let’s modify the server code to also “listen” to context signals -
- The line
ctx := r.Context()
retrieves the context associated with the request - We now use the same methodology as used in case of request — start a Goroutine which does the job (in this case just sleeps for 12 seconds and then writes “Hello There” ).
- After the Goroutine is spawned, the handler starts to wait on either a message on
Done
channel or from the channel fed by Goroutine - In our current case, since the timeout on the request is 6 second and the job takes 12 seconds, what we would see is the following error on the server side —
Error context canceled
when the context is canceled by the client.
A high level sequence diagram is shown below -
Next
This is just the tip of iceberg but hopefully provides enough “context” to try more complicated scenarios of Context
usage.