BackgrounDRbについて調べてみた
このページの情報は既にかなり古いです。私は現在あまり使っていませんし、追ってもいないのでこのページの情報が更新されることはあまり期待できないのでなるべく本家の情報なりを見た方がよいでしょう。2009年8月現在だと http://d.hatena.ne.jp/tech-kazuhisa/20090816/1250432286 とかで似たような情報をあつかってるみたいです。検索しても結構上にまだくるっぽいのでいちおう追記しておきました。
次回の第8回Rails勉強会@東京でセッション案に上がっていたBackgrounDRbというやつがなにやら面白そげなので調べてみた。
調べてみた感じ以下のページがよくまとまっている。
http://www.infoq.com/articles/BackgrounDRb
以下、この内容を大雑把に要約してみる。
Ruby on Railsは素晴らしいフレームワークですが、Webアプリケーションをどんどん拡張していくと、バックグラウンドで長い時間処理するようなタスクが必要になってくることがあります。このような処理をControllerとかに書いてしまうとWebサーバーの動作の妨げになります。Webサーバーをタイムアウトさせずにこれらの処理を手軽に行なえるようにしたRailsのプラグインがBackgrounDRbです。
BackgrounDRbサーバーはMiddleManオブジェクトを配信します。このオブジェクトはworkerクラスを管理します。このオブジェクトは
- @jobs = {job_key => running_worker_object}
- @timestamps = {job_key => timestamp }
というインスタンス変数を持ちます。MiddleManオブジェクトはDRbサーバーとRailsアプリケーションの仲立役として機能します。(ここら辺にある図は見ておいた方が良さそう。)
インストールは次のように行います。
script/plugin install svn://rubyforge.org//var/svn/backgroundrb
Pluginをインストールすると、generatorにworkerジェネレータが使えるようになります。
$ script/generate worker Foo class FooWorker < BackgrounDRb::Rails def do_works(args) end end
MiddleManオブジェクトを通じてRailsからFooWorkerオブジェクトがインスタンス化されると、do_worksメソッドが自動的にRailsとは独立したスレッドで動作します。そのため、Railsはdo_worksメソッドが終了するのを待たずに先に進めます。
BackgrounDRbを使えば、Ajaxリクエストを使うと新しいworkerオブジェクトを生成します。このとき、Viewではperiodically_call_remoteメソッドを利用すればジョブの進行状況を取得し、どうなっているか表示することが出来ます。FooWorkerオブジェクトを生成し、RailsのControllerから進行状況を表示するコードを以下に示します。
class FooWorker < BackgrounDRb::Rails attr_reader :progress def do_work(args) @progress = 0 calculate_the_meaning_of_life(args) end def calculate_the_meaning_of_life(args) while @progress < 100 # ここで計算します @progress += 1 end end end
コントローラは以下のようにします。
class MyController < ApplicationController # FooWorkerを起動するクラス def start_background_task session[:job_key] = MiddleMan.new_worker(:class => :foo_worker, :args => "do_workメソッドに渡される引数") # 追記 「)」が抜けておりました。お試しになった方申し訳ありません。 end # 進行状況を表示するクラス def get_progress if request.xhr? progress_percent = MiddleMan.get_worker(session[:job_key]).progress render :update do |page| page.call('progressPercent', 'progressbar', progress_percent) page.redirect_to(:action => 'done') if progress_percent >= 100 end else redirect_to :action => 'index' end end # タスクが終わったときに表示するメソッド def done render :text => "FooWorker task has completed" MiddleMan.delete_worker(session[:job_key]) end end
start_background_task.rhtmlはpage.call('progressPercent', 'progressbar', progress_percent)が実行されたときのViewを指定します。*1
<html> <head> <style type="text/css"> .progress{ width: 1px; height: 16px; color: white; font-size: 12px; overflow: hidden; background-color: #287B7E; padding-left: 5px; } </style> <%= javascript_include_tag :defaults %><!-- 本家にもないので注意! --> <script type="text/javascript"> function progressPercent(bar, percentage) { document.getElementById(bar).style.width = parseInt(percentage*2)+"px"; document.getElementById(bar).innerHTML= "<div align='center'>"+percentage+"%</div>" } </script> </head> <body> <div id='progressbar' class="progress"></div> <%= periodically_call_remote(:url => {:action => 'get_progress'}, :frequency => 1) %> </body> </html>
MiddleMan.new_workerメソッドはランダムなジョブキーを持つジョブを作成し、あとからセッションに格納します。ジョブキーに特定の名前を付けたい場合は以下のようにします。
MiddleMan.new_worker(:class => :foo_worker, :job_key => :my_worker, :args => "do_workerメソッドの引数") MiddleMan.get_worker :my_worker
BackgrounDRbの設定はRAILS_ROOT/config/backgroundrb.ymlで行います。load_railsオプションをtrueに設定すると、workerクラスにActiveRecordオブジェクトを使うことが出来ます。BackgrounDRbサーバーを起動する際にdatabase.ymlの情報を読んでデータベースに接続します。
このプラグインはActiveRecordのオブジェクトを含む巨大なオブジェクトをキャッシュする機能があります。レンダリングされたViewや巨大なクエリもMarshallダンプできるオブジェクトであれば何でもキャッシングすることが出来ます。以下のようにして行います。
# キャッシュするには次の2通りのやり方がある @posts = Post.find(:all, :include => :comments) MiddleMan.cache_as(:post_cache, @posts) @posts = MiddleMan.cache_as :post_cache do Post.find(:all, :include => :comments) end # 次のようにしてキャッシュを取り出す @posts = Middleman.cache_get(:post_cache) @posts = MiddleMan.cache_get(:post_cache){ Post.find(:all, :include => :comments) }
MiddleMan.cache_getがブロックをとるかとらないかは:post_cacheキーが存在しないときにnilを返すか、ブロックを実行するかである。
現在の実装だと役目を終えたworkerオブジェクトやキャッシュは手動で無効にしてやる必要がある。これにはMiddleMan.delete_worker(:job_key)とMiddleMan.delete_cache(:cache_key)を使う。Timeオブジェクトを取り、それよりもタイムスタンプが古ければ削除するというMiddleMan.gc!メソッドもある。次の例は、30分前のジョブは無効にするスクリプトである。これをcronで定期的に実行することで残った不要なジョブを自動的に消すことが出来る
#!/usr/bin/env ruby require 'drb' DRb.start_service MiddleMan = DRbObject.new(nil, "druby://localhost:22222") MiddleMan.gc!(Time.now - 60 * 30)
将来的にはオブジェクトを作成したときにGCが実行される時間をスケジュールしておけるようにするつもりのようです。
BackgrounDRbサーバーを起動したり停止したりするRake タスクも自動的にインストールされます。
$rake backgroundrb:start
$rake backgroundrb:stop
次のような用途にBackgrounDRbは使われているそうです。
- RSSアグリゲータとしてRSSフィードをダウンロードしてキャッシュする。
- (2番目はちょっとよく分からず)(追記: はてなスクリーンショットみたいなやつ)
- (これもよく分からない)(追記: Xenの仮想サーバーをWeb上から起動するみたいな?)
- Hyper Estraierのインデックスをバックグラウンドで作成したり、検索したりする
- IRCボットとRailsをつなぐ
多少端折りましたが、重要なところはだいたい網羅できてると思います。なんか変なところとかあればコメントとかTBでどうぞ。
追記
yamazさんに具体例の訳を手伝っていただきました。
*1:ちょっとマズいところがあったので本家のソースと差し替えて少し書き加えました