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
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
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
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)
1) what do you think about with_advisory_lock?
2) why not just
class Course < ApplicationRecord
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.