Manage channel operations using context

Home RSS

2017-05-05

I was recently working on a small personal project when I came across a problem during some testing. I had written an application that starts a HTTP server, accepts requests, and serves content from an in-memory data store.

The basic application logic is as follows:

  1. Start servers and listen for requests.
  2. Create a channel to handle data store read requests.
  3. Upon accepting a request, push a data store read request onto the channel.
  4. Send data from data store down returning data read request channel.
  5. Serve data to client.

So, the problem I noticed was the ability for the request to be cancelled at any point for one of several reasons: request timeout; client cancellation; server error; etc. Any one of these errors can happen at any point throughout the request, more importantly, once the individual request has been pushed onto the data read request channel. This is a huge problem as there could be potential items in this channel that are no longer needed, and because unbuffered channels in Go are blocking, the sender blocks until the receiver has received a value.

After a bit of trial and error, research, and unanswered questions, I decided to use the request context to try and solve this problem. The context package was introduced into the standard library in Go 1.7, but prior to this it was part of the golang.org/x/net/context library. To give you a bit of Background():

At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request. https://blog.golang.org/context

As mentioned earlier, the request can be cancelled at any time, therefore we have to check for this in several different places. Initially, I did this in the request handler, but after some more testing I realised the request could have made its way to the read channel. In that case, we also have to check for any cancellation while getting data from the data store.

I am still unsure if this is the best approach, but I have manually and unit tested several scenarios and I am pretty confident with this solution. Some basic pseudo code is as follows:

package main

import (
    "context"
    "net/http"
    "time"
)

type dataRequest struct {
    data chan string
    ctx  context.Context
}

func handler(reqStream chan dataRequest) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
        defer cancel()

        req := dataRequest{
            data: make(chan string, 1),
            ctx:  ctx,
        }

        select {
        case reqStream <- req:
            // request pushed to que
        case <-ctx.Done():
            // don't push onto reqStream if ctx done
        }

        select {
        case <-ctx.Done():
            // don't try and serve content if ctx done
        case data := <-req.data:
            // return data to client
        }
    }
}

func main() {
    dataReqs := make(chan dataRequest)
    go func() {
        for {
            select {
            case req := <-dataReqs:
                select {
                case <-req.ctx.Done():
                    // don't push onto data channel if ctx done
                case req.data <- "some data":
                    // get data from store
                }
            }
        }
    }()
    http.HandleFunc("/", handler(dataReqs))
    http.ListenAndServe(":8080", nil)
}

Resources