DEV Community

Augusts Bautra
Augusts Bautra

Posted on

2 1 1 1

Simple way to deal with race conditions in Rails

tl;dr

Wrap all .first_or_create!/find_or_create_by! calls in retryable block.

Retryable.retryable(
  on: [ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid]
) do .. end
Enter fullscreen mode Exit fullscreen mode

Discussion

Oftentimes in Rails apps we have a situation where only one of something is allowed. A user may be allowed only one profile picture, a question only one answer, only one pinned post etc.

In these situations I prefer using the "ensure" pattern with .first_or_create!/find_or_create_by!. There's a gotcha, however. Both of these methods are vulnerable to race conditions, especially if the ensuring logic is called in GET action context (though POSTs that do not disable to submit button may also suffer from this).

What happens is several near-parallel requests reach the .first_or_create!, and the check for any existing records returns false for both, so both proceed to creation, but only one will succeed in this, and the other will fail.

While we should strive to reduce unnecessary requests by disabling submit buttons after click etc., oftentimes the source of requests is not under our control - modern browsers tend to make pre-fetch requests etc., so handling the race condition is a must. Luckily this is easy.

I like retryable gem a lot for this, it has a clean, powerful DSL.

# in Gemfile
gem "retryable" # plus version lock
Enter fullscreen mode Exit fullscreen mode

Then wrap any .first_or_create!/find_or_create_by! call in a retrying block. By default it will retry just once and wait one second after a failure if there is one, you can tweak all that.

# before
user.filters.where(filter_name: self.class.name, context: context).first_or_create!

# after
Retryable.retryable(
  on: [ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid]
) do
  user.filters.where(filter_name: self.class.name, context: context).first_or_create!
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)