6
0

More than 1 year has passed since last update.

【Go】Graceful Shutdownではまったところ

Last updated at Posted at 2023-06-26

概要

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ページもいくつかありましたので、参考にされる際はご注意ください。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0