Resqueで色々やって、Redisに何が格納されているのか調べてみた
こんにちは、Go! Go! Heaven が頭から抜けないきたけーです。
最近、仕事や趣味でResqueをよく触っています。バックグラウンド処理をおこなう便利なライブラリなんですけど、「◯◯なときって何が起こるの?」と疑問に思うことが多かったので色々なケースでRedisに格納されている値を調べてみました。
ワーカのサンプル
今回はRailsプロジェクトの中に以下のような単純なワーカを用意しました。
class HogeWorker @queue = name class << self def perform(message) sleep 10 puts message end def perform_async(message) Resque.enqueue(self, message) end end end
起動するときは、
bundle exec rake resque:work QUEUE="*"
(事前にrakeタスクを用意しておく必要があります)
ジョブを投げるときは、
bundle exec rails c
をした後に、以下のようにします。
HogeWorker.perform_async("hello!")
ケース「ワーカが動いていないのにジョブを投げる」
まず気になっていたのはこれ。ワーカが動いていないのにジョブを投げたら何が起きるんでしょうか。
以下の様なコマンドでRedisのキーを監視してみました。
watch -n 1 "redis-cli keys 'resque:*'"
ジョブを投げると次のようなキーが増えました。ほう...
resque:queue:HogeWorker
おもむろにタイプを調べる。...配列ですね
redis-cli type 'resque:queue:HogeWorker' > list
中を覗いてみる。...なるほど、ワーカのクラスとenqueueしたときの引数が格納されてます。
redis-cli LRANGE 'resque:queue:HogeWorker' 0 -1 > 1) "{\"class\":\"HogeWorker\",\"args\":[\"hello\"]}"
試しにもう一個ジョブを投げる。...なるほどねー、次々に追加されていきます
1) "{\"class\":\"HogeWorker\",\"args\":[\"hello\"]}" 2) "{\"class\":\"HogeWorker\",\"args\":[\"hello2\"]}"
ワーカを起動すると、これを取り出して処理します。ジョブをたくさん投げて、ワーカが捌ききれなくなった場合も今回のように「resque:queue:ワーカ名」の配列に格納されていきます。
キューに追加される処理は、Resque.enqueueから辿って行き、Resque::Queue#pushを読むとしっくりきます。
ケース「ワーカがジョブを処理しているときにワーカを落とす」
次に、ワーカがジョブを処理しているときにおもむろにワーカを落としたらどうなるか調べてみましょう。
「おもむろに落とす」の定義
ワーカの動いている端末に対してCtrl-Cします。すなわちワーカのプロセスに対し、シグナルINTを送ります。
さきほどのようにキーを監視した状態でおもむろに落とすと、「resque:failed」というキーが追加されました。中身を覗いてみましょう。...ばっちり記録されています。おもむろに落としたときはこのキーの中身を覗けばよさそうですね。
redis-cli LRANGE 'resque:failed' 0 -1 > 1) "{\"failed_at\":\"2013/08/11 20:04:51 JST\",\"payload\": > {\"class\":\"HogeWorker\",\"args\":[\"hello\"]},\"exception\":\"Resque::DirtyExit\",\"error\":\"pid > 17776 SIGKILL (signal 9)\",\"backtrace\":[],\"worker\":\"kitak- > mba.local:11747:*\",\"queue\":\"HogeWorker\"}"
あれ...? シグナルINTを送ったはずなのにシグナルKILLを受け取ったことになってますね。
ちょっとソースコードの中を探検。
とりあえず、シグナルをキャッチしている場所を探してみましょう。ackやgrepで「INT」とスネークして、Resque::Worker#register_signal_handlersだと分かります。
ハンドラでshutdown!メソッドを呼び出してますね。!がつくと破壊的な印象がありますが、コメントをざっと読むとshutdownメソッドがジョブが終了してからワーカを終了するのに対して、終了を待たずにジョブのプロセスを殺すために!がついているのでしょう。実際にジョブを処理している子プロセスに対して、killメソッドを呼び出しています。
killメソッドが終着点のようです。いったん、プロセスに対してシグナルTERMを送って、少し待って、もし死んでいなかったらシグナルKILLを使って強制的に殺そうとするようです。なるほど、こういうテクニックがあるんですね。
子プロセスがお亡くなりになったら、親プロセスは、子プロセスがハンドラを定義してないシグナルで死んだかどうか(つまりシグナルKILLで死んだかどうか)調べて、ジョブオブジェクトのfailメソッドを呼び出します。このメソッドがRedisへの書き込みの薄いラッパーを呼び出す形になっていて、「resque:failed」キーの配列にさきほどターミナルで表示したような情報を追記していくようです。ふぅーーー
Rubyからのfailed情報の取得とリトライ
failed情報を全て取得する場合は、Railsコンソールで以下のように叩きます。
Resque::Failure.all(0, -1)
リトライは以下のように叩きます。
Resque::Failure.requeue(index)
最後のfailedをリトライするときは、こんなかんじです。
Resque::Failure.requeue(-1)
まとめ
Resqueは、バックエンドに使うストレージをRedisに限定しているのでRedisのキーを監視して、その中身を覗くことでジョブキューをどのように実現しているか調べることができます。調べた結果を使うことでResqueのコードリーディングを行う際に大体の当たりをつけることができます。便利。