feat: make response usage increment atomic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user