Pessimistic Locking

Judging from how few resources I have found detailing pessimistic locking usage in Rails - one is inclined to say that optimistic locking is by far the most common approach. Unfortunately optimistic locking simply does not provide any real guarantees once you have scaled past a single dyno (if it did at all).

When you deal with pessimistic locking one thing you have to handle carefully is database deadlocks - here is an example concern: https://gist.github.com/pik/96442de2af444ee3cdb1

Code implementing this concern may look like so:

Class Foo < ActiveRecord::Base
include Lockable
LOCKABLE = { on: :grandparent, through: :parent }
end

ActiveRecord::Base.transaction do
@foo.acquire_pessimistic_lock!
@foo.attributes = foo_params
@foo.save!
end

``

acquire_pessimistic_lock! will grab the lock on the LOCKABLE target (grandparent) making it impossible for other dependencies to manipulate dependent parent states at the same-time (if they could do so, they might race and invalidate AR validations which will still respond as true for the current thread/process using stale data).

The problem which keeps this from being a simple change is that there is a large codebase already written without pessimistic locking - and in fact the call to .save! might not always happen in the same module the changes are written in - since the dirty changes are clobbered on reload one consideration is to re-apply clobbered changes upon reload:

def acquire_pessimistic_lock!
# Note that lock! functions will clobber Dirty changes
unless transaction_has_lock_for(lock_target)
_future_state = lock_target.changes.each_with_object({}) { |(key, val), h| h[key] = val[1]}
lock = lock_target.lock!
transaction_add_lock(lock)
_future_state.each { |attr, value| lock.send(:write_attribute, attr, value) }
end
true
end

``

Another consideration would be to avoid sprinkling the codebase with explicit locking behaviour and to do this implicitly eg: before_save :acquire_pessimistic_lock!Enter code here…

and re-application of dirty state(as above), or by patching write_attribute:

def write_attribute(attr_name, value)
attribute_changed(attr_name, read_attribute(attr_name), value)
super
end

def attribute_changed(*)
acquire_pessimistic_lock!
end

``

I have so far not been very happy with any of the variations I could come up with, especially while implicit pessimistic locking maybe nice as it hopefully eliminates human-error as a potential bug-source, it may have some rather weird behaviour along the edge-cases – for example (below) - .lock! will call reload! which will scrap the child reference from @parent including any possible changes.

ActiveRecord::Base.transaction do
@parent.child.status = ‘archived’
@parent.lock!
@parent.child.save!
end

``

Most of the issues with the above come from acquiring the lock on the resource after the resource has been loaded (rather than in the initial select) - it’s tempting to want to add a patch to define_callbacks in ActiveRecord::QueryMethods but it’s not very simple - since the resources aren’t resolved until the entire query_chain is loaded (also ActiveSupport::Callbacks would need a wrapper to work correctly with modules in this case), so run_callbacks in the :lock method would be insufficient for implementing something like the lockable concern where the lock target can vary based on resource class.

Since data-integrity is a major concern and I think also a framework level as well as an application level concern - and Pessimistic locking has been left rather open-ended I was hoping to get some feedback / best-practice recommendations from Rails developers and others that have dealt with similar issues. Also it would be great to know if there any plans to implement any kind of standard for Pessimistic Locking as part of AR in the future.

Alex

Unfortunately optimistic locking simply does not provide any real

guarantees once you have scaled past a single dyno

Can you elaborate?

Here is one example from production - an order has a fulfillment - the fulfillment can be deleted if it is not ‘shipped’, very roughly:

update

transaction do
fulfillment.status = ‘shipped’
fulfillment.save!
StockMovement.new(fulfillment).save
end

delete

transaction do
fulfillment.destroy # validates that fulfillment.status = ‘unshipped’
end

``

Since StockMovements are not real foreign keys, nothing prevents the fulfillment from being deleted by a dyno handling a delete request even if another dyno has already created stock_movements since it’s validating with stale data.

