Goでgraceful shutdownをミニマムに書く

graceful shutdownの仕組みがわかってなかったので自分でミニマムに実装してみる。

コード量が小さく読みやすかったので以下のパッケージを参考にした。

github.com

github.com

実装

自分でミニマムに実装したコードは以下の通り。アクティブな接続が返ってこなかった場合とかも考えなくてはいけないが、ここでは理解のため手元で動かして簡単に試す程度の実装をしている。

package main

import (
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(10 * time.Second)
    w.Write([]byte("Hello, middleware!\n"))
}

func main() {
    http.HandleFunc("/", indexHandler)

    srv := &http.Server{Addr: ":8888", Handler: http.DefaultServeMux}

    var wg sync.WaitGroup
    srv.ConnState = func(conn net.Conn, state http.ConnState) {
        switch state {
        case http.StateActive:
            log.Println("StateActive!!!")
            wg.Add(1)
        case http.StateIdle:
            log.Println("StateIdle!!!")
            wg.Done()
        }
    }
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Fatal(err)
    }
    waitSignal(l)
    err = srv.Serve(l)
    wg.Wait()
    if err != nil {
        log.Fatal(err)
    }
}

func waitSignal(l net.Listener) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT)
    go func() {
        sig := <-c
        switch sig {
        case syscall.SIGINT:
            signal.Stop(c)
            l.Close()
        }
    }()
}

動作確認すると、

// server
$ go run test.go // サーバ起動

// client
$ curl localhost:8888 // curl実行

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!! // 特に何もしない

// client
$ curl localhost:8888
Hello, middleware! // 正常にレスポンスが返る

// client
$ curl localhost:8888
Hello, middleware!
$ curl localhost:8888 // 再度curl実行

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!!
2017/12/20 11:11:59 StateActive!!!
^C // Ctrl+CでSIGINTを送る

// client
$ curl localhost:8888
Hello, middleware!
$ curl localhost:8888
Hello, middleware! // 正常にレスポンスが返る

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!!
2017/12/20 11:11:59 StateActive!!!
^C2017/12/20 11:12:09 StateIdle!!! // 既に確立されたコネクションは正常にレスポンスを返している
2017/12/20 11:12:09 accept tcp [::]:8888: use of closed network connection
exit status 1 // 確立されたコネクションが全てレスポンスを返してからサーバシャットダウンしている

感想

ポイントは

  • プロセスを終了するシグナルをsignal.Notifyで捕まえて処理を変更すること
  • http.StateActiveな接続が存在する場合はプロセスがすぐに終了しないようにすること

だろうか。最初はリスナーをクローズすると確立されたConnも終了してしまうのかと思っていたが、ここに関してはTCPListener.Closeがうまい感じにやってくれている。このためリスナーをクローズしたタイミングで新規の接続は受け付けないといったことも実現できている。

https://golang.org/pkg/net/#TCPListener.Close

Close stops listening on the TCP address. Already Accepted connections are not closed.