Type a keyword and hit enter to start searching. Press Esc to cancel.

Currently Reading

Thread Safe Model Uniqueness Validations

Rails uniqueness validations are not thread safe!  This is a problem when even single threaded Rails servers are susceptible to race conditions.  Luckily, this is an easy problem to avoid.

If you have a multi-threaded Rails server like Puma or Passenger Enterprise, use asynchronous background processing with tools like Sidekiq, or have a load balanced environment with multiple web servers then the race condition is real!  Just defining a uniqueness validation will not keep your data unique and can cause a lot of headaches.

First, let’s look at why they are not thread safe with a simple example.  Take the following model.

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :email
      t.string :name
      t.timestamps
    end
  end
end
class User  < ActiveRecord::Base
  validates :email, :name, presence: true
  validates :email, uniqueness: true
end

Now for a simple race condition, two users attempt to make an account using the same dummy email address ‘john@doe.com’.  To validate the uniqueness, Rails will query the database for the existence of the email; SELECT COUNT(id) FROM users WHERE email = 'john@doe.com'.  Since both requests come in around the same time, the validation returns a count of 0.  Now, Rails will insert the new users, with non-unique email addresses!

Luckily the solution is simple.  Just add a unique index to the table:

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :email
      t.string :name
      t.timestamps

      t.index :email, unique: true
    end
  end
end

Or, if you are creating your model/migration with a generator:

rails generate migration create_users email:string:uniq:index name:string

If the uniqueness validation has a scope, then you will need to create the uniqueness validation on all attributes of the scope.  Take the following example, unique usernames for each company.

class User < ActiveRecord::Base
  validates :username, uniqueness: { scope: :company_id } 
end

The migration would look like this.

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.integer :company_id
      t.string :username

      t.index [:username, :company_id], unique: true
     end
  end
end

When there is a unique index on the table and this race condition occurs, the second model to be created will throw the ActiveRecord::RecordNotUnique error.  This is an error from the DBMS and not the rails validation, so you must catch it.  There are a few ways to handle it, this will depend on how the code is executed.

If the user is being created because of a controller action, then we can re-validate the model.  This will allow Rails to create an error with the correct validator.

class UsersController < ApplicationController
....

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
       render 'new'
    end

  rescue ActiveRecord::RecordNotUnique => e
    @user.validate
    render 'new'
  end

...
end

Another way to handle this is with a retry.  Retries can be useful if this user is being created by some background process.  Such as a Sidekiq job that imports users from a third party.  Just be sure to not cause an infinite loop of retries.

class UserWorker
...
  def perform(attributes)
    retry_count ||= 0
    
    User.create!(attributes)
    
  rescue ActiveRecord::RecordNotUnique => e
    if (retry_count += 1) <= 1
      retry
    else
      raise
    end
  end
...
end

That’s it!  You now have thread safe uniqueness validations that can handle race conditions.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *