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.