【Ruby】Parallel.mapでno _dump_data is defined for〜のエラーが発生する原因と回避例

  • 1
    いいね
  • 0
    コメント

問題

Rubyで並行処理を扱うGemのParallelで表題のエラーが発生したので原因を探った。

当該コードは以下。

require 'parallel'

class Example
end

classes = [Example.new, Example.new]

dump_target = Proc.new{}

# no _dump data is defined for ~~が発生する
results = Parallel.map(classes, in_processes: 2) do |example|
  example.instance_variable_set(:@new_variable, dump_target)
  example
end

処理内容としてはParallel.mapをprocess形式で利用し、mapに渡したExample Classのインスタンスを並列実行する。

各プロセス内では渡されたインスタンスに新しくインスタンス変数を定義する。

この際、インスタンス変数の値としてProc.new{}をセットする。

最後にExampleのインスタンスを返し、resultsは各プロセスで並列実行した結果を配列で受け取る。

これが正しく実行されるとresultsは[#Example:0x007fdb6b0a5548, #Example:0x007fdb6b0a5778]のような値がセットされているはずだ。

しかし、このコードはno _dump_data is defined for Procというエラーが発生する。

原因

原因は、Parallelのlib/parallel.rb内にある下記メソッドにてMarshal.dumpするところ。

    def process_incoming_jobs(read, write, items, options, &block)
      while !read.eof?
        data = Marshal.load(read)
        item, index = items.unpack(data)
        result = begin
          call_with_index(item, index, options, &block)
        rescue StandardError => e
          ExceptionWrapper.new(e)
        end
        Marshal.dump(result, write)
      end
    end

この最後のMarshal.dumpの箇所が表題のエラーの原因。

Marshal.dumpの引数に渡っているresultが先のサンプルコードでいうところの各プロセス内のExampleのインスタンス。

このExampleのインスタンスに新しく追加したインスタンス変数の値がProc.new{}だが、これはMarshal.dumpで書き出しができないオブジェクトになっている。

Marshal.dumpの公式リファレンスによると、

ファイルに書き出せないオブジェクトをファイルに書き出そうとすると 例外 TypeError が発生します。 ファイルに書き出せないオブジェクトは以下の通りです。

・名前のついてない Class/Module オブジェクト。(この場 合は、例外 ArgumentError が発生します。無名クラスについて は、Module.new を参照。)
・システムがオブジェクトの状態を保持するもの。具体的には以下のイン スタンス。Dir, File::Stat, IO とそのサブクラス File, Socket など。
・MatchData, Data, Method, UnboundMethod, Proc, Thread, ThreadGroup, Continuation のインスタンス。
・特異メソッドを定義したオブジェクト

とある。よってProc.newもこれにあたる。

解決法

もし、先のサンプルコードを正しく実行させるなら下記のようにインスタンス変数のcloneを作成する。

こうするとプロセス内で返り値にするインスタンスには変更がないのでエラーが発生しない。

results = Parallel.map(classes, in_processes: 2) do |example|
  _example = example.clone
  _example.instance_variable_set(:@new_variable, dump_target)
  example
end

results #=> [#<Example:0x007fdb6b0a5548>, #<Example:0x007fdb6b0a5778>]

実例

サンプルコードだと一体どこでこんなコード書くのかと疑問になると思うが、例えばPaperclipというファイルアップロードのための便利gemをParallelと利用しようとすると同様の問題にぶち当たる。

例えば下記のようなコード。ユーザーの既存のサムネイルに対して何か処理を加え、処理済みのモデルはそれを示すフラグのバルクアップデートをする場合。

User.find_in_batches do |users|
  results = Parallel.map(users, in_processes: 10) do |user|
    thumb_url = user.avatar.url

    # サムネイルに対して何か処理をする
    # ...
    # ...
    user.is_converted = 1
    user
  end
  # resultsのuserをバルクアップデートする処理
  # ...
end

このとき、user.avatar.urlを実行する箇所でPaperclipはuserに対してattachment_imageというインスタンス変数を定義する。

このattachment_imageはProcオブジェクトなので先のエラーが発生してしまう。

参考リンク