Sidekiq・Sidekiq-Cronの導入

Sidekiq・Sidekiq-Cronを導入して定期実行のバッチを管理する機構を作った記事を書きます！

## 概要
定期実行バッチを管理するためにsidekiq、sidekiq cronを導入
https://github.com/mperham/sidekiq
https://github.com/ondrejbartas/sidekiq-cron

## Sidekiqの前提をまとめておく

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2367608/b1c2ebe5-6f8c-389c-13b5-58af1050acf3.png)



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に変更
```config/application.rb
# sidekiq設定
config.active_job.queue_adapter = :sidekiq
```
- redisとの接続、sidekiq cronの設定

```config/initializers/sidekiq.rb
# 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:``にジョブが増えれば足していく

```config/sidekiq.yml
:concurrency: 1
:queues:
  - default
  - mailer
```

## Sidekiq、Redisを起動状態にする（起動していないとジョブをキューイングできない）

docker上で動かしている場合のコマンド例を貼っておきます。

```bash
$ 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/ディレクトリ 配下にジョブを作らないとエラーになる

### 実行
```app/jobs/generate_job.rb
class GenerateJob < ApplicationJob
  queue_as :default

  def perform(*args)
    p "Hello Active Job."
  end
end
```

- ``config/sidekiq.yml``にnameを書いておかないと実行できない。mailerも書いておかないといけない

```config/sidekiq.yml
:concurrency: 1
:queues:
  - default
  - mailer
```

適当なコントローラに埋め込んでみてリクエストを送って実行してみる

```rb
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."
```

- コンソールで試してもオッケー
```bash
$ 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>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2367608/29bd6fcf-fd87-6df1-f2e6-6e1853292aca.png)


- もちろんbinding.pryを仕込んでジョブを実行すればデバックも可能
```console(sidekiq(worker)が起動しているコンテナ)
$ 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">
```

```console(sidekiqログの方)
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コンテナ（サーバー）を停止している場合の動き
- ``#perform_later``を実行しても、キューにジョブが入るが待機状態のまま実行されず、キューに溜まっていく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2367608/f722c315-e589-a0f6-b515-e439f3b9a13e.png)


-  workerコンテナ（sidekiq）を起動するとキューで待機状態のままになったジョブが順番にバックグラウンドで実行される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2367608/1fdcce3f-9d42-32db-0474-d675babfed8c.png)


- sidekiqコンテナの起動とsidekiqサーバの起動はセットにしておかないとプロセスが2重になる模様。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2367608/e5a56a09-6a2a-1511-7acc-a346e908c043.png)



### mailerで非同期処理をする場合
 -  ``deliver_later`` or ``deliver_later(wait: 1.minute)``などを使うことでメール送信処理をredisにキューイングしてバックグラウンドで非同期処理できる

## Activejobを使わずにSidekiq::Workerというクラスを継承してジョブを実装する場合

- Sidekiqを、ActiveJobを経由せずに、直接使う時は、Sidekiq::Workerというクラスを継承してジョブを実装できる
※今回はActiveJobを使う方法を取るので、こちらは使わない。
- Sidekiq::Workerを使う場合はapp/ workers/ディレクトリ 配下に作らないとエラーになる

```app/workers/hard_worker.rb
class HardWorker
  include Sidekiq::Worker
  sidekiq_options queue: :worker

  def perform
    p "hard worler"
  end
end
```

### 実行
```bash
$ docker-compose run worker rails c
or
$ docker-compose run rails rails c
pry(main)> HardWorker.perform_async
```

```sidekiqのログ
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
```

- 時間差で実行する場合
```bash
$ 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の作成・実行方法](#active_jobの作成・実行方法)と同じ
- ``queue_as :name``で、name_sapceを作れるので、ジョブ（バッチ）ごとに作成した方が分かりやすそう
```app/jobs/matching_failed_order_check_job.rb
# 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に設定が反映される

```config/schedule.yml
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が起動した状態であれば、そのままバックグラウンドへジョブが移り非同期で実行される。

```bash
$ 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を同期実行する
```test_spec.rb
before do
  perform_enqueued_jobs do
    MatchingFailedOrderCheckJob.perform_later
  end
end
```

いかがでしたでしょうか？？
ご参考になれば幸いです！