Ruby アプリケーションの再起動動作
最終更新日 2023年03月20日(月)
Table of Contents
Heroku は (再起動または新しいデプロイなどのために) dyno をシャットダウンしようとする場合、まずその dyno 内のプロセスに SIGTERM シグナルを送信します。 この完全なプロセスは、「dyno および Dyno Manager」に記載されています。
この UNIX シグナルは、プロセスが “グレースフルシャットダウンする” 機会を提供します。Ruby VM はこのシグナルを受信し、プロセスに SignalException
を送信します。これは、そのプロセスが現在実行している処理に割り込むため、プロセスは自身をクリーンアップできます。
プロセスのクリーンアップの保証
プロセスは、どのように正確に自身をクリーンアップするのでしょうか。この時点で、ensure
キーワードを使用する Ruby プログラムがアクティブ化されます。次に例を示します。
thread = Thread.new do
begin
while true
sleep 1
end
ensure
puts "ensure called"
end
end
current_pid = Process.pid
signal = "SIGTERM"
Process.kill(signal, current_pid)
これを実行すると、次のように表示されます。
ensure called
Terminated: 15
開発者は、一時ファイルを削除したり、接続を閉じたりするような、慎重な扱いが必要な操作を自然にこの ensure ブロック内にラップします。これらの操作を ensure
ブロック内に配置することにより、プログラムが終了時に適切な処理を行う可能性を高くします。
すべての (スコープ内にある) ensure ブロックが呼び出されたら、プログラムは終了します。
Heroku は、アプリケーションに SIGTERM
を送信した後、最大 30 秒待機しててから (まだクリーンアップが完了されていない場合でも) そのアプリケーションを強制的にシャットダウンするために SIGKILL
を送信します。この例では、ensure
ブロックがまったく呼び出されることなく、プログラムは単純に終了します。
thread = Thread.new do
begin
while true
sleep 1
end
ensure
puts "ensure called"
end
end
current_pid = Process.pid
signal = "SIGKILL"
Process.kill(signal, current_pid)
出力は単純に次のようになります。
Killed: 9
これは、有名な $ kill -9
コマンドの実行に相当します。これは、Windows での CTRL+ALT+DELETE または OS X での “強制終了” と同じ意味です。
一部のプログラムが停止しない理由
一部のプログラムは、SIGTERM
の後、自身では決して終了しません。 次に例を示します。
thread = Thread.new do
begin
while true
sleep 1
end
ensure
while true
puts "ensure called"
sleep 1
end
end
end
current_pid = Process.pid
signal = "SIGTERM"
Process.kill(signal, current_pid)
出力は次のようになります。
ensure called
ensure called
ensure called
ensure called
ensure called
ensure called
ensure called
ensure called
ensure called
永久に続きます。while ループの代わりに、ensure
ブロックは接続を閉じようとし、それにリモートサーバーが応答しないことが想像されます。この場合、プログラムは決して終了せず、リクエストに応答することも停止することもできずにスタックします。
そのため、これは複雑で、長い、または完了することが不可能なタスクを ensure ブロック内で実行することにより、プログラムが自身を停止できなくする 1 つの方法です。
ensure ブロックを使用していない場合でも、それを実行する gem が間違いなく使用されています。それらのうちの 1 つが、プログラムの終了を阻止している可能性があります。この問題を解決するには、プロセスをただちに終了させる SIGKILL
をそのプロセスに送信します。
プログラムが自身を停止できなくなる可能性がある他の方法は、シグナルをキャッチした場合です。
Signal.trap('TERM') do
puts "Never going to die"
end
current_pid = Process.pid
signal = "SIGTERM"
Process.kill(signal, current_pid)
この Signal.trap
ブロックはシグナルをキャッチし、その元の作業を実行できないようにします。この機能を使用する良い理由は見つからず、プログラムではおそらく使用しない方が無難です。シグナルを捕捉する必要がある場合は、以前の動作 “シグナルが送信されたらコードを実行するが、Ruby でシグナルを捕捉しない” を呼び出すことができます。これもお勧めできませんが、プログラムはシグナルを受信すると、すぐに終了します。また、これが呼び出される保証もありません。より安全なアプローチは、必要に応じて ensure
ブロックを使用することです。
at_exit
アプリケーションが終了するときに、Ruby の at_exit を使用してコードを呼び出すこともできます。
at_exit { puts "done" }
current_pid = Process.pid
signal = "SIGTERM"
Process.kill(signal, current_pid)
# => done
# => Terminated: 15