From 85e0da8aebf8d2af669d8bbf4034834ec767b378 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Fri, 27 Feb 2026 14:32:24 +1100 Subject: [PATCH] Improve concurrency spec Add checks to see if breakpoint is actually reach and if we have a race condition. --- .../v1/customer_account_transaction_spec.rb | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/spec/requests/api/v1/customer_account_transaction_spec.rb b/spec/requests/api/v1/customer_account_transaction_spec.rb index 02312d1b07..ee8f642caa 100644 --- a/spec/requests/api/v1/customer_account_transaction_spec.rb +++ b/spec/requests/api/v1/customer_account_transaction_spec.rb @@ -129,13 +129,19 @@ RSpec.describe "CustomerAccountTransactions", swagger_doc: "v1.yaml", feature: : it "processes one transaction at the time, ensure correct balance calculation" do breakpoint.lock + breakpoint_reached_counter = 0 + + # Set a breakpoint when save is calle. If two requests reach this breakpoint at the + # same time, they are in a race condition but the the lock in the before_create callback + # should ensure they are excuted one after the other. allow_any_instance_of(CustomerAccountTransaction).to receive(:save) .and_wrap_original do |method, *args| + breakpoint_reached_counter += 1 breakpoint.synchronize { nil } method.call(*args) end - # Create two transactions in parallel + # Create two account transactions in parallel threads = [ Thread.new { login_as enterprise.owner @@ -147,14 +153,23 @@ RSpec.describe "CustomerAccountTransactions", swagger_doc: "v1.yaml", feature: : }, ] - # Wait for both to transaction creation to pause - # This can reveal a race condition. - sleep 0.1 + # Wait for the first thread to reach the breakpoint: + Timeout.timeout(1) do + sleep 0.1 while breakpoint_reached_counter < 1 + end + + # Wait for the second thread to reach the breakpoint to confirm we have a race condition + Timeout.timeout(1) do + sleep 0.1 while breakpoint_reached_counter < 2 + end # Resume and complete both transaction creation: breakpoint.unlock threads.each(&:join) + # There is no existing transaction, the thread are competing to create the first + # transaction. So if the last transaction balance is anything but the sum of the amount + # from both request, it means our datase locking is wrong. expect(CustomerAccountTransaction.last.balance).to eq(25) end end