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
Related
I'm developing a reservation system for stuff that you could rent.
I would like to restrict multiple users from reserving the same item.
I display a list, which user can click on the item to check the details.
If any user has already opened the detail view then other user can not open it at the same time.
I am maintaining a flag call is_lock to check if the record is already locked but I was facing issue when multiple users clicked on the same item at the same time.
So I implementing pessimistic lock, which reduced the rate of occurrence of this issue but multiple users opening the same item but it did not completely fixed the issue. I am still facing the same thing.
begin
Item.transaction do
item = Item.lock.where(id: item_id, is_lock: false)
item.is_lock = true;
item.save!
end
rescue Exception => e
# Something went wrong.
end
Above is the code that I have implemented.
Please let me know if I am doing anything wrong.
EDIT:
I've tried the solution provided by #rmlockerd in following way:
Run rails in 2 separate consoles.
Fetch record with lock that has id:100 from console-1.
Fetch to fetch the same record from console-2.
But the above test failed as I was able to fetch the same record from both console even though the record was locked from console-1.
Run rails in 2 separate consoles.
It might be misleading to just look at the snippet you provided, but there does seem like a possible race condition due to your .where predicate.
If User2 attempts to get a lock on the same item after User1 but before first the transaction commits, the .where will still return the original record with is_lock false. The default behaviour for .lock is to simply wait its turn for a lock. So User2 would block until the original transaction commits, then get a lock and proceed to set is_lock to true as well.
The good news is that when you get a lock, Rails reloads the record so you are getting the latest data. Checking is_lock after obtaining the lock should eliminate that race condition, like so:
Item.transaction do
item = Item.lock.find_by(id: item_id, is_lock: false) # only 1, so where is unnecessary
return if item.blank? || !item.is_lock
item.update!(is_lock: true)
end
# I have the lock stuff...
The .lock method also takes an optional 'locking clause' -- which varies based on the database you use -- that can be used to configure the locking behaviour. For example, if you use Postgres, you could do:
Item.transaction do
item = Item.lock('FOR UPDATE SKIP LOCKED').find_by(id: item_id, is_lock: false)
return if item.blank?
item.update!(is_lock: true)
end
The SKIP LOCKED clause directs Postgres to automatically skip any record that is already locked. In the race condition described above, the second call to .lock would bail immediately and return nil, so a simple check of item presence would suffice. Check out the Postgres or MySQL documentation if you're interested in database-specific locking clauses.
I am not sure how Ruby uses ActiveRecord to save data directly in the model code. How can I save data into the DB in the model itself?
Basically my code run into a race condition for the following reason (Ruby/rails + ActiveRecord + Sidekiq):
- My model does something like the following:
def is_present?(data)
memory['properties'].include?(data)
def update_mem(data, size)
if size != 0 && memory['properties'].length == amount
memory['properties'].shift
memory['properties'].push(data)
It checks if a specific value is present (is_present), if it is not present in the memory['properties'] yet, it will add it.
Clearly if there is only one thread accessing "memory", it works just fine but since Sidekiq is pretty fast there might be multiple threads running and will end up in a race condition (one thread write something, the other thread read what was in memory before).
"memory" is actually a column in a table (MySQL) and as soon as I write something in it with "memory['properties'].push(123)" I would like to save them immediately.
My question is, how can I prevent this weird race condition?
What I would like to do is to save the data directly in the DB using the model. The problem is that it seems is not working.
So, to access the data in the model, I use the following code:
memory['property'].push(123)
or
self.memory['property'].push(123)
They both reference the memory column in the DB table.
But then I want to do something like self.save! but it is not working at all.
I tried to add the following code in the model iteself:
self.memory_will_change!
self.memory['properties'].push(property)
self.save!
Unfortunately, it is not working and I cannot save the data into the DB.
This model is actually called via perform() using Sidekiq as per below:
model = Receivers.find(id)
model.receive(data)
model.time = Time.now
model.save!
So the time is updated correctly but the memory (which is updated in the model when i call "receive") it does not get updated. Anyone knows how to overcome this problem? I need to save the data into the DB directly in the model.
Thanks and I look forward to hearing from you.
One solution would be to use a lock so that only one process can access the table at a time. Once you are done processing, you would release the lock.
Check out the following link:
https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Locking/Pessimistic.html
Folks,
I am fairly new to transactions in activerecord in rails and I have a piece of code, where I do something like:
transaction do
specimen = Specimen.find_by_doc_id(25)
specimen.state = "checking"
specimen.save
result = Inventory.do_check(specimen)
if result
specimen.state="PASS"
else
specimen.state="FAIL"
end
specimen.save
end
My goal here for using a transaction is if I get an exception in Inventory.do_check(it is a client to external web-services and does a bunch of HTTP calls and checks) then I want the specimen.state to rollback to its previous value. I wanted to know if this will work as above? Also, it looks like on my development machine the lock is set on the entire Specimen table, when I try to query that table/model I get a BUSY exception(I am using SQLLite). I was thinking that the lock should only be set on that object/record.
Any feedback is much appreciated, as I said I am really new to this so my question may be very naive.
Implementation and locking depends on the DB. I don't use SQLLite and I won't be surprised if it locks the entire table in such case. But reading should still work, so it's probably because it doesn't allow two concurrent operations on a single connection, so is waiting for your transaction to finish before allowing any other operation. See, for example, this SO answer: https://stackoverflow.com/a/7154699/2117020.
However, my main point is you shouldn't be holding the transaction while accessing external services in any case. However it is implemented, keeping the transaction for seconds is not what you'd want. Looks like in your case all you want is to recover from an exception. Do you simply want to set the state to "FAIL" or "initial" as a result, or does do_check() modify your specimen? If do_check() doesn't modify the specimen, you should better do something like:
specimen = Specimen.find_by_doc_id(25)
specimen.state="checking"
specimen.save
# or simply specimen.update_attribute( :state, "checking" )
begin
specimen.state = Inventory.do_check(specimen) ? "PASS" : "FAIL"
rescue
specimen.state = "FAIL" # or "initial" or whatever
end
specimen.save
The locking is going to be highly dependent on your database. You could use a row lock. Something like this:
specimen = Specimen.find_by_doc_id(25)
success = true
# reloads the record and does a select for update which locks the row until the block exits (its wrapped in a transation)
specimen.with_lock do
result = Inventory.do_check(specimen)
if(result)
specimen.state="PASS"
else
specimen.state="FAIL"
end
specimen.save!
end
Checking the external site in a transaction is not ideal, but if you use with_lock and your database supports row lock, you should just be locking this single row (it will block reads, so use carefully)
Take a look at the pessimistic locking documentation in active record:
http://ruby-docs.com/docs/ruby_1.9.3-rails_3.2.2/Rails%203.2.2/classes/ActiveRecord/Locking/Pessimistic.html
I have the following code in a rails model:
foo = Food.find(...)
foo.with_lock do
if bar = foo.bars.find_by_stuff(stuff)
# do something with bar
else
bar = foo.bars.create!
# do something with bar
end
end
The goal is to make sure that a Bar of the type being created is not being created twice.
Testing with_lock works at the console confirms my expectations. However, in production, it seems that in either some or all cases the lock is not working as expected, and the redundant Bar is being attempted -- so, the with_lock doesn't (always?) result in the code waiting for its turn.
What could be happening here?
update
so sorry to everyone who was saying "locking foo won't help you"!! my example initially didin't have the bar lookup. this is fixed now.
You're confused about what with_lock does. From the fine manual:
with_lock(lock = true)
Wraps the passed block in a transaction, locking the object before yielding. You pass can the SQL locking clause as argument (see lock!).
If you check what with_lock does internally, you'll see that it is little more than a thin wrapper around lock!:
lock!(lock = true)
Obtain a row lock on this record. Reloads the record to obtain the requested lock.
So with_lock is simply doing a row lock and locking foo's row.
Don't bother with all this locking nonsense. The only sane way to handle this sort of situation is to use a unique constraint in the database, no one but the database can ensure uniqueness unless you want to do absurd things like locking whole tables; then just go ahead and blindly try your INSERT or UPDATE and trap and ignore the exception that will be raised when the unique constraint is violated.
The correct way to handle this situation is actually right in the Rails docs:
http://apidock.com/rails/v4.0.2/ActiveRecord/Relation/find_or_create_by
begin
CreditAccount.find_or_create_by(user_id: user.id)
rescue ActiveRecord::RecordNotUnique
retry
end
("find_or_create_by" is not atomic, its actually a find and then a create. So replace that with your find and then create. The docs on this page describe this case exactly.)
Why don't you use a unique constraint? It's made for uniqueness
A reason why a lock wouldn't be working in a Rails app in query cache.
If you try to obtain an exclusive lock on the same row multiple times in a single request, query cached kicks in so subsequent locking queries never reach the DB itself.
The issue has been reported on Github.
Basically I have this User model which has certain attributes say 'health' and another Battle model which records all the fight between Users. Users can fight with one another and some probability will determine who wins. Both will lose health after a fight.
So in the Battle controller, 'CREATE' action I did,
#battle = Battle.attempt current_user.id, opponent.id
In the Battle model,
def self.attempt current_user.id, opponent_id
battle = Battle.new({:user_id => current_user.id, :opponent_id => opponent_id})
# all the math calculation here
...
# Update Health
...
battle.User.health = new_health
battle.User.save
battle.save
return battle
end
Back to the Battle controller, I did ...
new_user_health = current_user.health
to get the new health value after the Battle. However the value I got is the old health value (the health value before the Battle).
Has anyone face this kind of problem before ???
UPDATE
I just add
current_user.reload
before the line
new_user_health = current_user.health
and that works. Problem solved. Thanks!
It appears that you are getting current_user, then updating battle.user and then expecting current_user to automatically have the updated values. This type of thing is possible using Rails' Identity Map but there are some caveats that you'll want to read up on first.
The problem is that even though the two objects are backed by the same data in the database, you have two objects in memory. To refresh the information, you can call current_user.reload.
As a side note, this wouldn't be classified a race condition because you aren't using more than one process to modify/read the data. In this example, you are reading the data, then updating the data on a different object in memory. A race condition could happen if you were using two threads to access the same information at the same time.
Also, you should use battle.user, not battle.User like Wayne mentioned in the comments.