Benito Serna Tips and tools for Ruby on Rails developers

Pick a safe previous date to avoid race conditions saving computed values

February 14, 2023

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 you can try to avoid race conditions when saving a computed value.

Using the account balance as an example

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.

The account balance example

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.

Tip: Pick a safe previous date

If your app is really concurrent and you can’t show posible off value, maybe you can pick a “safe previous date” like “yesterday” or “an hour ago” and present the value for that date.

You can run a daily rake task to save the balance with something like this:

class Account < ApplicationRecord
  has_many :entries

  def balance_at(datetime)
    entries.where(created_at: ..datetime).sum(:amount)
  end

  def update_safe_balance!
    self.safe_balance_time = 1.day.ago.end_of_day
    self.safe_balance = balance_at(safe_balance_time)
    save!
  end

  def update_safe_balance_later
    Account::UpdateSafeBalanceJob.perform_later(self)
  end
end

class Entry < ApplicationRecord
  belongs_to :account, touch: true
end

class UpdateSafeBalanceJob < ApplicationJob
  def perform(account)
    account.update_safe_balance!
  end
end

# on a daily task
Account.find_each(&:update_safe_balance_later)

And then you can use it in your views indicating the time of the calculation:

<p>
  Balance:
  <%= number_to_currency account.safe_balance %>
  <small>Updated <%= account.safe_balance_time %></small>
</p>

Are we really avoiding race conditions?

As far as I understand, yes! We are avoiding race conditions because we are calculating the balance for a “safe” point in time. What does “safe” means will depend on your use case.

Is this a perfect fix?

No, because you are not really showing the “current value”, and you will need to make sure that your users understand that this is a calculation for a previous point in time, and that can be confusing.

Reference

I learned this tip from recycledcoder on reddit.

Do you know other problems with this solution?

If you have experience with other problems with this solution, please leave a comment =)

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying 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 post by topic.