ActiveJobとSidekiqを組み合わせたときに、ジョブ単位のオプションはどこまで設定できるか？

# 背景
## 非同期処理
一般的なブラウザは、送信したHTTPリクエストが30秒以内に応答されない時はタイムアウトし、そのHTTPリクエストは失敗したと判定します。Webアプリケーションが長時間かかる処理を行いたい場合は、ブラウザがタイムアウトしないように、HTTPリクエストに応答を一旦返し、その後実際の処理を実行します。このような処理をリクエストの応答と実処理の完了が同期していない（同時でない）点から、非同期処理と呼びます。

## 非同期処理フレームワーク
非同期処理では、一つ一つの処理をジョブと呼びます。Webアプリケーションは、HTTPリクエストを受け取ると、次のように動作します。

1. リクエストに応じたジョブを作成
2. ジョブをキューと呼ばれる領域に保存
3. HTTPリクエストに応答を返す

非同期処理にはワーカーという実行プログラムがあります。ワーカーは、キューを監視し、キューにジョブが保存されたら、キューからジョブを取り出し、そのジョブを実行します。

ワーカーの処理が終わる前に新しいリクエストが来た場合、Webアプリケーションは新しいジョブをキューに保存します。キューには一定数のジョブが保存でき、ワーカーは実行中のジョブが完了したら、次のジョブがないかキューを確認します。キューが存在するおかげでWebアプリケーションは、ワーカーの実行状態を気にせずにジョブを作成できます。

