When multiple concurrent requests try to call controller reject action. In race conditions, it generates same comments
multiple times, although one of the request
and order
is rejected. But in race conditions, request
cannot identify order
was already rejected by one of the request
because all these processes executes in very little amount of time before order
changed to reject. To avoid this race condition issue, I have locked request
and order
in DB row level. But it's not working. Here are my sample codes:
I am using Ruby 2.5.8, Rails 5.0.7.
My Model
- Order has many requests
- Request has one approvers
class Request < ApplicationRecord
def self.reject_request(key)
request = Request.where(:reject_key => key).first
order = request&.order
raise ActiveRecord::Rollback if request.blank? || order.blank?
raise ActiveRecord::Rollback if request.rejected? || order.rejected?
order.lock!
request.lock!
request.status = 'rejected'
request.save!
order.update(status: 'rejected')
true
end
end
My Controller
class OrdersController < ApplicationController
def reject
@request ||= Request.find_by(reject_key: params[:reject_key])
@order = @request&.purchase_order
ActiveRecord::Base.transaction do
rejected = Request.reject_request(params[:reject_key])
@order.comments.create(comment: "Order rejected by approver: #{@request.approver.name}") if rejected
if rejected
redirect_to root_path, notice: 'Rejected'
else
redirect_to root_path, notice: 'Try again!'
end
end
end
end
Problem
There should be only one correct comment, but there are more incorrect comments generated as shown below.
Order rejected by approver: Joe (correct)
Order rejected by approver: Jack (not correct)
Order rejected by approver: Joe (not correct)
Order rejected by approver: Jack (not correct)
It's working when reject action is executed by clicking on reject link through application. But when multiple requests are running through multi-threaded process, then it does not work as I described above. How to allow only one request to perform the action and reject all other actions? Any feedback would be appreciated, thank you!
CodePudding user response:
You need to lock an order before checking it's status.
If 2 concurrent requests execute check for request.rejected? || order.rejected?
simultaneously with your solution, both gets false
and proceed consequentially under the lock.
Changes for your model
class Request < ApplicationRecord
def self.reject_request(key)
request = Request.where(:reject_key => key).first
order = request&.order
# make sure both request and order objects are not empty
raise ActiveRecord::Rollback if request.blank? || order.blank?
# lock your object
order.lock!
request.lock!
# put your conditions here after the `lock`
raise ActiveRecord::Rollback if request.rejected? || order.rejected?
request.status = 'rejected'
request.save!
order.update(status: 'rejected')
true
end
end