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

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は使われているそうです。

多少端折りましたが、重要なところはだいたい網羅できてると思います。なんか変なところとかあればコメントとかTBでどうぞ。

追記

yamazさんに具体例の訳を手伝っていただきました。

*1:ちょっとマズいところがあったので本家のソースと差し替えて少し書き加えました