Ruby: Right way to do optimistic locking? - ruby-on-rails

Context: Product is getting updated by multiple threads. So it leads the race condition. So I am using optimistic locking because it is frequently not updated. For latest update state of the product is determined by updated_at attribute.
For example, Prod (id=1) is updated at t1 on the machine:1. Same product(id=1) is updated at t2 on the machine:2. Now the state of Prod (id=1) on the machine:1 is stale.
Approach To determine stale: I will compare the value of updated_at on the machine with the store value of updated_atin the database value.
My main concern is setting the value of #original_updated_at. Should I use attr_writer :original_updated_at. Is this right way do optimistic locking.
attr_accessor : :original_updated_at
def original_updated_at
#original_updated_at || updated_at.to_f
end
def stale_object?
if updated_at.to_f > original_updated_at.to_f
#original_updated_at = nil
return true
else
#original_updated_at = updated_at.to_f
return false
end
end
def recalculate
tries = 0
begin
raise Product::StaleObjectError.new("StaleObjectError") if stale_object?
attributes["updated_at"] = Time.now.utc
Product.where(:id => self.id).update_all(attributes)
self.attributes = attributes
rescue Product::StaleObjectError => e
if tries < MAX_RETRIES
tries += 1
sleep(1 + tries)
reload
retry
else
raise Product::StaleObjectError("StaleObjectError")
end
end
end

It looks like you are using Rails? Not sure why you are trying to roll your own solution.
Just add a lock_version column to enable optimistic locking on DB level.
https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
Also: since you are talking about multiple machines, concurrency could come from multiple processes and multiple threads as well.
Since your legacy system breaks when adding the lock column some other solutions:
Fix the issues with optimistic locking
use pessimistic locking. Depending on the load scenario - how many concurrent read/write - operations this might work as well
Fix your locking code (not entirely sure how it works because you are leaving out some details of the class in question)
no need to raise, catch and re-raise (just use a loop and breakon success)
you are calling to_f on floats
I don't know what self.attributes = attributes is supposed to do (from the code you show it is unclear if attributes is something else than self.attributes)
using a timestamp might or might not be good enough to determine concurrent updates (two exact same timestamps might be generated, clocks might be off on multiple machines, clock might get reset or adjusted in between)
using sleep is a code smell
When you write code that is so complicated that you have to ask on the internet if it works, then you are probably going the wrong way. Fix the problems with optimistic locking instead of adding such a workaround.

Related

Pessimistic locking in Activerecord - with_lock is not working as expected

I'm trying to implement create_or_update method for User model, which will either create a new record or update the existing one.
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
The pitfall here is that this table is being updated concurrently by different processes and when they are trying to modify user with the same external_user_id then race condition happens and ActiveRecord::RecordNotUnique is raised. I tried to use database lock to solve this problem, but it didn't work as expected - exception is still raised sometimes.
The table structure looks like this:
create_table :users do |t|
t.integer :external_user_id, index: { unique: true }
# ...
end
updated method - what I'm doing wrong here?:
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.with_lock do
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
end
I can't use upsert, because I need model callbacks.
It probably can be fixed by adding
rescue ActiveRecord::RecordNotUnique
retry
end
but I want to use locks for best performance cause the race conditions is not a rare in this part of code.
UPD: added a gist to reproduce this race condition
https://gist.github.com/krelly/520a2397f64269f96489c643a7346d0f
If I'm understanding correctly, the error is likely still occurring in cases where two threads attempt to create a new User for an external id that doesn't exist.
In this situation, thread #1 would attempt to acquire a row lock on a row that does not exist - no locking would actually occur.
Thread #2 would be able to make the same find_or_initialize query while thread #1 is still building the record, and thus thread #2 would hit a uniqueness violation once it commits.
As you've already guessed, the only simple solution to this problem would be a rescue + retry. Thread #2 would just try again, and it would update the record created by thread #1.
An alternative solution would be an advisory lock - a lock with application-specific behavior. In this case, you would be saying that only one thread at a time can run create_or_update_user with a specific external ID.
Rails doesn't support advisory locks natively, but the linked article contains an example of how you would do it with Postgres. There's also the gem with_advisory_locks

