Stopping a listening HTTP Server in Go

One of the great things about Go is that you can have an application up and serving HTTP requests in no time at all. This is all done courtesy of the net/http package that comes with Go. You can register handlers and call http.ListenAndServe to serve HTTP requests until it encounters an error.

This works if nothing else is going on in your application. If your application needs to do something else, you can always launch http.ListenAndServe on a separate goroutine. In my case I have a specific need to shutdown many communicating goroutines in a specific order. The first goroutine that needs to shutdown is the one serving HTTP requests. The built-in HTTP server does not include any mechanism to shut it down. If you have it running on a separate goroutine when your main() function exits, then any pending requests are dropped. I needed an easy to way to shutdown the HTTP server.

Listening for the signal to shutdown

Any POSIX operating system allows for a process to be signalled. This is what the kill and killall shell utilities do on Unix-like machines, they signal the process to stop. An application can listen for these signals using the os/signal package. Calling signal.Notify allows an application to have POSIX signals sent down a pipe of type chan os.Signal. The specific signal to listen for shutdown is syscall.SIGINT.

By waiting for the signal, the process can know when to begin an orderly shutdown.

Stopping an HTTP server

The convenience function http.ListenAndServe in Go opens a listening socket on the provided address and serves it using either the default handler or the provided one. Taking a look at server.go I found that all this does is creates an instance of the http.Server struct and then calls the method ListenAndServe on it. This method just calls the Serve method with a signle argument: a listening socket. This listener can be anything satisfying the net.Listener interface. Since the Serve method is exported it can be called from my code.

Looking at the Serve method's code I found that it contains some logic to handle errors. But if an error doesn't satisfy net.Error it returns immediately. I decided the simplest approach is to pass in my own implementation of net.Listener that can be commanded to return an error when it is time for the server to shutdown. This allows for the goroutine that is running the Serve method to be shutdown.

I defined the following struct

type StoppableListener struct {
    *net.TCPListener          //Wrapped listener
    stop             chan int //Channel used only to indicate listener should shutdown
}

I embedded a type of *net.TCPListener so that my struct gains all the methods needed to satisfy the net.Listener interface. Constructing a StoppableListener is done thusly

func New(l net.Listener) (*StoppableListener, error) {
    tcpL, ok := l.(*net.TCPListener)

    if !ok {
        return nil, errors.New("Cannot wrap listener")
    }

    retval := &StoppableListener{}
    retval.TCPListener = tcpL
    retval.stop = make(chan int)

    return retval, nil
}

The New function takes an argument of net.Listener and uses a type assertion. This is done because convenience functions like net.Listen return a net.Listener. The channel is part of the struct only to signal it to shutdown, no messages are passed over it. Trying to read from a closed channel results in a detectable failure, which is used to signal the listener to shutdown.

This channel of course has to be checked. The Serve method of http.Server spends most of its time calling Accept on the net.Listener passed to it. I decided to hide the Accept method of the embedded *net.TCPListener.

func (sl *StoppableListener) Accept() (net.Conn, error) {

    for {
        //Wait up to one second for a new connection
        sl.SetDeadline(time.Now().Add(time.Second))

        newConn, err := sl.TCPListener.Accept()

        //Check for the channel being closed
        select {
        case <-sl.stop:
            return nil, StoppedError
        default:
            //If the channel is still open, continue as normal
        }

        if err != nil {
            netErr, ok := err.(net.Error)

            //If this is a timeout, then continue to wait for
            //new connections
            if ok && netErr.Timeout() && netErr.Temporary() {
                continue
            }
        }

        return newConn, err
    }
}

This definition of the Accept method calls the embedded version. But before doing that it sets a timeout. When the call Accept returns, it immediately checks the stop channel to see if it is closed. Reading from a stopped channel results in the select falling into that case immediately. When that case is reached, it returns StoppedError. The logic in Serve doesn't know what to do with StoppedError and gives up, allowing the goroutine running it to exit.

Originally, I was checking if a connection was returned from Accept before checking the stop channel. This worked too, but if the call to Accept always returned a valid connection, the goroutine would never shutdown. This could happen during a period of high load on the HTTP server.

You do of course need a way to call close on the stop channel.

func (sl *StoppableListener) Stop() {
    close(sl.stop)
}

Calling stop results in the listener shutting down the next time the call to Accept times out.

Drawbacks

This solution has a couple of problems.

The first is that the goroutine must be calling Accept on the listener for it to work at all. It is possible to serve requests on the same thread that listens for connections, but that would be non-idiomatic in go.

The second drawback is that shutdown is not immediate. You can't select against a net.Listener, so I use a timeout and check the channel after each timeout. In my case the server is very long lived, with no intention for frequent shutdowns so there is no real impact from this. Using the timeout does cause the goroutine to wake up and consume CPU cycles. There are more elaborate solutions to this problem involving the use of additional channels and goroutines, but I don't think the extra complexity is warranted.

Waiting for termination

The StoppableListener presented above just allows the shutdown of a goroutine sitting in the Serve method of http.Server to be signaled to shutdown, it doesn't actually guarantee that it has shutdown.

To do that, you can use the sync package. The sync.WaitGroup is used like a reverse semaphore. Instead of waiting for a value to become non-zero and decrementing it, it waits for the value to become zero.

Instead of calling Serve you can wrap it in a function that decrements a sync.WaitGroup on exit.

var wg sync.WaitGroup
go func() {
    wg.Add(1)
    defer wg.Done()
    server.Serve(stoppableListener)
}()

By using the defer feature of Go, the method call wg.Done() executes after server.Serve(stoppableListener) returns. After calling StoppableListener.Stop() the main() goroutine just calls wg.Wait() to be sure that the goroutine servicing HTTP requests has exited.

Source Code

The full source code that uses the concepts presented here is available on github.

Once you have Go installed and your GOPATH environmental variable set, to run the example just run the following commands.

go get github.com/hydrogen18/stoppableListener
go install github.com/hydrogen18/stoppableListener/example
$GOPATH/bin/example

The example listens on port 8080 on the localhost. You can test that it works by running the following command in a separate terminal.

curl http://127.0.0.1:8080

You should see the line "Hello HTTP!". In the original terminal you can press Ctrl+C to send the interrupt signal. You should see the following output.

Serving HTTP
^CGot signal:interrupt
Stopping listener
Waiting on server

That's all there is to it.


Copyright Eric Urban 2014, or the respective entity where indicated