Rails Advisory Locks

Advisory Lock: Imagine multiple processes trying to update a row in database. An advisory lock can be used to ensure only one process can modify the row at a time.

Even rails use postgres advisory locks when running migrations, If two processes at a same time try to migrate rails will throw ActiveRecord::ConcurrentMigrationError. We are using pgbouncer a connection pooler for PostgreSQL. So we are unable to use postgres advisory lock.

There is also another gem call Redlock which is also like advisory lock but for our case we were unable to use it.

Our case is simple, We have two models Invoice::Item and Invoice::TaxItem

Here amount 50,000 will be stored in invoice_items and amount 4,500 will be stored in tax_items table. Here when user changes amount to 60000 we have to calculate tax value and store it(We have to store tax value because GST percentage will be changed anytime in future). Same invoice can be edited by multiple users or In some rare cases when user edits amount to 60K and then changes to 70K in next second 70K request finishes first and then 60K request finished second. So in invoice_item table amount will be 70k but in tax_items table tax value will be for 60K. So we need a straight forward way to calculate tax amount, If one request starts to save the invoice_item next process should wait until the current request is finished, tax should be calculated in FIFO method.

We are going to use redis, and create a class RedisUn with four arguments lock_name, expires_in, wait_time, &block

RedisUniqueProcess will accept block, when block is called by multiple processes one by one it will get executed. How much time other process should wait can be given wait_time argument. For example we use counter cache to save invoice or purchase orders count based on users and count will be based on users permissions. When doing counter cache multiple processes try to access lock only one needs to be executed others can be skipped at that time we have to give wait_time as zero, If wait_time is nil process will wait indefinitely until the lock automatically expires.

RedisUniqueProcess.wait_until_execution("update_item_#{id}_lock") do
  calculate_new_tax_items
end

We tried many gems, things which were very complex and most of them never worked when scaling. Above code is so far working good.

Thanks for reading, If you like this share this post.