はじめに
大量なデータのインポートやメールの送信など、処理時間が長くなるタスクを実行する際は非同期で実行することが多いと思います。RailsではActive Jobという便利な仕組みにより、非同期処理を簡単に実装することができます。
Active Job単体でも使用することはできますが、プロセスがクラッシュしたりコンピュータをリセットしたりするとジョブが失われてしまいます。そのため、production環境では後に紹介するDelayed JobやSidekiqなどのライブラリと合わせて使用することが一般的です。
先日、業務でDelayed Jobを使う機会がありましたので、今回はActive Jobの基本的な説明と、バックエンドでジョブを実行するためのライブラリの一つであるDelayed Jobを紹介します。
Active Job
大量なデータのインポートやメールの送信など、様々な処理を非同期で並列的に実行できます。より詳しい説明はこちらから見ることができます。
Active Jobの役割
Active Jobの主な役割はジョブの処理とジョブ管理機能の制御を分離することです。これにより、ジョブの処理はジョブを実行する(SidekiqやDelayed Jobなど)のキューを管理するライブラリを意識する必要がなくなり、ジョブ管理機能ではキューの操作方法以外のことを気にする必要がなくなります。
さらに、ジョブごとに複数のキューを管理するライブラリを採用することができ、それらのライブラリを切り替える際にコードを書き換える必要がなくなります。
ジョブを作成する
ジョブはコントローラやモデルなどと同じようにRailsジェネレータで生成することができます。以下のコマンドを実行するとapp/jobsにジョブが生成されます。
1 2 | $ bin/rails g job mail_delivery create app/jobs/mail_delivery_job.rb |
生成されたコードは以下のとおりです。
1 2 3 4 5 6 7 | class MailDeliveryJob < ApplicationJob queue_as :default def perform(*args) # Do something later end end |
ジョブをキューに登録する
キューへのジョブの登録は以下のように行います。
1 2 3 4 5 6 7 8 | # 「キューイングシステムが空いたらジョブを実行する」とキューに登録する MailDeliveryJob.perform_later mail # 明日正午に実行したいジョブをキューに登録する MailDeliveryJob.set(wait_until: Date.tomorrow.noon).perform_later(mail) # 一週間後に実行したいジョブをキューに登録する MailDeliveryJob.set(wait: 1.week).perform_later(mail) |
また、ジョブに引数を渡す場合は以下のように行います。
1 2 3 | # `perform_now`と`perform_later`は`perform`を呼び出すので、 # 定義した引数を渡すことができる MailDeliveryJob.perform_later(mail_to, title) |
コールバック
Active Jobが提供するフックを用いて、以下のようにジョブのライフサイクル中に任意の処理を実行することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class MailDeliveryJob < ApplicationJob queue_as :default def perform(*args) # Do something later end private def around_cleanup(job) # performの直前に何か実行 yield # performの直後に何か実行 end end |
利用できるコールバックは以下のとおりです。
- before_enqueue
- around_enqueue
- after_enqueue
- before_perform
- around_perform
- after_perform
例外
ジョブの実行中に起こった例外は以下のようにキャッチする事ができます。
1 2 3 4 5 6 7 8 9 10 11 | class MailDeliveryJob < ApplicationJob queue_as :default rescue_from(ActiveRecord::RecordNotFound) do |exception| # ここに例外処理を書く end def perform(*args) # Do something later end end |
例外が発生したときに以下のようにジョブのリトライや破棄も行なえます。
1 2 3 4 5 6 7 8 9 10 11 | class MailDeliveryJob < ApplicationJob queue_as :default retry_on CustomAppException # defaults to 3s wait, 5 attempts discard_on ActiveJob::DeserializationError def perform(*args) # CustomAppExceptionかActiveJob::DeserializationErrorをraiseする可能性があるとする end end |
Delayed Job
Delayed Jobはジョブを実行するためのライブラリの一つです。
設定
まず、以下のGemをGemfileに追加してbundle installします。
1 | gem 'delayed_job_active_record' |
次に、ジョブのキューを保存するためのテーブルを作ります。
1 2 | $ bin/rails generate delayed_job:active_record $ bin/rake db:migrate |
実行すると下記のテーブルが作られます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | mysql> desc delayed_jobs; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | priority | int(11) | NO | MUL | 0 | | | attempts | int(11) | NO | | 0 | | | handler | text | NO | | NULL | | | last_error | text | YES | | NULL | | | run_at | datetime | YES | | NULL | | | locked_at | datetime | YES | | NULL | | | failed_at | datetime | YES | | NULL | | | locked_by | varchar(255) | YES | | NULL | | | queue | varchar(255) | YES | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | +------------+--------------+------+-----+---------+----------------+ |
最後に、Active Jobと連携させるための設定をapplication.rbに追加します。
1 | config.active_job.queue_adapter = :delayed_job |
ワーカーの起動
Delayed Jobを設定しただけではジョブを処理することはできません。以下のコマンドでワーカーを起動させます。
1 | $ bin/rake jobs:work |
その他のライブラリとの比較
Delayed Jobの他によく使われるライブラリとしてはSidekiqやResqueなどがあります。Delayed JobとこれらのGemの大きな違いとしては、Delayed Jobがキューの管理にDBを用いるのに対して、これら2つはRedisで管理します。Delayed JobはRedisを使用しないため容易に利用できますが、ジョブの処理に時間がかかります。上記の理由により、非同期処理を多用するサイトではSidekiqやResqueなどを使ったほうが良いでしょう。
さいごに
Railsでの非同期処理とライブラリについて紹介しました。Active Jobは非常に使いやすいので、重たい処理は非同期で処理してユーザビリティを高めていきましょう。