Ramy
Ramy

Reputation: 21261

after_commit callback is being called several times

update: Is it the case that a call to update_attributes gets it's own transaction?

I've looked at this question and for reasons in addition to that question, i've decided to go with after_commit as the proper hook. The problem is it's being called multiple (exactly three) times. The code is a little complex to explain, but basically there is a profile model that has

include Traits::Blobs::Holder

in holder.rb I have:

  module ClassMethods

    def belongs_to_blob(name, options = {})
      clazz = options[:class_name] ? options[:class_name].constantize : Blob
      foreign_key = options[:foreign_key] || :"#{name}_id"

      define_method "save_#{name}" do
        blob = self.send(name)
        if self.errors.any? && blob && blob.valid?
          after_transaction do
            blob.save!
            #self[foreign_key] = blob.id
            #save resume anyway
            self.update_attribute(foreign_key, blob.id)
          end
        end
      end
      after_validation "save_#{name}"

      belongs_to name, options

      accepts_nested_attributes_for name
    end

  end 

finally in profile.rb itself I have:

after_commit :send_messages_after_registration!

protected

def send_messages_after_registration!
  Rails.logger.debug("ENTERED : send_messages_after_registration " + self.owner.email.to_s)
  if self.completed?
    Rails.logger.debug("completed? is true " + self.owner.email.to_s)
    JobSeekerNotifier.webinar_notification(self.owner.id).deliver
    Resque.enqueue_in(48.hours, TrackReminderWorker, self.owner.id)
  end
end

it appears that the method is entered 3 times. I've been trying to figure this out for a few days so any guidance you can provide will be appreciated.

controller code:

def create
  @user = Customer.new(params[:customer].merge(
    :source => cookies[:source]
  ))
  @user.require_password = true

  respond_to do |f|
    if @user.save
      promote_provisional_user(@user)  if cookies[:provisional_user_id]

      @user.profile.update_attributes(:firsttime => true, :last_job_title => params[:job_title]) unless params[:job_title].blank?

      if params[:resume]
        @user.profile.firsttime = true
        @user.profile.build_resume(:file => params[:resume])
        @user.profile.resume.save
        @user.profile.save
      end
    ...
end

Upvotes: 4

Views: 3727

Answers (1)

Frederick Cheung
Frederick Cheung

Reputation: 84114

So it's happening 3 times because the profile is being saved 3 times: once when the user is saved (I assume that User accepts_nested_attributes_for :profile, once when you call update_attributes(:first_time => true,...) and once when you call save in the if params[:resume] block. Every save creates a new transaction (unless one is already in progress) you end up with multiple calls to after_commit

after_commit does take an :on option (which can take the values :create, :update, :destroy) so that you can limit it to new records. This would obviously fire on the first save so you wouldn't be able to see the profile's resumé and so on.

You could in addition wrap the entirety of those updates in a single transaction, in that case after_commit only gets called once, no matter how many saves take place inside the transaction by doing something like

User.transaction do
  if @user.save
    ...
  end
end

The transaction will get rolled back if an exception is raised (you can raise ActiveRecord::Rollback if you want to bail out)

Upvotes: 4

Related Questions