凄くシンプルに以下のようなコントローラとモデルがあるとする。
class UsersController < ApplicationController
def create
render json: User.create!(user_params).to_json, status: 201
end
def user_params
params.permit(:name)
end
end
class User < ActiveRecord::Base
validates name,
presence: true,
uniqueness: { :case_sensitive => false }
end
/users
へ同じname
パラメータでかつ同時POSTされたりすると、タイミングによってはuniqueness
バリデーションを抜けられることがある。
DBのテーブル定義でname
カラムにunique
インデックスを張っていれば、ActiveRecord::RecordNotUnique
がraiseされる。
エラーハンドラーがActiveRecord::RecordInvalid
を補足していて、モデルの#errors.messages
などをパラメータエラーのヒントとして返すようにしていても台無しになってしまう。
このケースでは再度バリデーションを通してActiveRecord::RecordInvalid
をraiseさせたい。
コントローラの処理をリトライする
class ApplicationController < ActionController::Base
# counter: リトライする回数
# exception: リトライする例外
def retry_action(counter = 1, exception = StandardError)
yield
rescue exception => e
retry if (counter -= 1) >= 0
raise e
end
def retry_from_record_not_unique
retry_action(1, ActiveRecord::RecordNotUnique) do
yield
end
end
end
class UsersController < ApplicationController
around_action :retry_from_record_not_unique, only: :create
def create
render json: User.create!(user_params).to_json, status: 201
end
def user_params
params.permit(:name)
end
end
リトライ回数と補足する例外を抽象化したretry_action
を定義し、リトライさせたい例外のコールバック用メソッドretry_from_record_not_unique
を作り、around_action
により対象のアクションをブロックで渡す。
こうするとレースコンディションにより、バリデーションを抜けてActiveRecord::RecordNotUnique
がraiseされても、再処理によってuniqueness
でバリデーションエラーが起こりActiveRecord::RecordInvalid
がraiseされる。
参考: http://stackoverflow.com/questions/9020648/handle-timeouterror-with-retry
アクションにrescue
はやすほうが、シンプルでわかりやすい気もする。。
class UsersController < ApplicationController
def create
render json: User.create!(user_params).to_json, status: 201
rescue ActiveRecord::RecordNotUnique => e
counter ||= 1
retry if (counter -= 1) >= 0
raise e
end
def user_params
params.permit(:name)
end
end
こういうのデバッグするのが難しくて、いい方法があったら知りたい。
ちなみに今回はUserモデルのバリデーション後にディレイを入れてデバッグしてた。。
class User < ActiveRecord::Base
validates name,
presence: true,
uniqueness: { :case_sensitive => false }
after_validation { sleep 3 }
end