ActiveRecord::RecordNotUniqueが起きたときにリトライ処理を正しく実装する
- MySQL5.6
- REPEATABLE READ
- rails5.2.2
- ruby2.6
文脈
ActiveRecord::RecordNotUnique
は、ユニーク制約を貼っているカラムに対して複数のコネクションから同時に同じ値の書き込み処理が行われた時に発生します。
WEBアプリケーションでこれが発生すると、1リクエストのみが正常終了し他のリクエストは異常終了してしまいます。
この手のエラーはよく発生しますが、発生頻度が低く致命的ではなかったり、RDBのトランザクションの知識が要求されるので放置気味な印象がありますが、
リクエストがエラーになるとユーザはエラー画面を見ることになるのでできれば直したいですね。
結論
railsドキュメントにあるようにtransaction(requires_new: true)
ブロックで囲ってその中で内でretry
を実行すればよいです。
https://github.com/rails/rails/blob/94b5cd3a20edadd6f6b8cf0bdf1a4d4919df86cb/activerecord/lib/active_record/relation.rb#L115-L161
begin
CreditAccount.transaction(requires_new: true) do
CreditAccount.find_or_create_by(user_id: user.id)
end
rescue ActiveRecord::RecordNotUnique
retry
end
説明
requires_new: true
は何かというと、すでにトランザクションの中にいる時にトランザクションをネストしてくれるようになります。
トランザクション中でトランザクションがネストしていないと、retryが走ったところで他トランザクションでコミットされたレコードが見えずに、ActiveRecord::RecordNotUnique
が永遠と発生して無限ループになってしまいます。
つまりトランザクションの中ではなかったら、requires_new: true
はなくても動作します。
しかし、今後rescue ActiveRecord::RecordNotUnique; retry
を実装するときは、呼び出し元の実装が変わって、トランザクション中でコールされてもいいようにtransaction(requires_new: true)
で囲っておくとよいでしょう。
これは余談ですが、find_or_create_byを使うと上記のようにretryを自分で実装する必要がありますが、rails6には create_or_find_by
というretry処理の実装が不要なメソッドがマージされました。
https://github.com/rails/rails/pull/31989
処理順番を入れ替えることでretryが不要になるのはおもしろいですね。
-
category:
- rails