LoginSignup
7
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-17

概要

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で置き換える必要がある
  • 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

参考

7
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5