PDFファイル生成GemとしてGroverはとても優秀なのですが、処理するデータ量によっては処理が重くなり、メモリ負荷やレスポンスタイムの懸念が付き纏います。
弊社プロダクトでも、もともと非同期処理でGroverを実装していなかったため、度々レスポンスが極端に遅くになりHeroku上でH12(タイムアウト)が発生することが有りました。
そこで今回は、Sidekiqを用いたGroverの非同期処理の実装例を紹介したいと思います。
実装例
下記は非同期処理の全体図になります。
請求書(Invoiceモデル)の内容をもとにPDFファイルを非同期で生成して、画面からダウンロードするというシナリオで実装イメージを説明していきます。
前提として、Invioceモデルにはpdf_fileというカラムを持っており、pdf_fileにはCarrierWaveをマウントしておきます。 aws-fogを使用してS3にPDFファイルを格納してクライアントからダウンロードさせるという構図になります。
① クライアントから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