ActiveRecord callbacks are an easy way to run code during the different stages of your model’s life.
For example, say you have a Q&A site, and you want to be able to search through all the questions. Every time you make a change to a question, you’ll want to index it in something like ElasticSearch. Indexing takes a while and isn’t urgent, so you’ll do it in the background with Sidekiq.
This seems like the perfect time to use an after_save
callback! So in your model, you’ll write something like:
This works great! Or, at least, it seems to. Until you queue a lot more jobs and see these errors show up:
Sure, Sidekiq will retry the job and it’ll probably work next time. But it’s still a little weird. Why can’t Sidekiq find the question you just saved?
A race condition between processes
Rails calls after_save
callbacks immediately after the record saves. But that record can’t be seen by other database connections, like the one Sidekiq is using, until the database transaction is committed, which happens a little later. This means there’s a chance that Sidekiq will try to find your question after you save it, but before you commit it. It can’t find your record, and it explodes.
This problem is so common that Sidekiq has an FAQ entry about it. And there’s an easy fix.
Instead of after_save
:
use after_commit
:
And your job won’t get queued until Sidekiq can see your model.
So, when you queue a background job or tell another process about a change you just made, use after_commit
. If you don’t, they might not be able to find the record you just touched.
But there’s one more problem…
OK, you switched a bunch of your after_save
hooks to use after_commit
instead. Everything seems to work. Time to check it all in and go home, right?
First, you’ll want to run your tests:
Whoops! Shouldn’t the test have queued the job? What just happened there?
By default, Rails wraps each test case in its own database transaction. This can really speed things up. It takes just one database command to undo all the changes you made during the test.
But this also means your after_commit
callback won’t run. Because after_commit
callbacks only run when the outermost transaction has been committed.
When you call save
inside a test case, it still commits a transaction (more or less), but that’s the second-most-outermost transaction now. So your after_commit
callbacks won’t run when you expect them to. And you can’t test what happens inside them.
This problem also has an easy fix. Include the test_after_commit
gem in your Gemfile:
And your after_commit
hooks will run after your second-to-last transaction commits. Which is what you were expecting to happen.
You might be thinking, “That’s weird. Why do I have to use a whole separate gem to test a callback that comes with Rails? Shouldn’t it just happen automatically?”
You’re right. It is weird. But it won’t stay weird for long.
Once Rails 5 ships, you won’t have to worry about test_after_commit
. Because this problem was fixed in Rails about a month ago.
In my own code, I use after_commit
a lot. I probably use it more than I use after_save
! But it hasn’t come without its problems and strange edge cases.
Version by version, though, it’s getting better. And when you use after_commit
in the right places, a lot of weird, random exceptions just won’t happen anymore.