Rails 7 introduces optional transaction arguments to the with_lock
method, enhancing concurrency control in web applications. Let’s explore the concepts of pessimistic and optimistic locking and how the new features in Rails 7 improve handling nested transactions.
Optimistic and Pessimistic Locking
Optimistic Locking allows multiple users to update the same record concurrently, validating changes only at the commit stage and rejecting conflicting updates, thus reducing lock overhead but risking conflicts. Pessimistic Locking prevents simultaneous updates by locking a record when a user starts updating it, blocking others until the lock is released, avoiding conflicts but potentially slowing down the application with high user access.
Using lock!
for Pessimistic Locking
In Rails, ActiveRecord::Locking::Pessimistic
supports row-level locking using the lock!
method within a transaction. This method is crucial for scenarios where simultaneous updates might lead to incorrect data.
For instance, if two users press the like button on a post at the same time, instead of the like count incrementing by two, it may only increase by one. To handle this, you can use lock!
within a transaction:
1
2
3
4
5
ActiveRecord::Base.transaction do
post = Post.find("45").lock!
post.comment_count += 1
post.save!
end
This code initiates a transaction, locks the post record, updates the like count, and saves the changes. The lock ensures that no other transaction can modify the record until the current transaction completes.
You can also use various locking strategies with lock!
, depending on your database. For example, FOR UPDATE NOWAIT
on PostgreSQL blocks other transactions trying to update, delete, or select the same row for update until the current transaction ends. If another transaction attempts to lock the same record, it will throw an error like:
1
2
TRANSACTION (1.9ms) ROLLBACK
PG::LockNotAvailable: ERROR: could not obtain lock on row in relation "posts" (ActiveRecord::LockWaitTimeout)
Simplifying with with_lock
The with_lock
method simplifies the locking mechanism by creating a transaction and applying a lock internally:
1
2
3
4
5
post = Post.find("45")
post.with_lock do
post.comment_count += 1
post.save!
end
Enhancements in Rails 7
Before Rails 7, adding transaction arguments like isolation
, requires_new
, and joinable
with with_lock
was not possible. Nested transactions required multiple transaction blocks with lock!
:
Before Rails 7:
1
2
3
4
5
6
7
8
9
10
11
ActiveRecord::Base.transaction do
post = Post.find("45").lock!
post.comment_count += 1
post.save!
ActiveRecord::Base.transaction(requires_new: true) do
author = post.author.lock!
author.posts_liked += 1
author.save!
end
end
With Rails 7:
Rails 7 allows you to use with_lock
for nested transactions in a more readable, simpler, and streamlined way.
1
2
3
4
5
6
7
8
9
10
11
12
post = Post.find("45")
post.with_lock do
author = post.author
post.comment_count += 1
post.save!
author.with_lock do
author.posts_liked += 1
author.save!
end
end