#271 Resque
- Download:
- source codeProject Files in Zip (206 KB)
- mp4Full Size H.264 Video (18.8 MB)
- m4vSmaller H.264 Video (12.1 MB)
- webmFull Size VP8 Video (13.8 MB)
- ogvFull Size Theora Video (28 MB)
今回のエピソードではRails 3.1の新機能シリーズはひと休みして、Railsアプリケーションでバックグラウンドジョブを処理する優れた方法であるResqueを紹介します。今までのエピソードでもバックグラウンド処理を取り扱う方法をいくつか紹介しました。それぞれ微妙に違うニーズに対応しており、Resqueも例外ではありません。エピソードの最後で、異なるニーズに合った正しいツールを選択するチップを紹介しますが、とりあえずはResqueを詳しく見ていき、アプリケーションに追加してみましょう。
使用するアプリケーションは、Pastieに少し似ている簡単なスニペット(コード片)共有サイトです。このサイトでは、コードのサンプルを入力して名称と使用言語を設定します。
スニペットを投稿すると、選択した言語の文法でシンタックスハイライトされて表示されます。
シンタックスハイライトは外部ウェブサービスで処理されるので、この部分をバックグラウンドジョブで扱うことにします。現在はSnippetController
のcreate
アクションの一部として、インラインで実行されています。
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save uri = URI.parse('http://pygments.appspot.com/') request = Net::HTTP.post_form(uri, {'lang' => ↵ @snippet.language, 'code' => @snippet.plain_code}) @snippet.update_attribute(:highlighted_code, request.body) redirect_to @snippet, :notice => "Successfully created ↵ snippet." else render 'new' end end
シンタックスハイライトは、スニペットが保存されるときに実行されます。これには、http://pygments.appspot.com/で公開されている、ローカルな環境に依存せずにハイライト機能を提供する、Trevor Turk開発によるサービスを利用しています。このコードが、サービスに対してPOSTリクエストで素のコード片と言語の種類を送信し、そのリクエストの応答をスニペットのhighlighted_code
属性に保存します。
一般的にRailsリクエストを介して外部サービスと通信することは推奨されません。これは、応答が遅くなってRailsプロセス全体とそれに接続しようとしている他のリクエストを待たせる可能性があるからです。外部リクエストは外部プロセスとして外に出すのがより良い方法でしょう。そこでResqueを設定し、リクエストをResque workerに移し替えていきます。
Resqueを動かす
Resqueは、永続的キーバリューストアのRedisに依存しています。Redis自体もとても優れていて、それだけで一つのエピソードにする価値があるのですが、今回はResqueと一緒に使用します。
OS Xを使っている場合、Redisをインストールする一番簡単な方法はHomebrewを利用する方法で、次のコマンドを実行します。
$ brew install redis
インストールされたら、次のコマンドでサーバを起動します。
$ redis-server /usr/local/etc/redis.conf
Redisが起動したら、アプリケーションのGemfile
にResqueを追加し、bundleを実行してインストールします。
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'resque'
次にResque Rakeタスクを追加します。このためにアプリケーションの/lib/tasks
ディレクトリにresque.rake
ファイルを追加します。このファイルにrequire "resque/tasks"
と指定し、gemのカスタムタスクがロードされるようにします。workerが起動したときにRails環境もロードします。
require "resque/tasks" task "resque:setup" => :environment
これによって、workerからアプリケーションのモデルにアクセスできるようになります。しかしworkerを軽くしたければ、Rails環境の全体をロードしないようカスタマイズするのもいいでしょう。
Resque workerを起動するのに使用するRakeタスクができました。実行時には、引数QUEUEを指定する必要があります。対象となる具体的なキューの名称を指定するか、どのキューでもいい場合は'*'と指定します。
$ rake resque:work QUEUE='*'
このスクリプトは何も出力を返しませんがちゃんと動作しています。
Webサービスのコードを移動する
これでResqueが設定できたので、ここからはWebサービスを呼び出すコードをバックグラウンド処理に移してworkerを通じて処理させることに集中します。workがResque workerに処理されるようジョブをキューに追加します。ここで再度create
アクションを見てみます。
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save uri = URI.parse('http://pygments.appspot.com/') request = Net::HTTP.post_form(uri, {'lang' => ↵ @snippet.language, 'code' => @snippet.plain_code}) @snippet.update_attribute(:highlighted_code, request.body) redirect_to @snippet, :notice => "Successfully created ↵ snippet." else render 'new' end end
Resque.enqueue
を呼び出してジョブをキューに追加します。このメソッドはいくつかの引数をとります。一つ目は、ジョブを処理するために使うworkerのクラスです。まだworkerを作成していませんが、このあとSnippetHighlighter
という名前で作成します。その他にworkerに渡したい追加の引数を指定します。今回は、スニペットを渡しますが、enqueue
に渡されたものはすべてRedisデータベースに格納するためにJSONに変換されます。これはつまり、ActiveRecordモデルのような複雑なオブジェクトを渡すべきではなく、 スニペット全体を渡す代わりにそのid
を渡して、worker内でスニペットを取得し直すようにします。
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save Resque.enqueue(SnippetHighlighter, @snippet.id) redirect_to @snippet, :notice => "Successfully created ↵ snippet." else render 'new' end end
次にworkerを作成して、コントローラから取り出したコードを移します。workerは/app
の下に新規作成したworkersディレクトリに入れます。このクラスを/lib
ディレクトリに入れることもできますが、/app
内に持つことでRailsのロードパスにあるので自動的にロードされます。
workerとは、2つの機能を持つだけのクラスです。最初にインスタンス変数@queue
でキューの名前を保持します。これによってworkerが扱うキューを制限します。次にperform
というクラスメソッドが必要です。これはenqueue
に渡された引数(今回の場合はスニペットのid
)を受け取ります。このメソッドに、create
アクションから抽出されたコード(リモートサーバを呼び出してハイライトされたコードを戻す)を加えて、@snippet_id
インスタンス変数の呼び出しを同じ名前のローカル変数に置き換えます。
class SnippetHighlighter @queue = :snippets_queue def self.perform(snippet_id) snippet = Snippet.find(snippet_id) uri = URI.parse('http://pygments.appspot.com/') request = Net::HTTP.post_form(uri, {'lang' => ↵ snippet.language, 'code' => snippet.plain_code}) snippet.update_attribute(:highlighted_code, request.body) end end
正しく動くかどうか見てみましょう。新しいスニペットを作って登録します。しかしスニペットはハイライトさせていません。
ハイライトは現在バックグラウンドで処理中のため、すぐに見ることはできません。数秒待ってからページを再読み込みしてもまだ適用されていないようなので、コードをデバッグして何がうまくいっていないか見てみましょう。ResqueはSinatraで書かれたWeb画面を持っています。これを使って、ジョブのモニタリングや管理を簡単に行うことができます。次のコマンドを実行して起動します。
$ resque-web
これを実行すると管理画面が表示され、ジョブが失敗していることがわかります。
失敗したジョブをクリックすると、エラーの詳細を見ることができ、uninitialized constant SnippetHighlighter
とあります。
SnippetHighlighter
クラスが見つからない状態ですが、これはクラスを実際に書く前にRakeタスクを起動したのが原因です。再起動して問題が解決するか見てみます。
rakeタスクを再起動したら、「Retry」をクリックしてジョブを再実行します。Overview(概要)ページを見てみると、失敗したタスクは一つのまま増えていないので、今回はジョブは正しく実行されたようです。これを確認するために最後にアップロードしたスニペットを読み込み直します。今度はコードがハイライトされているので、バックグラウンドジョブはうまく実行されたようです。
新しいスニペットをアップロードするとシンタックスハイライトされますが、ハイライトが表示されるまでに数秒の遅れがあります。
ResqueのWebインターフェイスを組み込む
Resqueの設定はすべて終わり、与えたジョブを処理できるようになりました。ResqueのWeb画面を、作成しているRailsアプリケーションに組み込むことができたら、別のプロセスをもう一つ立ち上げる手間がかからず便利でしょう。
Rails 3はRackアプリケーションとの相性がよく、SinatraもRackアプリケーションの一つなので、routesファイルでアプリケーションをマウントするだけで簡単に設定できます。
Coderbits::Application.routes.draw do resources :snippets root :to => "snippets#new" mount Resque::Server, :at => "/resque" end
これでResqueのWeb管理画面がhttp://localhost:3000/resque
でRailsアプリケーションにマウントされました。この機能正しく動作する形でResqueサーバがロードされるよう、 Gemfile
でresque
gemの行にrequireオプションを追加します。
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'resque', :require => 'resque/server'
アプリケーションのサーバを起動してhttp://localhost:3000/resque
にアクセスすると、ResqueのWeb画面が表示されます。
このページが誰にでも見える状態なのは好ましくありません。詮索好きな目から守るために認証機能を追加するにはどうすればいいのでしょうか? アプリケーションでDeviseなどを使っているのであれば、ルートをauthenticate
の呼び出しでラップすることでこのページをセキュアにできます。
Coderbits::Application.routes.draw do resources :snippets root :to => "snippets#new" authenticate :admin do mount Resque::Server, :at => "/resque" end end
このアプリケーションではDeviseなどの認証システムを使っていないので、代わりにHTTPベーシック認証を使用しましょう。そのために、config/initializers
ディレクトリにresque_auth.rb
という初期設定ファイルを作成します。
Resque::Server.use(Rack::Auth::Basic) do |user, password| password == "secret" end
このファイルで、RackアプリケーションであるResque::Server
のuse
を呼び出して、ベーシック認証を追加します。ブロックでパスワードが一致するかチェックします。もちろんこのコードをカスタマイズして、ユーザ名とパスワードを設定したりその他のロジックを追加することができます。サーバを再起動してページを再度読み込むと、ログイン画面が出てきて初期設定ファイルで指定したパスワードの入力を求められます。
Resque以外の選択肢
Resqueの紹介は以上です。それでは、Resqueとその他のバックグラウンド処理ツールからどれを選べばいいのでしょうか? Resqueの利点の一つは、管理画面からキューをモニターしたり失敗したジョブを再実行できる点です。もう一つResqueを勧める理由は、Githubで使われているからです。Githubで大量のデータを処理しているので、およそどのようなものを投げても処理できるでしょう。
Redisに依存していることが問題であれば、Delayed Jobも優れた選択肢でしょう。これについてはエピソード171 [動画を見る, 読む]で紹介しましたが、操作が簡単でジョブキューをアプリケーションのデータベースで管理します。しかし、アプリケーションのRails環境をすべてのworkerにロードします。workerを軽くしたいのであれば、ベストの方法ではないかも知れません。
ResqueとDelayed Jobはともにキューに対してpoll(先頭を取得および削除)するので、ジョブがキューに追加されてからそれが処理され始めるまでに少し遅れが発生します。キューが空の時が多く、ジョブがすぐに処理されてほしい場合には、エピソード243[動画を見る, 読む]で取り上げたBeanstalkdがより良い選択肢になるでしょう。Beanstalkdはpushイベントを介してジョブを処理するので、キューにジョブが追加されたらただちに処理を開始することができます。