In previous posts I described a service that talks to the European Central Bank (ECB) and fetches currency rate exchange data.

I don’t have any control over the remote server, and sometimes, the responses can be slow.

When the upstream service is slow, or failing, this will cause problems for the currency service.

Adding a simple test to the provider tests will show what happens…

func TestHTTPLiveProviderWithTimeout(t *testing.T) {
	stop := make(chan bool)
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		<-stop
	}))
	defer ts.Close()

	_, err := HTTPLiveProvider(ts.URL)()
	stop <- true

	if err != nil {
		t.Fatal("expected a timeout error")
	}
}

I can try out this test with:

$ go test -run 'TestHTTPLiveProviderWithTimeout$' .

But the test runner hangs, the test will never complete, because it’s waiting for the server to respond, and the server doesn’t respond until it receives a value on the stop channel.

I can force the Go test runner to timeout after a period, and this causes the test runner to panic:

$ go test -test.timeout 5s -run 'TestHTTPLiveProviderWithTimeout$' .
panic: test timed out after 5s

So, the default Go HTTP client will wait forever for a response.

Go 1.7 introduced contexts to the standard library, they’d been around for a while before that, but they landed in 1.7.

The context package:

Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.

Contexts provide two pieces of functionality, they can carry a deadline/timeout period, and they can act as a store for things like the user making the request, perhaps set by a middleware, or things that you want to ensure are available within the context of handling a request.

This doesn’t necessarily have to be an HTTP request, it can be be applied to a lot of things, and the database/sql DB type grew a number of new methods with Context in the name to indicate they accept a context.

The gRPC handler accepted a context.Context, but didn’t pass it on to the rates provider.

// RatesHandler is a micro service providing currency rate information.
type RatesHandler struct {
	liveProvider     parser.LiveProvider
	historicProvider parser.HistoricProvider
}

func (r *RatesHandler) Live(ctx context.Context, req *proto.LiveRequest, rsp *proto.LiveResponse) error {
	currency := req.GetCurrency()

	rates, err := r.liveProvider()
	if err != nil {
		return errors.InternalServerError("internal error", "error: %s", err)
	}
	...
	return nil
}

Notice that I’m getting the ctx parameter first, and that it’s not used in the code…

Upgrading the test to use a context with a timeout…

func TestHTTPLiveProviderWithContextTimeout(t *testing.T) {
	stop := make(chan bool)
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		<-stop
	}))
	defer ts.Close()

	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*400)
	defer cancel()
	_, err := HTTPLiveProvider(ts.URL)(ctx)
	stop <- true

	if err == nil {
		t.Fatal("expected a timeout error")
	}
	if e := err.Error(); e != "context deadline exceeded" {
		t.Fatalf("context: got %s, wanted %s", e, "context deadline exceeded")
	}
}

This time, I’m passing the context into the call to the HTTPLiveProvider, this requires some improvement in the interface:

// LiveProvider fetches the rates for the daily data.
type LiveProvider func(context.Context) (EuroRates, error)

// HistoricProvider fetches the historic rate data, and returns a map of dates
// to EuroRates.
type HistoricProvider func(context.Context) (map[string]EuroRates, error)

At this point, updating the code to take contexts is fairly simple, the two implementations both require to take a context, and the code is changd to update the closure to take a context.

func HTTPLiveProvider(url string) LiveProvider {
	return func(ctx context.Context) (EuroRates, error) {

and

func HTTPHistoricProvider(url string) HistoricProvider {
	return func(ctx context.Context) (map[string]EuroRates, error) {

The file-based providers must also implement the same interface, even tho’ they’ll likely discard the context.

Running the tests at this point doesn’t change anything, we’re not using the context, so the tests still hang.

This code brings in an external package, golang.org/x/net/context/ctxhttp, which packages the code in this early contexts blog post.

This makes it a really simple change…

// HTTPLiveProvider parses the specified URL and returns daily rates data.
// https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml
func HTTPLiveProvider(url string) LiveProvider {
	return func(ctx context.Context) (EuroRates, error) {
		resp, err := ctxhttp.Get(ctx, nil, url)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

The implementation of ctxhttp.Get takes a Context, an optional HTTPClient, and the URL you want to fetch and deals with waiting for the timeout on the context to fail.

$ go test -run 'TestHTTPLiveProviderWithContextTimeout$' .
ok  	github.com/bigkevmcd/euroxref/parser	0.420s

Notice that this test takes 0.420s, this is because it times out after 400ms.

It’s then a fairly simple task to fix up the gRPC handler to pass the context through…

func (r *RatesHandler) Live(ctx context.Context, req *proto.LiveRequest, rsp *proto.LiveResponse) error {
	currency := req.GetCurrency()

	rates, err := r.liveProvider(ctx)
	if err != nil {
		return errors.InternalServerError("internal error", "error: %s", err)
	}
	...
	return nil
}