To gracefully shutdown a http server requires a few steps.

The http library's serving function will block over there when getting started, until we call http.Server.Close(). As for us, we can invoke the starting function in a goroutine, and call close in another one.

Here is a full simple example:

package main

import (
    "context"
    "fmt"
    "github.com/argcv/stork/log"
    "github.com/gin-gonic/gin"
    "github.com/pkg/errors"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "time"
)

type HttpSrv struct {
    Port int

    server    *http.Server
    isStarted bool
    mtx       *sync.Mutex
}

func NewHttpSrv(port int) *HttpSrv {
    return &HttpSrv{
        Port:      port,
        server:    nil,
        isStarted: false,
        mtx:       &sync.Mutex{},
    }
}

func (srv *HttpSrv) Start() (err error) {
    srv.mtx.Lock()
    defer srv.mtx.Unlock()

    if srv.isStarted {
        return errors.New("Server is already started")
    }

    srv.isStarted = true

    // prepare router
    router := gin.Default()

    router.GET("/*uri", func(c *gin.Context) {
        uri := c.Param("uri")
        c.JSON(200, map[string]interface{}{
            "status": "ok",
            "uri":    uri,
        })
    })

    // prepare address
    addr := fmt.Sprintf(":%v", srv.Port)

    // prepare http server
    srv.server = &http.Server{
        Addr:    addr,
        Handler: router,
    }

    log.Infof("Starting http server: %v", srv)

    go func() {
        if err = srv.server.ListenAndServe(); err != nil {
            if err == http.ErrServerClosed {
                log.Infof("Server closed under request: %v", err)
            } else {
                log.Fatalf("Server closed unexpect: %v", err)
            }
        }
        // in case of closed normally
        srv.isStarted = false
    }()

    time.Sleep(10 * time.Millisecond)

    return
}

func (m *HttpSrv) Shutdown(ctx context.Context) (err error) {
    m.mtx.Lock()
    defer m.mtx.Unlock()

    if !m.isStarted || m.server == nil {
        return errors.New("Server is not started")
    }

    stop := make(chan bool)
    go func() {
        // dummy preprocess before interrupted
        //time.Sleep(4 * time.Second)

        // Close immediately closes all active net.Listeners and any
        // connections in state StateNew, StateActive, or StateIdle. For a
        // graceful shutdown, use Shutdown.
        //
        // Close does not attempt to close (and does not even know about)
        // any hijacked connections, such as WebSockets.
        //
        // Close returns any error returned from closing the Server's
        // underlying Listener(s).
        //err = m.server.Close()
        // We can use .Shutdown to gracefully shuts down the server without
        // interrupting any active connection
        err = m.server.Shutdown(ctx)
        stop <- true
    }()

    select {
    case <-ctx.Done():
        log.Errorf("Timeout: %v", ctx.Err())
        break
    case <-stop:
        log.Infof("Finished")
    }

    return
}

func main() {
    srv := NewHttpSrv(9990)
    if err := srv.Start(); err != nil {
        log.Fatalf("Start failed: %v", err)
        return
    }

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)
    // holding here
    // waiting for a interrupt signal
    <-quit
    log.Infof("[Control-C] Get signal: shutdown server ...")
    signal.Reset(os.Interrupt)

    // starting shutting down progress...
    log.Infof("Server shutting down")
    // context: wait for 3 seconds
    ctx, cancel := context.WithTimeout(
        context.Background(),
        3*time.Second)

    defer cancel()
    // call for shutdown
    if err := srv.Shutdown(ctx); err != nil {
        log.Errorf("Server Shutdown failed: %v", err)
    }
    log.Infof("Server exiting")
}


Yu

Ideals are like the stars: we never reach them, but like the mariners of the sea, we chart our course by them.

2 Comments

Piero · May 12, 2019 at 10:37

Google Chrome 74.0.3729.131 Google Chrome 74.0.3729.131 Mac OS X  10.13.3 Mac OS X 10.13.3
// context: wait for 3 seconds
    ctx, cancel := context.WithTimeout(
        context.Background(),
        3*time.Second)

Why 3 seconds?
What happen with connections if they are still open?
there is a way to wait until all http connections are closed?

    Yu · May 12, 2019 at 12:39

    Google Chrome 74.0.3729.131 Google Chrome 74.0.3729.131 Mac OS X  10.14.4 Mac OS X 10.14.4

    This line will generate a context with a deadline ( https://golang.org/pkg/context/#WithTimeout ), and pass to `srv.Shutdown(ctx)`. It will be effected in the select {} statement and return directly after 3 seconds.

    What should be noticed is the `time.Sleep(4 * time.Second)` in the `Shutdown` statement was intentionally used to generate a timeout and the `http.Server.Close()` will close all the connections immediately.

    I just update the code and removed the sleep, and called `http.ServerShutdown(ctx)` instead. If you really wish to use it to handle some real requests, please use this one.

    In generally an HTTP request should never execute too long time, hundreds of milliseconds is already to be a very dangerous behavior. I am afraid 3 seconds is a long enough time here. If you wish to “wait until all http connections are closed”, that’s easy, you can use the default ctx instead:

    ctx := context.Background()
    
    // remove the calling to cancel
    //  defer cancel()
    

Leave a Reply to Yu Cancel reply

Your email address will not be published. Required fields are marked *