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?
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.