WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Rails + BackgrounDRb でメール配信

皆さんご存知のとおり、 Rails をはじめとする多くの Web アプリケーションフレームワークは、リクエストベースで設計されています。つまり、すべての処理は HTTP リクエストごとに起動され、それにレスポンスを返すことで終了します。ほとんどの場合、これは非常にうまく機能しますが、タイムアウトが発生してしまうような時間のかかる処理、一定間隔で定期的に起動したい処理などを実現するには不向きです。

そんなときに便利なのが、本日ご紹介する Ruby on Rails プラグインの BackgrounDRb です。これを導入すると、 Rails 本体と独立して動作するバックグラウンド処理が簡単に実現できます。さらに素晴らしいことに、バックグラウンド側でも ActiveRecord が利用できるなど、シームレスな連携が実現されているのです。これはなかなか画期的ですね!

ということで、本日はこの BackgrounDRb の導入方法・利用方法などを、メール配信を題材にしてご紹介します。 Rails の可能性を大きく広げる優れたプラグインですので、ぜひご覧ください!

特徴

前述のとおり、 BackgrounDRb は Rails でバックグラウンド処理を実現するプラグインで、 Hemant Kumar 氏を中心に開発されているオープンソースソフトウェア (Ruby ライセンス or MIT ライセンス) です。以下の特徴があります。

  • Rails のプラグインとして実装されており、 Rails アプリケーションに容易に導入できる。
  • Rails のモデルやコントローラから簡単にバックグラウンド処理を起動できる。
  • Worker 内で ActiveRecord が使える。もちろん Rails 側と同じモデルが利用できる。
  • Worker 内で ThreadPool が使えるので、大量のネットワークアクセスなどを効率的に処理できる。 BackgrounDRb 側でサーバーを構築することも可能。
  • cron のように指定した時刻に Worker を起動することもできる。 cron を使うよりデプロイが格段に楽になる。

これより簡単な使い方をご紹介していきますが、なかなか多機能なプラグインなので、すべては網羅しきれません。より詳細な情報に関しては公式ページをご参照ください。

インストール

それでは、早速インストール方法を。 chronic と packet という Gem に依存しているので、まずはそれらをインストールしておきます。

gem install chronic packet

次に BackgrounDRb 本体ですが、 Rails プラグインですので、インストールは Rails のプロジェクトごとに行います。まだプロジェクトを作っていなければ、以下のようにして作っておきましょう。もちろん既存のプロジェクトにインストールすることも可能です。

rails bgdrb_test

プロジェクトが用意できたら、 ./script/plugin を使って通常どおりインストールできます。

./script/plugin install http://svn.devjavu.com/backgroundrb/trunk

ちなみに、公式サイトでは Subversion や Git を使って取得するように説明されています(最新のソースに簡単にアップデートできるからでしょうかね?)。このあたりは好みの問題でしょう。

最後に、初期設定用の rake タスクを実行します。

rake backgroundrb:setup

これでインストールは完了です。とてもお手軽ですね。

メール配信してみる

BackgrounDRb がインストールできたので、簡単なメール配信アプリを作ってみましょう。 Web フォームで登録した配信先に同報メールを送信するだけのものですが、 BackgrounDRb の機能を使って、バックグラウンドの処理状況を表示する機能も実装しています。このようにバックグラウンドで処理すれば、 HTTP のタイムアウトに関係なく多数の配信先にメールが送信できるというわけです。ただし、あくまでサンプルということで、エラーメールの処理などは省略しています。ご了承ください。

ベースとなるアプリケーションを実装

まずは Scaffold を使って受信リストを作成するインターフェースをでっちあげましょう。 migration も実行して、必要なテーブルを作成しておきます。

./script/generate scaffold Recipient name:string email:string
rake db:migrate

さらにメールを送信するための ActionMailer クラスも作成しておきます。 generate スクリプトで雛形を作り、 Model, View の記述と config での SMTP サーバーの設定などを行ってください。具体的な記述は本筋を外れるので、ここでは省略させていただきます。

./script/generate mailer BackgroundMailer post

Worker の作成

次はいよいよ BackgrounDRb 特有の作業です。メール配信を実行する Worker を作成します。こちらも ActionMailer 同様、 generate スクリプトで雛形 (lib/workers/delivery_worker.rb) を生成できます。

./script/generate worker delivery

バックグラウンドで実行する処理は、この delivery_worker.rb で定義されている DeliveryWorker クラスのメソッドとして記述します。ここではメール配信を実行するメソッド deliver を追加して、メール配信処理を実装することにしましょう。以下は追加後の delivery_worker.rb の全内容です。

class DeliveryWorker < BackgrounDRb::MetaWorker
  set_worker_name :delivery_worker
  def create(args = nil)
    # this method is called, when worker is loaded for the first time
  end
 
  def deliver(message)
    Recipient.find(:all).each do |recipient|
      register_status(:name => recipient.name, :email => recipient.email)
      BackgroundMailer.deliver_post(recipient, message)
    end
    register_status(nil)
  end
end

