I have an app where people sign up for items. Each item has a limited number of slots. How can I handle concurrency? I’ve tried like this in the Item class:
def sign_up(signup)
ActiveRecord::Base.transaction do
return 'Sorry, that item is full.' if full?
signups << signup
sheet.save!
nil
end
end
def full?
locked_signups = signups.lock(true).all
locked_signups.size >= max_signups
end
Is what I am trying to do even possible through AR? Do I need to implement my own locking via a column? Any suggestions are welcome.
UPDATE: I got this working per tadman’s answer. Here’s the code that works:
rows_updated = ActiveRecord::Base.transaction do
Item.connection.update "update items set signup_count=signup_count+1 where id=#{ActiveRecord::Base.sanitize(self.id)} and signup_count<quantity"
end
return 'Sorry, that item is full. Refresh the page to see what\'s still open.' if rows_updated < 1
I can think of two approaches to this sort of problem that are reliable.
Counter Column
You’ll create a “remaining stock” column and update it atomically:
You’ll have to bind to the
:countand:idvalues accordingly. If this query runs, it means there was a sufficient number of signups left.Reserved Signups
Create the signup records in advance and allocate them:
This will update zero or more signup records, so you’ll have to check that you reserved the correct count before committing your transaction.