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
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
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.
Happy locking!