(Pattern) Asynchronize With Job

Sometimes Ruby code takes too long for web standarts, or there is some heavy processing.
A pattern I use (and have trouble remembering) is to write code in a modular way.
Consider this:

class User < ActiveRecord::Base  

  def self.make_everyone_work_out
    User.all do |user|
      user.work_out
    end
  end  

  def work_out
    warm_up!
    run!    
    pump_iron!

    return true
  end
end

Now calling User.make_everyone_work_out will make you wait till everyone is done exercising, and who has the time for that?!

You can refactor by identifying (or refactoring out!) the method that does the most work (pun not intended) and takes the longest.
In this example the culprit is the work_out method.
We need to make a job to delegate this method to.

1. Start by defining a job

# in /app/jobs/user_workout_job.rb

module UserWorkoutJob
  @queue = :user_workout_job # can be anything, but keeping naming consistent is easier to follow

  # allows convenient enqueuement
  def self.queue!(user_id:)
    return Resque.enqueue(self, user_id: user_id)
  end

  # must be defined to actually work via Resque or other adapter
  def self.perform(user_id:)
    # check out gem 'retryable'
    Retryable.retryable(tries: 3, on: JOB_ERRORS, sleep: lambda {|n| 4**n }) do      
      user = User.find(user_id)
      user.work_out

      puts "Job made user ##{user_id} work out!"
    end
  end

end

There are several gotchas, like jobs not liking ActiveRecord objects as arguments, since they are costly to serialize.
To avoid this problem, pass simple objects like numerics and strings as arguments.
in this instance, :user_id is passed as argument to job and user object is reloaded from the id.

All a clean job should do is load an object or two and call the time-consuming method on it, which is what we have now.

2. Replace method invocations with Job invocations

Going back, there was a class method

def self.make_everyone_work_out
  User.all do |user|
    user.work_out
  end
end

Replace the costly work_out invocation with Job

def self.make_everyone_work_out
  User.all do |user|
    UserWorkoutJob.queue!(user_id: user.id)
    puts "Enqueued UserWorkoutJob for User ##{user.id}"
  end
end

3. See how it works

Naturally, for jobs to work you will need a job handler like Sidekiq or Resque set up for your app, but that is a topic for another post.
Assuming you can run jobs, to test in development you need to open the console in one terminal tab, and run the do-all development worker via rake resque:work QUEUE='*' Call User.make_everyone_work_out and you should see console output telling you that jobs were enqueued, and in worker tab you should see output about them being performed.

Hooray, no more synchronious holdups!

job

Written on April 27, 2016