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のコードリーディングを行う際に大体の当たりをつけることができます。便利。