12/08/2018, 17:08

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 ...

Screenshot from 2016-03-22 17:41:35.png

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:

  1. Optimistic Locking
  2. 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
0