Rails optimistic locking, pessimistic locking and how to solve StaleObjectError

Optimistic locking and pessimistic locking are two techniques used in database management systems to handle concurrent access to shared data.

Optimistic locking

Optimistic locking assumes that conflicts between processes are unlikely and involve placing a tag in the record, indicating that the record has been modified. Then, when a process attempts to update the record, the tag is checked to ensure that the record has not been modified by another process. If the tag indicates a change, the process is informed of the conflict and must take additional steps to resolve it.
Optimistic locking is often used in cases where conflicts are rare to avoid excessive locking and delays in accessing the data. In Rails, all you need to do is add an extra column to the table. The rest is supported out of the box.

# optimistic locking is supported by rails out of the box by adding a column into the table
class AddLockVersionToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :lock_version, :integer, default: 0, null: false
  end
end

p1 = User.first
p2 = User.first

p1.email = "[email protected]"
p1.save!
p2.email = "[email protected]"
p2.save! # raises an ActiveRecord::StaleObjectError
p2.destroy! # this will also throw an ActiveRecord::StaleObjectError
Optimistic locking in Rails

Pessimistic locking

Pessimistic locking involves locking the data as soon as a process gains access to it, preventing other processes from accessing the data until the lock is released. This technique is used when we expect many processes to try to access the same data at the same time.

User.transaction do
  users = User.where(id: [1, 2])
  user1 = users.first
  user2 = users.last
  
  user1.lock! # block row for any other processes
  user2.lock! # block row for any other processes
  
  user1.balance -= 100
  user1.save!
  user2.balance += 100
  user2.save!
end

# or
user = User.first
user.with_lock do
  # user is already blocked for any other processes
  user.balance -= 100
  user.save!
end
Pessimistic locking in Rails

How to solve StaleObjectError

If you see this error, you can use the reload method to refresh the data and retry the action.

def do_some_things
  begin
    current_user.update(security_token: nil)
  rescue ActiveRecord::StaleObjectError
    current_user.reload # reload the data and try again
    retry
  end
end

Or you can try to lock the object using the pessimistic locking described above.