Reputation: 2444
I have a simple race condition. I have a website where people can vote on photos, but maximum 10 votes are allowed.
When a user submits a vote, I update a num_votes column in the photos table for that specific photo. I do this for easy lookup for the number of votes.
How can I make sure that the vote.save and the num_votes update happen in the same transaction?
Thanks!
Upvotes: 3
Views: 2255
Reputation: 161
If it is a simple race condition then you should resolve it as a race condition. Try using some locking mechanism. Redis is good to go: redis locking for ruby
RedisLocker.new('vote_#{@photo.id}').run! { @photo.vote }
# ... photo model
def vote
if num_votes <= 10
self.num_votes += 1
save
end
end
Upvotes: 2
Reputation: 84114
You don't need explicit locks for this
Photo.where(:id => photo_id).where('num_votes < 10').update_all('num_votes = num_votes+ 1')
will update the number of votes for that photo, but only if there are less than 10 votes. You can check the return value of update_all
to see if anything was actually updated: the return value is the number of updated rows. If the update fails then don't create the vote (or if you have already created the vote, rollback the transaction).
Optimistic locking uses a similar technique to detect attempts at concurrent updates: it places a condition on the update that ensures that nothing will happen if someone has snuck in there before you and then checks the number of updated rows.
Upvotes: 0
Reputation: 1127
In order to achieve this you have to use some kind of locking. Basically you have 3 options: optimistic/pessimistic rails locking and some external locking backend (like Redis::Lock).
I personally would go for pessimistic locking if high performance is not really the case here
photo = Photo.find(photo_id)
photo.with_lock do
photo.num_votes += 1
photo.save!
end
I should also point out that sticking to only wrapping incrementing num_votes and save into one transaction would not solve the race-condition. Most RDBMS by default work in read committed mode. Which doesn't prevent such a race condition.
FYI See Pessimistic and Optimistic Locking reference
Upvotes: 4
Reputation: 9700
Well, Rails/Postgres supports transactions. You can simply declare one, on any ActiveRecord model:
Photo.transaction do
Vote.create(:whatever)
Photo.votes = thing
Photo.save!
end
If an exception is raised during the transaction block (say, by calling .save!
on an invalid model), the transaction is rolled back and any database changes that would have happened in there aren't committed (in this case, the Vote record doesn't get inserted). You'll still need to rescue and handle the exception, of course.
Incidentally, storing number of associated objects in a record for easy lookup is a pretty common pattern, known as a counter cache, and Rails supports those as well - you might want to look into formally making num_votes
a counter cache (the default name would be photos.votes_count
, but it's not required). You might still want a transaction to check that it doesn't exceed the limit, though.
Upvotes: 0