基本的には、 Recipient テーブルの行をすべて読み出して、 ActionMailer で各々にメールを送信しているだけです。 ActiveRecord, ActionMailer の機能が普通に利用できているのがおわかりいただけるでしょう。このように、 Rails と同じプログラミングモデルでバックグラウンド側も実装できるのが BackgrounDRb の大きなメリットです。

また、もうひとつのポイントがメール送信の直前に呼び出している register_status メソッドです。これは BackgrounDRb の API で、引数として渡したオブジェクトを Rails 側に引き渡し、 Web アプリケーションから参照できるようにします。これにより、バックグラウンド処理の状況を Web アプリケーションに反映させることができます。ここでは Web ページに処理状況を表示するために、現在処理中の配信先の情報を渡しています。

Rails からの呼び出し

定義した deliver メソッドは Rails 側のコントローラやモデルなど、任意の場所から呼び出せます。ここでは RecipientsController に deliver アクションを追加し、そこから起動することにしましょう。全体を引用すると長くなるので、以下に変更後の index, deliver メソッドを抜き出しました。これ以外は Scaffold そのままです。

def index
  @recipients = Recipient.find(:all)
  @status     = MiddleMan.worker(:delivery_worker).ask_status
 
  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @recipients }
  end
end
 
def deliver
  MiddleMan.worker(:delivery_worker).deliver
  redirect_to :action => 'index'
end

バックグラウンド処理を起動しているのは deliver メソッドの最初の行です。基本的に、 BackgrounDRb の機能は MiddleMan というオブジェクト経由で利用します。 MiddleMan.worker メソッドで先に定義した DeliveryWorker クラスに対応するプロキシーオブジェクトを取得し、そのメソッドを呼ぶことでバックグラウンド処理が起動しています。

また、 index ページに現在の Worker の処理状況を表示するため、 ask_status メソッド(前述の register_status と対になるメソッドで、引き渡されたオブジェクトを返します)を使って処理中の配信先を取得し、インスタンス変数に格納しています。

最後に、 app/views/recipients/index.html.rb にメール配信フォームと状態表示を追加します。以下、追加分です。

<p>
  現在の配信状態:
  <% if @status -%>
    <span style="color:green; font-weight:bold;"><%= h "#{@status[:name]} <#{@status[:email]}>" %>へ配信中</span>
  <% else -%>
    <span style="color:green; font-weight:bold;">配信完了</span>
  <% end -%>
</p>

<% form_tag :action => 'deliver' do -%>
  <%= text_area_tag 'message', nil, :cols => 80, :rows => 20 %>
  <%= submit_tag '配信開始' %>
<% end -%>

これで最低限の処理が実装できました。あとは BackgrounDRb と mongrel などの Rails をホストする Web サーバーを起動すれば使えるようになります。

./script/backgroundrb start
mongrel_rails start -d

この後、 http://localhost:3000/recipients にアクセスすれば、 index ページにアクセスできます。自分のメールアドレスをいくつか登録して、メールを送信してみてください。

スケジューリング

上記のように Rails 側から明示的に起動する方法のほかに、 cron のように時間指定で定期的にバックグラウンド処理を起動することも可能です。 cron から rake タスクを呼び出しても ActiveRecord や ActionMailer などは利用できますが、それと比べても BackgrounDRb には以下のような利点があります。

  • Rails 環境の初期化は BackgrounDRb サーバーの起動時に済んでいるので、定期タスクの起動が速い。
  • Rails プロジェクト内ですべての設定が完結するので、デプロイが楽になる。

とくに後者はなかなか魅力的ではないでしょうか。 cron は Rails プロジェクト自体をデプロイした後に手動で設定する必要がありますが、 BackgrounDRb なら、プロジェクトのソースをチェックアウトし、 mongrel と BackgrounDRb を起動するという一貫した手順でデプロイが完結します。複数のマシンにデプロイしたり、オープンソースプロジェクトとして不特定多数に公開する場合には、とくに便利ですね。

それでは、以降でそのような定期的な処理の起動方法をご紹介します。あまり実用的でない例で申し訳ありませんが、単純に一秒ずつカウントアップするカウンタを実装してみましょう。

Worker の作成

まずは Worker を作成しましょう。手順は上述のメール配信とまったく同じで、 generate スクリプトで雛形を生成し・・・

./script/generate worker count

Worker クラスにメソッドを追加します。

class CountWorker < BackgrounDRb::MetaWorker
  set_worker_name :count_worker
  def create(args = nil)
    # this method is called, when worker is loaded for the first time
    @counter = 0
  end
 
  def count_up()
    @counter += 1
    register_status(@counter)
  end
end

起動タイミングの指定

次に、 BackgrounDRb の設定ファイル (conf/backgroundrb.yaml) に起動スケジュールを設定します。以下の ":schedules:" 以降がその設定で、 ":count_worker:" が起動する Worker の名前、 ":count_up:" が起動するメソッド、 ":trigger_args:" が起動タイミングです。 ":trigger_args:" の指定は cron とほぼ同じですが、秒と年が加わって「秒 分 時 日 月 曜日 年」となっています。

