Gopher image by Maria Letta

Stopping HTTP Server Gracefully: Context vs Channels vs SyncGroup

Akshay Gollahalli

--

Text version of the code is available here

I am developing a CLI application that requires it to authenticate and obtain a token from an API. I had a problem of gracefully shutting down the HTTP server from another function (handle), in this case, after a token is received. Go 1.8 introduced Shutdown that gracefully shuts down the server without interrupting any active connections.

In this post, we will look at using three ways to tell the server to shut down gracefully. Also, I am using Gorilla’s mux router.

Before we go into the details, there are few common functions between these three implementations:

  1. There are two handles (routes); HomeHandler — that routes to http://127.0.0.1:8000/, which is our index page and ExitHandler — that routes to http://127.0.0.1:8000/exit, which is used to shut down the server.
  2. The server always starts in a goroutine.
  3. The program doesn’t exit until some kind of wait request is completed.

Using with Channels

Channels are like pathways; it joins goroutines to send and receive messages. For example:

See https://play.golang.org/p/BC3IBnNjzb5

In the above example, we created a channel called message, then an anonymous goroutine that prints a text to console runs, and finally, a string is sent to the message channel. The channel doesn’t let the program end unless a message is received to receivedMessage. Once a message is received, the text is assigned to receivedMessage, then prints it out and eventually exits the program.

Using the channels, let’s see how we can shut down the server from a different handle:

In the above example, we have a global channel — stopHTTPServerChan (line 13) of type bool. In the main() function, let’s a make a channel that has a data type of `bool` and assign it to our global variable stopHTTPServerChan (line 34). When the server starts it won’t end abruptly, because of <-stopHTTPServerChan (line 58), the program waits here till a boolean signal is received. Under ExitHandler() send a message, a boolean signal as stopHTTPServerChan <- true (line 30, this could also be false), so whenever you go to http://127.0.0.1:8000/exit, a signal is sent to stopHTTPServerChan, once the boolean value is received, the wait is over then it proceeds to the next line.

Using with Context

Note: According to the documentation, contexts should never be stored in struct type, but rather it should be passed through as an argument to a function. There are a few exceptions; context.CancelFunc is one such exception that can be put in a structure.

Contexts in the backend use channels to send and receive messages, but in a server scenario, every request received runs on a goroutine. Some HTTP requests might take more time than required, for fewer request the server should usually be able to handle them without using too many resources. Still, when there are 1000’s of request per-second the system might crash or take more resources. For this reason, the context library comes with few helpful functions, such as — WithCancel, WithDeadline, WithTimeout, and WithValue — that helps in destroying a request if it takes more time that is allocated to it.

See https://play.golang.org/p/3scpKiCypIS

Above example works exactly like channels example, only that you don’t have to make a channel and looks pretty.

From the above example, let’s create a httpServerHelper structure with cancelFunc of type context.CancelFunc (line 13–15). The context.WithCancel(), returns a context and a cancel function, let's assign it to stopHTTPServerCtx and cancel (line 36), assign the cancel() function to httpServerHelper structure’s cancelFunc and call it serverHelper (line 37). When you go to http://127.0.0.1:8000/exit, cancel() function is invoked which sends a Done() signal at line 61. When there is no error and all the channels are executed, context.Canceled returns a string else an Error.

Using with WaitGroup

Unlike channels and context, SyncGroup doesn’t use channels but uses a low level “mutual exclusion locks”. sync.WaitGroup structure has three functions Add(), Done(), and Wait(). Add() takes in an integer value greater than 0, Done() decrements the integer value by 1, and Wait() stops the program going further till the integer becomes 0.

See https://play.golang.org/p/OkdAXI8DdOz

In the above example, assign sync.WaitGroup{} to wg, because there is only one goroutine so adding 1 should be enough, if there are say four goroutines then you should have wg.Add(4) also, wg.Done() should be called four times. Once, goroutine reaches wg.Done() the 1 is decremented to 0, eg.Wait() check for it and continues the program.

In the above program, let’s create a global variable wg of type sync.WaitGroup (line 14). In the main() function add wg.Add(1) (line 37). In the ExitHandler add defer wg.Done() (line 26), the `defer` keyword executes at the end of the function. wg.Wait() checks the counter has decremented to zero or not, in this case, it would when you visit http://127.0.0.1:8000/exit, this then continues to the next line.

Conclusion

Channels are the star of Go language’s coroutines, but they make it easier for users to use different ways to control the execution of coroutines in a program. It is up to you which one to use, and it mostly depends on the problem too, for a simple coroutines communication, channels should be fine. Context, on the other hand, was made to use with servers but can also be used in simple programs. The sync library was developed to have low-level APIs for Go language to use internally. Still, SyncGroup can be used as a high-level API to control coroutines execution too.

I hope this post helps you in choosing the right one.

--

--

Akshay Gollahalli

PhD student, currently doing research on Spiking Neural Networks and Brain Computer Interfaces