はじめに
Active Job にてRailsバックグラウンドジョブを実行する際の、排他制御(ロック)に関するメモ
目的: ジョブのキューイング自体をロックしたい
(ジョブの処理をロックするだけではなく、そもそもEnqueしない)
Active Job自体の使い方は↑こちらの記事を大いに参考にしています。
(ジョブ内での処理のロック方法は、こちらにも記載されています)
環境
Ruby on Rails (最近6.0系にアップデート), MySQL, Resque
- scheduler が各ジョブを時間ごとに管理し
- Redis がジョブのキューを保持し
- Resque がキューをもとにジョブを管理・実行
やりたいこと
そもそも Active Job でロックを導入する理由はなんでしょうか?
それは、同じジョブがEnqueされすぎて詰まるのを避けるためです。
例えば、処理に30分かかるジョブがあったとします。にもかかわらず、
スケジューラーで15分ごとに非同期実行しようとしたらどうなるでしょうか。
every 15.minutes do
HogeHogeJob.peform_async; # 実際には処理に30分かかる
end
Redisは何も考えず、スケジューラーの伝えるままにジョブをキューに入れまくります。
結果、1つ目の HogeHogeJob
が終わらないうちに2つ目が実行。。
どんどん積み上がり、最悪DBもデッドロックされ、処理が遅くなっていきます。
「いや、ちょうど良い時間を設定すればイイだけじゃん」と思われるかもしれませんが、
処理時間が読みきれないジョブなどを考えると、対策しておきたいですね。
ジョブの処理をロック(with_advisory_lock)
こちらにも書かれている方法。処理をロックするのであれば、こちらで十分かと思います。
今回はwith_advisory_lock Gemを使います。
Gemを追加した上で、ジョブの perform
メソッドの処理をwith_advisory_lockでロックします。
class HogeHogeJob < ApplicationJob
queue_as :default
def perform
ActiveRecord::Base.with_advisory_lock("#なんかしらのロック名") do
# 実際の処理
# 〜〜〜
end
end
end
とすると、#なんかしらのロック名
で処理がロックされ、それが終わるまで
次の #なんかしらのロック名
は実行されません。
with_advisory_lock はRails 6系もサポートしているのが嬉しい
次のジョブをキャンセルする
ちなみに with_advisory_lock は、ロック中にきた次のジョブは実行待ちとなり、キャンセルはされません。
次のジョブをドロップしたい場合は Redlock なんかを使うと良いです
ジョブのキューイングをロックしたい
今回やりたいことはこちら。
上記の処理をロックする方法では、そもそも HogeHogeJob
がキューイングされることはロックされません。
- scheduler
- → Redisにキュー
- → Resqueが実行(ここで初めてロック)
もし複数のジョブをカツカツで回している場合などでも、キューイング自体は止められません。
activejob-locking
そこで今回は activejob-locking というGemを使って、ジョブのキューイング自体をロックします。
activejob-locking もRails6系をサポートしてくれてます。
使い方は簡単。Gemを追加し、
class HogeHogeJob < ApplicationJob
include ActiveJob::Locking::Serialized
queue_as :default
def lock_key
self.class.name
end
def perform
# 実際の処理
# 〜〜〜
end
end
とすると、
activejob-lockingがクラス名でロック管理し、同じジョブがキューイングされることを防いでくれます。
この場合、1つ目のジョブが終わるのを待って、2つ目のジョブがキューされるようになります。
次のジョブをキャンセルする
上記の
include ActiveJob::Locking::Serialized
では、2つ目のジョブは1つ目が終わるのを待ち、その後実行されます。
もし、ロック時には他のジョブがキャンセルされるようにしたい場合は、
include ActiveJob::Locking::Unique
とすれば2つ目はエンキューされた後、1秒後にドロップされるようになります。
ちなみに1秒という時間は、オプションの :lock_acquire_time にて変更できます。
その他参考
初学者なりに書いてますので、表現のおかしい箇所などあればぜひご助言ください🙇