概要
Marshal.load
でクラスが存在しない場合は const_missing
はコールバックされません。この挙動はconst_missing
を利用した仕組み(例えばRailsのオートロード)で思わぬ落とし穴になることがあります。
前提ソフトウェア
ソフトウェア | バージョン | 備考 |
---|---|---|
Ruby | 2.6.3p62 | - |
const_missingとMarshal.load
RubyのModule#const_missing
はスクリプトの実行中に参照した定数が定義されていない時にコールバックされます。const_missing
をオーバーライドすることで任意の処理を実行することができます。
ところがconst_missing
はMarshal.load
で未定義のクラスが現れた時はコールバックされません。以下は検証コードです。
class Hoge; end
open('hoge.marshal', 'w') do |f|
p hoge = Hoge.new #=> #<Hoge:0x00007f8f34116de8>
# MarshalでHogeクラスをシリアライズして書き出し
f.puts Marshal.dump(Hoge.new)
end
class Hoge; end
# もちろんHogeが定義されていればMarshalでデシリアライズできる
p Marshal.load(File.read('hoge.marshal')) #=> #<Hoge:0x00007f8f34116de8>
class Module
def const_missing(id)
if id == :Hoge
eval "class Hoge; end", Object::TOPLEVEL_BINDING
Hoge
end
end
end
# const_missingしておけば本来はconst_missingが呼ばれる
p Hoge.new #=> #<Hoge:0x0000555de1e9fd60>
class Module
def const_missing(id)
if id == :Hoge
eval "class Hoge; end", Object::TOPLEVEL_BINDING
Hoge
end
end
end
# ところがMarshal.loadではconst_missingが呼ばれず即座にArgumentErrorになる
Marshal.load(File.read('hoge.marshal')) #=> ArgumentError
少し調べたところRubyにはこの件に関連する以下のissuesがあるようです。
Railsのオートロードと解決策
Railsのオートロードはconst_missing
を利用した仕組みの一つです。Railsのオートロードではクラス(定数)が定義されていない時にautoload_paths
以下のファイルを自動でロードするようになっています。
Rails5以降のproductionモードではeager_load = true
がデフォルトなのでなかなか起こりませんが、developmentモードで開発中の場合ならオートロードがうまくいかず問題になることがあるかも知れません。そのような場合は問題となるクラスを先にrequire
するかRails.application.earger_load!
してしまうのが良いでしょう。
暗黙的にMarshal.loadするGem
明示的にMarshalを使っていない場合でも内部でMarshal.dump
, Marshal.load
するGemでこの問題に遭遇することがあります。
例えばParallelはプロセスによる並列化を行う場合フォークしたプロセスでMarshal.dump
して親プロセスでMarshal.load
する挙動をします。(Parallelについての詳しい説明は Rubyで並列処理を行うparallel gemの使い方と勘所 をご覧下さい)
このような場合はフォーク先のプロセスでオートロードされたクラスが親プロセスに渡る時、親プロセスでは未ロードでconst_missing
もコールバックされないため例外が発生します。