Sidekiq をバックエンドに ActiveJob を導入

Heroku に 30 秒でレスポンスを返さないといけないルールがあったのを忘れていたので、 急遽 Rails アプリで時間がかかる処理を非同期にすることにした。

Rails で非同期というと Resque や Sidekiq が今のところ人気だけど、 今回は Rails 4.2 で追加予定の ActiveJob を使うことにした。 バックエンドには Sidekiq。

Redis をインストール

バックエンドに使う Sidekiq は Redis が必要なので、 Vagrant + Chef + Berkshelf で構築している開発環境にインストールする。

Berksfile に

cookbook "redisio"

を追加し

rm -rf cookbooks
berks vendor cookbooks

で Cookbook をインストール。

redisio のレシピを使うように Vagrantfile を

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  # ...

  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["./chef/cookbooks", "./chef/site-cookbooks"]

    # ...

    # Redis のレシピを追加
    chef.add_recipe "redisio::install"
    chef.add_recipe "redisio::enable"
  end
end

という風に修正したら、vagrant provision で chef-solo を実行。

ActiveJob と Sidekiq をインストール

ここからは vagrant ssh で開発環境に入っての作業。

Gemfile に

gem "activejob", "4.2.0.beta1", require: "active_job/railtie"
gem "sidekiq"
gem "sinatra"

を追加して bundle install

ActiveJob は gem を直接使う。 Rails 4.1 のアプリで使いたかったので、activesuport 4.2.0.beta2 が不要な、4.2.0.beta1 を使用。 active_job/railtie を require しないと、ActiveJob が Rails に読み込まれなかった。

あとは config/environment/development.rb で ActiveJob の設定を記述。 バックエンドで Sidekiq を使うように指定する。

Rails.application.configure do
  # ...

  # ActiveJob のバックエンドに Sidekiq を指定
  config.active_job.queue_adapter = :sidekiq
end

ローカルにインストールした Redis を使うので、Sidekiq の Redis 設定は省略可能。

Sidekiq の Web コンソールをマウント

ちゃんとジョブが実行されたか確認したいので、Sidekiq の Web コンソールをマウントする。 sinatra もインストールしたのは、これが理由。 Sidekiq の Web コンソールは Sinatra を別にインストールしないといけない。

config/routes.rb を次のように修正。

Rails.application.routes.draw do
  # ...

  # Sidekiq の Web コンソールをマウント
  mount Sidekiq::Web => "/sidekiq"
end

ジョブを作成

次のコマンドでジョブを生成。

bundle exec railg generate job calc_score

すると

class CalcScoreJob < ActiveJob::Base
  def perform(*args)
  end
end

みたいな雛形が app/jobs/calc_score_job.rb に生成されるので、 perform メソッドに非同期で実行したい処理を書く。 こんな感じ。

class CalcScoreJob < ActiveJob::Base
  def perform(*args)
    project_id = args[0]
    project = Project.find(project_id)
    project.calc_score # 30 秒以上かかる集計処理
  end
end

コントローラーでジョブを使う

コントローラーで作成したジョブを使ってみる。

ドキュメントには perform_later を使うと書いてあったけど、 RubyGems.org からインストールした 4.2.0.beta1 にはまだ実装されていないので、NoMethodError。 代わりに古い APIenqueue を呼び出す。

class ProjectsController < ApplicationController

  # ...

  def calc_score
    # 本当は
    # CalcScoreJob.perform_later(data)

    # ジョブ実行
    CalcScoreJob.enqueue(@project.id)
    respond_to do |format|
      format.html { redirect_to @project }
    end
  end
end

ジョブを実行

Rails プロジェクト直下に、次の内容を書いた Procfile を作成する。

web: bundle exec unicorn -p $PORT -c config/unicorn.rb
worker: bundle exec sidekiq

Foreman を使って Rails サーバーと Sidekiq ワーカーを起動。

web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
worker: bundle exec sidekiq

コントローラーのアクションを呼び出してジョブを実行したら、 Sidekiq の Web コンソールで確認してみる。

ブラウザで http://localhost:8080/sidekiq にアクセスして

f:id:griefworker:20140928122506p:plain

Processed の数が増えていれば、ActiveJob のバックエンドとして Sidekiq がちゃんと動いたことになる。