--- 
:backgroundrb: 
  :port: 11006
  :ip: 0.0.0.0
:schedules:
  :count_worker:
    :count_up:
      :trigger_args: * * * * * * *

その他の作業

あとは、カウンターの値を表示するコードを app/views/recipients/index.html.rb に追加します。 Worker との通信を View で行うのはよろしくないのですが、サンプルということでご勘弁ください。

起動時点からの経過秒数 :
  <%= MiddleMan.worker(:count_worker).ask_status %> 秒

実装が終了したら、 BackgrounDRb サーバーと、念のため mongrel も再起動しましょう。これで変更が反映されます。デバッグモードの Rails と違って、ソースを変更しただけでは反映されないので注意してください。

mongrel_rails stop
./script/backgroundrb stop
./script/backgroundrb start
mongrel_rails start -d

以上で、バックグラウンド処理の定期的な実行が実現できました。今回は cron 的な指定方法をご紹介しましたが、このほかにもいくつかスケジュールの指定方法があります。詳しくは公式サイトのこちらのページをご覧ください。

リファレンス

このように便利な BackgrounDRb ですが、残念ながらドキュメントはあまり充実していません。そこで、全てではありませんが、よく使うと思われるメソッドをまとめておきました。公式サイトのドキュメントとともにご活用ください。

Worker 内で使えるメソッド

Worker 側の処理中で利用できるメソッドです。このほかにスレッドプールや TCP/IP 通信の機能もありますので、詳細はこちらのページをご参照ください。

registar_status(obj)
obj で指定した任意のオブジェクトを Rails 側に送信する。 Rails 側では MiddleMan.ask_status でこのオブジェクトを取得できる。
logger.info(msg), logger.debug(msg), logger.error(msg)
文字列 msg をログ (RAILS_ROOT/log/backgroundrb_*.log) に出力する。
add_timer(seconds) { … }
seconds 秒後にブロックで与えた処理を実行する。
add_periodic_timer(seconds) { … }
seconds 間隔で定期的にブロックで与えた処理を実行する。

MiddleMan

Rails 側で利用できる MiddleMan のメソッドです。こちらは、通常使用するメソッドはだいたい網羅できているかと思います。

worker(worker_name, job_key = nil)
指定したワーカーを取得する。
new_worker(:worker ⇒ worker_name, :job_key ⇒ job_key)
worker_name で指定したクラスの新しいワーカーを起動する。
delete_worker(:worker ⇒ worker_name, :job_key ⇒ job_key)
指定したワーカーを削除(停止)する。 MiddleMan.worker(worker_name, job_key).delete と同じ。
worker_info(:worker ⇒ worker_name, :job_key ⇒ job_key)
指定したワーカーの情報を取得する。 MiddleMan.worker(worker_name, job_key).worker_info と同じ。
all_worker_info()
すべてのワーカーの情報を取得する。
ask_status(:worker ⇒ worker_name, :job_key ⇒ job_key)
指定したワーカーのステータスを取得する。 MiddleMan.worker(worker_name, job_key).ask_status と同じ。
query_all_workers()
すべてのワーカーのステータスを取得する。

Tips

最後に、使ってみて気付いた Tips などをまとめておきます。実際に活用する際の参考にしてください。

./script/console を活用しよう
MiddleMan のインターフェースは ./script/cosole からも利用できます。開発中の実験などはこちらを使うと便利です。
Worker 内での例外
Worker 内で例外が発生すると RAILS_ROOT/log/backgroundrb_server_*.log にスタックトレースが保存されるので、そこを見るとだいたいの問題がわかります。
Worker の停止への対応
Worker 内で例外が発生すると Worker が停止してしまい、以降のメソッド呼び出しが無視されるようになります。最終的にはメソッド内で例外をトラップし、外に出さないようにすべきです。さらに念を入れるなら、 Worker のメソッドを呼ぶ前に worker_info[:status] をチェックし、 :stopped になっていたら Worker を起動し直すという手もあります。
Worker メソッドの多重起動
Worker のメソッドをひとつ呼び出し、それが終了しないうちに同じ Worker のメソッドを再度呼び出すと、二回目以降の呼び出しはペンディングされて順次処理になります。つまり、同じ Worker がマルチスレッドで動作することはありません(スレッドプールや Ruby のスレッドを明示的に利用した場合は別ですが)。もしひとつの Worker クラスの処理を並列動作させたいなら、 new_worker に別々の job_key を指定して、複数の Worker を生成してください。
BackgrounDRb ってなんて読むの?
わかりません(笑)。たぶん「バックグラウンドルビー」もしくは「バックグラウンドディーアールビー」とかですかね。正しい読み方をご存知でしたらぜひ教えてください。

以上、本日は Rails でバックグラウンド処理を実現する BackgrounDRb プラグインをご紹介しました。ある程度の規模の Web アプリケーションになると、リクエストベースの処理だけですべてを済ますのはなかなか難しいので、 BackgrounDRb は非常に有用なプラグインと言えるでしょう。メール配信のみならず、さまざまな応用が考えられますので、ぜひ活用してください!

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles