もう少しSIGTERMと仲良くする

本稿は[Heroku Advent Calendar 2020](https://qiita.com/advent-calendar/2020/heroku)の6日目の記事です。5日目はsho7650さんによる「[[Heroku] index.html をダイレクトに開く静的コンテンツサイトを、すぐ公開したい](https://qiita.com/sho7650/items/47e56d35e56415df5669)」でした。buildpackは奥が深いよ!! 7日目はkimihomさんによる「[Heroku Addons で SendGrid を使ってる方が必要な年内対応](https://www.bokukoko.info/entry/2020/12/07/080000)」です。SendGridをご利用の方はお早めに！

Herokuではdynoの停止時にすべてのプロセスにSIGTERMを送り、メインプロセスの停止を待ってからdynoを停止させます([graceful shutdown](https://devcenter.heroku.com/articles/dynos#graceful-shutdown-with-sigterm))。ここでメインプロセスがシェルの場合には、SIGTERMの送信直後にシェルが停止するためdynoが停止されてしまいます。本稿ではシェルがメインプロセスの場合にもSIGTERMを無視するようにして、graceful shutdownするようにしてみます。

# Dynoでのプロセスの起動
Dynoの起動後に`ps -xf`コマンドを実行してプロセスツリーを確認してみると、Herokuではbashをログインシェルとして起動し`Procfile`の内容を実行させることで、スタートアップスクリプトの読み込みもおこなっているようです。

```
bash --login -c "puma -C config/puma.rb"
```

# Bashによるプロセスの実行
多くのHerokuアプリでは、プラットフォームが起動したbashは`Procfile`で指定されたコマンドをexecして自プロセスと置き換えます。Dynoの停止時にプラットフォームから全てのプロセスに送られたSIGTERMは`Procfile`で指定されたプロセスが直接受け取ることになります。Pumaなど多くのサーバはSIGTERMを受け取るとgraceful shutdownするようになっています。

プロセスツリーには`Procfile`で指定されたコマンドの親プロセスは見られません。

```
puma -C config/puma.rb
```

Bashがコマンドをexecできない場合には、指定されたコマンドをforkして子プロセスとして実行します。プラットフォームから送られたSIGTERMはbashにも届き、bashは停止し、アプリのプロセスが稼動を続けていてもdynoは停止されてしまいます。

[Bashのソースコードのコメント](https://github.com/bminor/bash/blob/ad1b3e6/builtins/evalstring.c#L409-L420)によれば、スタートアップスクリプトにtrapがあった場合にはbashはコマンドをexecするのではなくforkするようです。この場合プロセスツリーには`Procfile`で指定されたコマンドの親プロセスとして`bash`が見られます。

```
bash --login -c "puma -C config/puma.rb"
 \_ puma -C config/puma.rb
```

Bashは起動するコマンドが複文だった場合にもコマンドをforkするようです。

```
bash --login -c "puma -C config/puma.rb & sidekiq &"
 \_ puma -C config/puma.rb
 \_ sidekiq
```

# Forkするアプリでもgraceful shutdownしてもらう
メインプロセスがforkされるアプリでもgraceful shutdownさせるには、SIGTERMを受け取ってもbashが停止しないように、SIGTERMを無視させると良さそうです。`Procfile`の最初に下記を実行します。

```sh
trap '' SIGTERM
```

ついでに、複数稼動しているプロセスのいずれかが停止してしまった場合に、他のプロセスにもSIGTERMを送り、graceful shutdownしてもらいます。いずれかのバックグラウンドプロセスが停止するまで`wait -n`で待ち、`kill`コマンドですべてのバックグラウンドプロセスにSIGTERMを送り、そして、すべてのプロセスが停止するまで`wait`で待ちます:

```
wait -n; kill -SIGTERM -$$: wait
```

これらを併せて、pumaとsidekiqをバックグラウンドで起動しているアプリケーションでsidekiqがちゃんとshutdownするようになりました。

```
web: trap '' SIGTERM; puma -C config/puma.rb & sidekiq & wait -n; kill -SIGTERM -$$; wait
```

アプリケーションログを確認すると、`trap`の追加前にはbashがシグナルを受けて停止していた(exit statusが143だった)のに対して:

```
2020-10-27T18:28:53.674073+00:00 heroku web.1 - - Restarting
2020-10-27T18:28:53.676489+00:00 heroku web.1 - - State changed from up to starting
2020-10-27T18:28:54.965022+00:00 heroku web.1 - - Stopping all processes with SIGTERM
2020-10-27T18:28:55.263424+00:00 heroku web.1 - - Process exited with status 143
```

`trap`の追加後にはそれぞれのバックグラウンドプロセスが停止してからbashが停止する様子が確認できます:

```
2020-11-04T19:25:17.574465+00:00 heroku[web.1]: Restarting
2020-11-04T19:25:17.587549+00:00 heroku[web.1]: State changed from up to starting
2020-11-04T19:25:18.621855+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2020-11-04T19:25:18.660761+00:00 app[web.1]: [7] - Gracefully shutting down workers...
2020-11-04T19:25:18.660678+00:00 app[web.1]: pid=8 tid=1hsg DEBUG: Got TERM signal
2020-11-04T19:25:18.660736+00:00 app[web.1]: pid=8 tid=1hsg INFO: Shutting down
2020-11-04T19:25:18.660788+00:00 app[web.1]: pid=8 tid=1hsg INFO: Terminating quiet workers
2020-11-04T19:25:18.661283+00:00 app[web.1]: pid=8 tid=3fw8 INFO: Scheduler exiting...
2020-11-04T19:25:19.164122+00:00 app[web.1]: pid=8 tid=1hsg INFO: Bye!
2020-11-04T19:25:19.272989+00:00 heroku[web.1]: Process exited with status 0
```