Rails | Avoiding race conditions while updating particular field in DB

2 Min. Read
Sep 19, 2018

 Ruby On Rails Race Condition Update Database PG

It’s okay to make mistakes when you are up to building simple applications. But for mission critical applications like Banking/Payment Processors, you gotta be very careful and keep listening to seniors and experts in critical matters.

Lets say you are working on a feature to update the total amount deposited in a particular account. Simple code will do like

1
2
3
4
# just after amount is debited from payee's account
def credit_receiver_account(debited_amount)
  update_attribute :total_amount, total + debited_amount
end

What’s wrong with the code above?

Consider that the receiver account belongs to a conglomerate like google who keeps receiving payments every milliseconds. So, there is always chances that the variable total contains an older/obsolete data. This can be a chaotic situation. This happens because the ruby object corresponding to the database row can represent different values.

How to avoid such race condition?

Just like to stop the race you stop/pause all others and let only one to run, you will lock the data to maintain consistency.

There are two types of locking – optimistic and pessimistic


What is Optimistic Locking?

Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened, an ActiveRecord::StaleObjectError exception is thrown if that has occurred and the update is ignored.

Usage

Active Record supports optimistic locking if the lock_version field is present. Each update to the record increments the lock_version column and the locking facilities ensure that records instantiated twice will let the last one saved raise a StaleObjectError if the first was also updated. Example:

1
2
3
4
5
6
7
8
p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.first_name = "should fail"
p2.save # Raises an ActiveRecord::StaleObjectError

Optimistic locking will also check for stale data when objects are destroyed. Example:

1
2
3
4
5
6
7
p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.destroy # Raises an ActiveRecord::StaleObjectError

Using Pessimistic locking

1
2
# select * from accounts where id=1 for update
Account.lock.find(1)

Call lock(‘some locking clause’) to use a database-specific locking clause of your own such as ‘LOCK IN SHARE MODE’ or ‘FOR UPDATE NOWAIT’. Example:

1
2
3
4
5
6
7
8
9
Account.transaction do
  # select * from accounts where name = 'shugo' limit 1 for update
  shugo = Account.where("name = 'shugo'").lock(true).first
  yuko = Account.where("name = 'yuko'").lock(true).first
  shugo.balance -= 100
  shugo.save!
  yuko.balance += 100
  yuko.save!
end

Shorter Syntax

The race condition can be fixed using Pessimistic Locking technique using the locking feature of the Database. This technique not only protects from stale-object updates but also from concurrent updates(occurring almost simultaneously) in real-world (very rare though).

1
2
3
4
5
def credit_receiver_account(debited_amount)
  self.with_lock do
    update_attribute :total_amount, total_amount + debited_amount
  end
end

You might also like

How To Apply For Adding Category To Driving License In Nepal Online 

ShivaShiva Bhusal (CTO) 0 Min. Read Feb 16, 2020

Its easy to apply for adding category driving license online here in Nepal. Just goto the link mentioned with mentioned document on time.
Read More..

Ruby Object Model 

ShivaShiva Bhusal (CTO) 2 Min. Read Dec 28, 2019
 Ruby Object Model



By default, its not that easy to configure your application with i18n. If you wish to support your local language like `esp`, `np`, `hi`, `mx`, etc. you need to follow the steps i tell you.
Read More..


Write To Me

Hire me shiva bhusal
We'll never share your email with anyone else.
I'll never share your email with anyone else.