Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What happens if code inside after_commit callback fails? #12

Closed
RichStone opened this issue Nov 30, 2020 · 6 comments
Closed

What happens if code inside after_commit callback fails? #12

RichStone opened this issue Nov 30, 2020 · 6 comments

Comments

@RichStone
Copy link

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! :)

@RichStone RichStone changed the title What happens if code inside the callback fails? What happens if code inside after_commit fails? Nov 30, 2020
@RichStone RichStone changed the title What happens if code inside after_commit fails? What happens if code inside after_commit callback fails? Nov 30, 2020
@RichStone
Copy link
Author

RichStone commented Nov 30, 2020

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
Copy link
Owner

Envek commented Nov 30, 2020

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
Copy link
Owner

Envek commented Nov 30, 2020

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?

@RichStone
Copy link
Author

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
Copy link
Owner

Envek commented Nov 30, 2020

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.

@RichStone
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants