Webtech Walker

HerokuでResqueを使うときに優雅に再起動する

Ruby製のジョブキューサーバーであるResqueはHerokuのWorkerプロセスで動かそうとすると一つ問題があった。

シグナルハンドリングの問題なんだけど、Herokuはworkerプロセスを再起動するときにSIGTERMを送り、プロセスが終了したら再度プロセスを起動する。SIGTERMを送ってworkerが10秒間プロセスが終了しなかったらSIGKILLで強制終了させる。のでworker側はSIGTERMを受け取ったら10秒以内に安全に(今あるジョブを終了するなりなんなりして)プロセスを終了する必要がある。

そのようなHerokuの挙動は以下に書いてある。

Managing Heroku Processes | Heroku Dev Center

一方で、Resqueのシグナルハンドリングがどうなっているかというと、SIGTERMで強制終了するようになってる。

resque/README.markdown at 1-x-stable · defunkt/resque

なのでHerokuでResqueを使った場合、再起動するときに安全にプロセスが再起動できないという問題があったというわけ。

なのでこんな感じのforkしてシグナルハンドリングの部分だけパッチ当てたやつとかもあった。

mjezzi/resque-cedar · GitHub

で、それがResqueの1.22.0で解決されたみたい。

1.22.0ではTERM_CHILD=1というのを環境変数で設定すればマスタプロセスがSIGTERMを受け取ったときに、起動している子プロセスに対してSIGTERMを送り、子プロセスがRESQUE_TERM_TIMEOUTで設定された秒数の間に終了しなかったら子プロセスにSIGKILLを送って強制終了させるという機能が実装された。これによって

$ TERM_CHILD=1 RESQUE_TERM_TIMEOUT=10 QUEUES=* rake resque:work

のように起動し、worker側でSIGTERMをハンドリングすることで安全に再起動できるようになる。

試してみる

# worker.rb

require 'resque/errors'

class SampleWork
  @queue = :test

  def self.perform
    sleep 10
    puts 'complete job!'
  rescue Resque::TermException
    sleep 2
    puts 'graceful shutdown!'
  end
end
# clinet.rb

require 'resque'
require './worker'

Resque.enqueue SampleWork
# Rakefile

require 'resque/tasks'
require './worker'

こんな感じのworkerをつくって試してみる。Resque::TermExceptionという例外でResqueからのSIGTERMをキャッチできるようなのでこのworkerはSIGTERMを受け取ったら2秒待って文字列を出力した後終了することが期待される。

まずは普通にworkerを起動してみる。

$ QUEUES=* rake resque:work                       

この状態でclinet.rbでジョブを登録して10秒以内にResqueのマスタプロセスに対してSIGTERMを送る。

$ kill -TERM {pid}

そしたら起動していたワーカープロセスは何も出力されずに終了した。これはSIGTERMを受け取ったら強制終了というResqueのドキュメントに書いてある挙動なので正しい。

次にTERM_CHILDRESQUE_TERM_TIMEOUTを指定して起動してみる。

$ TERM_CHILD=1 RESQUE_TERM_TIMEOUT=10 QUEUES=* rake resque:work

同じようにclinet.rbでジョブを登録して10秒以内にResqueのマスタプロセスに対してSIGTERMを送る。そうすると2秒後に

$ TERM_CHILD=1 RESQUE_TERM_TIMEOUT=10 QUEUES=* rake resque:work
graceful shutdown!

となってプロセスが終了して期待通り動いた。

次にRESQUE_TERM_TIMEOUT=1としてタイムアウトを1秒に指定する。

$ TERM_CHILD=1 RESQUE_TERM_TIMEOUT=1 QUEUES=* rake resque:work

これで同じようにジョブを登録してSIGTERMを送ったら何も出力されずに終了した。sleepが2秒でタイムアウトが1秒なのでSIGTERMのハンドリングで終了するより先にタイムアウトして終了したことがわかる。

Resque 2.xではこれがデフォルトになるとかなんとか(要確認)。

参考

このエントリーをはてなブックマークに追加