This is just one of a number of cases of an otherwise very normal transactional process breaking since Rails does validation and relational logic on the ruby level (not supporting real database foreign keys) - which means pessimistic locking or isolation level serializable are a must.

Not sure what you mean about Rails “not supporting real database foreign keys”.

  1. They are officially supported (as of Rails 4.2, I think) and documented in the RailsGuides.

  2. It’s always been easy to add support via gems like Foreigner. I’ve used foreign keys in every Rails system I’ve worked on, when I had the option.

Cheers,

Al

Since you are already working with transactions and since serializable transactions alone solve your example problem, I guess you can use the built-in support for specifying an isolation level for transactions that exists since Rails 4 1. Do you still need any extra pessimistic locking given that this support exists? If so, what kind of a scenario would still need it?

](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)


Since you are already working with transactions and since serializable transactions alone solve your example problem, I guess you can use the built-in support for specifying an isolation level for transactions that exists since Rails 4 1. Do you still need any extra pessimistic locking given that this support exists? If so, what kind of a scenario would still need it?

](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)



Ufuk Kayserilioglu

Here is one example from production - an order has a
fulfillment - the fulfillment can be deleted if it is not
‘shipped’, very roughly:

`#
update

transaction do

fulfillment.status = ‘shipped’

fulfillment.save!

StockMovement.new(fulfillment).save
end

delete

transaction do

fulfillment.destroy # validates that fulfillment.status =
‘unshipped’
end`

Since StockMovements are not real foreign keys, nothing
prevents the fulfillment from being deleted by a dyno handling a
delete request even if another dyno has already created
stock_movements since it’s validating with stale data.

This is just one of a number of cases of an otherwise very
normal transactional process breaking since Rails does validation
and relational logic on the ruby level (not supporting real
database foreign keys) - which means pessimistic locking or
isolation level serializable are a must.

Unfortunately optimistic locking simply does not provide any
real guarantees once you have scaled past a single dyno

Can you elaborate?

You received this message because you are subscribed to the Google
Groups “Ruby on Rails: Core” group.

To unsubscribe from this group and stop receiving emails from it,
send an email to rubyonrails-co...@googlegroups.com.

To post to this group, send email to rubyonra...@googlegroups.com.

Visit this group at https://groups.google.com/group/rubyonrails-core.

For more options, visit https://groups.google.com/d/optout.

You problem hast nothing todo with multiple dynos. This race condition can occour as soon as you have multiple workers or threats (whatever your server uses). But
its a normal issue and in my point of view you can handle it with optimistic locking just fine.

When you use optimistic locking rails prepends on every update und delete (when you use object.destroy) a “AND locking_version=?”, object.locking_version.

In your case when your #update transaction happens it’s gets for instance lock_version 1 and also your #delete transaction gets lock_version 1.

Say the #delete transaction is faster so you will get an ActiveRecord::StaleObjectError within you #update action or vice versa.

You only have to handle the ActiveRecord::StaleObjectError. Without handling this error the transaction will be rolledback (the slower one)

But this is only true as long as you use a Isolation level which does not read UNCOMMITTED data.

This should be the default in most cases.

You can test it this way (as long as you dont use rails identy map).

use same id for find

fulfillment1 = ```Fulfillment.find 1 ```fulfillment2 = ```Fulfillment.find 1 ``
fulfillment1.status = ‘…’
`
fulfillment2.status = '...'

fulfillment1.save!
``fulfillment2.save! # should raise an exception, take a look at the generated SQL-Statement

``regards
dieter

Sorry, yes you are correct that was a poor example and for that particular condition optimistic locking will validate correctly. On the other-hand conditions which use data from a reference but do not update that reference will still be able to race .e.g.