一般にキューに入れられたジョブは先入れ先出しで処理されます。このためキューは[キュー](https://ja.wikipedia.org/wiki/%E3%82%AD%E3%83%A5%E3%83%BC_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF))と呼ばれるます。

以下の機能をまとめて提供するプログラムを非同期処理フレームワークと呼びます。

- ジョブを定義するフォーマット
- キューの実装
- ワーカーの実装
- キューとワーカーを管理する機能


## ActiveJobとSidekiq
Ruby on RailsというWebアプリケーションフレームワークには、非同期処理を実行するために[ActiveJob](https://railsguides.jp/active_job_basics.html)という機能があります。ActiveJobでは、処理を実行する非同期処理フレームワークをいくつかの候補から選択することが出来ます。その1つに[Sidekiq](https://sidekiq.org/)があります。

SidekiqはRubyで書かれた非同期処理フレームワークです。Ruby on Railsと関わりなく広く使われています[^3]。Sidekiq自身には豊富な機能がありますが、ActiveJobから利用する場合にはいくつかの制限があります。その1つに`sidekiq_options`メソッドを使った、ジョブ単位のオプション設定があります。

[^3]: たとえば[RubyGems.org](https://rubygems.org/gems/sidekiq)からは6000万回ダウンロードされています。

## ジョブ単位のオプション設定

Sidekiqを、ActiveJobを経由せずに、直接使う時は、Sidekiq::Workerというクラスを継承してジョブを実装します。Sidekiq::Workerには`sidekiq_options`メソッドがあります。`sidekiq_options`を使うとジョブを登録するキューや、ジョブが失敗したときのリトライ処理の有無が設定できます。次の４項目を設定できます。

- queue
- retry
- backtrace
- pool

詳しくは[Advanced Options · mperham/sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Advanced-Options)を読んでください。

# 命題

ActiveJobからSidekiqを使う際に、ジョブにsidekiq_optionsを設定できるでしょうか？

# 結論

Sidekiq 6.0.1 + Rails 6.0.2の組合せで、できるようになるらしい。

## Sidekiq 6.0.1
https://github.com/mperham/sidekiq/wiki/Active-Job
> As of Sidekiq 6.0.1 you can sidekiq_options in your ActiveJobs and configure the standard Sidekiq retry mechanism.


翻訳すると...

> Sidekiq 6.0.1以降、ActiveJobsでsidekiq_optionsを使用して、標準のSidekiq再試行メカニズムを構成できます。

## Rails 6.0.2
https://github.com/mperham/sidekiq/issues/4281#issuecomment-533840940
> Oh and you need Rails master too, it needs Rails 6.0.1.

翻訳すると...

> ああ、Railsのmasterブランチも必要です。Rails6.0.1が必要です。

その後、該当コミットがRails 6.0.1に入らなかったため、6.0.2が必要になりました。

https://github.com/mperham/sidekiq/issues/4281#issuecomment-554664409

> Update: support code will only make it to Rails 6.0.2


# その他の悪あがき

現時点で、ActiveJobからsidekiq_optionsは設定できませんが、ジョブ単位にいくつかの振る舞いを変更することが出来ます。

## queue
ActiveJobから`queue_as`メソッドを使えばジョブを登録するキューを指定することが出来ます。Sidekiqではキュー毎にジョブのチェック頻度が設定できます。これを使ってジョブ単位の優先順位を設定できます。
詳しくは[Active Job の基礎 - Rails ガイド](https://railsguides.jp/active_job_basics.html#%E3%82%AD%E3%83%A5%E3%83%BC)や[Advanced Options · mperham/sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Advanced-Options)を読んでください。


## retry

### リトライ回数を増やす

Sidekiqではリトライ回数を設定することが出来ます。ジョブが失敗した時に何回リトライするかを設定します。ジョブ単位の設定ではありませ。すべてのジョブで共通の設定です。
詳しくは[Error Handling · mperham/sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry)を読んでください。

Sidekiqでのリトライ回数に、ActiveJobでのリトライ回数を追加できます。

[Active Job · mperham/sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Active-Job) によると


> The default AJ retry scheme is 3 retries, 5 seconds apart. Once this is done (after 15-30 seconds), AJ will kick the job back to Sidekiq, where Sidekiq's retries with exponential backoff will take over.


翻訳すると...

> 「デフォルトのActiveJob再試行スキームは、5秒間隔で3回再試行されます。これが完了すると（15〜30秒後）、ActiveJobはSidekiqにジョブをキックバックします。Sidekiqは指数関数的なバックオフを使用した再試行を引き継ぎます。」

ActiveJobでは[retry_on](https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on)メソッドを使ってリトライ回数を設定できます。例えば、次のジョブは3回リトライします。[^2]

[^2]: retry_onは[Rails 5.1.0](https://github.com/rails/rails/blob/v5.1.0/activejob/CHANGELOG.md)で追加された機能です。それ以前のバージョンのActiveJobを使う場合は自分で実装する必要があります。[ActiveJobでリトライ制御 - Qiita](https://qiita.com/necojackarc/items/b4a8ac682efeb1f62e74)などを参考にしてください。

```rb
class AlwaysFailJob < ApplicationJob
  retry_on ZeroDivisionError, attempts: 3

  def perform(params)
    p "run job !!!"
    1/0
  end
end
```

このジョブを実行すると、ActiveJobが3回リトライしたあとで、Sidekiqは既定回数リトライをします。
つまり、ActiveJobがリトライする回数分、リトライ数を増やせます。


### リトライ回数を0にする

ActiveJobは[dicard_on](https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-discard_on)メソッドを使って、特定の例外が起きた時に、ジョブを成功したことに出来ます。例えば、次のジョブは実行するとZeroDivisionErrorが起きますが、成功します。

```rb
class AlwaysSuccessJob < ApplicationJob
  discard_on ZeroDivisionError

  def perform(params)
    p "run job !!!"
    1/0
  end
end
```

ジョブは成功するので、Sidekiqはリトライしません。

## backtrace

ジョブの失敗時に例外のbacktrace（Ruby以外の言語ではスタックトレースとも呼ばれます）を保存して、Sidekiqの管理UIから参照するためオプションです。このオプションはActiveJobから設定出来ません。

## pool

Sidekiqは[Redis](https://redis.io/)というインメモリデータベースをつかって、ジョブを保存するキューを実装しています。

ジョブを登録するときに使用するRedisへのコネクションプールを指定するオプションです。このオプションはActiveJobから設定出来ません。

このオプションの使い道は知りません。

## Sidekiq::Workerを使う
つまりActiveJobを使うのをやめます。

RSpecでテストを書く時に[ActiveJob::TestHelper](https://api.rubyonrails.org/classes/ActiveJob/TestHelper.html)が使えません。[^1]

[^1]: ActiveJob::TestHelperの使用例が知りたい方は[RSpec でキューイングした ActiveJob を同期実行する - Qiita](https://qiita.com/upinetree/items/41a2a8fe9e1dd7c291ab)や[Delayed Job Queue Adapter in RSpec with Rails 5.1 - Today I Learned](https://til.hashrocket.com/posts/chdlp2pbwb-delayed-job-queue-adapter-in-rspec-with-rails-51)を読んでください。

代わりにSidekiq::Testingを使います。例えば、次のようにテストを書きます。

```rb
require 'rails_helper'
require 'sidekiq/testing'
Sidekiq::Testing.fake!

describe ExampleJob do
  it do
    expect {
      ExampleJob.perform_async nil
    }.to change(ExampleJob.jobs, :size).by(1)
  end
end
```

詳しくは[Testing · mperham/sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Testing)を読んでください。


# 参考

- [Allow Sidekiq retry options in ActiveJob by pirj · Pull Request #4213 · mperham/sidekiq](https://github.com/mperham/sidekiq/pull/4213)