概要
net/httpパッケージのServer.Shutdownを利用しているのになぜかGraceful Shutdownされないことがあったので、原因と対策をまとめました。
GracefulにShutdownしない実装
以下のコードを実行し、ブラウザから http://localhost:8080/ にアクセスすると5秒後に「hello world」と表示されます。
ブラウザから http://localhost:8080/ にアクセスした直後にCtrl+Cでプログラムを終了すると、GracefulにShutdownするなら「hello world」と表示されるはずですが、実際は表示されません。
package main
import (
"context"
"io"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
io.WriteString(w, "hello world")
})
server := &http.Server{
Addr: ":8080",
Handler: nil,
}
go func() {
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Println(server.ListenAndServe())
}
原因
最後の行で実行しているListenAndServe()は、Shutdown()が実行されると直ちにErrServerClosedが返されます。
そのためゴルーチンで実行しているShutdown()が完了するより前にmain関数が終了し、
main関数が終了したことでmain関数で実行しているゴルーチンが強制終了されてしまいます。
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.
対策
対策は以下2つのパターンが考えられます。
1.WaitGroupを利用する
Shutdown()のコメントに記載されている通りに、main関数がShutdown()が完了を待ってから終了するように実装を修正します。
修正にはWaitGroupを利用します。
package main
import (
"context"
"io"
"log"
"net/http"
"os"
"os/signal"
"sync"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
io.WriteString(w, "hello world")
})
server := &http.Server{
Addr: ":8080",
Handler: nil,
}
wg := &sync.WaitGroup{}
//WaitGroupのカウンタを1増やす
wg.Add(1)
go func() {
//ゴルーチンが完了してからWaitGroupのカウンタを1減らす
defer wg.Done()
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Println(server.ListenAndServe())
//WaitGroupのカウントが0になるまで待機する
wg.Wait()
}
2.ゴルーチンでサーバーを起動し、main関数でシャットダウンする実装に修正する
main関数でサーバを起動し、ゴルーチンでシャットダウンする実装ではmain関数側でゴルーチンの完了を待機する必要がありましたが、ゴルーチンでサーバを起動してmain関数でシャットダウンするように実装すればゴルーチンの完了を待機する必要はありません。
package main
import (
"context"
"io"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
io.WriteString(w, "hello world")
})
server := &http.Server{
Addr: ":8080",
Handler: nil,
}
go func() {
log.Println(server.ListenAndServe())
}()
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
}
まとめ
net/httpパッケージのServer.Shutdownではまったところについてまとめました。
GracefulにShutdownしない実装を例としてまとめているWebページもいくつかありましたので、参考にされる際はご注意ください。