-
Notifications
You must be signed in to change notification settings - Fork 647
Background PDF creation via delayed_job gem
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:
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
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
To get this to work in Rails 6, change
av = ActionView::Base.new()
av.view_paths = ActionController::Base.view_paths
to
av = ActionView::Base.new(ActionView::LookupContext.new(ActionView::ViewPaths.all_view_paths))
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 }
You no longer need to create a view to render from. Simply use ApplicationController.render
instead of av.render
in the examples above and don't bother about creating av
in the first place.
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.