A fulfillment has fulfillment_line_item’s which check that fulfillment.status != ‘shipped’ to validate a destroy on a fulfillment_line_item. This later case will validate on the stale fulfillment.status - but since only the fulfillment_line_item (not the fulfillment) is to be updated in such a transaction no error will be noted even if fulfillment.lock_version has changed (since no update to the parent would have been attempted).

In this case I would still use optimistic locking. You can simply call .touch on the associated object (in both transaction), so the lock_version
is increased.

Pessimistic locking, the way you want to use it, decreases performance because all reads have
to wait until the lock is gone. For the web this is realy a bad thing. Futhor more the chance of a deadlock
is every time ahead of you.

And worst of all, such a pessimistic lock can take a undetermined amount of time.
During a Request this can lead to timeouts on client site. Handle client timeouts
can be a nightmare too.

But that’s only my opinion.

Pessimistic locking, the way you want to use it, decreases performance because all reads have
to wait until the lock is gone. For the web this is realy a bad thing. Futhor more the chance of a deadlock
is every time ahead of you.

Reads should not be locked by a FOR UPDATE lock (http://www.postgresql.org/docs/9.4/static/explicit-locking.html#LOCKING-ROWS). I agree potential dead-locks are a problem if you have slightly complicated hierarchies in your transactions.

And worst of all, such a pessimistic lock can take a undetermined amount of time.
During a Request this can lead to timeouts on client site. Handle client timeouts
can be a nightmare too.

That’s only undermined if there is an open transaction which holds that lock for an undetermined time right? Heroku for examples gives only 30 seconds to a db connection but generally you would have to monitor the locks.

In this case I would still use optimistic locking. You can simply call .touch on the associated object (in both transaction), so the lock_version

is increased.

So if I understand correctly your over-all suggestion is to .touch all Objects involved in validations which otherwise would not have been written to in the current transaction. Although it seems touch updates the updated_at column (this can be worked-around I suppose). One concern here is if there are a number of validations each one has to remember to .touch on the object - and if the same object is used in multiple validations that will touch multiple times in one transaction.

Pessimistic locking, the way you want to use it, decreases performance because all reads have
to wait until the lock is gone. For the web this is realy a bad thing. Futhor more the chance of a deadlock
is every time ahead of you.

Reads should not be locked by a FOR UPDATE lock (http://www.postgresql.org/docs/9.4/static/explicit-locking.html#LOCKING-ROWS). I agree potential dead-locks are a problem if you have slightly complicated hierarchies in your transactions.

It’s database and isolation level depenentend if the lock will also block reads (for instance mysql can block reads http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html)

And worst of all, such a pessimistic lock can take a undetermined amount of time.
During a Request this can lead to timeouts on client site. Handle client timeouts
can be a nightmare too.

That’s only undermined if there is an open transaction which holds that lock for an undetermined time right?

Not only. Even if you’r transaction is determined, you don’t know how many transactions are handled parallel and all
updates with “select for update” have to wait until the previous transactions are finished.

Heroku for examples gives only 30 seconds to a db connection but generally you would have to monitor the locks.

But how long does a user wait until he press abort or reload? That’s another issue you have to keep in mind and 30 seconds is a realy long time
for a web request.

In this case I would still use optimistic locking. You can simply call .touch on the associated object (in both transaction), so the lock_version

is increased.

So if I understand correctly your over-all suggestion is to .touch all Objects involved in validations which otherwise would not have been written to in the current transaction. Although it seems touch updates the updated_at column (this can be worked-around I suppose). One concern here is if there are a number of validations each one has to remember to .touch on the object - and if the same object is used in multiple validations that will touch multiple times in one transaction.

Generaly spoken it is true, but like every time it depends on you implementation what can be done to simplify this. For instance you can use service objects or interactors to overcome
this limitation. But I think a discussion into this direction is out of scope of the general discussion about pessimistic locking. So I have to say, yes, you have to touch all objects which
are used within such a validation and it can result in multiple updates for one record only to increase the lock_version.