How to Avoid Race Conditions in Rails
Tags: ruby, rails
Published: 2022-05-24 21:32:27

*This article was originally written by [Godwin Ekuma](https://www.honeybadger.io/blog/authors/godwinekuma/) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/avoid-race-condition-in-rails/).*

When two users read and update a database record at the same time, you might run into critical problems that are undesirable. Let's say that for some reason, a customer clicks the pay button on the checkout page of an e-commerce website. It is possible to have a scenario where a particular customer is charged twice for the same order because the two requests to charge the order were made at almost the same time. This situation is called a "race condition."

>A race condition occurs when two or more threads can access shared data and try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Therefore, the result of the data change is dependent on the thread scheduling algorithm (i.e., both threads are "racing" to access/change the data). Problems often occur when one thread does a "check-then-act" (e.g., "Check" if the value is X, and then "act" to do something that depends on the value being X) and another thread does something to the value in between the "check" and the "act" processes. —[Stack Overflow](https://stackoverflow.com/questions/34510/what-is-a-race-condition)

In this post, we will take a look at locking, database constraints, and uniqueness, which are some of the approaches used to avoid race conditions in a Rails application.

## Locking

When your system allows multiple users to operate on the same records, you want to avoid situations where one user overrides changes made by another user without even looking at them.

For example, in an e-commerce system, you often have multiple admins managing the product inventory. A race condition occurs when two admins try to update the same product:

- *Admin 1* adds a new product "Bench Fitness Equipment for Home."
- *Admin 2* sees the new product and decides to edit the product's name to be more descriptive and changes it to "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version."
- At the same time, *Admin 1* notices that the initial name was not descriptive and decides to rename it to "Adjustable Workout Foldable Bench Fitness Equipment for Home Gym."
- *Admin 1* saves the new name a few milliseconds after *Admin 2*, overriding *Admin 2's* more descriptive naming.

Locking is a way to prevent such a scenario.

### Optimistic locking

>Optimistic locking allows multiple users to access the same record for edits and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened; an ActiveRecord::StaleObjectError exception is thrown if it has occurred, and the update is ignored.
-[Rails Guide](https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html)

To implement optimistic locking in Rails, add a `lock_version` column to the table in which you want to place the lock. Rails will automatically check this column before updating the record. Each update to the record increments the `lock_version` column, and the locking facilities ensure that records instantiated twice will let the last one saved raise a StaleObjectError if the first was also updated.

```ruby
product1 = Product.find(1)
product2 = Product.find(1)

product1.name =  "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"
product1.save

product2.name = "Adjustable Workout Foldable Bench Fitness Equipment for Home Gym"
product2.save # Raises an ActiveRecord::StaleObjectError
```

### Pessimistic locking

Pessimistic locking locks a record until all transactions on the record are done. Once the record is locked, another user cannot modify the record until the lock is released. While optimistic locking is used when database transactions are less likely to happen, it is used when database transaction conflicts are more likely to happen. It provides support for row-level locking using SELECT … FOR UPDATE and other lock types.

To implement pessimistic locking in Rails, chain `ActiveRecord::Base#find` to `ActiveRecord::QueryMethods#lock`:

```ruby
product = Product.lock.find(1) #lock the record

product.name = "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"

product.save! #release the lock

```

Alternatively, you can use the `ActiveRecord::Base#lock!` method to lock a record by its ID:

```ruby
  product = Product.find(1)
  order = Order.find(order_id)
  ActiveRecord::Base.transaction do
    product.lock! 
    product.name  = "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"
    product.save!
    order.paid!
  end
```

You can also start a transaction and acquire a lock at the same time using `with_lock`:

```ruby
product = Product.find(1)
product.with_lock do #lock the record
product.name = "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"

product.save!
end
```

### Advisory locking

Advisory locking is a mechanism used to prevent concurrent execution of code without necessarily locking the database table or row. In core Ruby, this is implemented using [mutex](https://en.wikipedia.org/wiki/Mutual_exclusion). In Rails, the Ruby gem [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock) can be used to add advisory locking (mutexes) to ActiveRecord when used with MySQL or PostgreSQL.

This gem automatically includes the `WithAdvisoryLock` module in all of your ActiveRecord models. Here's an example of how to use it when the `Product` is an ActiveRecord model, and lock name is a string:

```ruby
Product.with_advisory_lock("product_lock") do
  product = Product.find(1)

  product.name =  "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"
  product.save
end
```

**What happens:**

- The thread will wait indefinitely until the lock is acquired.
- While inside the block, you will exclusively own the advisory lock.
- The lock will be released after your block ends, even if an exception is raised in the block.

## Use a unique index instead of uniqueness validation

Rails' `ActiveRecord` validation, such as the one below, is not a database-level validation, It is an application-level validation and works well if there is no race condition:

Let's look at a typical real-life example. During the signup process, you require users to provide phone numbers and passwords. You don't want multiple users to sign up with the same phone number, so you add a uniqueness validation that should throw an error if there are duplicate phone numbers.

```ruby
class User < ApplicationRecord
  validates :phone_number, uniqueness: true
end
```

If a user mistakenly clicks the signup button two or more times in a row within a few milliseconds of each other, which often occurs in a web application, you are quite likely to have the following scenario:

- *Request 1* - Check if a user exists with that phone number and proceed, as no user is found.
- *Request 2* - Check if a user exists with that phone number and proceed, as no user is found.
- *Request 1* - Arrive at the code that inserts data into the database. A new user record gets created.
- *Request 2* - Arrive at the code that inserts data into the database. A new user record gets created.

*Request 2* passes the uniqueness validation because at the point of checking if a user exists, *Request 1* is yet to save the phone number to the database. Hence, both requests eventually get to insert new records into the database.

The way to prevent this kind of race condition is by adding a unique index constraint, such as the one below, which performs validation on the database level.

```ruby
class AddUniqueIndexToUsers < ActiveRecord::Migration
  def change
    # Have the database raise an exception anytime any process tries to
    # submit a record that has a code duplicated for any particular account
    add_index :users, :phone_number, unique: true
  end
end
```

## Sidekiq unique jobs

If you use Sidekiq workers to make changes to your database, you can use [SidekiqUniqueJobs](https://github.com/mhenrixon/sidekiq-unique-jobs) to add unique constraints to Sidekiq queues. Uniqueness is achieved by acquiring locks for a hash of a queue name, a worker class, and a job's arguments. By default, only one lock for a given hash can be acquired. If an attempt to acquire a new lock is made, an exception `SidekiqUniqueJobs::ScriptError` is raised.

Usage with Sidekiq is simple; all you have to do is configure it as a middleware for both the Sidekiq client and server. Add the following code to your `/config/initializers/sidekiq.rb`:

```ruby
Sidekiq.configure_server do |config|
  config.redis = { url: ENV["REDIS_URL"], driver: :hiredis }

  config.client_middleware do |chain|
    chain.add SidekiqUniqueJobs::Middleware::Client
  end

  config.server_middleware do |chain|
    chain.add SidekiqUniqueJobs::Middleware::Server
  end

  SidekiqUniqueJobs::Server.configure(config)
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV["REDIS_URL"], driver: :hiredis }

  config.client_middleware do |chain|
    chain.add SidekiqUniqueJobs::Middleware::Client
  end
end

```

## Conclusion

Race conditions, if not prevented, can lead to data integrity issues and sometimes even security issues. If you understand what can cause them and how to avoid them, you can build systems with consistent data and avoid spending hours debugging data integrity issues.