概要
net.httpサーバをCtrl+Cで終了できるようにしたい。現在処理しているリクエストに関しては処理が終わるのを待ってgracefulに終了したい。
以下のようなコードだとCtrl+Cで停止させると、リクエストが処理中でもプロセスが終了してまう。
func main(){
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Hello World")
})
http.ListenAndServe(":80", nil)
}
Ctr+Cでgracefulに終了させるためには以下の処理を追加で実装する必要がある
- Ctrl+Cのイベントを受け取る(SIGTERMのシグナルを受け取る)
- signalを受け取ったらListenしているソケットを閉じる(新しいコネクションを受け付けないため)
- 現在処理しているリクエストの終了を待ち、プロセスを終了させる
修正点
Ctrl+Cのイベントを受け取る
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
シグナルをchannelで受け取る
signalを受け取ったらListenしているソケットを閉じる
laddr, _ := net.ResolveTCPAddr("tcp", ":80")
listener, _ := net.ListenTCP("tcp", laddr)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
go func() {
sig := <-c
listener.Close()
}()
ListenAndServeではhttpモジュール内で生成されたlistenerへの参照が得られないので、listenerを生成して後でhttp.Serveで渡すように修正
現在処理しているリクエストの終了を待ち、プロセスを終了させる
http.ServerのConnState変数に関数を設定する事でコネクションの状態が変化した時にコールバックを呼んでもらえる
var activeConWg sync.WaitGroup
var numberOfActive = 0
func connectionStateChange(c net.Conn, st http.ConnState) {
if st == http.StateActive {
activeConWg.Add(1)
numberOfActive += 1
} else if st == http.StateIdle || st == http.StateHijacked {
activeConWg.Done()
numberOfActive -= 1
}
log.Printf("Number of active connection: %d\n", numberOfActive)
}
sync.WaitGroupを使ってアクティブなコネクションが増えた時はAddを、IdleかHijackedのステータスになったらDoneを呼ぶ事でmain側でアクティブなコネクションが0になったらWaitから抜ける事が出来る.
srv := &http.Server{Handler: nil, ConnState: connectionStateChange}
srv.Serve(listener)
activeConWg.Wait()
コールバック関数をしかけたhttp.Serverのインスタンスを作り、Serveを呼び出す.
Serveから戻ったら(net.ListenerのCloseの呼び出しでServeから戻る)アクティブコネクションが0になるのを待って終了させる。
コード
最終的なコードは以下
package main
import (
"fmt"
"html"
"log"
"net"
"net/http"
"os"
"os/signal"
"sync"
"time"
)
var activeConWg sync.WaitGroup
var numberOfActive = 0
func connectionStateChange(c net.Conn, st http.ConnState) {
if st == http.StateActive {
activeConWg.Add(1)
numberOfActive += 1
} else if st == http.StateIdle || st == http.StateHijacked {
activeConWg.Done()
numberOfActive -= 1
}
log.Printf("Number of active connection: %d\n", numberOfActive)
}
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
laddr, _ := net.ResolveTCPAddr("tcp", ":80")
listener, _ := net.ListenTCP("tcp", laddr)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
go func() {
sig := <-c
fmt.Print(sig)
listener.Close()
}()
srv := &http.Server{Handler: nil, ConnState: connectionStateChange}
srv.Serve(listener)
activeConWg.Wait()
}
参考資料
http.ServerのServeの処理
https://github.com/golang/go/blob/master/src/net/http/server.go#L2273-L2289
- AcceptでTemporaryと判断されないエラーを起こせばServeのイベントループから抜ける
- net.ListenerのCloseメソッドを呼び出す事でイベントループから抜けれる
ConnState(関数じゃなくて定数の方)が取る値
https://github.com/golang/go/blob/master/src/net/http/server.go#L2140-L2186
http.Server内でsetState関数が呼ばれた際にConnState関数が呼ばれる
https://github.com/golang/go/blob/master/src/net/http/server.go#L1471-L1475
net.Errorについて書かれている
https://blog.golang.org/error-handling-and-go
- 当初のコードでnet.Errorをハンドルしてたので調べてたが、不要だったのでコードからは消した
Stopping a listening HTTP Server in Go
http://www.hydrogen18.com/blog/stop-listening-http-server-go.html
- 当初参考にしたブログ記事
- Timeout使ってAcceptから定期的に戻ってフラグをチェックするような感じで書かれていたが、listner直接Closeすればもっと簡単に書けそうだったので結局真似しなかった