はじめに
Sidekiq v7.3に Sidekiq::IterableJob
と呼ばれる機能が導入されました。長時間実行されるジョブが安全にデプロイできるようになったとのことです。
本記事では Sidekiq::IterableJob
に関連するWiki/Mike Perhams氏の記事/実装を参考に調査を行い、分かったことをまとめます。
参考資料
Sidekiq::IterableJob が導入された経緯
元々は2022年11月からfatkodima/sidekiq-iteration: Make your long-running sidekiq jobs interruptible and resumable. というGemが存在していました。
2024/4/26にSidekiq 開発者・メンテナーのMike Perham氏より顧客のデプロイを迅速かつ安全に提供するためにSidekiq本体に取り込まないかという提案があり、そこからIterationとしてSidekiq本体に取り込まれたようです。
何が変わるのか
Sidekiq 開発者・メンテナーのMike Perham氏の 記事 Iteration and Sidekiq 7.3.0 | Mike Perham には以下の説明がありました。
A common cause of long-running jobs is processing a large amount of data in a loop. For example, below we iterate through each each Product in our database. If there are one million Products, this might take a while!
Note a major flaw in the code above: if an error occurs or a deploy is triggered, the job will restart Product processing from the very beginning.
確かにこれは問題です。ジョブが最初から最初から実行されることでジョブが完了するまで時間がかかったり、またはジョブが実行中の場合はデプロイができないなどの障壁が生まれます。
class ProductImageChecker
include Sidekiq::Job
def perform(*args)
Product.all.each do |product|
# do something with product
product.check_image
end
end
end
With Sidekiq::IterableJob, we break the loop into discrete chunks which Sidekiq knows about, allowing Sidekiq to break the processing at any point in the loop. Notice we don’t provide a perform method but rather two methods which control the work loop:
class ProductImageChecker
include Sidekiq::IterableJob
def build_enumerator(*args, cursor:)
active_record_records_enumerator(Product.all, cursor: cursor)
end
def each_iteration(item, *args)
item.check_image
end
def on_complete
logger.info { "Finished checking product images!" }
end
end
-
#perform
メソッドは実装しない -
#build_enumerator
,each_iteration
の2つを実装する
必要があるようです。
※ コードの引用元: Iteration and Sidekiq 7.3.0 | Mike Perham
API
#perform
の代わりに実装必須のメソッドはこちら
build_enumerator
- ジョブの繰り返し処理を設定する
each_iteration
- ループの各反復で実行される作業を定義
ヘルパーメソッド
配列、ActiveRecord::Relation、CSVなどに応じたヘルパーメソッドが用意されているため、ループさせたいデータの種類に応じて使い分けるようです。
参考: lib/sidekiq/job/iterable/enumerators.rb
コールバック
便利なコールバックメソッドが用意されていました。
-
on_start
: ジョブが初めて開始されたとき -
on_resume
: ジョブが中断後、再開されたとき -
on_stop
: 処理が終了したとき(完了または中断が原因である可能性があります) -
on_complete
: Enumerator が終了したとき
注意点
This feature should be considered BETA until the next minor release.
Sidekiq Changes 7.3.0にある「次のマイナーリリースまでベータ版として見なされる」の記載がありました。ですので、まだ本番環境なので利用するには控えた方が良さそうです。
このBETAに関する記載が ChangeLog にしか書かれておらず、ほとんどの人は見落としそうな予感...。
疑問
調査の過程で気になったことがいくつかありました
Sidekiq::IterableJob は何をトリガーとしてジョブを中断させるのか
How it works に以下の説明がありました。
After every iteration, Sidekiq calls Sidekiq::Job#interrupted?. This is a new method available to all Sidekiq::Jobs which returns true if the Sidekiq process is in the process of shutting down.
Sidekiq::Job#interruputed? は以下の実装になっていました。
def interrupted?
@_context&.stopping?
end
この@_context
で呼ばれる .stopping?
が
Sidekiq::Processor#stopping?
Sidekiq::Process#stopping?
Sidekiq::Launcher#stopping?
のどれを指すかは分かりませんが、上記3メソッドを確認すると Sidekiq プロセスを「quiet」状態または「shutting down」状態かどうかを判定していました。
つまり「quiet」状態または「shutting down」状態をトリガーに中断をするようです。
中断を検知すると何をするのか
How it works に以下の説明がありました。
If interrupted? is true, the IterableJob will flush its current state to Redis and raise Sidekiq::Job::Interrupted, so it can be re-enqueued and restarted with the latest cursor. The retry subsystem ignores this exception but other subsystems (like Batches) will treat it as a failure.
IterableJobは現在の状態をRedisに保存、 Sidekiq::Job::Interrupted
例外を発生させることで、再エンキューとSidekiqの再起動時に最新のカーソルからジョブが再開できるようにしているようです。
まとめ
Sidekiq::IterableJob
で提供されているAPI, ヘルパーメソッド, 中断・再開に関する仕組みについてまとめました。率直な感想としてはデプロイ時におけるジョブの安全な中断と再開、ジョブの実行時間の最小化という点で Sidekiq::IterableJob
機能はとても有効と感じました。但しまだBETA扱いとのことでしたので、Iterationの利用は次のマイナーリリースまで待つべきと思います。今後に期待です。