概要
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のリトライが実行されるわけではなさそう
-
https://github.com/mperham/sidekiq/wiki/Active-Job#customizing-error-handling
retry_onの挙動に関しての調査
retry_onはrescue_fromを生成するための提供されているメソッドで、実際に呼ばれているのはretry_jobであることがわかりました
retry_onで制御できる範囲を超えた場合は、rescue_fromからretry_jobを呼び出す処理をApplicationJobへ追加する必要があります
制御できる範囲は、再試行までの遅延間隔、再試行の上限、上限を超えた場合の処理です
実装すべきコード
jobの例外処理を定義します
RETRY_LIMIT回だけretry_jobを行い、超えた場合は、必要な処理を行った後raise error
として処理します
StandardErrorでエラーをcatchするようにしているので、条件や処理が異なるエラーは別途rescue_fromを定義していく形です
# 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自体に実装されているコードを参考に作成しています
# 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を指定します
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: '"サンプル" <sample@example.com>'
layout 'mailer'
self.delivery_job = MailDeliveryJob
end
参考
- ActiveJobとSidekiqを組み合わせたときに、ジョブ単位のオプションはどこまで設定できるか? - Qiita
- Sidekiq アンチパターン: 序 - SmartHR Tech Blog
- Railsでメールを扱うときに困ったこと - Qiita
- 追加された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