DEV Community

Cover image for How to prevent race conditions in Rails apps
Harut Margaryan
Harut Margaryan

Posted on

How to prevent race conditions in Rails apps

Imagine a case where you have a model with callback when 2 requests are send at the same time, and a new record is created in that model with callbacks. Your goal is to trigger the callback 1 time to make sure no race conditions happens.

Lets have an example to go more deeper into that.

We have a courses and students table and each course can have many students. We wanna trigger an email when the specific course got its first student. Here if 2 or more students try to enter the course at the same time we can get several emails but our goal is to be just informed via 1 email.

class Course extend ActiveRecord
  has_many :students
  after_create :notify_via_email


  def notify_via_email
    if students.count == 1
      # send email to the administration
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we have in general and there is no way to tell whether this is the second student or the first because of race condition.

There is a good solution which makes possible to prevent race conditions using redis or other in memory storage to store, have a sidekiq worker and make it to work as a queue so that one callback can be processed at any given time.

class EmailExecutorJob < ApplicationJob
  def perform(student)
    lock_key = "EmailExecutorJob:#{student.id}"

    if redis.get(lock_key)
      self.class.set(wait: 1.second).perform_later(student)
    else
      redis.set(lock_key, student.id, 1.second)
      if students.count == 1
        # send email to the administration
      end
      redis.delete(lock_key)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

And in the students model we can have this code

class Course extend ActiveRecord
  has_many :students
  after_create :notify_via_email


  def notify_via_email
    EmailExecutorJob.perform_later(self)
  end
end

Enter fullscreen mode Exit fullscreen mode

As you can see we converted model callback to trigger EmailExecutorJob which checks whether other job is running and delays the current one. We are storing current student id in redis with prefixed key and that way we clearly can tell whether there is another job for the same name is running or not.
In this way we can prevent any race conditions.

P.S.
I haven't tested this and give just an example, but if you get stuck anywhere feel free to contact and I will be glad to help with such scenarios.

Top comments (2)

Collapse
 
kortirso profile image
Bogdanov Anton

1) what do you think about with_advisory_lock?
2) why not just class Course < ApplicationRecord

Collapse
 
mharut profile image
Harut Margaryan

Nice suggestion,
but 1 thing this works with models and tightly coupled with db locks and you have to install a gem for this. My approach is more generic and can be applies on other scenarios as well.