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

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでAcitiveJobとDelayedJobを使ってバックグランド処理を行う

ActiveJob Rails中級 リファレンス

f:id:nipe880324:20150115212630j:plain:w480
Photo by Flickr: @Doug88888's Photostream


メール送信、大量データのインポート/エクスポートなど長い時間がかかる処理はバックグラウンドで処理するのが一般的です。
Railsでバックグラウンド処理を実現するためには、Sidekiq, Resque, Delayed Jobといったgemが有名です。

また、Rails4.2からActive Jobが追加されました。これは、たくさんあるバックグラウンド処理を行うgemへの共通インターフェースのようなものが追加されました。
メリットとして、バックグラウンド処理のgemがどれでも、ソースコードは同じように記述できるようになります。ちなみに、ActiveJobだけではバックグラウンド処理はできないので、バックグラウンド処理のgemを入れる必要があります。

この記事では、DelayedJobでのバックグラウンド処理の実施方法から、ActiveJobとDelayedJobを組み合わせたバックグラウンド処理の実施方法までを説明します。


動作確認

  • Rails 4.2
  • Active Job 4.2
  • delayed_job 4.0.6
  • delayed_job_active_record 4.0.3

目次

3. リファレンス

3.5. ジョブのコールバックの一覧


1. Delayed Jobのインストールと使い方

まずは、Active Jobと連携しない、Delayed Job単体での使い方を見ていきます。

1.1. DelayedJobのインストール

まず、GemfileにORM(オブジェクト・リレーション・マッパー)に合わせたdelayed_jobのgemを追記します。
ORMがActiveRecordの場合、

gem 'delayed_job_active_record'


ODM(オブジェクト・ドキュメント・マッパー)がMongoidの場合、

gem 'delayed_job_mongoid'


次に、delayed_jobをインストールします。

bundle install


ActiveRecordの場合、ジョブを管理するテーブルが必要となるので、次のコマンドで作成します。

bin/rails g delayed_job:active_record
bin/rake db:migrate

今回は、ActiveRecordでの説明を行います。それ以外のORMを利用している場合は、GitHubのページを確認してください。



1.2. ジョブを登録する(エンキュー)

delayメソッドを呼ぶことにより、バックグラウンドで処理を実行することができます。

# フォアグラウンドで実行される
@letter.deliver

# バックグラウンドで実行される
@letter.delay.deliver


たとえば、次のような時間のかかるdeliverメソッドがあったとします。

# app/controllers/letters_controller.rb
class LettersController < ApplicationController
  # POST/letters/1/deliver
  def deliver
    @letter = Letter.find(params[:id])
    @letter.deliver
    redirect_to letters_url, notice: "手紙を送りました。"
  end
  ...
end


# app/models/letter.rb
class Letter < ActiveRecord::Base
  def deliver
    sleep 10 # 処理に時間がかかることを擬似的に実施
    update(delivered_at: Time.zone.now)
  end
end


次のような画面でDeliverボタンを押します。
f:id:nipe880324:20150115211836j:plain:w480


deliverメソッドは時間のかかる処理(sleep 10)なので、10秒後にリダイレクトされます。
f:id:nipe880324:20150115211848j:plain:w480


しかし、次のようにdelayをメソッドをチェインさせてみましょう。

# app/controllers/letters_controller.rb
@letter.delay.deliver


そして、ジョブを処理するワーカーを起動させます。

bin/rake jobs:work


そして、またDeliverボタンを押すと、すぐにリダイレクトされます。
バックグラウンドで処理されるため、deliverd_atがまだ設定されていません。f:id:nipe880324:20150115212009j:plain:w480


バックグラウンドで処理が走っているので、10秒後にブラウザをリロードすればちゃんと設定されています。
f:id:nipe880324:20150115212030j:plain:w480




1.3. ジョブを実行する(ワーカーの起動・停止・再起動)

開発時

rakeタスクでフォアグラウンドでワーカーを走らせ、ジョブを処理します。
Ctrl+Cでワーカーを終了できます。

bin/rake jobs:work

本番時

デーモンでワーカーを走らせ、ジョブを処理します。
デーモンで動かすので、daemonsというgemをGemfileに追加し、bundle installを実施しておく必要があります。

# 別々のプロセス内で2つのワーカーを走らせる
RAILS_ENV=production bin/delayed_job -n 2 start

# ワーカーを停止させる
RAILS_ENV=production bin/delayed_job stop

# ワーカーを再起動させる(ワーカー数は2つ)
RAILS_ENV=production bin/delayed_job -n 2 restart


2. ActiveJobとの連携

ActiveJobの目的は、Railsにジョブのインフラを追加することで、Sidekiq, Resque, Delayed Jobといった実際にバックグラウンドでジョブを実行するgemの差分をほぼ意識しないでジョブを扱えるようにすることです。




2.1. バックグラウンドジョブとのアダプターを設定

Sidekiq, Resque, Delayed Jobに応じて、application.rbに設定を追加する必要が有ります。

実際に指定する値は、Active Job adaptersを参照してください。

Delayed Jobの場合は、:delayed_jobを指定します。

# config/application.rb
module DelayedJobTestApp
  class Application < Rails::Application
    ...

    # Gemfileにアダプターのgemを記載されており、gemがインストール済みであり、
    # アダプターのインストール方法を既に実施しているようにしてください。
    config.active_job.queue_adapter = :delayed_job
  end
end


2.2. ジョブを作成

次のコマンドでジョブを作成できます。

bin/rails g job news_deliver
      invoke  test_unit
      create    test/jobs/news_deliver_job_test.rb
      create  app/jobs/news_deliver_job.rb


