世界線を超える

開発環境のRailsは、監視対象のファイルが更新されるたびに定数空間を再生成する。ファイルを更新するたびに新たな世界線に遷移すると言っても良い。全ての定数が再読込される訳ではなく、Rails.configuration.autoload_paths に登録され、autoload経由で読み込まれた定数のみが対象になる。このとき、監視対象外の空間から、監視対象の定数を参照していたらどうなるか。例えばlibディレクトリをautoload_pathsに登録していたとして、libディレクトリ内で読み込まれるrack middlewareをRailsのrack middleware stackに追加していたらどうなるのだろうか。

2つの世界

現在の世界で同名の定数が新しく読み込まれようとしたとき、運が良ければ、この現象を検知する仕組みが働いて例外が発生する。しかし運が悪ければ、2つの同名の定数が同じ世界に存在することになる。ファイルの変更は反映されず、定数からはもはや参照することのできない存在が誕生する。

この現象は、config/initializers/*.rb の初期化処理のような「アプリケーションの起動時に1度だけしか実行されない」という箇所で、グローバル変数やクラスのインスタンス変数を利用した場合に起こることがある。端的に言えば、非autoload対象 が autoload対象 を参照してはいけない。住む世界が違う。

autoload前に解放する

これに対処するための方法は幾つかあるが、1つは Rails.configuration.to_prepare を利用する方法がある。これは、autoloadの直前に必ず実行されるコールバックを指定できる機能だ。まさにこれから再構築されようとしている定数を、非autoload対象の存在から解放してやればよい。しかし、この方法では対処できない場合も存在する。例えば、先ほど述べたrack middleware stackの例。なぜなら、rack middleware stackは初めて利用されるときに凍結されるため、後から手を加えることはできないようになっているからだ。そのため、定数への参照を解放することは叶わない。

例えばautoloadを

他の方法として、そのような非autoload対象から参照される定数を、同じく非autoload対象にしてしまうという方法がある。libディレクトリに含まれる全てのファイルをautoload対象にしている場合が多いが、これを2つのディレクトリに分割するということになる。libの中に2つのディレクトリを置いて分割する方法よりは、新たに非autoload対象のファイルを入れるためのディレクトリを用意した方が、既に存在するコードへの影響が抑えられるだろう。例えば直接lib以下のファイルをファイルパスで参照しているものなどへの影響が考えられる。

前日譚

開発環境でファイル編集時に分岐した世界線の記憶が徐々にメモリに積み重なった結果、世界が壊れた

— ☻ (@r7kamura) 2014, 9月 30

世界線で困ってる人間、欧米にめっちゃ居た

— ☻ (@r7kamura) 2014, 9月 30

autoload対象のrack middlewareをdevelopment環境で利用してしまうとファイル更新時に導かれることが分かった

— ☻ (@r7kamura) 2014, 9月 30

autoload_once_paths というの使えばいいっぽい

— ☻ (@r7kamura) 2014, 9月 30

やったか……?

— ☻ (@r7kamura) 2014, 9月 30

凍ってる………

— ☻ (@r7kamura) 2014, 9月 30

色即是空空即是色色即是空空即是色

— ☻ (@r7kamura) 2014, 9月 30

倒した……

— ☻ (@r7kamura) 2014, 9月 30