Sidekiqのエラーハンドリング [翻訳]

# Sidekiqのエラーハンドリング

最終更新 2019/07/20 編集者 Scott E. Knight

このことについて言及したくはないのですが、ワーカーはジョブの実行中に例外を起こすことがあります。これは避けることのできない事実です。

Sidekiqはすべてのタイプのエラーを処理するための数多くの機能を持っています。

[![エラーハンドリング](http://img.youtube.com/vi/dGpbYqP5lr8/0.jpg)](https://www.youtube.com/watch?v=dGpbYqP5lr8&list=PLjeHh2LSCFrWGT5uVjUuFKAcrcj5kSai1&index=2)


## ベストプラクティス

1. **エラー集約サービスを使用する。** Honeybadger、Airbrake、Rollbar、BugSnag、Sentry、Exceptiontrap、Raygunなど。これらはすべて機能セットと価格が似ています。いずれかを選択してください。エラー集約サービスは、ジョブに例外が発生するたびにメールを送信します（Honeybadgerなどのよりスマートなものは、1番目、3番目、および10番目の同一エラーで電子メールを送信するため、1000個のジョブが失敗しても受信ボックスが圧迫されません）。

1. **Sidekiqでジョブによって発生した例外をキャッチする。** Sidekiqの組み込み再試行メカニズムは、これらの例外をキャッチし、ジョブを定期的に再試行します。 エラー集約サービスは例外が起きたときにあなたに通知してくれるでしょう。 バグを修正し、修正をデプロイすれば、Sidekiqはジョブを正常に再試行します。

1. 25回の再試行（約21日）以内にバグを修正しない場合、Sidekiqは再試行を停止し、ジョブをDeadセットに移動します。 Web UIを使用すれば、バグの修正後、6か月以内であればいつでも手動でジョブを再試行できます。

1. 6か月が経過すると、Sidekiqはジョブを破棄します。

## エラーハンドラー

Sidekiqのグローバルなエラーハンドラーに登録することで、Sidekiqの内部で起きた例外を通知することができます。
エラー集約サービスのgemをあなたのアプリケーションのGemfileに追加することで、それらのサービスを自動的に統合することができます。

`call(exception, context_hash)` に応答するクラスを実装すれば、独自のエラーハンドラーを作成することもできます。

```ruby
Sidekiq.configure_server do |config|
  config.error_handlers << Proc.new {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
end
```

注意：上記で紹介したエラーハンドラーは、Sidekiqサーバープロセスのみで有効であることに注意してください。例えば、Railsコンソールからは利用することができません。

## バックトレースのロギング

ジョブのバックトレースロギングを有効にすると、ジョブのライフタイム全体にわたってバックトレースが保持されます。
注意：それぞれのジョブのバックトレースは、Redisで1〜4kのメモリを消費する可能性があります。そのため、大量の失敗したジョブはRedisのメモリ使用量を大幅に増やす可能性がある点に注意が必要です。

```ruby
sidekiq_options backtrace: true
```

保持するデータ量の上限を超えないために、Redisに保持するバックトレースの行数を指定したり、エラー集約サービスが保存するエラーとバックトレースの量を指定するようにしてください。

```ruby
sidekiq_options backtrace: 20 # 上から20行だけ残す
```

## 自動的なジョブの再試行

Sidekiqは失敗したジョブをエクスポーネンシャル・バックオフを使って再試行します。
その際の式は `(retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))` です。（例：15, 16, 31, 96, 271, ... 秒 + [ランダムな時間](https://github.com/mperham/sidekiq/issues/480)）

この場合、再試行はおよそ21日間に渡って25回行われることになります。その時間内にバグを修正すると、ジョブは再試行され、正常に処理されることになります。

25回の再試行の間にバグが修正されなかった場合、SidekiqはそのジョブをDeadジョブキューに移動します。こうなってしまったジョブを動かすには手動による操作が必要です。

次の行をsidekiq.ymlに追加することで、再試行の最大回数をグローバルに設定できます。

```yaml
:max_retries: 1
```

近似的に計算した再試行の待ち時間は下記の表の通りです。

<pre>
 # | 次の再試行までの時間      | トータル待ち時間
 -------------------------------------------
 1 |     0日  0時間  0分 30秒 |     0日  0時間  0分 30秒
 2 |     0日  0時間  0分 46秒 |     0日  0時間  1分 16秒
 3 |     0日  0時間  1分 16秒 |     0日  0時間  2分 32秒
 4 |     0日  0時間  2分 36秒 |     0日  0時間  5分  8秒
 5 |     0日  0時間  5分 46秒 |     0日  0時間 10分 54秒
 6 |     0日  0時間 12分 10秒 |     0日  0時間 23分  4秒
 7 |     0日  0時間 23分 36秒 |     0日  0時間 46分 40秒
 8 |     0日  0時間 42分 16秒 |     0日  1時間 28分 56秒
 9 |     0日  1時間 10分 46秒 |     0日  2時間 39分 42秒
10 |     0日  1時間 52分  6秒 |     0日  4時間 31分 48秒
11 |     0日  2時間 49分 40秒 |     0日  7時間 21分 28秒
12 |     0日  4時間  7分 16秒 |     0日 11時間 28分 44秒
13 |     0日  5時間 49分  6秒 |     0日 17時間 17分 50秒
14 |     0日  7時間 59分 46秒 |     1日  1時間 17分 36秒
15 |     0日 10時間 44分 16秒 |     1日 12時間  1分 52秒
16 |     0日 14時間  8分  0秒 |     2日  2時間  9分 52秒
17 |     0日 18時間 16分 46秒 |     2日 20時間 26分 38秒
18 |     0日 23時間 16分 46秒 |     3日 19時間 43分 24秒
19 |     1日  5時間 14分 36秒 |     5日  0時間 58分  0秒
20 |     1日 12時間 17分 16秒 |     6日 13時間 15分 16秒
21 |     1日 20時間 32分 10秒 |     8日  9時間 47分 26秒
22 |     2日  6時間  7分  6秒 |    10日 15時間 54分 32秒
23 |     2日 17時間 10分 16秒 |    13日  9時間  4分 48秒
24 |     3日  5時間 50分 16秒 |    16日 14時間 55分  4秒
25 |     3日 20時間 16分  6秒 |    20日 11時間 11分 10秒
</pre>

ヒント：上記の表は `rand(30)` が常に15を返すという仮定の元に計算されています。


## Web UI

SidekiqのWeb UIには、失敗したジョブを見るためのRetriesタブ、Deadタブがあります。
これらのタブを使うことで、それらのジョブを実行、調査、削除することができます。


## Dead Set（デッドセット）

Dead Setはすべての再試行に失敗したジョブを保持しています。Sidekiqがそれらのジョブを再試行することはありません、再試行したい場合は、Web UIから手動で操作する必要があります。

Dead Setの大きさはデフォルトでは10,000個または6ヶ月間のどちらかに制限されるため、無限に大きくなることはありません。

**再試行が0回以上に設定されているジョブのみがDead Setに移動します。** 特定の種類のジョブを何が起こっても1回だけ実行する場合は、`retry: false' を使用してください。


## 設定

再試行の回数は自分の好きな回数に設定することができます。デフォルトは25回です。

```ruby
class LessRetryableWorker
  include Sidekiq::Worker
  sidekiq_options retry: 5 # 5回再試行し、それでも失敗する場合はDeadキューに送られる

  def perform(...)
  end
end
```

特定のワーカーのみで再試行機能を無効にできます。

```ruby
class NonRetryableWorker
  include Sidekiq::Worker
  sidekiq_options retry: false # 失敗したジョブは破棄されます

  def perform(...)
  end
end
```

再試行をせずにDeadキューに送られます。

```ruby
class NonRetryableWorker
  include Sidekiq::Worker
  sidekiq_options retry: 0

  def perform(...)
  end
end
```

Deadキューに送られることだけを無効化します。

```ruby
class NoDeathWorker
  include Sidekiq::Worker
  sidekiq_options retry: 5, dead: false # 再試行を5回行い、それでも失敗する場合は破棄される

  def perform(...)
  end
end
```

必要に応じて、再試行の待ち時間を `sidekiq_retry_in` でカスタマイズできます。

```ruby
class WorkerWithCustomRetry
  include Sidekiq::Worker
  sidekiq_options retry: 5

  # 現在の再試行回数と発生した例外が引数として渡されます。ブロックの戻り値はintegerが必須です。
  # 戻り値は再試行の待ち時間に使われます。単位は秒です。戻り地がnilの場合はデフォルトの値が使用されます。
  sidekiq_retry_in do |count, exception|
    case exception
    when SpecialException
      10 * (count + 1) # 例：10, 20, 30, 40, 50
    end
  end

  def perform(...)
  end
end
```

あらかじめ決められた再試行回数に達したとき、`sidekiq_retries_exhausted` が**定義されている場合に限り**、Sidekiqはこのメソッドを呼び出します。このメソッドは引数としてジョブに渡されたパラメーターを受け取ります。

このメソッドはジョブがDeadキューに移される直前に呼び出されます。

```ruby
class FailingWorker
  include Sidekiq::Worker

  sidekiq_retries_exhausted do |msg, ex|
    Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
  end

  def perform(*args)
    raise "or I don't work"
  end
end
```

## Deadキューへジョブが送られたことの通知

`sidekiq_retries_exhausted` コールバックはワーカークラスで指定することを想定しています。
Sidekiq バージョン5.1から、グローバルなコールバックも用意されています。

```ruby
# イニシャライザーに入れてください
Sidekiq.configure_server do |config|
  config.death_handlers << ->(job, ex) do
    puts "Uh oh, #{job['class']} #{job["jid"]} just died with error #{ex.message}."
  end
end
```

上記のコールバックを使えば、何かしらの例外が起きたときにメール送信やSlackへの通知等を行うことができます。


## プロセスのクラッシュ

もしSidekiqプロセスでセグメンテーション違反やRuby VMのクラッシュが起きると、そのときに実行されていたすべてのジョブは失われてしまいます。
[Sidekiq Pro](https://sidekiq.org/products/pro.html)はこのような状況でもジョブを[失わず済む方法（reliable queueing）](https://github.com/mperham/sidekiq/wiki/Reliability)を提供しています。


## 無駄な投資を避けるために（No More Bike Shedding）

Sidekiqの再試行メカニズムはベストプラクティスの詰め合わせです。
それにも関わらず、多くの人々が、彼ら固有のエッジケースに対応するための様々なオプションを提案し続けています。

この状況は狂気の沙汰です。

既存のSidekiqの再試行メカニズムでうまく動くようにコードを設計するか、JobRetryクラスにあなた自身のロジック用のパッチを当ててください。
再試行メカニズムの機能的な変更を受け入れる予定はありません。もしそうして欲しい場合は、「Sidekiqを既に使っている数千人ものユーザーが、なぜその機能を必要とするのか」についての非常に説得力のあるユースケースを用意してください。