Playback speed
×
Share post
Share post at current time
0:00
/
0:00
Transcript

Enqueue job in after_commit callback by Synvert

Synvert provides the ability to write code snippets that can automatically rewrite your source code. This video demonstrates how to automatically use after_commit callback to enqueue jobs.

Phantom Data in Rails Callbacks

In Rails, when you enqueue a job in an after_create or after_update callback, the job may start executing before the database transaction is fully committed. This can lead to phantom data problems where the job cannot access the latest state of the record. Here’s an example to illustrate:

class User < ApplicationRecord
  after_create :enqueue_welcome_email

  private

  def enqueue_welcome_email
    WelcomeEmailJob.perform_later(self.id)
  end
end
class WelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome_email(user).deliver_now
  end
end

In the User model, we enqueu a WelcomeEmailJob in after_create callback, and in this job, we attempt to find the user and send a welcome email. If the job starts executing before the transaction is committed, the user record may not yet exist in the database, leading to an ActiveRecord::RecordNotFound error.

A Better Approach: Using after_commit Callbacks

To avoid such issues, it’s better to use after_create_commit and after_update_commit callbacks. These callbacks are executed after the database transaction is committed, ensuring that the latest data is available when the job runs:

class User < ApplicationRecord
  after_create_commit :enqueue_welcome_email

  private

  def enqueue_welcome_email
    WelcomeEmailJob.perform_later(self.id)
  end
end

Synvert Snippet to Automate Refactoring

To help Rails developers refactor their codebases, I’ve created a Synvert snippet that automatically identifies after_create and after_update callbacks that enqueue jobs and converts them to use after_create_commit and after_update_commit instead. You can find this snippet in the Synvert Ruby snippets repository.

This snippet calls ruby/parse helper to easily find all job classes and mailer classes in the Rails application. I’ll write another post to explain how to use the ruby/parse helper in detail.

Here’s how the snippet works:

  1. It finds after_create, after_update and after_save callbacks, and remember the InsertAction to insert _commit to the callback name

  2. It then finds method names that perform a job or deliver a mailer

  3. It checks if the callback method performs a job or deliver a mailer, even if the method is called inside another method invoked in the callback method.

  4. If the method performs a job or delivers a mailer, it adds the InsertAction and insert _commit to the callback name

In the test code, you will see the snippet rewrites the after_create callback to after_create_commit callback

  1. when the callback method performs a ActiveJob job.

  2. when the callback method performs a Sidekiq job.

  3. when the callback method delivers a mailer.

Practical Example: Applying the Synvert Snippet

Let’s explore how this snippet can be applied to a real-world Rails application. Suppose we have a Rails application with several models using after_create and after_update callbacks to enqueue jobs. By running the Synvert snippet, we can refactor these callbacks to use after_create_commit and after_update_commit.

Running the Synvert Command

Execute the following command to refactor your code:

synvert-ruby --run rails/enqueue_job_in_after_commit_callback

Check the changes using:

git diff

Here’s an example of the transformation in one of the project files:

diff --git a/app/models/order.rb b/app/models/order.rb
index 2f5967d4..31dce1bf 100644
--- a/app/models/order.rb
+++ b/app/models/order.rb
@@ -8,7 +8,7 @@ class Order < ApplicationRecord
              optional: true
   belongs_to :plan, foreign_key: 'fastspring_product_path', primary_key: 'fastspring_product_path', optional: true

-  after_create :slack_notify
+  after_create_commit :slack_notify

   def raw_data=(data)
     self.fastspring_subscription_id = data['items'].first['subscription']

Here is the definition of slack_notify method:

  def slack_notify
    message = "New order is created, #{raw_data})"
    SlackNotify::SimpleMessageJob.perform_later(message)
  end

Ensuring Code Integrity Post-Refactoring

After applying the snippet, it’s crucial to verify the integrity and functionality of the code:

bundle exec rspec spec

This ensures that all changes preserve their intended behaviors without introducing errors.

Conclusion

I hope this snippet helps you upgrade your codebase to use safer callbacks in Rails, avoiding issues with phantom data. If you encounter any issues or have feedback, I am eager to assist. Stay tuned for my video demonstration where I will walk you through the entire process. Happy coding!