Sidekiq: Workers sometimes can't find records

Created on 30 Jul 2012  路  15Comments  路  Source: mperham/sidekiq

I have a message model with an after_create callback that queues up a sidekiq worker to email the message to the user.

In my rspec test I call .drain on the worker class to send the message and this works most of the time, but sometimes I get ActiveRecord::RecordNotFound errors. Since this worker is created via an after_create callback it should be there (and is in most test runs).

Any idea what's up?

class Message < ActiveRecord::Base
  after_create :send_new_message_email

  def send_new_message_email
    EmailMessageJob.perform_async id
  end
end
class EmailMessageJob
  include Sidekiq::Worker

  def perform(message_id)
    message = Message.find(message_id) <<<<< ActiveRecord::RecordNotFound

    # do stuff
  end
end

ruby 1.9.3 / rails 3.2.7

Most helpful comment

Use after_commit.

All 15 comments

Use after_commit.

Use after_commit on: :create instead. Sidekiq is finding out about the job before the database record has it committed.

Thanks, guys. Unfortunately, that made one test worse and didn't help in another test. I updated my message model to:

class Message < ActiveRecord::Base
  after_commit :send_new_message_email, on: :create

  def send_new_message_email
    EmailMessageJob.perform_async id
  end
end

and now this block is failing:

        expect {
          Message.create(
            body: intro_message_body,
            sender_id: sender.id,
            recipient_id: recipient.id,
          )

        }.to change{ EmailMessageJob.jobs.size }.by(1)

because of:

result should have been changed by 1, but was changed by 0

Another one of my tests that uses selenium is still failing with the same RecordNotFound error.

Did I miss something?

It's essentially impossible to test after_commit in rspec because your tests normally run within one big transaction, thus after_commit is never called.

Ah, that makes sense. Shoulda clarified this was in the context of rspec. I'll add a sleep before calling .drain unless there's another way I should be handling that.

Thanks!

@chris-lee To have rspec examples not run in a transaction you'll have to turn off RSpec's transactional fixtures and use something like the DatabaseCleaner gem to cleanup your test database. I'm using this approach to markup which tests need transaction callbacks to run: http://www.denniskuczynski.com/2012/06/22/changing-individual-test-configuration-based-on-passed-in-options.html

Thanks, @denniskuczynski! I'll give that a shot.

@denniskuczynski @chris-lee the option I use is monkey patching from here https://gist.github.com/1169763

Turns out the truncation was a bit of a red herring...

When running the full test suite there were other tests that created EmailMessageJobs, but the queue wasn't getting drained at the time. After those tests completed, the Messages associated with the jobs would get cleared out because of the transactions, but the job itself stayed in the redis queue. Later, when I called EmailMessageJob.drain it would try to run jobs for deleted messages. Boom.

My fix was to call EmailMessageJob.jobs.clear before my tests, but I was wondering if there's an equivalent command to clear jobs for all workers rather than one at a time.

Nope, there is no way to clear all jobs for all workers at once.

I bumped into the same issue with STI case:

class PrintJob < ActiveRecord::Base
  include Sidekiq::Worker

  after_commit :enqueue_job, :on => :create

  def enqueue_job
    # To be overridden
  end
end

class ChildOnePrintJob < PrintJob
  def perform(job_id)
    job = PrintJob.find(job_id)
    # do something with the job
  end

  private

    def enqueue_job
      Sidekiq::Client.enqueue(self.class, self.id)
    end
end


class ChildTwoPrintJob < PrintJob
  def perform(job_id)
    job = PrintJob.find(job_id)
    # do something with the job
  end

  private

    def enqueue_job
      Sidekiq::Client.enqueue(self.class, self.id)
    end
end

after_commit trick doesn't help at all. Any idea?

You can manually run the after_commit callback for your unit tests by using run_callbacks(:commit). I have something like:

assert_difference('ImageWorker.jobs.size') do
  @img.save
  @img.run_callbacks(:commit)
end

Weebit hacky, but it works for unit test needs.

For someone who might still be running in same issue:

Rails' after_commit needs to be used with precautions and understanding.

In my specific case, it was not useful to use the after_commit callback and I had to use the after_save (have to call on both create/update).

The issue can better be handled by using sidekiq's enqueuing:

earlier I had this:

after_save :notify
def notify
  MyMailer.notify(self, param1).deliver_later
end

which was having the above mentioned issues, however the sidekiq's retry mechanism was processing it successfully the next time but I wanted a clean way where it would not raise the error and should not queue in retries.
And this simple tweak fixed it:

after_save :notify
def notify
  MyMailer.notify(self, param1).deliver_later(wait: 30.seconds)
end

~ sur

Another option if you're on Rails 3/4 and having issues with the after_commit not running in specs is to use the test_after_commit gem: https://github.com/grosser/test_after_commit. (See also http://stackoverflow.com/questions/22988968/testing-after-commit-with-rspec-and-mocking)

I am using rails 4.2 with sidekiq and facing the same issue as the original question, i have used after_commit :on_create and this is not in tests but in actual background job. Any suggestions?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fatcatt316 picture fatcatt316  路  4Comments

edgarjs picture edgarjs  路  3Comments

sandstrom picture sandstrom  路  3Comments

aglushkov picture aglushkov  路  3Comments

andrewhavens picture andrewhavens  路  4Comments