SidekiqをActive Jobから利用する [翻訳]

# SidekiqをActive Jobから利用する

最終更新 2019/09/18 編集者 Mike Perham

Rails 4.2からActive Jobが導入されました。Active Jobはジョブキューシステムのための標準的なインターフェースです。
Active JobはSidekiqと一緒に利用することができます。

## Active Jobのセットアップ

Active Jobのアダプターとして `:sidekiq` を指定する必要があります。そうしないとデフォルトの `:async` という値になってしまいます。
この設定は `config/application.rb` に記載してください。

```ruby
class Application < Rails::Application
  # ...
  config.active_job.queue_adapter = :sidekiq
end
```

新しいジョブの生成にはRailsのジェネレーター機能を使うことができます。

```shell
rails generate job Example
```

上記のコマンドを実行すると `app/jobs/example_job.rb` というファイルが作成されます。

```ruby
class ExampleJob < ActiveJob::Base
  # defaultという名前のキューを利用する
  queue_as :default

  def perform(*args)
    # ジョブの中身
  end
end
```

## 使用方法

Sidekiqを単体で使う場合のジョブと同じように、どこからでもキューにジョブをプッシュすることができます。

```ruby
ExampleJob.perform_later args
```

上記のコードを実行後すぐに、Sidekiqはジョブを実行します。何らかの理由でジョブが失敗した場合、Sidekiqは通常どおり再試行します。


## エラーハンドリングをカスタマイズする

Active Jobは、Sidekiqの再試行機能を完全にはサポートしていません。
代わりに、特定の例外が発生したときに再試行するための抽象化された仕組みがあります。

```ruby
class ExampleJob < ActiveJob::Base
  rescue_from(ErrorLoadingSite) do
    retry_job wait: 5.minutes, queue: :low_priority
  end

  def perform(*args)
    # Perform Job
  end
end
```

デフォルトのActive Jobの再試行方法は5秒間隔で3回です。
これが完了すると（15〜30秒後）、Active JobはSidekiqにジョブを戻します。Sidekiqはエクスポーネンシャルバックオフを使用して再試行を引き継ぎます。

Sidekiq 6.0.1以降では、ActiveJobsから `sidekiq_options` を使用することで、標準のSidekiq再試行メカニズムを利用できます。

```ruby
class ExampleJob < ActiveJob::Base
  sidekiq_options retry: 5

  def perform(*args)
    # Perform Job
  end
end
```

## Action Mailer

Action Mailerには、`＃deliver_later` という非同期でメールを送信するためのメソッドが付属しています。（メールはバックグラウンドジョブで送信されます）。

Active JobがSidekiqを使用するように設定されている場合に限り、`#deliver_later` を使用できます。
Sidekiqだけを使用する場合とは異なり、Active Jobを使用すると、ActiveRecordインスタンスがグローバルIDでシリアライズされます。シリアライズされたレコードは後でデシリアライズされます。

メール送信ジョブは mailers というキューにプッシュされます。Sidekiqを起動する際にこのキューを指定することを忘れないようにしてください。

```shell
bundle exec sidekiq -q default -q mailers
```

メール送信ジョブをキューに送る基本的なコードは下記の通りです。

```ruby
UserMailer.welcome_email(@user).deliver_later
```

ジョブキューに送らずに同期的にメールを送信するコードは下記の通りです。

```ruby
UserMailer.welcome_email(@user).deliver_now
```

Sidekiqでは、遅延を設定してメールを送信するオプションがありました。Active Jobでも同様に遅延を設定することができます。

Sidekiqだけで遅延を設定するコードは下記の通りです。

```ruby
UserMailer.delay_for(1.hour).welcome_email(@user.id)
UserMailer.delay_until(5.days.from_now).welcome_email(@user.id)
```

Active Job経由で同様のことを行うコードは下記の通りです。

```ruby
UserMailer.welcome_email(@user).deliver_later(wait: 1.hour)
UserMailer.welcome_email(@user).deliver_later(wait_until: 5.days.from_now)
```

## グローバルIDを使用する

RailsのグローバルID機能を使うことで、ActiveRecordオブジェクトを `#perform` の引数として渡す際にシリアライズすることができます。そのため、

```ruby
def perform(user_id)
  user = User.find(user_id)
  user.send_welcome_email!
end
```

上記のコードは、下記のコードで置き換えることができます。

```ruby
def perform(user)
  user.send_welcome_email!
end
```

残念なことに、上記のコードの違いによって、Active Jobを使う場合とそうでない場合で例外処理に違いが生じています。
ジョブがエンキューされる前までは `User` モデルのレコードが存在し、`perform` が実行される前にそのレコードが削除された場合、通常Sidekiqでは下記のように場合分けして処理します。

```ruby
def perform(user_id)
  user = User.find_by(id: user_id)

  if user
    user.send_welcome_email!
  else
    # user レコードが削除されている場合の処理
  end
end
```

それがActive Jobを使用する場合は、`User` のインスタンスをデシリアライズする際に `perform(user)` がレコードが存在しないことに対する例外を `raise` するようになります。
その場合の例外処理は下記の通りです。

```ruby
class MyJob < ActiveJob::Base
  rescue_from ActiveJob::DeserializationError do |exception|
    # user レコードが削除されている場合の処理
  end

  # ...
end
```

## ジョブID

ActiveJobには、Sidekiqにとって意味のない独自のジョブIDがあります。 SidekiqのJIDを取得するには `provider_job_id` を使用します。

```ruby
job = SomeJob.perform_later
jid = job.provider_job_id
```

## パフォーマンス

ベンチマークによると、ActiveJobを使うとRedisへのジョブのプッシュがおよそ2〜20倍遅くなります。そして、ジョブを処理する際のオーバーヘッドが3倍ほどあります。（Rails 5.1.4 と Sidekiq 5.1.1 の場合）
この件については [このイシュー](https://github.com/mperham/sidekiq/issues/3782) のコメントを読んでください。

## キュー名のプリフィックス

Active Jobでは、キュー名にプレフィックスを設定できます。 環境固有のプレフィックスを使用しないでください。
各環境では完全に別れた個別のRedisデータベースを使用する必要があります。
そうしないと、すべての環境で同じRetryセットとScheduledセットが共有され、混乱が生じる恐れがあります。

## 有償版のSidekiqとの併用

Sidekiq ProおよびSidekiq Enterpriseの機能の一部は、ActiveJobと一緒に使用しようとすると、予測しない壊れ方をする可能性があります。
たとえば、バッチ内でのActiveJobsの使用は基本的なユースケースでは機能しますが、ActiveJobの再試行メカニズムと併用しようとすると失敗します。
何か疑わしい挙動が起きた場合は、Active JobとSidekiqのネイティブなAPIを併用しないようにしてください。