Benito Serna Tips and tools for Ruby on Rails developers

Use after_touch to avoid to handle race conditions saving computed values

March 2, 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 to help you avoid save the wrong value due race conditions while trying to save 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: Use after_touch to trigger the operation

As far as I understand, if you are using Postgres with the default isolation level Read Committed, you can compute and save the value on an after_touch like this:

class Account < ExampleRecord
  has_many :entries

  after_touch :update_balance

  def create_entry(amount:)
    entries.create(amount: amount)
  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

Why does it work?

Or at least, why do I think that it works…

Apparently after_touch works something like this:

PostgreSQL uses by default the Read Committed isolation level.

In this mode (as far as I understand), if within a transaction there is an UPDATE of a row and in another concurrent transaction an UPDATE of the same row is attempted, PostgreSQL will wait for the first transaction to be committed before continuing with the second one.

This makes the operations we perform on after_touch to be isolated and avoids the problem of saving a wrong error due to another transaction registering another entry for the same account concurrently.

Do you want to test it?

You can check the examples to explore possible race conditions when caching custom computed values in Rails.

And more specifically you can test this example that uses after_touch.

Are we avoiding race conditions?

We are not avoiding race conditions, other problems can happen, but we are at least avoiding saving the wrong value due to race conditions.

Is this a perfect fix?

No, if the calculation of the value that you are trying to save takes a lot of time and your application is really concurrent, it can queue up many transactions and cause other problems.

Also the reason why it works, can be a little obscure.

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.