Search code examples
httpgoshutdownpanic

HTTP Server Shutdown Randomly Panic


I am writing a package containing a controller that can start an HTTP server and a watchdog to stop the server when a specific HTTP request is given. However, the program will randomly crash because of nil pointer when the watchdog attempts to shutdown the HTTP server. It will crash about twice in 3 attempts. I have simplified the code below. If the code works as normal, it should shutdown the HTTP server after the first request. However, it will properly shutdown only once in three attempts. The other two attempts will end up in nil pointer panic.

// Controller is the controller of signal package.
// It controls the signal sub http server and make responses
// when a specific signal is given.
// It has two concurrent threads, one being the sub http server goroutine,
// the other being the WatchDog thread for rapid responses and timeout implementation.
type Controller struct {
    signal       chan int
    signalServer http.Server // The sub http server used.
}

// Start starts signal server and watchdog goroutine.
func (c *Controller) Start() {
    go c.signalServer.ListenAndServe()
    c.watchDog()
}

// Stop stops only the signal server.
// Watchdog need not and cannot stop as it can only be stopped from inside.
// Anyway, watchdog invokes Stop().
func (c *Controller) Stop() {
    log.Println("Stopping Signal server.")
    c.signalServer.Shutdown(nil)
}

// cSignalHandler gets a http handler that can access signal controller.
func (c *Controller) cSignalHandler() func(w http.ResponseWriter, r *http.Request) {
    // This is the actual handler.
    // This implementation can perform tasks without blocking this thread.
    // This handler will send 1 to channel for watchdog to handle.
    return func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Stopped"))
        c.signal <- 1
    }
}

// watchDog monitors signal through signal controller's signal channel.
// It performs responses to the signal instead of http server.
// It will shutdown http server when receives value from channel.
func (c *Controller) watchDog() {

    <-c.signal // Signal received.
    c.Stop() // Stop signal sub http server when watchDog exits.

    return
}

func main() {
    con := new(Controller)

    con.signal = make(chan int, 1)
    mux := http.NewServeMux()
    mux.HandleFunc("/signal/", con.cSignalHandler())
    con.signalServer = http.Server{
        Addr:    ":" + strconv.Itoa(8888),
        Handler: mux,
    }
    con.Start()
}

Also, switching the signalServer field to *http.Server will not help, adding defer to c.Stop() also have no effect. Even switching the signalServer field to *http.Server and check its nil will not help. That means,

type Controller struct {
    signal       chan int
    signalServer *http.Server // The sub http server used.
}

func (c *Controller) Stop() {
    log.Println("Stopping Signal server.")
    if c.signalServer != nil {
        c.signalServer.Shutdown(nil)
    }
}

And changes related code in main() will still randomly crash.

I have no idea what is going on here. I am using golang-1.9 on a 4.13.0-32-generic GNU/Linux machine.

The output stacktrace is below:

2018/02/14 13:37:50 Stopping Signal server.
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x5eab91]

goroutine 1 [running]:
net/http.(*Server).Shutdown(0xc420084ea8, 0x0, 0x0, 0x0, 0x0)
        /usr/lib/go-1.9/src/net/http/server.go:2506 +0x1b1
main.(*Controller).Stop(0xc420084ea0)
        /home/bhat/Dev/Projects/Go/signal/controller.go:31 +0x91
main.(*Controller).watchDog(0xc420084ea0)
        /home/bhat/Dev/Projects/Go/signal/controller.go:59 +0x45
main.(*Controller).Start(0xc420084ea0)
        /home/bhat/Dev/Projects/Go/signal/controller.go:23 +0x53
main.main()
        /home/bhat/Dev/Projects/Go/signal/main.go:18 +0x1a9
exit status 2

Solution

  • It's panicking because you are sending a nil context while shutting down the server.

    for {
        if srv.closeIdleConns() {
            return lnerr
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
        }
    }
    

    This is the snippet from http.Server.Shutdown. Since context is nil and function expects you to send a non-nil context, its panicking. You can fix it by sending context.Background()

    c.signalServer.Shutdown(context.Background())