Background PDF creation via delayed_job gem

Juanito Fatas edited this page Apr 1, 2015 · 5 revisions

Generating PDFs in your Rails app can be resource-intensive, especially if multiple people are generating PDFs at the same time. This can be mitigated by using a background task queue via delayed_job or resque. I went with delayed_job to avoid the extra overhead of setting up Redis. Here's the working code:

Controller

class DocsController < ApplicationController
  def generate_pdf
    @doc = Doc.find(params[:id])

    # enqueue our custom job object that uses delayed_job methods
    Delayed::Job.enqueue GeneratePdfJob.new(@doc.id)

    # update the status so nobody generates a PDF twice
    doc.update_attribute(:status, 'queued')
  end
end

Delayed Job

I put this code in /lib/generate_pdf_job in my Rails app, but you can put it wherever it makes sense. Note that I'm not including any layouts when rendering my PDF view, because it's a standalone view without headers or footers. Also note that I'm passing a local 'doc' variable to the view, rather than an instance @doc variable.

NB! In some cases it's better to use cells gem.

class GeneratePdfJob < Struct.new(:doc_id)

  # delayed_job automatically looks for a "perform" method
  def perform
    # create an instance of ActionView, so we can use the render method outside of a controller
    av = ActionView::Base.new()
    av.view_paths = ActionController::Base.view_paths

    # need these in case your view constructs any links or references any helper methods.
    av.class_eval do
      include Rails.application.routes.url_helpers
      include ApplicationHelper
    end

    pdf_html = av.render :template => "docs/pdf.html.erb", :layout => nil, :locals => {:doc => doc}

    # use wicked_pdf gem to create PDF from the doc HTML
    doc_pdf = WickedPdf.new.pdf_from_string(pdf_html, :page_size => 'Letter')

    # save PDF to disk
    pdf_path = Rails.root.join('tmp', "#{doc.id}.pdf")
    File.open(pdf_path, 'wb') do |file|
      file << doc_pdf
    end

  end

  # delayed_job's built-in success callback method
  def success(job)
    doc.update_attribute(:status, 'complete')
  end

  private

    # get the Doc object when the job is run
    def doc
      @doc = Doc.find(doc_id)
    end
end

But if you get an error like :

ActionView::Template::Error: undefined method `protect_against_forgery?' for #<ActionView::Base:0xc421704>

you can define a protect_against_forgery method before the variable declaration av :

ActionView::Base.send(:define_method, :protect_against_forgery?) { false }

View

This lives in /app/views/docs/pdf.html.erb:

<!DOCTYPE html>

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Language" content="en-us" />
  <title>PDF Doc</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <%= wicked_pdf_stylesheet_link_tag 'pdf' %>
</head>
<body>

  <% @doc = doc %>    
  <div class="page">
    <p>
    The doc ID is: <%= @doc.id %>.
    </p>
  </div>
  
</body>
</html>

This is a very simple example. There's obviously lots of other stuff you could do in the "perform" method, like send an email with the PDF attached, and a number of other callbacks supported by delayed_job. Switching to background PDF generation made a huge difference in the CPU activity on my production server, simply because I no longer had multiple instances of wkhtmltopdf running at the same time.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.