ORMs like Rails’ ActiveRecord are great, but sometimes they make it so easy that we’re not thinking about what’s happening underneath, resulting in hard to track down bugs.
Race conditions are particularly hard to track down and reason about. If you Google for how to increment a column in Rails, you’ll see a lot of solutions using #increment
and #increment!
. If you look at the source for these methods, you’ll see:
def increment(attribute, by = 1)
self[attribute] ||= 0
self[attribute] += by
self
end
def increment!(attribute, by = 1)
increment(attribute, by).update_attribute(attribute, self[attribute])
end
What’s the problem? Well let’s say we are incrementing a click count whenever a user clicks on a page:
class PagesController < ApplicationController
def click
@page = Page.find(params[:id])
@page.increment!(:clicks, 1)
end
end
Now let’s imagine two requests come in at the exact same time.
Since #increment
is incrementing the column in-memory, we have a race condition!
Both requests could increment the column at the same time, in-memory, resulting in a count that only increments by one instead of two.
How do we avoid this race condition? We can push the concern of incrementing to the database. Rather than incrementing in memory, let’s have the database do it:
Page.where(id: params[:id]).update_all("click = click + 1")
This results in the following SQL:
UPDATE "pages" SET count = count + 1 WHERE "pages"."id" = $1;
Updates like this are atomic and concurrent requests won’t be a problem. In our example above, the click count would be incremented by two, just as it should!
Note that this is exactly what the lesser-known #update_counters
method does. Next time you need to increment or decrement some database columns, I hope this tip comes in handy!