An ActiveRecord Transaction Gotcha
I was doing some work with ActiveRecord transactions and I noticed a strange behavior where some of my after_commit callbacks (yes, unfortunately I had to use callbacks) were not being called.
After endless digging, I realized that the problem would only occur when the code in the transaction saved the record multiple times. From there, I quickly realized how easy it is to misuse after_commit and introduce bugs that are very hard to track down, especially in complex projects.
Let’s see how!
Suppose that we want to notify an administrator of all changes happening to a particular model in our application. Some of these changes happen in a DB transaction:
Since we are smart developers and we don’t want to send an email when the transaction fails for some reason, we decide to only send our email in an after_commit callback, like this:
In case you don’t know, previous_changes is an ActiveRecord method that returns the changes made to the model before it was saved. It’s part of the dirty tracking module and it’s actually quite useful.
When we test our app, we observe that the only change that the administrator is being notified of is the one to the last_updater field.
The D’Oh Moment
The explanation is quite simple (once you figure it out): previous_changes is reset every time we save the model, not just when the transaction is closed.
This means that when the after_commit callback is executed, previous_changes only contains the most recent changes rather than all of them. This concern was also raised in Rails’ GitHub project.
You can test this by inspecting previous_changes after the first update and after the second one. You’ll see that it contains all the attributes you’re looking for in the first instance, and last_updater only in the second.
Although counter-intuitive, this makes perfect sense if you understand how ActiveRecord dirty tracking works and what a DB transaction is.