ジョブファイルの規約として、app/jobs配下に配置し、ActiveJob::Baseを継承している必要があります。
Rakeタスクで作成された値は次のようになっています。

# app/jobs/news_deliver_job.rb
class NewsDeliverJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end


queue_asで、どのキューでジョブを走らせるかをを設定できます。デフォルト値は:default
そして、performメソッドにジョブに行わせたい処理を記載します。


では、コントローラーのdeliverメソッドの時間がかかる処理をperformメソッド内に移動させます。

# app/jobs/news_deliver_job.rb
class NewsDeliverJob < ActiveJob::Base
  queue_as :default

  def perform(letter_id)
    Letter.find(letter_id).deliver
  end
end


そして、コントローラーで、ジョブを呼び出します。

# app/controllers/letters_controller.rb
class LettersController < ApplicationController
  # POST/letters/1/deliver
  def deliver
    NewsDeliverJob.perform_later(params[:id])
    redirect_to letters_url, notice: "手紙を送りました。"
  end
  ...


ジョブの呼び出し方には次のように時間を指定して呼び出すこともできます。

# 明日の午後に実行される
NewsDeliverJob.set(wait_until: Date.tomorrow.noon).perform_later(params[:id])

# 1週間後に実行される
NewsDeliverJob.set(wait: 1.week).perform_later(params[:id])


ワーカーを起動します。

bin/rake jobs:work


画面から「Deliver」ボタンを押してみると、先ほどと同じようにすぐに画面遷移します。
f:id:nipe880324:20150115212009j:plain:w480


そして、数秒後に画面をリロードすると、Delivered atの値が設定されています。
f:id:nipe880324:20150115212030j:plain:w480



2.3. Active Jobの例外処理

ジョブ内でエラーが発生した時のエラーハンドリングには、rescue_fromを使います。

# app/controllers/letters_controller.rb
class NewsDeliverJob < ActiveJob::Base
  queue_as :default

  # performメソッド内でActiveRecord::RecordNotFoundが発生した場合、
  # ログに出力する
  rescue_from(ActiveRecord::RecordNotFound) do |exception|
    Rails.logger.error "Letterレコードは見つかりませんでした。"
  end

  def perform(letter_id)
    # 例外処理のハンドリングのために無理やり例外を発生させる
    raise ActiveRecord::RecordNotFound
    Letter.find(letter_id).deliver
  end
end


画面から「Deliver」ボタンを押すと、次のようにログにエラーメッセージが表示されます。もちろん、画面の値は更新されません。

# log/development.log
2015-01-15T20:55:20+0900: [Worker(host:nac.local pid:51669)] Job ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper (id=7) RUNNING
[ActiveJob] [NewsDeliverJob] [ecf4386f-5d10-4bc2-bfb3-994a45633070] Performing NewsDeliverJob from DelayedJob(default) with arguments: "1"
[ActiveJob] [NewsDeliverJob] [ecf4386f-5d10-4bc2-bfb3-994a45633070] Performed NewsDeliverJob from DelayedJob(default) in 0.53ms
Letterレコードは見つかりませんでした。


3. リファレンス

3.1. ジョブファイルの作成

次のコマンドでジョブを作成するか、app/jobs配下にActiveJob::Baseを継承したジョブクラスを作成する方法でジョブファイルを作成します。

bin/rails g job news_deliver
      invoke  test_unit
      create    test/jobs/news_deliver_job_test.rb
      create  app/jobs/news_deliver_job.rb


performメソッドにジョブで実行したい処理を記述します。
queue_asで、どのキューでジョブを走らせるかをを設定できます。デフォルト値は:default

# app/jobs/news_deliver_job.rb
class NewsDeliverJob < ActiveJob::Base
  queue_as :default

  def perform(letter_id)
    Letter.find(letter_id).deliver
  end
end


3.2. ジョブ内の例外処理

ジョブ内でエラーが発生した時のエラーハンドリングには、rescue_fromを使います。

# app/controllers/letters_controller.rb
class NewsDeliverJob < ActiveJob::Base
  queue_as :default

  # performメソッド内でActiveRecord::RecordNotFoundが発生した場合、
  # ログに出力する
  rescue_from(ActiveRecord::RecordNotFound) do |exception|
    Rails.logger.error "Letterレコードは見つかりませんでした。"
  end

  def perform(letter_id)
    # 例外処理のハンドリングのために無理やり例外を発生させる
    raise ActiveRecord::RecordNotFound
    Letter.find(letter_id).deliver
  end
end


3.3. ジョブの実行

ジョブの呼び出し方には次のように時間を指定して呼び出すこともできます。

# すぐにバックグラウンドで実行さえっる
NewsDeliverJob.perform_later(params[:id])

# 明日の午後にバックグラウンドで実行される
NewsDeliverJob.set(wait_until: Date.tomorrow.noon).perform_later(params[:id])

# 1週間後にバックグラウンドで実行される
NewsDeliverJob.set(wait: 1.week).perform_later(params[:id])


3.4. ワーカーの起動/停止

ジョブ関連のRakeタスクが使えます。

rake jobs:check[max_age]  # Exit with error status if any jobs older than max_age seconds haven't been attempted yet
rake jobs:clear           # Delayed Job のキューをクリアな状態にする
rake jobs:work            # Delayed Job ワーカーを起動させる
rake jobs:workoff         # Delayed Job ワーカーを起動させ、すべてのジョブが完了したら終了する


3.5. ジョブのコールバックの一覧

Active Jobは、ジョブのライフサイクルで次のコールバックを定義しています。

  • before_enqueue
  • around_enqueue
  • after_enqueue
  • before_perform
  • around_perform
  • after_perform


以上です。