読者です 読者をやめる 読者になる 読者になる

bekkou68の日記

開発しているサービス, IT技術, 英語など。

『Working with Unix Processes』の Appendix: How Resque Manages Processes を読みました

Unix 勉強会

はじめに

Ending の続きです。

まとめ

なぜ Resque が job を実行するのに fork しているのかというと、ワーカーのプロセスが使うメモリ量の肥大化を防ぐためだ。job を実行する度に fork するのでメモリは使うデメリットはあるが、実行後のクリーンアップが保証されるメリットがある。肥大化の背景には Ruby において GCレベルでブロック単位の開放は滅多にされないことがある。

ジョブを実行しているコードのリーディング大会

本文中のコードは実際のコードを簡略化して抜き出したものです。実際のコードを読んでみたら複雑なことをやっていました。勉強会メンバーで本で得た知識をもとに読み解いてみました。

# fork をラップすることで、fork をサポートしていない
# プラットフォームでも動作するようにしている。
#
# *注意*
# fork をサポートしてないプラットフォームではメモリ管理のメリットは失われる
def fork(job,&block)
  # 二度目以降はムダな処理をしない
  return if @cant_fork

  run_hook :before_fork, job if will_fork? 

  begin
    if Kernel.respond_to?(:fork)
      # MRI など fork をサポートしている場合
      Kernel.fork &block if will_fork?
    else
      # IronRuby など fork をサポートしていない場合
      raise NotImplementedError
    end 
  rescue NotImplementedError
    # fork をサポートしていない場合はフラグを立て、
    # block は実行せず終了
    @cant_fork = true
    nil 
  end 
end
...
# job を実行する度に実行されるメソッド
def work(interval = 5.0, &block)
  ...
  loop do
    ...
    if job = reserve(interval)
      ...
      # だいぶトリッキーなので注意 ※A
      if @child = fork(job) do
          # fork をサポートしているプラットフォームの子プロセスの場合
          unregister_signal_handlers
          procline "Processing #{job.queue} since #{Time.now.to_i}"
          reconnect
          perform(job, &block)
        end

        # fork をサポートしているプラットフォームの親プロセスの場合
        srand # Reseeding. see http://redmine.ruby-lang.org/issues/4338
        procline "Forked #{@child} at #{Time.now.to_i}"
        begin
          Process.waitpid(@child)
        rescue SystemCallError
          nil
        end
        job.fail(DirtyExit.new($?.to_s)) if $?.signaled?
      else
        # fork がサポートされていないプラットフォームの場合

        procline "Processing #{job.queue} since #{Time.now.to_i}"

        # ここで reconnect が走るケースはどういう時だろう?
        # オーバーライドした fork が nil を返すときは @cant_fork は
        # true になっているはず
        reconnect unless @cant_fork

        perform(job, &block)
      end
      done_working
      @child = nil
    else
      ...
    end
  end

  unregister_worker
rescue Exception => exception
  unregister_worker(exception)
end
...
# Redis へのコネクションが親プロセスとシェアされてしまうことを避ける
def reconnect
  ...
  redis.client.reconnect
  ...
end

コードリーディングをしていて驚いたことを列挙します。

  • そもそも fork がラップされていること
  • IronRuby など fork を実装していないプラットフォームも考慮されていること

この本を読まなかったらここまで読み解くことはできませんでした。コードリーディング力の高まりを感じます^^ 読んでてコメントがあまりなくて苦労しましたw
なお if @child = fork(job) あたりのコードの解説は本文でとても詳しく書いてあります。

forkブロックと if文が組み合わさったときの挙動

※A の挙動を読み解くのにかなり苦労しました。わかりやすくするために次のコードを書いてみました。ファイル名は fork-if.rb です。

puts "#{Process.pid}: before if"

if child = fork do
    puts "#{Process.pid}: in fork block"
  end

  puts "#{Process.pid}: in if [true]"
else
  puts "#{Process.pid}: in if [false]"
end

puts "#{Process.pid}: after if"

実行します。

$ ruby fork-if.rb
2069: before if     # parent
2069: in if [true]  # parent
2069: after if      # parent
2070: in fork block # child

else 文は実行されていないことが分かります。fork にブロックを渡すかどうかで戻り値が 2つ返ってくるかどうかの挙動が変わるようです。シンプルな fork のケースはこちらをご覧ください。
Resque では fork をオーバーライドすることで forkブロック・if文・else文の実行の 3つの分岐を実現しているのですね。すごいマジック!

気になる

srand ではどんなバグを回避しているのか?

そのうち調べる。

procline とは

procline という単語はどういう意味なのだろう? たぶん process line のことかなあ。

フレーズ

  • bloat: 膨らむ
  • go awry: 予想が外れる
  • clean slate: 何かの比喩表現? 「まっさらな状態」というニュアンスかな
  • spike: 一気に増える(釘が打ち付けられるイメージ。「【名】釘」「【動】釘で打ち付ける」という意味もある。お試しプログラムのことをスパイクと呼ぶけど、あれはどういうイメージから来ているのかなあと思った)
  • use up: 使い切る