I’m hard at work on iEye these days.

To take care of images, we use Paperclip, and are loving it. But according to New Relic’s RPM, the slowest part is when we upload a image, and it creates a few different versions (not that it was any surprise that that was slow). I searched a bit around for a easy way to make Paperclip work with delayed_job, but soon concluded that I would have to do it myself. Which I did.

ImageJob

The first step was to create a small wrapper-class for my model. I’m attaching images to a Image-model, so this new class was called ImageJob (and lives in lib/, since it’s not really a model).

(This is of course not the first step, but I image that getting delayed_job installed and ready is not a problem.)

class ImageJob < Struct.new(:image_id)
  def perform
    Image.find(self.image_id).perform
  end
end

I tried to just enqueue the Image itself, but that gave me some errors when de-serializing the object from the database. And we don’t really need to store the entire model in the database when we can just fetch it when we need it.

Processing

Then I added a column to the images-table, letting me know if it was still processing the image.

class AddProcessingToImage < ActiveRecord::Migration
  def self.up
    add_column :images, :processing, :boolean, :default => true
  end

  def self.down
    remove_column :images, :processing
  end
end

It defaults to true, since Rails refused to set it to true before Paperclip started doing its thing. Might have been solved with some more investigation, but this worked quickly. This means that for an existing site, you would have to create a job for each existing image, and let them be processed, or go change each existing image to be done.

Stopping Paperclip

Then it was time to change the Image creation process, so we halted Paperclip if it was just created.

So in the model (Image in this case), I hooked into Paperclip, and stopped it. Then added a job for it to be processed later.

before_attachment_post_process do |attachment|
  # Stop any further processing by Paperclip
  false if attachment.processing?
end
after_create do |image|
  Delayed::Job.enqueue ImageJob.new(image.id)
end

Performing the resizing

# Force Paperclip to reprocess the image, and
# since we set processing to false before,
# it will manage to generate thumbnails on this attempt.
def perform
  self.processing = false
  self.attachment.reprocess!
  self.save
end

Default images when processing

Since Paperclip doesn’t really know (or care) that we stopped it from creating images, it still returns the URLs for images that don’t exist. And we don’t want to serve non-existing images to visitors.

So instead of calling @image.attachment.url(:thumbnail) in my views, I added a little helper method to the Image-model with the same syntax, but returns the URL for a placeholder image if this image is still processing.

# Convenience method to avoid calling @image.attachment.url in views,
# and also makes it possible to do a little dirty hack, returning
# different URL for images that are pending
def url(style = :original)
  if self.image && processing? && style != :original
    return attachment.send(:interpolate, @@default_url, "pending_#{style}")
  end
  attachment.url(style)
end

The default URL we use is stored as @@default_url = "/system/:class/missing/:style.:locale.png", and gives URLs like /system/images/missing/pending_thumbnail.en.png when we are waiting for the thumbnail.

Running the worker

To actually get the workers running, and the jobs done, I use God to make sure it keeps running, and a configuration based on what GitHub uses

ENV['RAILS_ENV']  ||= "staging"
ENV['RAILS_ROOT'] ||= "/var/www/iEye/beta/current"
ENV['GOD_UID']    ||= "ieye"
ENV['GOD_GID']    ||= "users"

God.pid_file_directory = "#{ENV['RAILS_ROOT']}/log"

2.times do |num|
  God.watch do |w|
    w.name     = "ieye-beta-dj-#{num}"
    w.group    = 'ieye-beta-dj'
    w.uid      = ENV['GOD_UID']
    w.gid      = ENV['GOD_GID']
    w.interval = 30.seconds
    w.start    = "rake -f #{ENV['RAILS_ROOT']}/Rakefile jobs:work RAILS_ENV=#{ENV['RAILS_ENV']}"
    w.stop     = "kill -9 `cat #{w.pid_file}`"

    # restart if memory gets too high
    w.transition(:up, :restart) do |on|
      on.condition(:memory_usage) do |c|
        c.above = 300.megabytes
        c.times = 2
      end
    end

    # determine the state on startup
    w.transition(:init, { true => :up, false => :start }) do |on|
      on.condition(:process_running) do |c|
        c.running = true
      end
    end

    # determine when process has finished starting
    w.transition([:start, :restart], :up) do |on|
      on.condition(:process_running) do |c|
        c.running = true
        c.interval = 5.seconds
      end

      # failsafe
      on.condition(:tries) do |c|
        c.times = 5
        c.transition = :start
        c.interval = 5.seconds
      end
    end

    # start if process is not running
    w.transition(:up, :start) do |on|
      on.condition(:process_running) do |c|
        c.running = false
      end
    end
  end
end

This starts two job runners, and keeps them running. It’s important to restart them after each deploy, so in my deploy.rb, right before I touch tmp/restart.txt, I also have sudo god restart ieye-beta-dj to take care of that. Might also be wise to have god start after a reboot, as I found out after researching why things weren’t being run.