Compare commits

...
Author SHA1 Message Date
Shivam Mishra 2eff74c854 feat: make response usage increment atomic 2026-01-08 21:26:14 +05:30
2 changed files with 51 additions and 3 deletions
@@ -16,9 +16,17 @@ module Enterprise::Account::PlanUsageAndLimits
end
def increment_response_usage
current_usage = custom_attributes[CAPTAIN_RESPONSES_USAGE].to_i || 0
custom_attributes[CAPTAIN_RESPONSES_USAGE] = current_usage + 1
save
increment_sql = <<~SQL.squish
custom_attributes = jsonb_set(
custom_attributes,
'{#{CAPTAIN_RESPONSES_USAGE}}',
to_jsonb(COALESCE((custom_attributes->>'#{CAPTAIN_RESPONSES_USAGE}')::int, 0) + 1),
true
)
SQL
updated = self.class.where(id: id).update_all(increment_sql)
reload if updated.positive?
end
def reset_response_usage
+40
View File
@@ -78,6 +78,46 @@ RSpec.describe Account, type: :model do
expect(responses_limits[:current_available]).to eq captain_limits[:startups][:responses] - 1
end
it 'handles concurrent increments without losing updates' do
# Simulate concurrent calls to increment_response_usage
# This tests that the atomic SQL update prevents race conditions
concurrent_calls = 50
# Use a barrier to ensure all threads start at the same time
# This maximizes the chance of overlapping reads/writes
ready = Concurrent::CountDownLatch.new(concurrent_calls)
start = Concurrent::CountDownLatch.new(1)
threads = concurrent_calls.times.map do
Thread.new do
# Reload account in each thread to simulate separate workers
thread_account = Account.find(account.id)
# Signal ready and wait for all threads to be ready
ready.count_down
start.wait
# Now all threads will execute simultaneously
thread_account.increment_response_usage
end
end
# Wait for all threads to be ready
ready.wait
# Release all threads at once
start.count_down
# Wait for all threads to complete
threads.each(&:join)
# Verify all increments were captured without lost updates
expect(account.reload.custom_attributes['captain_responses_usage']).to eq concurrent_calls
responses_limits = account.usage_limits[:captain][:responses]
expect(responses_limits[:consumed]).to eq concurrent_calls
expect(responses_limits[:current_available]).to eq captain_limits[:startups][:responses] - concurrent_calls
end
it 'reseting responses limits updates usage_limits' do
account.custom_attributes['captain_responses_usage'] = 30
account.save!