Testing pessimistic locking with Rails

To get a better understanding of pessimist locking (with InnoDB), I tried to run this code in my Rails application:
Thread.new do
Account.transaction do
account = Account.lock(true).first
account.balance += 250
account.save!
end
end
Thread.new do
Account.transaction do
account = Account.lock(true).first
account.balance += 500
account.save!
end
end
It actually works, account.balance then contains 750, then 1500 on the next hit. Without locking, it just takes the last thread into consideration and the result is 500.
Is that a dumb test to try out the difference between locking or not? I think I understand the principle of pessimist locking, but not sure though.
This test does allow you to observe pessimistic locking behaviour, but when you remove locks it doesn't switch to optimistic locking. It just does no locking at all.
Also, to be more idiomatic you could do this:
Thread.new do
account = Account.first
account.with_lock do
account.balance += 250
account.save!
end
end
with_lock starts a transaction and acquires a lock in one go.
To observe optimistic locking behaviour you need lock_version field to be present in your model. Rails will then do record version check automatically and throw ActiveRecord::StaleObjectError on conflict.

How to record time of last activity in Rails app?

In my Rails application I would like to record the time a user was last_seen.
Right now, I do this as follows in my SessionsHelper:
def sign_in(user)
.....
user.update_column(:last_seen, Time.zone.now)
self.current_user = user
end
But this is not very precise because a user might log in at 8 a.m. and in the evening the last_seen database column will still contain that time.
So I was thinking to update last_seen whenever the user takes an action:
class ApplicationController
before_filter :update_last_seen
private
def update_last_seen
current_user.last_seen = Time.zone.now
current_user.save
end
end
But I don't like that approach either because the database gets hit with every action that a user takes.
So what might be a better alternative to this?
Rails actually has this sort of behavior built in with touch:
User.last.touch
#=> User's updated_at is updated to the current time
The time it takes in any well-provisioned DB to handle updating a single column like this should be well under 5ms, and very likely under 1ms. Provided you're already going to be establishing that database connection (or, in Rails' case, using a previously established connection from a pool), the overhead is negligible.
To answer your question about whether your code is slower, well, you're thinking about this all wrong. You can optimize an already very fast operation for performance, but I instead you worry more about “rightness”. Here is the implementation of ActiveRecord's touch method:
def touch(name = nil)
attributes = timestamp_attributes_for_update_in_model
attributes << name if name
unless attributes.empty?
current_time = current_time_from_proper_timezone
changes = {}
attributes.each do |column|
changes[column.to_s] = write_attribute(column.to_s, current_time)
end
changes[self.class.locking_column] = increment_lock if locking_enabled?
#changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1
end
end
Now you tell me, which is faster? Which is more correct?
Here, I'll give you a hint: thousands of people have used this implementation of touch and this very code has likely been run millions of times. Your code has been used by you alone, probably doesn't even have a test written, and doesn't have any peer review.
“But just because someone else uses it doesn't make it empirically better,” you argue. You're right, of course, but again it's missing the point: while you could go on building your application and making something other humans (your users) could use and benefit from, you are spinning your wheels here wondering what is better for the machine even though a good solution has been arrived upon by others.
To put a nail in the coffin, yes, your code is slower. It executes callbacks, does dirty tracking, and saves all changed attributes to the database. touch bypasses much of this, focusing on doing exactly the work needed to persist timestamp updates to your models.

Rails 3.2 ActiveRecord concurrency

