Rails, callbacks, workers, and the race you never expected to lose
logicalfriday.com
logicalfriday.com
These days if I find myself wishing I have a callback that probably means a related model method or controller needs to be one line longer than it is right now. (e.g. I moved the welcome email out of the after_save and into the controller in charge of online signups months ago. This was the right call, as it means that e.g. my magic administrator create-a-free-account-for-a-customer-who-pays-via-PO button doesn't actually mail them prior to their account being configured correctly.)
One thing to keep in mind when moving logic out of callbacks is that you'll no longer get the implicit transaction that ActiveRecord wraps around the whole callback cycle, so you have to be a bit more hands-on with managing transactions.
Nothing against the OP, it does explain a common edge case that many people will run into and for that it's a good article, but I wish more people would see the violations of SRP going on here and try to lead people away from this trap.
I'd recommend watching http://www.youtube.com/watch?v=CGN4RFkhH2M (Hexagonal Rails GoRuCo talk) for one way to re-architect to get away from these inter dependencies.
As a sort of half-way house between using lifecycle callbacks and sticking everything in the controller, I've just moved the main app I work on to a pub-sub event system, which is working out quite nicely. Controllers or mutator methods no longer handle post-action tasks, they just announce what they did, and any interested listeners can act upon the event announced. This way we can have a listener that handles (for example) all post-action emailing, and we can test the logic for that in isolation. Controller logic is cleaned up too, so testing them becomes easier. On the flip side we have to write more comprehensive integration tests to make sure our listeners are all hooked up correctly, but it seems like a pretty good trade-off so far.
Enjoyed the OP nonetheless, though - I didn't know about ActiveModel#previous_changes, that's pretty nifty to have.
In the meantime, the pub/sub setup we've got is pretty similar to the one presented a little way down the following article:
https://www.theagileplanner.com/blog/building-agile-planner/...
Our listeners sit completely separate from the controllers, though, and we're not using them for anything like handling controller response logic (I'm not sure how I feel about that, although the idea is certainly interesting).
To tell a quick little story, I have experienced this issue in a system on a very unpredictable way. It cost me hours (days?) of repeated bug hunting to figure out what was happening.
So, our users will save an "Entity". The entity is actually pretty complex, and has the potential to span 40-50 tables. (O how I wish I used a Document Store rather than relational). Anyway, when the "entity" gets saved, I need to update Solr search, with an updated reflection of the model. There are two flags within this model, "enabled" and "deleted", and I use these flags to filter on the Documents within Solr.
All jobs are sent to Solr by going through a Gearman Job Server worker process.. However, the worker, was receiving the job before the database had persisted the updated model, so it was always one snapshot before the user initialized the request.
But oddly enough, this issue would only ever happen when the server was under very very small load. I could not for the life of me track this issue down, and it took me months before I realised this was almost akin to a race condition, especially since when under load the bug never happened... And.. If I artificially added load, the bug wouldn't happen. It would only happen when the server was running smoothly.
Once I spotted this, I was able to fork the request off to Gearman at the correct time, (rather than as a prePersist lifecycle event that I was previously).
I definitely learnt a lot from this little issue. It was such a simple issue, that eluded me for months, and just goes to show, that not all code runs the same... given different conditions, (ie Load), you will find that your code can behave differently.
In traditional Rails design, the controller is exactly the right place for this. Another option is a service layer of some kind, if you do that, e.g. a DCI context, or just another Plain-Ol-Ruby-Object that is in charge of the logic of signing up a user.
The best part of separating out the logical business sequence of signing up a user into its own object (I can see this method in my head, something like save_new_user, sign_in_user, queue_welcome_email) is that it becomes massively easier to test - you can just mock the various calls to its collaborators' public methods because they should all have unit tests.
Anyway, I agree with pretty much everything you say. As I mentioned elsewhere in this thread, we've gone down a pub-sub event system for handling our generic post-action tasks. The controller announces a :create_user event, and any interested listeners can respond appropriately. And yes, one of the biggest benefits of this has been the ability to test that event-handling logic in isolation; stubbing and mocking collaborators, and generally having the sort of testing fun you're normally not supposed to have with Rails.
Note that even in the proposed solution, after_commit hooks offer no guarantee that the job will ever be enqueued, if for example redis is unavailable, or your process gets reaped between when the database commit and when the after_commit hook gets fired -- the commit has already happened. So if it's really important that your asynchronous task be queued, keeping the queue in your DB has some advantages.
Let me know if anyone is interested and I can share some code.
And I liked author's solution of tracking attribute changes. But may be there is a cleaner way. I had similar problem where, I needed a handle on after_commit :on => :update but in observer.
The DCI technique is to use the model as a persistence layer, and organize business logic into modules organized by use cases. With this technique my system has become much easier to understand and test.
Its great to see people experimenting with architecture styles like DCI and HexRails.