When saving computed values in the database in your rails app, you must be aware that is possible to find unexpected errors in the result thanks to race conditions.
I have already shared an exercise to help you get more sensitivity about when an implementation can save a wrong value thanks to race conditions.
Here I want to share one tip to help you avoid save the wrong value due race conditions while trying to save a computed value.
To talk about something concrete I will use the “account balance” as an example, but you can use this approach for different types of calculations.
Imagine that you have an Account
record that has many entries
, and you want to update the balance
each time an Entry
is created. The balance
is the sum of the amount
of each entry
.
Imagine each account will need to create many entries concurrently, maybe on different background jobs or different requests. So if you want to calculate the balance and save it just after an entry is created, you could have problems with race conditions.
Instead of “just” saving the balance and maybe save a wrong value, you can wrap the full operation in a with_lock
block.
By calling with_lock
you will start a transaction and acquire a lock for the row in one go. The block is called from within a transaction, and the object is already locked.
class Account < ExampleRecord
has_many :entries
def create_entry(amount:)
with_lock do
# This block is called within a transaction,
# and account is already locked
entries.create(amount: amount)
update_balance
end
end
def update_balance
balance = entries.balance
update!(balance: balance)
end
end
class Entry < ExampleRecord
belongs_to :account, touch: true
def self.balance
sum(:amount)
end
end
You can learn more of with_lock
and the ActiveRecord::Locking::Pessimistic
module on the rails api.
We are not avoiding race conditions, other problems can happen, but we are at least avoiding saving the wrong value due to race conditions.
No, locks can produce other type of problems, like deadlocks.
If you have experience with other problems with this solution, please leave a comment =)
Here I try to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or all posts.