Sidekiq・Sidekiq-Cronを導入して定期実行のバッチを管理する機構を作った記事を書きます!
概要
定期実行バッチを管理するためにsidekiq、sidekiq cronを導入
https://github.com/mperham/sidekiq
https://github.com/ondrejbartas/sidekiq-cron
Sidekiqの前提をまとめておく
RailsとSidekiqはRedis(同じエンドポイント)に対してJobをenqueue/dequeueするため、双方からアクセスする構成です。
- Active Job は Rails におけるバックグラウンドジョブを動かすための共通インタフェース
- RailsのActive Jobで提供されるのはジョブをメモリに保持するインプロセスのキューイングシステムだけなので、 Rails を再起動するとジョブは全て消失(それはきつい)
- 故にバックグラウンドジョブを動かす Sidekiq、Resque、Delayed Job などのうちどれかをアダプタとして利用してRedisにジョブをキューイングして保存し、永続化する必要
- Sidekiq を使うためには Client, Redis, Server の 3 つが必要
- Client は Rails アプリケーション自身
- Server は Rails と別プロセスとして、
bundle exec sidekiq -e {environment} -C config/sidekiq.yml
で起動しておくと、キューにjobを入れて非同期で実行できる - Redis は Job をキューイングするために使う
- Client, Server, Redis は同一ホスト(サーバー)にインストールするかどうかは時と場合による
- sidekiqをインストールしたサーバー(EC2など)を別で置くケースは、ジョブの処理が大きいなどジョブ処理用のサーバーごと分離してジョブの処理を行うことで負荷を分散したい時などに行う
- SidekiqではジョブのことをWorkerと呼ぶ。ActiveJobでは、スレッドのことをワーカーと呼ぶ。
- SidekiqとRedisが動いている環境ではキューに入ったジョブは即時でバックグラウンドで非同期で実行される or 実行待ちの時間の設定がされていれば実行予定がセットされる。
- キューには順番でジョブが入っていき、前のジョブが実行されてキューからなくなったら、キューで待っている次のジョブが実行される。(キューが空であればそのまま即実行 or 実行予定がセットされる)
初期設定
https://github.com/mperham/sidekiq/wiki/Using-Redis#using-an-initializer
https://github.com/mperham/sidekiq/wiki/Advanced-Options
- activejobのアダプターをインプロセスからsidekiqに変更
# sidekiq設定
config.active_job.queue_adapter = :sidekiq
- redisとの接続、sidekiq cronの設定
# frozen_string_literal: true
redis_config = Rails.application.config_for('settings/redis')
Sidekiq.configure_server do |config|
config.redis = { url: "redis://#{redis_config[:host]}:#{redis_config[:port]}/#{redis_config[:db]}" }
end
Sidekiq.configure_client do |config|
config.redis = { url: "redis://#{redis_config[:host]}:#{redis_config[:port]}/#{redis_config[:db]}" }
end
# sidekiq_cronの設定
schedule_file = 'config/schedule.yml'
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) if File.exist?(schedule_file) && Sidekiq.server?
- config/sidekiq.ymlの設定
-
:queues:
にジョブが増えれば足していく
-
:concurrency: 1
:queues:
- default
- mailer
Sidekiq、Redisを起動状態にする(起動していないとジョブをキューイングできない)
docker上で動かしている場合のコマンド例を貼っておきます。
$ docker-compose run worker bundle exec sidekiq -e development -C config/sidekiq.yml
$ docker-compose run redis redis-server
active_jobの作成・実行方法
-
Rails コマンドの generate コマンドを使ってjobを作成できるhttps://railsguides.jp/active_job_basics.html
-
ジョブが実行すべきタイミングになると perform メソッドが呼び出されるので、このメソッドにジョブで実行したい処理を記述する(命名をperformにしてその中に処理を書かないと動かない。他の名前だとNotImplementedErrorになる。privateメソッドを作って、perform内で呼び出すことは可能)
-
ActiveJobを使う場合はapp/ jobs/ディレクトリ 配下にジョブを作らないとエラーになる
実行
class GenerateJob < ApplicationJob
queue_as :default
def perform(*args)
p "Hello Active Job."
end
end
-
config/sidekiq.yml
にnameを書いておかないと実行できない。mailerも書いておかないといけない
:concurrency: 1
:queues:
- default
- mailer
適当なコントローラに埋め込んでみてリクエストを送って実行してみる
class Admin::SessionsController < Admin::Base
layout 'admin_nomenu'
skip_before_action :require_login
def new
GenerateJob.perform_later
redirect_to admin_root_path if login_as_admin?
end
end
sidekiqのログ
// バックグラウンドで非同期で実行される
2021-05-22T04:34:46.386Z pid=1 tid=ffl class=GenerateJob jid=a6747cb6120e09a29c0493c4 INFO: start
"Hello Active Job."
- コンソールで試してもオッケー
$ docker-compose run rails rails c
pry(main)> GenerateJob.perform_later
Enqueued GenerateJob (Job ID: 0688c4b8-12d8-4843-aae3-2761fcf546c3) to Sidekiq(default)
=> #<GenerateJob:0x000055eb32211c68
@arguments=[],
@exception_executions={},
@executions=0,
@job_id="0688c4b8-12d8-4843-aae3-2761fcf546c3",
@priority=nil,
@provider_job_id="9fd3d20c4cc9e5c764ba6a54",
@queue_name="default">
- 時間差で実行する場合(ダッシュボード上の「予定」に入る)
$ docker-compose run rails rails c
pry(main)> GenerateJob.set(wait: 30.second).perform_later
Enqueued GenerateJob (Job ID: b6af416f-612c-4532-9a12-ad6de704c8dc) to Sidekiq(default) at 2021-05-22 14:36:25 UTC
=> #<GenerateJob:0x000055eb30c4d738
@arguments=[],
@exception_executions={},
@executions=0,
@job_id="b6af416f-612c-4532-9a12-ad6de704c8dc",
@priority=nil,
@provider_job_id="077176171f42709641ce3c2c",
@queue_name="default",
@scheduled_at=1621694185.2296805>
- もちろんbinding.pryを仕込んでジョブを実行すればデバックも可能
$ docker-compose run rails rails c
pry(main)> MatchingFailedOrderCheckJob.perform_later
Enqueued MatchingFailedOrderCheckJob (Job ID: 650e9b38-88c9-4450-bc85-5d5f4cb1ea21) to Sidekiq(check)
=> #<MatchingFailedOrderCheckJob:0x000055d8d3828c80
@arguments=[],
@exception_executions={},
@executions=0,
@job_id="650e9b38-88c9-4450-bc85-5d5f4cb1ea21",
@priority=nil,
@provider_job_id="37127426cba7046a871db5b5",
@queue_name="check">
From: /app/app/jobs/matching_failed_order_check_job.rb:8 MatchingFailedOrderCheckJob#perform:
6: def perform
7: binding.pry
=> 8: hoge
9: hoge
10: end
[1] pry(#<MatchingFailedOrderCheckJob>)>
sidekiqコンテナ(サーバー)を停止している場合の動き
-
workerコンテナ(sidekiq)を起動するとキューで待機状態のままになったジョブが順番にバックグラウンドで実行される
mailerで非同期処理をする場合
-
deliver_later
ordeliver_later(wait: 1.minute)
などを使うことでメール送信処理をredisにキューイングしてバックグラウンドで非同期処理できる
Activejobを使わずにSidekiq::Workerというクラスを継承してジョブを実装する場合
- Sidekiqを、ActiveJobを経由せずに、直接使う時は、Sidekiq::Workerというクラスを継承してジョブを実装できる
※今回はActiveJobを使う方法を取るので、こちらは使わない。 - Sidekiq::Workerを使う場合はapp/ workers/ディレクトリ 配下に作らないとエラーになる
class HardWorker
include Sidekiq::Worker
sidekiq_options queue: :worker
def perform
p "hard worler"
end
end
実行
$ docker-compose run worker rails c
or
$ docker-compose run rails rails c
pry(main)> HardWorker.perform_async
2021-05-22T14:30:56.063Z pid=1 tid=ff1 class=HardWorker jid=85565f72c7f8900ba12f1702 INFO: start
"hard worler"
2021-05-22T14:30:56.164Z pid=1 tid=ff1 class=HardWorker jid=85565f72c7f8900ba12f1702 elapsed=0.101 INFO: done
- 時間差で実行する場合
$ docker-compose run worker rails c
pry(main)> GenerateJob.set(wait: 30.second).perform_later
Enqueued GenerateJob (Job ID: b6af416f-612c-4532-9a12-ad6de704c8dc) to Sidekiq(default) at 2021-05-22 14:36:25 UTC
=> #<GenerateJob:0x000055eb30c4d738
@arguments=[],
@exception_executions={},
@executions=0,
@job_id="b6af416f-612c-4532-9a12-ad6de704c8dc",
@priority=nil,
@provider_job_id="077176171f42709641ce3c2c",
@queue_name="default",
@scheduled_at=1621694185.2296805>
ダッシュボード用語
- 待機状態・・プロセスに渡っていないRedisに積まれたジョブ
- ビジー・・現在実行中のジョブでスレッドベースで並列処理(concurrencyの設定数が並列処理の限度)※マルチプロセスだとプロセス数×concurrencyの数が上限
- 予定・・set(wait: 30.second)などで実行が予約されて時間差で実行される予定のジョブ
sidekiq cron
定期実行したいジョブ(バッチ処理)を作成
- ジョブの作り方は上記のactive_jobの作成・実行方法と同じ
-
queue_as :name
で、name_sapceを作れるので、ジョブ(バッチ)ごとに作成した方が分かりやすそう
# frozen_string_literal: true
class MatchingFailedOrderCheckJob < ApplicationJob
queue_as :matching_failed_order_check
def perform
logger = ActiveSupport::Logger.new('log/batch.log')
logger.info '============================'
logger.info "[INFO] バッチ処理開始します #{Time.zone.now}"
logger.info "[INFO] バッチ処理完了しました #{Time.zone.now}"
logger.info '============================'
end
end
-
config/schedule.yml
で実行タイミングを設定し、bundle exec sidekiq -e {environment} -C config/sidekiq.yml
を再起動するとcronに設定が反映される
hogehoge_job:
cron: '0 0 * * 1-7' # 毎日0:00に実行
class: 'HogeJob'
queue: default
hugahuga_job:
cron: '0 0 * * 1-7' # 毎日0:00に実行
class: 'HugaJob'
queue: default
- 実行確認はこの方法と同じ
- 以下の方法などで、ジョブのインスタンスを作ったりしてenqueしたりもできる。キューに入れただけでは本来ジョブは実行されないが、sidekiqが起動した状態であれば、そのままバックグラウンドへジョブが移り非同期で実行される。
$ docker-compose run worker rails c
job = Sidekiq::Cron::Job.find('matching_failed_order_check_job')
job.enque!
テストの書き方
- RSpec でキューイングした ActiveJob を同期実行してテストする。
- ActiveJob::TestHelper を使う
- ActiveJob::TestHelperをincludeして使えるようになる
#perform_enqueued_jobs
は、ブロック内のActiveJobを同期実行する
before do
perform_enqueued_jobs do
MatchingFailedOrderCheckJob.perform_later
end
end
いかがでしたでしょうか??
ご参考になれば幸いです!