Rails と Thread と autorequire2009/05/28

忘備録として

Railsというか、autorequire が効いている環境で、Threadを使う場合には注意が必要だ。

模擬コード

def send_many_mail(recipients)
  recipuents.each_slice(1_000) do |recipients_sub|
    Thread.new do
      recipients_sub.each do |recipient|
        TestMailer.deliver_test_mail(recipient)
      end
    end
  end
end

意図としては、大量にメールを送信したいという要件があって、それらを多重でなんか頑張ってみようというもの。

コードを簡単にするために、受け取り人の数で each_sliceしているけど、実際にはスレッド数を予め決めておいて、その数に受け取りメールアドレスを分割すべき。

また、これらのスレッドがちゃんと天寿を全うするには(全てのメール送信が完了するまで生き続けるためには)、これらスレッドにjoinするか、素でこれらのスレッドより長いライフタイムを持つプロセスの下で生成される必要がある。

要は、単純に controller のメソッド内で、Thread.new しても、大抵の場合は上手くいかないという話し。

小さな不特定多数のプロセスをspinするのが目的なら、BackgrounDrbが便利だし、今回のようにバッチっぽい処理をブラウザからキックしたい場合は、予め 自前でdrb のプロセスを立てておくのが良いと思う。

このあたりは機会があったら別のエントリを書くかも。

問題点

で、先程のコードは、上手く動くかもしれないし、動かないかもしれない。

実際、大抵の場合は上手くいくのだけれど、プロセスを(再)起動した直後に、メール送信に失敗することがある。

何が問題かというと、autorequireがthread safeでないということで、

プロセスの(再)起動時において、
Thread.new に突入した時点では TestMailer は未定義である

ということである。

そこで、autorequire が がりがりと
TestMailer のソースファイルを読み込んでクラスを定義する

のだが、

複数のスレッドで autorequire がクラス定義を始めてしまうことになる

為に

Object is not missing constant TestMailer!

で落ちる訳だ。

対処法

要は Thread.new する前にクラス定義が終了していれば良いので、模擬コードはこんな感じになる。

def send_many_mail(recipients)
  TestMailer
  recipuents.each_slice(1_000) do |recipients_sub|
    Thread.new do
      recipients_sub.each do |recipient|
        TestMailer.deliver_test_mail(recipient)
      end
    end
  end
end

もっと前に明示的に require を書くのが正しい気がしないでもない。