行動すれば次の現実

テック中心の個人ブログ

Groverで非同期にPDFファイルを生成する方法 | Rails

PDFファイル生成GemとしてGroverはとても優秀なのですが、処理するデータ量によっては処理が重くなり、メモリ負荷やレスポンスタイムの懸念が付き纏います。

弊社プロダクトでも、もともと非同期処理でGroverを実装していなかったため、度々レスポンスが極端に遅くになりHeroku上でH12(タイムアウト)が発生することが有りました。

そこで今回は、Sidekiqを用いたGroverの非同期処理の実装例を紹介したいと思います。

実装例

下記は非同期処理の全体図になります。

請求書(Invoiceモデル)の内容をもとにPDFファイルを非同期で生成して、画面からダウンロードするというシナリオで実装イメージを説明していきます。

前提として、Invioceモデルにはpdf_fileというカラムを持っており、pdf_fileにはCarrierWaveをマウントしておきます。 aws-fogを使用してS3にPDFファイルを格納してクライアントからダウンロードさせるという構図になります。

Grover非同期処理の全体図

① クライアントからPDF生成アクション(POST)を実行します

クライアントは請求書を作成するために画面からPDF生成アクションを実行します。

後述するenumステータスによって、画面上に「PDF生成ボタン」「PDF作成中ローディング」「PDFダウンロードボタン」の出力を切り替えるなどすると良いかと思います。

② PDF生成アクションがPDF生成ワーカー(Sidekiq)にリクエストします

PDF生成アクションではWorkerに対してPDF生成処理のみを要求して、即座にレスポンスを返します。 Invoiceモデルには必要に応じてenumで処理状態のステータスを管理すると良いでしょう。

# endpoint
#    invoices/:invoice_id/pdf_files(post)
# action
#    invoices::pdf_files_controller#create

# スタータスの例
# - requested: 処理をリクエスト済み
# - creating: PDF作成中
# - created: PDF作成済み
# - failure: PDF作成失敗

def create
  invoice = Invoice.find(params[:invoice_id])
  invoice.requested!
  CreateInvoicePdfWorker.perform_async(invoice.id)
  redirect_to invoices_url
end

③ PDF生成ワーカーがGroverでPDFを生成してS3に配置します

ワーカーでは、PDFファイルをローカルに作成して、CarrierWave経由でS3にアップロードさせます。 コメントで処理の内容を記載していますので、詳しくはそちらを参照ください。

class CreateInvoicePdfWorker
  include Sidekiq::Worker
  def perform(invoice_id)
    # 対象のInvoiceを取得してステータスを作成中に変更します
    invoice = Invoice.find(invoice_id)
    invoice.creating!
    # html文字列を生成します
    # 生成処理は簡略化のためにダミーのHTML文字列としています
    html = "<div>請求書</div>"
    temp_file_name = "/tmp/invoice_#{invoice.id}_#{Time.zone.today.strftime('%Y%m%d%S')}.pdf"
    # pathを指定すると任意のローカルファイルにPDFファイルを書き出しをしてくれます
    Grover.new(html, path: temp_file_name).to_pdf
    File.open(temp_file_name) do |f|
      # 事前にInvoiceモデルのpdf_file項目にCarrierWaveをマウントしておきます
      # (aws-fogを使用してS3に配置します)
      # PDFファイルの内容をinvoice.pdf_fileに反映します
      invoice.pdf_file = f
    end
    # ステータスを作成済みに更新します
    invoice.created!
    # 一時ファイルは削除しておきます
    File.delete(temp_file_name)
  end
end

④ クライアントがPDF取得アクション(GET)を実行します

クライアントは請求書が「作成済み」になったら、PDF取得アクションでファイルをダウンロードします。 CarrierWaveでpdf_fileをS3にマウントしていますので、S3のパスは隠蔽したままダウンロードさせることができます。

# endpoint
#    invoices/:invoice_id/pdf_files(get)
# action
#    invoices::pdf_files_controller#index

def index
  invoice = Invoice.find(params[:invoice_id])
  send_data(invoice.pdf_file.read, filename: invoice.pdf_file_identifier)
end