Search code examples
ruby-on-railsrubyrace-condition

Race condition in pessimistic locking example of rails documentation


I'm looking at the following example of the Rails documentation:

account = Account.first
account.with_lock do
  # This block is called within a transaction,
  # account is already locked.
  account.balance -= 100
  account.save!
end

My understanding is that the first method returns a record straight from the database, rather than a relation (similar to find). Now suppose balance = 200 and we have two concurrent requests, where both of them execute account = Account.first before they reach the with_lock block. Both requests now have 200 in memory.

Then, one request locks the record and modifies balance to 100. But when it unlocks the record, the second request will not read balance again, but it will have the old value of 200, so we'll still end up with race condition in this case. So my question is, shouldn't account = Account.first also be wrapped in with_lock block to truly avoid race condition?


Solution

  • with_lock reloads the object before yielding to the block, from the docs:

    Wraps the passed block in a transaction, reloading the object with a lock before yielding.

    The with_lock example from the docs is (almost) equivalent to:

    account = Account.first
    account.transaction do
      account.reload(lock: true)
      account.balance -= 100
      account.save!
    end
    

    The reload call ensures that the object's attributes get updated from the database.