Sometimes you don’t want a user doing two things at once (or two users doing something to the same third party at once). Dealing with this sort of issue is quite fiddly as its easy to overlook them and hard to track them down when they happen as they tend to be heavily timing dependant. There’s a few tricks in the rails toolbox for dealing with them.
There’s a related issue that this post is not about: suppose two users bring up the edit form for some object. They each make unrelated changes to the form and save, one after the other. The second user will squash the changes made by the first user. Oops. I’m not concerned about that, only with what happens if the two requests are actually concurrent, whereas here the two save requests could be minutes apart.
One way of solving this problem is for editable objects to have an edited_by association and disallowing edits if someone else is in the process of editing. Another way is to get clever about how you apply changes to attributes. While not directly relevant, the things outlined here may still be useful, for example if you go down the route of only allowing the user named in the edited_by association to edit the record then you need to be able to reliably set that field even if two users click the edit button at the same time.
And now, our feature presentation
When I was at school the teacher used to dish out ‘bon points’ when you did something good. You could later trade those in for stickers and stuff. In a world very far removed from French primary school classrooms of 1990, the teacher might have written a webapp allowing you to do this (probably complete with an obnoxious facebook app where you could show everyone how many magic stars you have).
The people table has a magic_stars column, and we want people to be able to spend their magic stars on various perks. if someone has been a good boy, you might want to use the credit method:
1 2 3
You need to be a little bit careful, as there’s a race condition here: suppose two people try to give someone a magic star at the same time
|Person 1||Person 2|
|loads child (3 stars)||loads child (3 stars)|
|adds 1||adds 1|
|saves (4 stars)|
|saves (4 stars)|
The 2 requests execute in separate mongrels (or mod_rails listeners etc…) and are completely oblivious to each other and will happily overwrite the change made by the other one. Rails’ optimistic locking will help us here: Just add an integer lock_version column to the model (don’t forget to make it default to 0) and the second ‘bad’ save will raise ActiveRecord::StaleObjectError. We can rewrite our credit method like this:
1 2 3 4 5 6
So how does optimistic locking work? The key (unsurprisingly) is in the lock_version column. Assume that we loaded a person object with a lock_version of 4 and we now want to save it. We increment our lock version to 5. If no one else has touched this object then in the database it will still have a lock_version of 4.
Rails appends “WHERE lock_version = 4” to the update query, and then examines the number of rows that were updated (the database driver will return this). If we get 1 then we know everything went ok. If 0 rows were updated then we know that it must have been because someone touched that row and so we raise StaleObjectError.
Optimistic looking is nice because we’re not sitting on a database lock at any point , so we won’t hold up anyone else. The most obvious limitation is that it only protects you if you are actually modifying a row (more on that later).
The name optimistic locking suggests that there is an alternative, and there is: ask the database to get an actual lock for you. You can do this in two ways in rails: you can pass :lock => true to a finder (instead of passing true you can pass an sql fragment if you want to change the type of lock acquired) or you can call the lock! method (which is effectively a reload with :lock => true). It’s important to note that a lock is held as long as the current transaction. In particular if you aren’t inside a transaction then nothing happens, or in other words
1 2 3 4
doesn’t accomplish anything at all. Inside a transaction however, you’ll get an exclusive lock and no other transaction will be able to get a lock for that row. We can rewrite our buy method to look like
1 2 3 4 5 6
The lock stops another connection from fiddling with that person at the same time. It’s just a row lock, so it won’t stop you editing a different person at the same time. If someone called our credit method from above at the same time that would be ok, as updating the customer row to change the amount of stars they have also requires an exclusive lock, so we’re in no danger here.
We can use these locks in more general ways, even if we don’t want to actually lock the customer row. For example if you have a constraint like a person may only borrow a certain number of books then you pretty much have to do it in ruby which renders you vulnerable to the various race conditions mentioned before.
You’re not modifying any rows (just adding one to a join table), so optimistic locking is no good. You can however lock the person row. You’re not actually changing it at all, just using the database as a synchronisation method. We can repackage this up as something we can use all over our app:
1 2 3 4 5 6
Then in your app you can do things like
1 2 3
It should be an easy exercise for the reader to put this in a form where any ActiveRecord model has this exclusive method.
People sometimes ask about the difference between
Person.transaction, the instance method
ActiveRecord::Base.transaction. There is no difference between the first two – the instance method just calls
self.class.transaction. The transaction method on
ActiveRecord::Base or some other model class only differ if the models use a different database connection. Most of the time, the three can be used interchangeably.
If you use a database without row locks (ie mysql with myisam tables) you’ll probably get something horrible like a table lock.
Do be careful about deadlocks though.
The sleep function can be rather helpful when you’re trying to force particular bits of code to overlap with each other in particular ways.
Other of course than the implicit locks that happen when you update a row