How do I avoid a race condition in my Rails app?

Optimistic locking is the way to go, but as you might have noticed already, your code will never raise ActiveRecord::StaleObjectError, since child object creation in has_many association skips the locking mechanism. Take a look at the following SQL:

UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481)

When you update attributes in the parent object, you usually see the following SQL instead:

UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1

The above statement shows how optimistic locking is implemented: Notice the lock_version = 1 in WHERE clause. When race condition happens, concurrent processes try to run this exact query, but only the first one succeeds, because the first one atomically updates the lock_version to 2, and subsequent processes will fail to find the record and raise ActiveRecord::StaleObjectError, since the same record doesn’t have lock_version = 1 any longer.

So, in your case, a possible workaround is to touch the parent right before you create/destroy a child object, like so:

def attend(user)
  self.touch # Assuming you have updated_at column
  attendance = self.attendances.create(:user_id => user.id)
rescue ActiveRecord::StaleObjectError
  #...do something...
end

It’s not meant to strictly avoid race conditions, but practically it should work in most cases.

Leave a Comment