I have one application that is a task manager.
Each user can select a new task to be assigned to himself.
Is there a problem of concurrency if 2 users accept the same task at the same moment?
My code looks like this:
if #user.task == nil
#task.user = #user
#task.save
end
if 2 diferent users, on 2 diferent machines open this url at the same time. Will i have a problem?
You can use optimistic locking to prevent other "stale" records from being saved to the database. To enable it, your model needs to have a lock_version column with a default value of 0.
When the record is fetched from the database, the current lock_version comes along with it. When the record is modified and saved to the database, the database row is updated conditionally, by constraining the UPDATE on the lock_version that was present when the record was fetched. If it hasn't changed, the UPDATE will increment the lock_version. If it has changed, the update will do nothing, and an exception (ActiveRecord::StaleObjectError) will be raised. This is the default behavior for ActiveRecord unless turned off as follows:
ActiveRecord::Base.lock_optimistically = false
You can (optionally) use a column-name other than lock_version. To use a custom name, add a line like the following to your model-class:
set_locking_column :some_column_name
An alternative to optimistic locking is pessimistic locking, which relies on table- or row-level locks at the database level. This mechanism will block out all access to a locked row, and thus may negatively affect your performance.
Never tried it but you may use http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
You should be able to acquire a lock on your specific task, something like that:
#task = Task.find(some_id)
#task.with_lock do
#Then let's check if there's still no one assigned to this task
if #task.user.nil? && #user.task.nil?
#task.user = #user
#task.save
end
end
Again, I never used this so I'd test it with a big sleep inside the lock to make sure it actually locks everything the way you want it
Also I'm not sure about the reload here. Since the row is locked, it may fail. But you have to make sure your object is fresh from the db after acquiring the lock, there may be another way to do it.
EDit : NO need to reload, I checked the source code and with_lock does it for you.
https://github.com/rails/rails/blob/4c5b73fef8a41bd2bd8435fa4b00f7c40b721650/activerecord/lib/active_record/locking/pessimistic.rb#L61

when is a pessimistic lock released in rails?

Assuming I'm doing something like this (from the Active Record Querying guide)
Item.transaction do
i = Item.first(:lock => true)
i.name = 'Jones'
i.save
end
Is the lock automatically released at the end of the transaction? I've looked at the Active Query guide and the ActiveRecord::Locking::Pessimistic docs, and couldn't find where it explicitly says where the lock is released.
Locking is not a function of rails, it is just adding the lock statement to the query, which will vary depending on the database that you are using. Pessimistic Locking takes a "pessimistic" view in thinking that every query is subject to corruption. So it is going to lock selected rows until you are finished with the transaction. so Lock > query > unlock. While these are fairly consistent database to database, it might be good to read up on the database documentation that you using for any database-specific things you should know.
Here is a good thread on optimistic vs. pessimistic locking that explains it better than I can. Optimistic vs. Pessimistic locking
Yes, the lock automatically released at the end of the transaction because this kind of lock is applicable to transactions only. It does not make sense to lock the record this way (pessimistic lock) outside the transaction.
Pessimistic locks are enforced on DB level.
Below is a description with examples for mysql:
http://dev.mysql.com/doc/refman/5.0/en/innodb-lock-modes.html
I acknowledged the problem with pessimistic lock within transaction during rspec tests.
For some reason on different systems (I found this because of CI failed to run spec) record is still locked and cannot be fetched.
So code and rspec example are below.
class FooBar
def foo
Model.with_lock do
model.update(bar: "baz")
end
end
end
red example
it "updates with lock" do
expect { Foobar.foo }.to change { model.reload.bar }.to("baz")
end
but correct green example should look like this
it "updates with lock" do
Foobar.foo
expect(model.reload.bar).to eq("baz")
end
I believe you'll want an "ensure" block to be certain the lock is released.
http://ruby-doc.org/core/classes/Mutex.src/M000916.html has:
def synchronize
lock
begin
yield
ensure
unlock
end
end
http://yehudakatz.com/2010/02/07/the-building-blocks-of-ruby/ seems to suggest, however, that the block structure of that method will automatically unlock.

Resources