问题
I am trying to implement the Optimistic Locking for Race Condition. For that, I added an extra column lock_version in the Product: Model through migration.
#Product: Model's new field:
# attribute_1
# lock_version :integer(4) default(0), not null
before_validation :method_1, :if => :recalculation_required_attribute
def method_1
####
####
if self.lock_version == Product.find(self.id).lock_version
Product.where(:id => self.id).update_all(attributes)
self.attributes = attributes
self.save!
end
end
Product Model has an attribute_1. If recalculation is required for attribute_1 then before_validation: method_1 will call.
I am using optimistic locking using lock_version. However, update_all will not increase the lock_version. So I start usingsave!. Now I am getting a new error: SystemStackError: stack level too deep because self.save! triggers the before_validation: method1. How to stop infinite loop of call back and handle optimistic locking in the above case.
回答1:
Possible Solution:
class Product < ApplicationRecord
before_validation :reload_and_apply_changes_if_stale, on: :update
def reload_and_assign_changes_if_stale
# if stale
if lock_version != Post.find(id).lock_version
# store the "changes" first into a backup variable
current_changes = changes
# reload this record from "real" up-to-date values from DB (because we already know that it's stale)
reload
# after reloading, `changes` now becomes `{}`, and is why we need the backup variable `current_changes` above
# now finally, assign back again all the "changed" values
current_changes.each do |attribute_name, change|
change_from = change[0] # you can remove this line
change_to = change[1]
self[attribute_name] = change_to
end
end
end
end
Important Notes:
the
before_validationabove STILL DOES NOT GUARANTEE that the race condition will be avoided! because see the example below:class Product < ApplicationRecord # this triggers first... before_validation :reload_and_apply_changes_if_stale, on: :update # then, this triggers next... before_update :do_some_heavy_loooong_calculation def do_some_heavy_loooong_calculation sleep(60.seconds) # ... of which during this time, this record might already be stale! as perhaps another "process" or another "server" has already updated this record! endmake sure that the
before_validationabove is at the very top of your Post model, so that that callback will be triggered first before any of your other before_validations (or even any subsequent callbacks:*_update, or*_save), as perhaps you might have one or two subsequent callbacks that are dependent on the current state of the attributes (i.e. it's doing some calculation, or checking against some boolean-flag attribute), which then you need to reload first (as is above), before doing these calculations.the
before_validationabove will only work for "calculations/dependencies" in your model callbacks, but will not work properly if you have calculations/dependencies outside of yourProductmodel's callbacks; i.e if you have something like:class ProductsController < ApplicationController def update @product = Product.find(params[:id]) # let's assume at this line, @product.cost = nil (no value yet) @product.assign_attributes(product_attributes) # let's assume at this line, @product.cost = 1100 # because 1100 > 1000, then DO SOME IMPORTANT THING! if @product.cost_was.nil? && @product.cost > 1_000.0 # do some important thing! end # however, when `product.save` is called below and the `before_validation :reload_and_apply_changes_if_stale` is triggered, # of which let's say some other "process" has already updated this # exact same record, and thus @product is reloaded, but the real DB value is now # @product.cost = 900; there's no WAY TO UNDO SOME IMPORTANT THING! above @product.save end end
The notes above are why by default Rails do not auto-reload that attributes as a before_validation or something, because depending on your application/business logic, you might want to "reload" or "not-reload", and this is why by default Rails instead raises an ActiveRecord::StaleObjectError (see docs) for you to specifically rescue, and handle what to do accordingly if this race-condition happened.
来源:https://stackoverflow.com/questions/52624818/ruby-how-to-handle-optimistic-locking-using-update-allattributes