As you are reading this blog, I assume you love to play this web development game as much as we do! But blinded by this love we forget too often about a very important fact: This business is a highly concurrent one and we should care about this!
98% of all tasks we cope with in our every day coding we could just neglect this concurrency and treat our controllers like there are very loooonely guys. But what about that other 2 percent? What about these racing conditions which could happen with no need for AJAX or other complicated stuff.
Let’s stay simple: Imagine your customer asks you to implement a stupid counter which counts the views of ads. You could do this:
1
2
3
4
5
| # let's assume you always want to edit this first ad ;)
ad = Ad.find(:first)
ad.counter += 1
ad.save! |
But wait a minute! What happens if this occurs: Customer A and customer B gets served with the same ad in nearly the same moment:
- The ad counter is at 1234
- Customer A loads the ad
- Customer B loads the ad
- Customer A counts the counter up to 1235
- Customer B counts the counter up to 1235
- Customer A saves the ad, new count in the db: 1235
- Customer B saves the ad, new count in the db: 1235
Damn, we just missed a view and got a classic race condition. Ok, let’s forget about all these: It’s not important if we do not count every 10.000th view … Let’s assume it is.
What to do? You could wrap this with a transaction like:
1
2
3
4
5
6
7
| Ad.transaction do
# let's assume you allways wants to edit this first ad ;)
ad = Ad.find(:first)
ad.counter += 1
ad.save!
end |
But what for? As long as you not run your DBMS with an isolation level of ’serializable’ (and I hope you don’t!) there comes no advantage from the use of a transaction. Customer A and B would both open a transaction, read the line, do the update and write it back. There is nothing the transaction could complain about, it’s still atomic, the data is consistent after the commit, the transaction itself is isolated (the race condition did not affect the fulfillment of the transaction! Both transaction can fulfill their work without any technical hassle) and last but not least the commited data is durable. So the only thing you get is a nice, fully working transaction, the race condition is still there.
So what is the solution? Simple: Locking. As I stated before you could (DON’T DO THAT) use an isolation level of ’serializable’. But why would this work? Because is serializes any transaction so that they are executed one by one… Not a performance booster you may think… And you are right! In addition to that this isolation level is tending to produce errors because many DBMS do not really serialize the transactions etc etc etc… Let’s stop talking about this seldom useful stuff and carry on.
In Rails (and in Hibernate and in nearly any other OR mapper I think) there are two ways of doing locks like this explained before in a more ’sensitive’ manner. Pessimistic Locking and Optimistic Locking.
I’m not a database professional, so please be patient when I’m saying something stupid, but I think the main difference between these both is that an optimistic lock is handled by the software you code (or the framework you use of course), while the pessimistic one is handled by the DBMS you use.
So how could you solve the above problem in Rails? It’s as simple as you might expect it to be: Just add an integer column named ‘lock_version’ to the Ads table, initialize it to 0 for any row and then when your are ready try this:
(I will write down the two customer example as it would be concurrently executed, I’ll hope this does not cause any confusion)
1
2
3
4
5
6
7
8
| ad1 = Ad.find(:first)#Customer A
ad2 = Ad.find(:first)#Customer B
ad1.counter += 1 #Customer A
ad2.counter += 1 #Customer B
ad1.save! #Customer A
# The line below should raise an ActiveRecord::StaleObjectError
ad2.save! #Customer B |
What rails does is simple: It counts up the lock_version field of an row on any update. If there is an update call it checks if the lock_version field is still the same as in the fetched row and if not, it raises the error. Then it’s your part to handle it.
Ok, you just got an Optimistic Locking alá Rails. Congratulations.
The other way is to use Pessimistic Locking and let the DBMS do all the work. This is how you could do that in Rails:
1
2
3
4
5
| Ad.transaction do
ad = Ad.find(:first, <strong>:lock => true</strong>)
ad.counter += 1
ad.save!
end |
Now there are no raised exceptions when you run into a race condition, because you never run into one. The DBMS (hopefully) will read-lock the selected rows as soon as you are selecting them (Rails uses the SQL-92 ‘SELECT … FOR UPDATE’ statement for that and MySQL’s InnoDB 5, Postgres and all other major players do support such a row wise locking. There might be some DBMS that lock the whole table instead of a few rows (like MySQL did in previous versions) which would be a hard performance impact as you could imagine. So read the manual of the DBMS of your choice for that issue) and any other select, which would like to select one of the locked rows has to wait until the transaction that encapsulates the ‘FOR UPDATE’ statement is commited (that’s the reason why we need a transaction for the Pessimistic Locking).
You could easily play with these locking a little bit if you open up two terminals, connect to your DB, begin a transaction in terminal 1, then do a SELECT … FOR UPDATE statement in terminal 1 and try to do the same SELECT statement (with or without FOR UPDATE) in terminal 2. You will see, that terminal 2 did not return from it’s ‘waiting state’ until you do not close the transaction in terminal 1.
Ok, I know that many of you just know about that since ages but my intention for this post is not that I would like to write something no one has done ever before! A few days ago I had to think about this issue again and realized how easy it is to forget about it. So if you have not forgot it and knew everything: perfect. If not this hopefully is a tiny reminder.
Cheers,
Thorben
FEtMab-Team