問題
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で書き出しができないオブジェクトになっている。
ファイルに書き出せないオブジェクトをファイルに書き出そうとすると 例外 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オブジェクトなので先のエラーが発生してしまう。