Home > Enterprise >  How to allow only one request to perform action and reject all other concurrent requests?
How to allow only one request to perform action and reject all other concurrent requests?

Time:06-08

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
  • Related