How to avoid race condition in a rails model that records payments and running balance?

Viewed 4991

I have a simple model, Payments, which has two fields amount and running_balance. When a new payment record is created, we look for the running_balance of its previous payment, say last_running_balance and save last_running_balance+amount as the running_balance of the current payment.

Here are our three failed attempts to implement the Payments model. For simplicity, assume the previous payment always exists and ids are increasing as payments are created.

Attempt 1:

class Payments < ActiveRecord::Base
    before_validation :calculate_running_balance
    private
    def calculate_running_balance
        p = Payment.last
        self.running_balance = p.running_balance + amount
    end
end

Attempt 2:

class Payments < ActiveRecord::Base
    after_create :calculate_running_balance
    private
    def calculate_running_balance
        p = Payment.where("id < ?", id).last
        update!(running_balance: p.running_balance + amount)
    end
end

Attemp 3:

class Payments < ActiveRecord::Base
    after_commit :calculate_running_balance
    private
    def calculate_running_balance
        p = Payment.where("id < ?", id).last
        update!(running_balance: p.running_balance + amount)
    end
end

These implementations may cause race conditions in the system as we are using sidekiq to create payments in the background. Suppose the last payment is payment 1. When two new payments, say payment 2 and payment 3 are created at the same time, their running_balance might be computed based on the running balance of payment 1 because it might be the case that when payment 3 is figuring out its running balance payment 2 has not been saved to the database yet.

In particular, I am interested in a fix that avoids the running condition. I am also keen on looking at other rails apps that implement similar payment systems.

1 Answers
Related