Rails Active Jobとは?非同期ジョブ処理の実装とSolid Queue・リトライ設定
Active Jobは、Ruby on Railsで非同期ジョブを共通のインターフェースで扱うためのフレームワークです。メール送信や画像変換のようにレスポンスを待たせたくない処理をバックグラウンドへ逃がし、バックエンド(実行エンジン)を差し替えても同じジョブコードのまま動かせます。本記事では、ジョブの作成から perform_later での実行、キュー制御、retry_on によるリトライ、そして Rails 8 で既定になった Solid Queue までを、実際のコード付きで整理します。
まとめ:Active Job運用の要点
- Active Jobは「ジョブの共通API」であり、実際にジョブを動かすには Solid Queue や Sidekiq などのバックエンドが要る。
- ジョブは
ApplicationJobを継承して定義し、perform_laterで非同期、perform_nowで同期実行する。 - Rails 8.0 以降の新規アプリは Solid Queue が既定。DBだけで動きRedis不要のため、まずはこれで足りるか判断する。
- 失敗時は例外ごとに
retry_on(再試行)とdiscard_on(破棄)を使い分け、ジョブは冪等に書く。 - 1秒未満の即時応答が要る処理や、キューの遅延が許されない処理には向かない。
Active Jobとは何か:非同期処理を担う共通レイヤー
Webリクエストの中で重い処理を同期実行すると、その分だけレスポンスが遅れ、リクエストがタイムアウトする原因にもなります。Active Jobは、こうした処理を「ジョブ」として切り出し、後からバックグラウンドで実行させる仕組みです。Rails 4.2 で標準搭載され、それ以前はバックエンドごとに異なっていた記述を共通APIに統一しました。
Active Jobが解決する課題
Active Jobの本質は「ジョブの書き方を1つに揃える抽象レイヤー」である点です。ジョブ本体は Active Job の書式で書き、実際の実行は Solid Queue・Sidekiq・Resque といったバックエンドが担います。バックエンドを乗り換えてもジョブコードを書き直す必要がなく、開発環境では同期実行、本番ではSolid Queueといった切り替えも設定1行で済みます。
バックエンド未設定時の同期実行という落とし穴
Active Jobを入れただけでは処理はバックグラウンドに回りません。既定のキューアダプタは環境によって異なり、バックエンド未設定だと perform_later がその場で同期実行される(:async や :inline)ことがあります。非同期を成立させるには、後述のバックエンドを明示的に設定します。設定は環境ファイルで行います。
# config/application.rb もしくは config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
ジョブの作成から実行までの基本フロー
ジョブの生成・定義・登録・実行という一連の流れを、最小のコードで押さえます。
ジェネレータとApplicationJobによるジョブ定義
ジョブはジェネレータで雛形を作り、ApplicationJob を継承したクラスとして定義します。実際の処理は perform メソッドに書きます。
$ rails generate job guests_cleanup
class GuestsCleanupJob < ApplicationJob
queue_as :default
def perform(guest)
guest.destroy
end
end
queue_as は、このジョブを流すキュー名です。キューはバックエンド側で優先度や並列数を割り当てる単位になるため、重要度の異なる処理は :mailers や :low_priority のように分けておくと運用しやすくなります。
perform_laterとperform_nowによる非同期・同期実行
定義したジョブは perform_later でキューへ登録し、バックグラウンドで実行させます。同じ引数を perform_now に渡せば、その場で同期実行されます。どちらも内部的には perform を呼ぶため、処理本体は共通です。
GuestsCleanupJob.perform_later(guest) # キューに登録して即座に戻る
GuestsCleanupJob.perform_now(guest) # その場で実行し完了を待つ
perform_later は登録した時点で戻り、ジョブの戻り値は取得できません。処理結果が必要なときは、ジョブ内でDB更新やActive Cableでの通知を行い、呼び出し側は結果を直接受け取らない設計にします。なお引数にActive Recordオブジェクトをそのまま渡せるのはGlobalIDによるシリアライズのおかげで、キューにはID相当の文字列だけが積まれ、実行時にレコードを引き直します。対象が削除済みだと後述の ActiveJob::DeserializationError が発生します。
set(wait:)による実行タイミングの制御
遅延実行や日時指定は set で行います。数分後・翌日といった相対指定と、特定日時の絶対指定の両方が可能です。
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
GuestsCleanupJob.set(queue: :low_priority).perform_later(guest)
ただし遅延の精度はバックエンド依存で、キューが詰まっていれば指定時刻ちょうどには走りません。分単位の厳密なスケジューリングが要る場合は、cron相当の別機構と組み合わせます。
バックエンドの選択:Solid QueueとSidekiqの使い分け
Active Jobで最も判断が要るのがバックエンド選定です。Rails 8で状況が大きく変わりました。
Rails 8既定のSolid Queue(Redis不要のDBキュー)
Solid Queueは Rails 8.0 から新規アプリの既定バックエンドになった、データベース駆動のジョブキューです。ジョブをアプリのDB(PostgreSQL・MySQL・SQLite)に保存し、FOR UPDATE SKIP LOCKED を使って複数ワーカーで安全に取り出します。Redisなどの外部ミドルウェアが不要で、遅延実行・並列数制御・キュー単位の優先度・同時実行数の制限といった機能を標準で備えます。新規プロジェクトや、インフラを増やしたくない小〜中規模のアプリでは、まずSolid Queueで足りるかを起点に考えるのが現実的です。
SidekiqやResque・Delayed Jobとの比較と選定基準
大量ジョブを低レイテンシで捌く実績で選ぶなら Sidekiq が依然として有力です。Redisを前提に高いスループットとダッシュボードを提供し、既存のRedis基盤があるチームでは移行コストも小さくなります。導入手順はSidekiqとは?Rubyで非同期処理を簡単に実現するバックグラウンドジョブツールで解説しています。ResqueもRedisベースですが、新規採用は減少傾向です。Delayed JobはDBのみで動きますが、Solid Queueが同じDB駆動でより高機能なため、これから選ぶ理由は乏しくなりました。判断軸は「Redisを新規に持ち込めるか」「秒間ジョブ数」「既存資産」の3点で、迷ったらSolid Queueから始め、スループットが頭打ちになった段階でSidekiqを検討する順が無難です。
| バックエンド | ストア | Redis | 位置づけ |
|---|---|---|---|
| Solid Queue | アプリDB | 不要 | Rails 8既定・新規の第一候補 |
| Sidekiq | Redis | 必須 | 高スループット・実績豊富 |
| Resque | Redis | 必須 | 旧来型・新規採用は減少 |
| Delayed Job | アプリDB | 不要 | Solid Queueに置き換わりつつある |
リトライとエラー処理で信頼性を確保する
ジョブは外部APIやDBに依存するため、失敗を前提に設計します。Active Jobは例外に応じた挙動を宣言的に書けます。
retry_onによる例外別の再試行
一時的な失敗(ネットワークタイムアウトなど)は retry_on で再試行します。wait は再実行までの待機で、既定は3秒、:polynomially_longer を指定すると試行回数に応じて待機時間を多項式的に広げます(旧称 :exponentially_longer の後継で、増加は指数ではなく多項式です)。attempts は試行回数(既定5、:unlimited で成功まで無制限)、jitter は待機時間に加える揺らぎ(既定0.15=15%)です。
class RemoteSyncJob < ApplicationJob
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
def perform(record)
ExternalApi.sync(record)
end
end
上限回数まで再試行しても失敗した場合は例外が再送出されるため、最終的な失敗を握りつぶさないよう、監視やデッドレター相当の受け皿を用意します。
discard_onと冪等性の設計
再試行しても無意味な失敗は discard_on で破棄します。代表例は、対象レコードが既に削除されていて発生する ActiveJob::DeserializationError です。
discard_on ActiveJob::DeserializationError
リトライを前提にする以上、同じジョブが2回走っても結果が壊れないこと(冪等性)が必須です。「メールを1通だけ送る」ような副作用は、送信済みフラグの確認や一意制約で二重実行を防ぎます。なお retry_on・discard_on のハンドラは記述の下から上へ探索されるため、より具体的な例外を後ろに書きます。
コールバックとRSpecによるテスト
ジョブの前後処理と、非同期コードの検証方法を押さえます。
enqueue/performのコールバックによる前後処理
Active Jobは登録前後(before_enqueue / after_enqueue)と実行前後(before_perform / after_perform)にフックを持ちます。ログ出力や計測など、処理本体と切り離したい関心事をここへ寄せます。
class ReportJob < ApplicationJob
before_perform { |job| Rails.logger.info("start #{job.class}") }
after_perform { |job| Rails.logger.info("done #{job.class}") }
def perform(report_id)
Report.find(report_id).generate!
end
end
ActiveJob::TestHelperとrspec-railsによるジョブのテスト
非同期処理のテストでは「ジョブが正しくキューへ積まれたか」と「ジョブ実行後に期待どおりの状態になるか」を分けて検証します。前者はrspec-railsが提供する have_enqueued_job マッチャで、後者は ActiveJob::TestHelper の perform_enqueued_jobs で確認します。
expect { GuestsCleanupJob.perform_later(guest) }
.to have_enqueued_job(GuestsCleanupJob).with(guest)
perform_enqueued_jobs do
GuestsCleanupJob.perform_later(guest)
end
expect(Guest.exists?(guest.id)).to be false
RSpecの基礎はRSpecとは何か?テスト駆動開発におけるRSpecの役割と概要、Rails層のテスト構成はリクエストスペックとは?基本概念と目的についての説明を参照してください。
Active Jobを使うべきでない場面
Active Jobは万能ではありません。次のケースでは、別の手段を検討したほうが結果的に安全です。
即時性が要る処理には向きません。キューに積んでから実行されるまでには必ず遅延があり、ユーザー操作の直後にミリ秒単位で結果を返す用途(決済の同期確定、リアルタイム在庫引き当てなど)は同期処理として扱うべきです。厳密なスケジューリングにも不向きです。wait_until は「その時刻以降に実行」を保証するだけで、キューの混雑時には指定時刻に走りません。定時バッチはcronやスケジューラと組み合わせます。数時間かかる長時間ジョブは、途中でデプロイやワーカー再起動が入ると最初からやり直しになるため、Rails 8.1で導入された中断・再開機構を使います。詳細はActive Job Continuationsとは何か?長時間ジョブを中断・再開可能にする新機能の基本概念で解説しています。ジョブは「失敗しても安全に何度でも流し直せる、比較的短い単位」に保つのが原則です。
よくある質問
Active JobとSidekiqの違いは何ですか?
Active Jobはジョブの書き方を統一する共通インターフェースで、Sidekiqは実際にジョブを実行するバックエンドです。両者は競合せず、Active Jobのジョブコードを書き、実行エンジンとしてSidekiqを設定する組み合わせが一般的です。
Active JobとDelayed Jobの違いは何ですか?
Delayed Jobはバックエンドの1つで、Active Jobはその上位の抽象レイヤーです。Active Job経由で書いておけば、Delayed JobからSolid QueueやSidekiqへジョブコードを変えずに乗り換えられます。
Active Jobを入れれば自動でバックグラウンド実行になりますか?
なりません。バックエンドを設定しないと perform_later が同期実行される場合があります。Rails 8の新規アプリならSolid Queueが既定なので、ワーカープロセスを起動すれば非同期になります。
perform_laterした処理の結果を受け取れますか?
戻り値としては受け取れません。結果はジョブ内でDBに保存する、通知を送るなどの副作用で扱い、呼び出し側は直接の戻り値に依存しない設計にします。
リトライが上限に達したジョブはどうなりますか?
retry_on の attempts を超えると例外が再送出され、ジョブは失敗として扱われます。失敗を検知するために監視を入れ、破棄してよい例外は discard_on で明示的に切り分けます。