LoginSignup
5
5

More than 5 years have passed since last update.

レースコンディションによりRecordInvalidを抜けてRecordNotUniqueを引いてしまうときの対処

Last updated at Posted at 2015-08-27

凄くシンプルに以下のようなコントローラとモデルがあるとする。

users_controller.rb
class UsersController < ApplicationController
  def create
    render json: User.create!(user_params).to_json, status: 201
  end

  def user_params
    params.permit(:name)
  end
end
user.rb
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させたい。

コントローラの処理をリトライする

application_controller.rb
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
users_controller.rb
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はやすほうが、シンプルでわかりやすい気もする。。

users_controller.rb
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モデルのバリデーション後にディレイを入れてデバッグしてた。。

user.rb
class User < ActiveRecord::Base
  validates name,
    presence: true,
    uniqueness: { :case_sensitive => false }

  after_validation { sleep 3 }
end
5
5
0

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
5
5