Envek/after_commit_everywhere

What happens if code inside after_commit callback fails?

RichStone opened this issue · 6 comments

Hey there,

in this example:

  def call
    ActiveRecord::Base.transaction do
      create_user!
      after_commit { some_important_stuff }
    end
  end

If something bad inside some_important_stuff happens and throws an error, then anything that was persisted in create_user! won't be rolled back, since when calling after_commit we already are not inside the transaction anymore, right?

Thank you! :)

OK, I think I've just found that the answer is yes, nothing will be rolled back:

When a transaction completes, the after_commit or after_rollback callbacks are called for all models created, updated, or destroyed within that transaction. [...]

The code executed within after_commit or after_rollback callbacks is itself not enclosed within a transaction.

https://guides.rubyonrails.org/active_record_callbacks.html#transaction-callbacks

But I still wonder if it would be possible to rollback everything that happened in the transaction if something inside some kind of a after_commit callback fails?

Envek commented

if it would be possible to rollback everything that happened in the transaction if something inside of a after_commit fails

No. COMMIT to the database was issued and is already completed successfully. You should write some logic of reverting changes by hand, at application level.

You may want to take a look at Sagas pattern or some other mechanism of distributed transactions. There were a few gems that helps to build such workflows, like dirty_pipeline gem (however it is not finished) or others, but I can't recommend anything right now.

Envek commented

Can you tell a bit more about your some_important_stuff? What it does? Sends request to remote APIs? Changes something in another database? Anything else?

Thanks a lot for your quick answer! I'll be looking into those solutions!

Can you tell a bit more about your some_important_stuff? What it does? Sends request to remote APIs? Changes something in another database? Anything else?

Yeah, this is basically what some_important_stuff is trying to do...

  • take the created user
  • create a few related objects
  • set some defaults on the user
  • send an email
  • talk to remote APIs

To make sure everything gets rolled back if any of those steps fail it looks like this now:

  def call
    ActiveRecord::Base.transaction do
      create_user!
      some_important_stuff
    end
  end

I use sidekiq to talk to remote APIs and sidekiq tries to access the user before it's persisted in the DB which is the reason why I was looking for a different solution

Envek commented

Sending emails and requesting remote APIs definitely should be outside of transaction. I would create some statuses for user (like remote_api_state: pending | completed | failed column maybe) or things like that to be able to track users that are not fully completed their creation.

But setting defaults and creating related objects should be inside transaction that create user (unless it is a result from these remote APIs, of course)

So consider to split your some_important_stuff to many methods and spread them across many before_commit and after_commit hooks.

very cool, I'll let you know how this one played out!

some_important_stuff is already in different methods so that it's probably just a matter of ordering them correctly and decide how to handle failures in the after_commits/before_commits properly.