RailsでSidekiqを使う際に気をつけたいこと

本記事は[iCARE Advent Calendar 2020](https://qiita.com/advent-calendar/2020/icare) の16日目です。

## はじめに
iCAREでは[Carely](https://www.carely.jp/)というシステムをRuby on Railsで開発しており、
非同期処理のために[Sidekiq](https://github.com/mperham/sidekiq)というライブラリを使っています。

簡単に非同期処理を作れるのですが、
「重そうだから、とりあえず非同期にしちゃう！」というノリで作ると痛い目に会います(会いました)。
ということで、Sikideqを扱う際に気をつけている点をまとめてみました。

## 1. そもそも非同期にするのか
しなくて済むなら、大半は非同期にしない方が良いと思っています。
非同期という特性上、どうしてもユーザーを待たせてしまいますし、
Job完了後の導線や、失敗時の考慮などすることが増えます。

クエリの改善などで、同期処理に出来ないかは考えるべきかと思います。

例えば、「レコードを集計してCSVをダウンロードする」処理であれば
- index張られているか？
- N+1起こってないか？
- 生SQLを書いて、1クエリでデータを取得できないか？
- ダウンロード出来る件数を制限できないか？(悪手な気もする)
などを確認して、それでも無理なら非同期にする。
というような、同期処理での実装を試みるフローは大事だと思います。

もちろんプロダクトによりますし、
ボタン毎に同期/非同期が違ったりするとユーザーが混乱するので、
適宜判断の必要はあります。

## 2. queueの優先順位
Jobが増えると事業的に優先度が高いもの/低いものが出てきます。
例えば、
- 外部システムとの連携(優先度: 高)
- ユーザー全員へのちょっとしたお知らせ(優先度: 低)
などです。
ユーザーが数百なら問題ないと思いますが、数十万のデータがあると、
Sidekiqのスレッドを埋め尽くしてJobが詰まり、
大事な処理が中々行われないかもしれません。
queue毎に重み(優先度)をつけて、適切な順番で行われるようにしたいです。

```config/sidekiq.yml
:queues:
  - [call_api, 2]
  - [send_mail, 1]
```
[Sidekiqのオプションを参考](https://github.com/mperham/sidekiq/wiki/Advanced-Options#queues)

## 3. queueの制限
queueの優先順位をつけても、
意図せず重いJobが連打されると詰まることがあります。
そういった場合、uniq制限をかけるべきかもしれません。

Sidekiqのenterprise版だと、以下のような方法で出来るようです。

```ruby

class MyWorker
  include Sidekiq::Worker
  sidekiq_options unique_for: 10.minutes

  def perform(...)
  end
end
```
※[Sidekiqのサンプルより引用](https://github.com/mperham/sidekiq/wiki/Ent-Unique-Jobs#use)

ただenterprize版だと、お金もかかってしまいます。
そういったときは、
Sidekiqのqueueにuniq制限を入れてくれる、[sidekiq-unique-job](https://github.com/mhenrixon/sidekiq-unique-jobs)や
Redisに対し排他制御を入れてくれる[redis-mutex](https://github.com/kenn/redis-mutex)
などを参考にしてみてもいいかもしれません。

## 4. Jobの実行
ここまで制御周りを考えていましたが、やっとJobの実行を考えてみます。
例えばこんなJobがあるとして、

```ruby
class HardWorker
  include Sidekiq::Worker

  def perform(name, count)
    # do something
  end
end
```

以下で実行出来ます。

```ruby
HardWorker.perform_async('bob', 5)
```

[Sidekiqのドキュメントより引用](https://github.com/mperham/sidekiq/wiki/Getting-Started)

が、`perform_async`で呼び出すとすぐにJobが実行されてしまいます。
- Jobの予約をする
- Jobで扱うデータの作成を待つ
- 複数DBを扱っていて、readとwirteを分けているので、レプリケーション遅延が発生する
場合などはJobを待たせる必要があります。

そういった場合

```ruby
HardWorker.perform_in(5.seconds, 'bob', 5)
```
で少し待たせて呼び出すようにします。

#### (補足)Jobを定時実行するとき
Jobを定時実行をしたい場合は、もう少し考える事が増えます。
cronを使うことが多いと思うのですが、複数台のサーバあると、同時にJobが実行されたりしてしまいます。

cron用のサーバーとSidekiqを動かすサーバを分けることも出来ますが、
お金もかかり、cronサーバーに負荷が集中する可能性もあります。

定時実行に関しては
Sidekiqの[enterprize版](https://github.com/mperham/sidekiq/wiki/Ent-Periodic-Jobs)がサポートしていたり
gemの[sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron)などもあるので参考にしても良いと思います。

## 5. Jobの失敗
連携先のapiが落ちている、予想外のデータがあるなど、Jobが失敗するときはあります。
そのときに再実行するのか、エラーとして処理するのかは考えておくべきだと思います。

### 再実行
SidekiqではJobが失敗したときに、間隔を開けて25回までリトライしてくれます。
([詳しくはSidekiqのリトライのページ](https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry))

便利ですが、例えば、
メール送信が失敗し続け、15回目で成功。
その間に2日ぐらい経っている
なんてこともありえます。
そのJobはリトライすべきなのか、するとしても何回が適切なのかは考えたいです。

### エラーの通知
非同期という特性上、実行ボタンを押したタイミングでエラーを出せません。
エラー通知はどこに表示するのか？表示した上でどうやって気づいてもらうのか？
などを考える必要があります。
毎日使うようなシステムなら画面表示だけで良いですが、
月1回だけ使うようなシステムなら、メールやチャットアプリへの通知も必要かもしれません。

## まとめ
基本的なことではありますが、過去に僕がやらかしたことをベースにまとめてみました。
ただ、それ以上に大事なのは、
- ユーザーの方がどう使うのか？
- 自分たちのシステムがどういう設計なのか？
を考えながら仕様を作ることなのかなと思います。

## iCAREテックブログもよろしくね！！
https://www.icare.jpn.com/dev_cat/