Kevin Sylvestre
Kevin Sylvestre

Reputation: 38092

Inline `after_commit` Callbacks in Rails

I've got a sidekiq job that needs to be run after the commit, but only in some situations and not all, in order to avoid a common race condition.

For example, the below after_commit will always fire but the code inside will only execute if the flag is true (previously set in the verify method).

class User < ActiveRecord::Base
  ...
  after_commit do |user|
    if @enqueue_some_job
      SomeJob.new(user).enqueue
      @enqueue_some_job = nil
    end
  end

  def verify
    @enqueue_some_job = ...
    ...
    save!
  end
end

The code is a bit ugly. I'd much rather be able to somehow wrap the callback inline like this:

class User < ActiveRecord::Base
  def verify
    if ...
      run_after_commit do |user|
        SomeJob.new(user).enqueue
      end
    end
    ...
    save!
  end
end

Does anything built into Rails exist to support a syntax like this (that doesn't rely on setting a temporary instance variable)? Or do any libraries exist that extend Rails to add a syntax like this?

Upvotes: 4

Views: 2751

Answers (3)

RamPrasad Reddy
RamPrasad Reddy

Reputation: 125

Assuming that you need to verify a user after create.

after_commit :run_sidekiq_job, on: :create
after_commit :run_sidekiq_job, on: [:create, :update] // if you want on update as well.

This will ensure that your job will run only after a commit to db.

Then define your job that has to be performed.

def run_sidekiq_job
  ---------------
  ---------------
end

Hope it helps you :)

Upvotes: -1

Kevin Sylvestre
Kevin Sylvestre

Reputation: 38092

Found a solution using a via a concern. The snippet gets reused enough that it is probably a better option to abstract the instance variable and form a reusable pattern. It doesn't handle returns (not sure which are supported via after_commit since no transaction is present to roll back.

app/models/concerns/callbackable.rb

module Callbackable
  extend ActiveSupport::Concern

  included do

    after_commit do |resource|
      if @_execute_after_commit
        @_execute_after_commit.each do |callback|
          callback.call(resource)
        end
        @_execute_after_commit = nil
      end
    end
  end

  def execute_after_commit(&callback)
    if callback
      @_execute_after_commit ||= []
      @_execute_after_commit << callback
    end
  end

end

app/models/user.rb

class User < ActiveRecord::Base
  include Callbackable

  def verify
    if ...
      execute_after_commit do |user|
        SomeJob.new(user).enqueue
      end
    end
    ...
    save!
  end
end

Upvotes: 3

max
max

Reputation: 102433

You can use a method name instead of a block when declaring callbacks:

class User < ActiveRecord::Base
  after_commit :do_something!

  def do_something!
  end
end

To set a condition on the callback you can use the if and unless options. Note that these are just hash options - not keywords.

You can use a method name or a lambda:

class User < ActiveRecord::Base
  after_commit :do_something!, if: -> { self.some_value > 2 }
  after_commit :do_something!, unless: :something?

  def do_something!
  end

  def something?
    true || false
  end
end

Upvotes: 0

Related Questions