Sidekiqで引数追加を行う際に気をつけること
はじめに
非同期処理に引数を追加する修正で本番環境にエラーを発生させてしまった経験での学びです。
最初はレビューで防げたものの、根本的な理解が不足していたため、後日同様のミスでダウンタイムを発生させてしまいました。
この事象から時間は経っていますがなぜ?と聞かれてすぐに説明できるよう、振り返りも含めて残しておこうと思います。
なにが起きたのか
安易に引数追加を行ったことでデプロイ後ArgumentErrorが発生してしまいました。
以下のような修正を加えたとします。
# デプロイ前のコード
def perform(user_id, message)
end
# デプロイ後のコード
def perform(user_id, message, priority)
end
この時以下のエラーが発生します。
ArgumentError: wrong number of arguments (given 2, expected 3)
なぜ起こるのか
SidekiqはジョブをRedisにJSON形式で保存します。参考: Sidekiq Job Parameters
{
"class": "HogeWorker",
"args": [1, "Hello"], // デプロイ前は引数2つ
"jid": "abc123",
"created_at": 1234567890
}
このジョブがデプロイ後のコードで実行されると、引数の数が合わずArgumentErrorが発生します。
Redisにジョブが残っていなくても注意
デプロイ前のジョブが残っていない場合でも注意する必要があります。ローリングデプロイ中は新旧のコードが混在します
例えば、ローリングデプロイの場合はデプロイ中に新旧のコードが混在するため、以下のようなことが考えられます。
- 新コードのワーカーがデプロイされる
- そのワーカーが引数を3つでジョブを登録
- 旧コードのワーカーがそのジョブを取得(引数が2つのつもり)
- ArgumentErrorが発生
対策
後方互換性を保つ設計にすることで対策できます。
正解とは限りませんが参考までに、今考えられる対策方法です。
ハッシュ引数を用いる
def perform(user_id, options = {})
message = options["message"] || "Default message"
priority = options["priority"] || "normal" # 後から追加しても安全
end
このoptionsのようにハッシュとして引数を定義しておくことで、後に要素として追加するだけで値を渡すことが可能です。
ジョブを設計する際に引数が追加されそうな場合はあらかじめ用意しておくのがよさそうです。
デフォルト引数で定義
デフォルト値を設定した引数であればエラーは出ないはずです。
def perform(user_id, message, priority = "normal") # デフォルト値を設定
end
# 既存のジョブも新規のジョブも両方正常に動作する
まとめ
非同期ということで時間差があるということへの意識が足りていませんでした。
このような事象は互換性を保つことで防ぐことができます。非同期処理への理解を深めて安全に変更を加えられるようにしたいです。
本番環境で動いているジョブの修正はリスクが高いため、できれば最初の段階で拡張性を意識した設計にできると後の自分が喜びそうです。
番外編: より複雑な場合の移行方法(By Claude)
Claudeが提案してくれた移行方法です。
デフォルト引数が使えない場合(引数の順序変更など)は以下のような方法が使えそうです。
def perform(*args)
# 引数の数で処理を分岐
case args.size
when 2
# 旧形式
user_id, message = args
priority = "normal"
when 3
# 新形式
user_id, message, priority = args
else
raise ArgumentError, "Invalid arguments"
end
# 統一された処理
process_message(user_id, message, priority)
end
private
def process_message(user_id, message, priority)
end
こちらはとても複雑になるのでどうしても引数を変更したい場合の最終手段ですかね。。。
Discussion