Railsで非同期処理のエラーハンドリングを行う

## 概要
railsでメール送信まわりでエラーが起きたときにはretryしたい、それ自体はretry_onで実装可能でした
retry_onだと、newrelicにメトリクスを送信するなどの処理を組み込むことが難しかった
非同期処理の再試行をおこなうためのretry_onというメソッドが実装されているが、特定の条件下でジョブの再試行をしたり、例外ログを追加したい場合はretry_onだけでは実現できない場合がありました
追加すべきコードとその結論を出すために調査した内容をまとめています

### 環境
* Ruby on Rails 5.2
* sidekiq 5.2.9
* newrelic

### 達成したいこと
- メール送信失敗時のretryが上限付きで設定できる
- 処理ごとにwaitやattempsの回数を設定できる
    - ネットワークエラーなどの場合は、そこそこwaitをつけたいが、即時retryで良いものもあるため
- retryが上限に達した場合に任意の処理を組み込める
    - ここでは、newrelicへエラーメトリクスを送信するという処理を実装します

## 調べたこと

### Mailerに関して
- 標準のActionMailerは、エラーハンドリングで例外が発生した場合、呼び出し側で検知できないため、ActionMailer::DeliverJobを自前のJobで置き換える必要がある
  - 自前のJobに置き換えるための実装はこのPRによって取り込まれた => https://github.com/rails/rails/pull/29457
- Mailerの非同期処理も内部的にはAvtiveJobを使っているので、retryまわりはJobのretry処理を制御できれば達成できそう

### Jobに関して
- https://railsguides.jp/active_job_basics.html#失敗したジョブをリトライまたは廃棄する
  - retry_onで実現可能
  - Jobクラスごとにattempsとwaitの設定ができる
  - `wait: :exponentially_longer` を指定することで試行回数が進むごとにwaitが大きくなる
    - 計算ロジックを自作することも可能
- 懸念点・備考
  - https://github.com/mperham/sidekiq/wiki/Active-Job#customizing-error-handling
    - Rails6.x以降だとsidekiqのエラーハンドリング機能がAJから利用しやすくなっている
    - 「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.」
      という記載がある、exponential backoffが何を示すのが不明。挙動を見た感じ、自動的にsidekiqのリトライが実行されるわけではなさそう

### retry_onの挙動に関しての調査
retry_onはrescue_fromを生成するための提供されているメソッドで、実際に呼ばれているのはretry_jobであることがわかりました
retry_onで制御できる範囲を超えた場合は、rescue_fromからretry_jobを呼び出す処理をApplicationJobへ追加する必要があります

retry_onの実装: https://github.com/rails/rails/blob/7b5cc5a5dfcf38522be0a4b5daa97c5b2ba26c20/activejob/lib/active_job/exceptions.rb#L45-L59

制御できる範囲は、再試行までの遅延間隔、再試行の上限、上限を超えた場合の処理です

## 実装すべきコード
jobの例外処理を定義します
RETRY_LIMIT回だけretry_jobを行い、超えた場合は、必要な処理を行った後`raise error`として処理します
StandardErrorでエラーをcatchするようにしているので、条件や処理が異なるエラーは別途rescue_fromを定義していく形です

```app/jobs/application_job.rb
# frozen_string_literal: true

class ApplicationJob < ActiveJob::Base
  RETRY_LIMIT = 3

  rescue_from StandardError do |error|
    if executions <= RETRY_LIMIT
      retry_job(wait: delay, error: error)
      NewRelic::Agent.notice_error(error) # ここになんらかの処理を追加したい場合、retry_onだと実装できない
    else
      NewRelic::Agent.notice_error(error)
      raise error
    end
  end

  # retryが実行されるまでの間隔を決定するロジック
  #
  # 再試行の回数が増えるごとにwaitする時間が増える
  # executions: 再試行回数
  # 以下の秒数waitされる
  #
  # - 1回目: 61s
  # - 2回目: 92s
  # - 3回目: 303s
  def delay
    60 + executions**5
  end
end
```

mailerで利用するJobの作成です、rails自体に実装されているコードを参考に作成しています

```app/jobs/mail_delivery_job.rb
# frozen_string_literal: true

# see https://github.com/rails/rails/blob/v5.2.4.3/actionmailer/lib/action_mailer/delivery_job.rb
class MailDeliveryJob < ApplicationJob
  queue_as :mailers

  def perform(mailer, mail_method, delivery_method, *args)
    mailer.constantize.public_send(mail_method, *args).send(delivery_method)
  end
end
```

mailerで非同期処理で使用するJobを指定します

```app/mailers/application_mailer.rb
# frozen_string_literal: true

class ApplicationMailer < ActionMailer::Base
  default from: '"サンプル" <sample@example.com>'
  layout 'mailer'
  self.delivery_job = MailDeliveryJob
end
```

### 参考
- [ActiveJobとSidekiqを組み合わせたときに、ジョブ単位のオプションはどこまで設定できるか？ \- Qiita](https://qiita.com/ledsun/items/3f5cdecd4d53d66d0254)
- [Sidekiq アンチパターン: 序 \- SmartHR Tech Blog](https://tech.smarthr.jp/entry/2017/04/20/165555)
- [Railsでメールを扱うときに困ったこと \- Qiita](https://qiita.com/luccafort/items/68b6da5eb622a81e44da)
- 追加されたPR: https://github.com/rails/rails/pull/25991
  - > Declarative exception handling of the most common kind: retrying and discarding.
- 該当コード: https://github.com/rails/rails/blob/d2d8b2892dafff70d7ae44d0a5cce5d834fc708e/activejob/lib/active_job/exceptions.rb#L56
- [\[Rails\] rescue\_fromに関する留意点 \- Qiita](https://qiita.com/nagata03/items/232f2749294d88719542)