「AWS無料相談会」をオンラインで開催中

CarrierWaveのversionsでPDFの各ページ画像をアップロードしようとしてハマったこと

土居です。本日はRailsアプリケーションで、CarrierWave(ファイルアップロード機能を提供するGem)の機能を一部誤って使っていたためにハマってしまったことについて紹介したいと思います。

PDFの各ページを画像化してアップロード

CarrierWaveを使ってPDFアップロードをしているシステムで、新たにPDFの各ページを画像化してアップロード(+表示)することが必要になりました。

画像化自体は元々今回の対応とは異なる箇所で行っており、アップロードするPDFの先頭ページのみ対象として画像化(サムネイル生成)していました。ですので、今回はその部分の応用でできるのでは?という目論見がありました。

CarrierWaveのversions機能

CarrierWaveのUploaderにはversionsという機能があり、アップロードするファイルに対して別バージョンのファイルを作成してアップロードできます。前述のサムネイルはこの機能を用いていました。
versionを追加

PDFのページごとにversions生成

公式ドキュメントやブログ記事などを見ている限り、versionsはあらかじめ必要なものをUploader内に名前付きで定義しておくもののようでしたが、version関数を動的に呼び出してやればページごとのversionとしてアップロードできるのでは?と実装し、実際に手元ではうまくいっているように見えていました。

ドキュメントにあるversionの書き方。
versionは**:thumb**の固定

class MyUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  process resize_to_fit: [800, 800]

  version :thumb do
    process resize_to_fill: [200,200]
  end
end

今回やろうとした書き方。
アップロードしたPDFファイルのページ数でループし、version関数を1ページごとに呼び出している。
versionが1, 2, 3とページごとに生成される。

class MyUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  process convert_pages

  def convert_pages
    def convert_pages
      # アップロードファイルのページ数を取得
      pages = Magick::Image.read(current_path).length

      # ページ数の分、version関数を呼び出し
      (1..pages).each do |page|
        MyUploader.version :"#{page}" do
          # convert_to_imageは引数で与えた該当ページを画像化する関数
          process convert_to_image: [page]
          process convert: :png

          def full_filename(for_file)
            super(for_file).sub(/.pdf/, '.png')
          end
        end
      end
    end
  end
end

デプロイして動作させると問題発生

ひととおりのテストをしたのち、デプロイした環境で動作確認をすると画像生成ができていないことや、アップロード時にサーバーエラーで落ちてしまうなどの問題が起きてしまいました。
どうしてだろう?問題が毎回起きるわけではなく安定感がないことや、デプロイしてサーバー再起動時には必ず起きるなどの現象も見られました。

冗長化構成だと起きる

問題の起きる環境は2台構成であり、どうもそこに不安定な動作の原因があるようでした。
CarrierWaveのversionsはクラス変数として保持されており、ファイルをアップロードするときに向いていた系にしかversions(ファイルのページ数によって変化する)が反映されていなかったのです。
また、ローカル環境ではどこかのタイミングで一度保持されたversionsが残っていたため画像生成ができていたようですが、サーバーが再起動するたびにversionsは0個になってしまうと考えられます(私のUploaderでは、実際にアップロードするまでversionが生成されないため)。そのため、デプロイ直後の初回アップロードは必ず失敗してしまいます。

versionsは今回の用途に適さない

結局、CarrierWaveのversionはあらかじめ決まったものを定義しておくもので、システムの操作によって動的に更新するのは厳しいのだと思いました。(どうしても後から変更したい場合はサーバーに入って直接、rails cでmodelに対してrecreate_versions!関数を用いればversionsの再作成することは可能なようです)

対応

versionsを使わないことを決め、以下の選択肢を考えました。

  1. 今回の部分だけ、CarrierWaveを使わず自前でアップロード
  2. ALBのターゲットグループで、該当機能のリクエストを単一サーバーのみに振る
  3. versionsを最初から100ページ分くらい定義しておく

2は一部機能とはいえ、せっかく冗長化しているのをやめるのはよくない。
また、3はできそうでしたがあまりにも動作が重すぎました。
結果、1を採ることに。
RMagickで画像化し、aws-sdkでアップロードする形としました。

次回は

Shrineという別のアップローダーを用いると、動的なversionも簡単にできる。というようなことを目にしたので、
全体的にCarrierWaveから置き換えてまでやるべきかというのもあるので、別の新規サービスなど機会があれば試したいですね。

参考

https://github.com/carrierwaveuploader/carrierwave
https://github.com/rmagick/rmagick
https://github.com/shrinerb/shrine