Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go における Gra ceful Shutdown とハマりどころ

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for ogidow ogidow
November 09, 2026
240

Go における Gra ceful Shutdown とハマりどころ

Avatar for ogidow

ogidow

November 09, 2026

Transcript

  1. 送信したらマークしてマーク済みはスキップする func notify(w http.ResponseWriter, r *http.Request) { ctx := r.Context()

    // このリクエストの ctx id := r.URL.Query().Get("id") for _, u := range users { if isSent(id, u) { // 送信済みならスキップ continue } sendMail(u) // 1通1秒かかる markSent(ctx, id, u) // 送信済みとして DB に記録する } } これでやり直しても送信した人には届かない © LayerX Inc. 9
  2. 問題はメール送信とマークの間 for _, u := range users { if isSent(id,

    u) { // 送信済みならスキップ continue } sendMail(u) // メールは届く // ← SIGTERM markSent(ctx, id, u) // 実行されない } ここで が来てサーバが終了 記録が残らないのでやり直すと同じ人にまた届く © LayerX Inc. 10
  3. Shutdown を呼ぶと受付を止めて処理中のリクエストを待つ func main() { // SIGTERM を受けるとこの ctx がキャンセルされる

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() srv := &http.Server{Addr: ":8080", Handler: http.HandlerFunc(notify)} go srv.ListenAndServe() <-ctx.Done() // SIGTERM が来るまでここで待つ // 処理中のリクエストが終わるのを最大10秒待つ shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() srv.Shutdown(shutdownCtx) } © LayerX Inc. 13
  4. 1通送るたびにキャンセルされていないか確認する func notify(w http.ResponseWriter, r *http.Request) { ctx := r.Context()

    // このリクエストの ctx id := r.URL.Query().Get("id") for _, u := range users { if ctx.Err() != nil { // キャンセルされていたら抜ける return } if isSent(id, u) { // 送信済みならスキップ continue } sendMail(u) // 1通1秒かかる markSent(ctx, id, u) // 送信済みとして DB に記録する } } © LayerX Inc. 14
  5. SIGTERM が来てもキャンセルされず if では抜けない for _, u := range users

    { if ctx.Err() != nil { // nil → return } if isSent(id, u) { // 送信済みならスキップ continue } sendMail(u) // メールを送り続ける markSent(ctx, id, u) // 送信済みとして DB に記録する } のまま 抜けない 10秒たつと Shutdown が時間切れになりプログラムが終了する 送信とマークの間で終了するとやり直したときに二重に届く © LayerX Inc. 16
  6. Shutdown はハンドラに何も伝えない Shutdown gracefully shuts down the server without interrupting

    any active connections. ... and then waiting indefinitely for connections to return to idle ... Shutdown は処理中の接続を中断せずにサーバを止める 接続がアイドルに戻るのを無期限に待つ net/http ドキュメント(Server.Shutdown)と日本語訳 © LayerX Inc. 17
  7. main が終わるとハンドラも途中で消える main Shutdown で待っている ハンドラ メールを送り続ける(ctx はキャンセルされない) つまり 0秒:SIGTERM

    10秒:main が終わる → プログラムも終わる SIGTERM が来てもハンドラは気づかずメールを送り続ける 10秒たつと main が終わりハンドラも途中で消える 送信とマークの間で消えるとやり直したときに二重に届く © LayerX Inc. markSent 18
  8. キャンセルは親から子にしか伝わらない parent, cancel := context.WithCancel(context.Background()) child, _ := context.WithCancel(parent) other,

    _ := context.WithCancel(context.Background()) cancel() child.Err() // context canceled ← 子には伝わる other.Err() // nil ← 親が違うと伝わらない © LayerX Inc. 19
  9. main の ctx とハンドラの ctx はつながっている? func main() { //

    SIGTERM ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) // notify srv := &http.Server{Addr: ":8080", Handler: http.HandlerFunc(notify)} ... } でキャンセルされる をこのサーバのハンドラとして登録する func notify(w http.ResponseWriter, r *http.Request) { // ctx := r.Context() ... } ハンドラが見ている r.Context() © LayerX Inc. の親は誰? 20
  10. r.Context() の親は net/http の中で作られる // net/http (*Server).Serve // baseCtx r.Context()

    baseCtx := context.Background() if s.BaseContext != nil { baseCtx = s.BaseContext(origListener) } この BaseContext をもとに が作られる を渡していないので親は context.Background() srv := &http.Server{Addr: ":8080", Handler: http.HandlerFunc(notify)} main の ctx とは親が違うので SIGTERM は伝わらない © LayerX Inc. 21
  11. BaseContext に main の ctx を渡す srv := &http.Server{ Addr:

    ":8080", Handler: http.HandlerFunc(notify), BaseContext: func(net.Listener) context.Context { return ctx }, } © LayerX Inc. 22
  12. r.Context() の親が main の ctx になる main の ctx(signal.NotifyContext) BaseContext

    で親にする r.Context()(ハンドラが見ている) SIGTERM のキャンセルがハンドラまで伝わる SIGTERM が来た時点で もキャンセルされる ハンドラは次の で気づく r.Context() ctx.Err() © LayerX Inc. 23
  13. SIGTERM が来たら if で抜けるようになった for _, u := range users

    { if ctx.Err() != nil { // context canceled → return } ... } 直す前 Shutdown は10秒待って時間切れ ハンドラは途中で消える © LayerX Inc. ここで抜ける 直した後 Shutdown はすぐ戻る ハンドラは if で抜ける 24
  14. 送信中に SIGTERM が来ると markSent が失敗する for _, u := range

    users { if ctx.Err() != nil { return } ... sendMail(u) // この送信中に SIGTERM が来ると markSent(ctx, id, u) // ctx } はキャンセル済みで失敗する 記録が残らないのでやり直すと同じ人にまた届く © LayerX Inc. 25