Rails Add ActiveRecord.after_all_transactions_commit Callback

In Rails applications, it is common to perform actions that depend on the successful completion of database transactions. For instance, sending a notification email after a record is updated or triggering a background job.

However, if these actions are initiated within a transaction, there’s a risk they might be executed before the transaction is fully committed.

This can lead to errors such as ActiveJob::DeserializationError or RecordNotFound especially in environments where the job queue is fast but the database might be slow.

Consider a scenario where we confirm a user and want to send a notification email afterwards:

def confirm_user(user)
  User.transaction do
    user.update(confirmed: true)
    
    UserConfirmationMailer.with(user: user).deliver_later
  end
end

This code might work in development but fail in production due to timing issues with the database and job queue.

If UserConfirmationMailer job runs before the transaction commits, it might fail because the user’s state hasn’t been persisted yet.

After

Rails introduces the ActiveRecord.after_all_transactions_commit callback.

ActiveRecord::Base.transaction now yields an ActiveRecord::Transaction object. This allows registering callbacks directly on the transaction object.

ActiveRecord.after_all_transactions_commit callback allows us to perform actions after all database transactions have been properly persisted regardless of whether the code is inside or outside a transaction block and needs to execute after the state changes have been persisted.

This is especially useful for background job enqueuing and notification triggers.

def confirm_user(user)
  User.transaction do
    user.update(confirmed: true)
    
    ActiveRecord.after_all_transactions_commit do
      UserConfirmationMailer.with(user: user).deliver_later
    end
  end
end

In this example, the UserConfirmationMailer job is enqueued only after the transaction involving the user update is fully committed.

Here are the few examples of using ActiveRecord.after_all_transactions_commit outside of transaction

def update_order(order, order_params)
  order.update(order_params)

  ActiveRecord.after_all_transactions_commit do
    order.update_order_summary
  end
end
def create_user(user_params)
  user = User.create(user_params)

  ActiveRecord.after_all_transactions_commit do
    CreateUserProfileJob.perform_later(user)
  end
end

ActiveJob now automatically defers enqueuing jobs until after the transaction commits. If the transaction is rolled back, the job will be dropped.

This prevents common errors such as RecordNotFound caused by jobs executing before the transaction commits.

Conclusion

The ActiveRecord.after_all_transactions_commit significantly enhance how Rails handles background jobs and transaction callbacks, reducing common errors and making the process more robust and flexible.

  • Reliability: Ensures that dependent actions are only executed after all transactions are committed, preventing common errors.

  • Decoupling: Allows us to separate transaction management from model callbacks, leading to cleaner and more modular code.

  • Flexibility: Useful for both inside and outside transaction blocks, making it versatile for various use cases.

This feature enhances the robustness of background job processing and other asynchronous operations, making Rails applications more stable and easier to maintain.

Need help on your Ruby on Rails or React project?

Join Our Newsletter