特定URLへのアクセスを契機としたHTTPサーバのGraceful Shutdown
Goで特定のURLへのアクセス(例:GET /shutdown
)を受けたらHTTPサーバを停止するにはどうすればよいか考えてみました。
HTTPサーバを停止する
http.Server
にはGraceful Shutdownを行う Shutdown
メソッドがあります。/shutdown
へのリクエストを受けた契機で Server.Shutdown
を実行すれば停止できそうです。
func main() { m := http.NewServeMux() s := http.Server{Addr: ":8000", Handler: m} m.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { s.Shutdown(context.Background()) }) if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } log.Printf("Finished") }
HTTPサーバを実行して /shutdown
にリクエストを投げると、期待通りにHTTPサーバが終了することが分かります。
% go run main.go 2018/03/28 20:22:23 Finished
% curl -v localhost:8000/shutdown * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8000 (#0) > GET /shutdown HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.54.0 > Accept: */* > * Empty reply from server * Connection #0 to host localhost left intact curl: (52) Empty reply from server
レスポンスを返す
Shutdown
メソッドを実行する前にレスポンスを出力してみます。
func main() { m := http.NewServeMux() s := http.Server{Addr: ":8000", Handler: m} m.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) // レスポンスを返す if err := s.Shutdown(context.Background()); err != nil { log.Fatal(err) } }) if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } log.Printf("Finished") }
残念ながらレスポンスは返ってきません。HTTPサーバがレスポンスを返す前に停止してしまうためです。
% curl -v localhost:8000/shutdown * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8000 (#0) > GET /shutdown HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.54.0 > Accept: */* > * Empty reply from server * Connection #0 to host localhost left intact curl: (52) Empty reply from server
解1: goroutineからShutdownを呼び出す
そこで、HTTPハンドラとは別のgoroutineから Shutdown
メソッドを実行してみます。
func main() { m := http.NewServeMux() s := http.Server{Addr: ":8000", Handler: m} m.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) go func() { if err := s.Shutdown(context.Background()); err != nil { log.Fatal(err) } }() }) if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } log.Printf("Finished") }
レスポンスが返された後にHTTPサーバが停止していることが分かります。
% go run main.go 2018/03/28 22:43:09 Finished
% curl -v localhost:8000/shutdown * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8000 (#0) > GET /shutdown HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 28 Mar 2018 13:43:09 GMT < Content-Length: 2 < Content-Type: text/plain; charset=utf-8 < * Connection #0 to host localhost left intact OK
手元で試した限りでは Shutdown
メソッドは何回も呼ばれても問題ないようです。
% for i in {1..10}; do curl localhost:8000/shutdown &; done ... OKOKOK ...
解2: context.Contextで待ち合わせる
Shutdown
メソッドのコメントに気になる記述がありました。Shutdown
メソッドが制御を返してからプログラムを終了すべきとのことです。
When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return.
そこで、Context
がキャンセルされたらHTTPサーバを停止してプログラムを終了するようにします。
func main() { m := http.NewServeMux() s := http.Server{Addr: ":8000", Handler: m} ctx, cancel := context.WithCancel(context.Background()) defer cancel() m.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) // shutdown endpointへのリクエストを受けたらcontextをキャンセルする cancel() }) go func() { if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }() select { case <-ctx.Done(): // contextがキャンセルされたらHTTPサーバを停止する s.Shutdown(ctx) } log.Printf("Finished") }
レスポンスが返された後にHTTPサーバが停止していることが分かります。
% go run main.go 2018/03/28 20:33:53 Finished
% curl -v localhost:8000/shutdown * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8000 (#0) > GET /shutdown HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 28 Mar 2018 11:33:53 GMT < Content-Length: 2 < Content-Type: text/plain; charset=utf-8 < * Connection #0 to host localhost left intact OK
リクエストの内容を使った後続処理
HTTPサーバを停止した後、リクエストのクエリパラメータを使って後続処理を行うことを考えます。
リクエストを受けたらchannelにクエリパラメータを送るように修正します。
func main() { m := http.NewServeMux() s := http.Server{Addr: ":8000", Handler: m} codeCh := make(chan string) m.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) // クエリパラメータをchannelに送る codeCh <- r.URL.Query().Get("code") }) go func() { if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }() select { case code := <-codeCh: // HTTPサーバを停止した後にクエリパラメータを使った処理を行う s.Shutdown(context.Background()) log.Printf("Got code=%s", code) } log.Printf("Finished") }
/shutdown
にリクエストを投げると、クエリパラメータが表示されてからプログラムが終了することが分かります。
% go run main.go 2018/03/28 23:16:26 Got code=abc 2018/03/28 23:16:26 Finished
% curl -v "localhost:8000/shutdown?code=abc" * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8000 (#0) > GET /shutdown?code=abc HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 28 Mar 2018 14:16:26 GMT < Content-Length: 2 < Content-Type: text/plain; charset=utf-8 < * Connection #0 to host localhost left intact OK
まとめ
本稿で紹介した方法を使うと、シグナルの代わりにREST APIでHTTPサーバを停止することが可能です。また、int128/kubeloginではこの方法を応用することでOpenID Connectの認可コードの受け取りと後続処理を行っています。