Marshal.loadでクラスが存在しない場合はconst_missingはコールバックされない
タグ: ruby / 初版公開: 2020-03-14

概要

Marshal.load でクラスが存在しない場合は const_missing はコールバックされません。この挙動はconst_missingを利用した仕組み(例えばRailsのオートロード)で思わぬ落とし穴になることがあります。

前提ソフトウェア

ソフトウェアバージョン備考
Ruby2.6.3p62-

const_missingとMarshal.load

RubyのModule#const_missingはスクリプトの実行中に参照した定数が定義されていない時にコールバックされます。const_missingをオーバーライドすることで任意の処理を実行することができます。

ところがconst_missingMarshal.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もコールバックされないため例外が発生します。