ActiveRecord TransactionLock and Testing
Transaction Locking is helpful for preventing race conditions when updating records in the database and ensuring atomic updates. It ensures the ACID Properties. ACID Properties of Transactions Atomicity, Consistency, Isolation, and Durability are the main properties of a transaction ...
Transaction Locking is helpful for preventing race conditions when updating records in the database and ensuring atomic updates. It ensures the ACID Properties.
ACID Properties of Transactions
Atomicity, Consistency, Isolation, and Durability are the main properties of a transaction operation. These properties are related and should be considered together. Each of these properties described in detaill below.
Atomicity
Transaction is a combination of multiple separate actions. And Atomicity ensure that either all the operations should be finished successfully or none of the operation will make effect in the database.
Consistency
It means that transaction will change the state of the database from one consistent state no another consistent state. And transaction cant break this consistency rules.
Isolation
Any changes made in any step of the transaction cant be visible to one another until the full transaaction process complete. it can be implemented differently in different database.
Durability
If transaction completed successfully, the result cant be lost even in abnormal crash of the program. Once commited, the data cant be lost and result cant be undo.
Active Record provides two locking mechanisms:
- Optimistic Locking
- Pessimistic Locking
Optimistic Locking
With Optimistic locking, multiple users can access the same record for edits. It checks that if another process has made any changes meanwhile its open for one process.An ActiveRecord::StaleObjectError exception is thrown for this and the update will be ignored.
To use optimistic locking, the table needs to have a integer column called lock_version. When the record is updated, Active Record will increments the lock_version column. If an update request is made with a lower value in the lock_version field than the current value of lock_version column in the database, the update request will fail with an ActiveRecord::StaleObjectError. Example:
u1 = User.find(1) u2 = User.find(1) u1.first_name = "abc" u1.save u2.name = "efg" u2.save # This will fail with an error
The programmer then need to handle the situation after the exception rises.
Pessimistic Locking
Pessimistic locking uses a locking mechanism provided by the database system. It lock exclusively the selected row and before finished by one process no other process cant read or write in this time.
User.transaction do u = User.lock.first u.name = 'abc' u.save! end
How to test in rails
For developers concurrency is a hard thing to rightly implemented, and unfortunately it is much more complex to test as well. For this purpose, we’ll use the gem fork_break in rails which allows us to start subprocesses to execute our code, and synchronize them from the parent process using breakpoints.
You can read the documentation from here: https://github.com/forkbreak/fork_break
make sure to set use_transactional_fixtures to false in your rspec describe block.
self.use_transactional_fixtures = false
You can wirte a function and use it to test any method with concurrent processes.
def create_concurrent_calls(object, method, params=[]) processes = 2.times.map do |i| ForkBreak::Process.new do |breakpoints| original_method = object.method(method) object.stub(method) do |*args| value = original_method.call(*args) breakpoints << method value end object.send(method, *params) end end processes.each{|process| process.run_until(method).wait} processes.each{|process| process.finish.wait} end
Now lets test one method in User model named transaction_method. A model rspec test would then be like :
describe "User" do let!(:user){FactoryGirl.create :user} context "when concurrent calls to #transaction_method" do it "should only make change once" do create_concurrent_calls(user, :transaction_method, [params]) expect(change_made).to eq(1) end end end