Sidekiq のリトライ処理についてまとめた
業務で Sidekiq を使う時、ジョブの中でやりたいことが Slack へ通知を送ることだったのですが、
API を使うと当然ネットワークのエラーなど開発者にはどうにも出来ないエラーが起こり得ます。
そんな時のために Sidekiq のリトライ処理について調査したのでまとめておこうと思います。
前提
- ruby 2.6.3
- rails 6.0.3
- sidekiq 6.4.1
目次
- Worker を生成する
- リトライ処理を書く時に気をつけること
- リトライ処理を書く
Worker を生成する
まずは Sidekiq の Worker を作成します。
$ rails generate sidekiq:job SlackNotify
ジョブでやりたいことは Slack へ通知を送ることとします。
class SlackNotifyWorker
include Sidekiq::Job
def perform(*args)
# Do something
end
end
リトライ処理を書く時に気をつけること
リトライ処理を書く前に考えるべきことは、ジョブが冪等になっているかどうかです。
ジョブをリトライした時に副作用が生じないようにジョブを設計しなければいけません。
例えば、複数の Slack チャンネルに通知したい場合を考えます。
def perform(channels)
channels.each do |channel|
SlackNotifier.call(channel)
end
end
これだと1個目のチャンネルへの通知は成功したけど、2個目のチャンネルへの通知に失敗して
しまった際ジョブをリトライした時に、再度1個目のチャンネルに通知が飛んでしまうと言う
副作用が発生してしまいます。この様な副作用が発生しないように、1つのチャンネルに通知する
ジョブを実装しなければいけません。
def perform(channel)
SlackNotifier.call(channel)
end
このジョブでは副作用は発生しません。ジョブを実装する際は冪等性に気を付けましょう。
リトライ処理を書く
Sidekiq はジョブの実行に失敗した時、デフォルトで25回リトライします。
sidekiq.yml
の中で max_retries
を指定することでデフォルト値を変更することも
出来ますし、sidekiq_options
を使ってジョブごとに指定する事も出来ます。
:max_retries: 5
私はジョブを実装する時は他の開発者のためにも明示的にリトライ回数を指定しておいた方が
良いと思っています。
class SlackNotifyWorker
include Sidekiq::Job
sidekiq_options retry: 5
def perform(channel)
SlackNotifier.call(channel)
end
end
またリトライ回数だけでなく、リトライに失敗した時に実行される処理も定義出来ます。
私は最初ジョブを実装した時、下記の様に例外処理を書いていたのですが、sidekiq_options
みたいにリトライに失敗した時の処理を定義出来たらなーと思い Wiki をちゃんと読んでみると
sidekiq_retries_exhausted
というメソッドがありました。
class SlackNotifyWorker
include Sidekiq::Job
sidekiq_options retry: 5
def perform(channel)
SlackNotifier.call(channel)
rescue StandardError => e
# Do something
end
end
こんな感じで書けます。
class SlackNotifyWorker
include Sidekiq::Job
sidekiq_options retry: 5
sidekiq_retries_exhausted do |msg, ex|
ExceptionNotifier.call(msg, ex)
end
def perform(channel)
SlackNotifier.call(channel)
end
end
Sidekiqのコードを読んでみると、定義されたブロックは retries_exhausted
というメソッドの
中で呼び出される様です。引数に msg
と exception
を渡してますね。
def retries_exhausted(jobinst, msg, exception)
begin
block = jobinst&.sidekiq_retries_exhausted_block
block&.call(msg, exception) # ←ココ
rescue => e
handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
end
send_to_morgue(msg) unless msg["dead"] == false
Sidekiq.death_handlers.each do |handler|
handler.call(msg, exception)
rescue => e
handle_exception(e, {context: "Error calling death handler", job: msg})
end
end
渡された msg
の中身は
# msg
{
"retry"=>5,
"queue"=>"article_notifier",
"args"=>["invalid_channel"],
"class"=>"SlackNotifyWorker",
"jid"=>"ab88ef1f049ed91d5731ff18",
"created_at"=>1649118311.218128,
"enqueued_at"=>1649118311.2181807,
"error_message"=>"channel_not_found",
"error_class"=>"Slack::Web::Api::Errors::ChannelNotFound",
"failed_at"=>1649118311.6029007,
"retry_count"=>5
}
こんな風になっていました。 exception
は msg[’error_class’]
でも確認出来るSlack::Web::Api::Errors::ChannelNotFound
です。
ちなみにリトライの間隔も指定することが出来ます。デフォルトでは
(count**4) + 15 + (rand(10) * (count + 1))
となっているのですが、 sidekiq_retry_in
を利用して等間隔でリトライさせる事も出来ます。
class SlackNotifyWorker
include Sidekiq::Job
sidekiq_options retry: 5
sidekiq_retries_exhausted do |msg, ex|
ExceptionNotifier.call(msg, ex)
end
sidekiq_retry_in do |count, ex|
10
end
def perform(channel)
SlackNotifier.call(channel)
end
end
count
にはリトライした回数が、 ex
には例外クラスが入っています。
以上でリトライ処理の実装が完了となります。
Discussion