autoload pathsを拡張するとき、eager_loadについて覚えておいてください(日本語訳)

Appフォルダ内に新たにフォルダ追加する際、お世話になった記事なので備忘録に日本語訳にしました。原文にはない補足は(*)付けています。訳が間違っていればご指摘頂けますと幸いです。 blog.arkency.com November 9, 2014


config.autoload_paths を知ってますよね。あなたが作った .rb ファイルを置くためのディレクトリを追加できる設定です(app/* のすぐに使える機能の他に)。ここでは config.autoload_paths + = %W(#{config.root}/extras) を例にお話しします。しかし、それを最も一般的に使うのはlibディレクトリを追加するときです(特にRails2 から Rails3 への移行後)。

別の(あまり一般的ではない)ユースケースは、トップレベルのディレクトリにいくつかのコンポーネント(例えば、notification_center など)を追加するときです。コンポーネントがあなたのAppに連携するために、通常は以下のように設定します。

config.autoload_paths += %W( #{config.root}/notification_center )

or

config.autoload_paths += %W( #{config.root}/notification_center/lib )

1つだけ言及し忘れていることを除けば、この設定で問題ないです。その1つとは、本番環境でどのように機能するのか、そしてそれをうまく機能させるために対応する方法についてです。

2つのファイルを見てみましょう。

# root/extras/foo.rb
class Foo
end

and

# root/app/models/blog.rb
class Blog < ActiveRecord::Base
end

設定ファイルの内容はこのようになります。

# root/config/application.rb
config.autoload_paths += %W( #{config.root}/extras )

開発環境では大丈夫。

開発環境時の動作を確認しましょう。

defined?(Blog)
# => nil
defined?(Foo)
# => nil

Blog
# => Blog (call 'Blog.connection' to establish a connection)
Foo
# => Foo

defined?(Blog)
# => "constant"
defined?(Foo)
# => "constant"

簡単な例からわかるように、最初は何も読み込まれていません。BlogもFooも定義されていない。
それらを使おうとするとrailsの 自動読込み(autoloading)が割り込まれます。const_missing(*クラスやモジュールで定義されていない定数(またはクラス)にアクセスしようとしたときに呼び出される)が実行され、規約とbangに基づいて(*const_missing(name) の引数でクラスを検索し、存在しない場合は例外を発生させる)適切なディレクトリ内のクラスを検索します。app/models/blog.rbが読込まれ、BlogクラスはBlog定数の下で定義されるようになりました。extras/foo.rbFooクラスも同様です。

本番環境(production)ではEager Loading が作動しますよね。

開発環境とは違い、本番環境では状況が少し異なります。

defined?(Blog)
# => "constant"

defined?(Foo)
# => nil

Blog
# => Blog (call 'Blog.connection' to establish a connection)
Foo
# => Foo

defined?(Blog)
# => "constant"
defined?(Foo)
# => "constant"

Blogを初めて使おうとする前から、Blogクラスは既に読み込まれており、定義されています。どうして?eager loadingのおかげです。

本番環境で処理を早くするためにRailsは(開発環境とは)少し違った方法を使います。アプリケーションは実行前に、できるだけ多くのコードを読み込むために*.rbファイルを必要とします。これによって、アプリケーションを実行開始したときに、規約に基づいてファイルからクラスを探すことに時間を費やさず、すぐにリクエストを処理できます。

もう1つ理由があります。webサーバ(unicorn, passenger, など)がworkerを生成するためにfork形式(*リクエストを並行で処理するために、プロセスを分岐して子プロセスを生成する)を使用している場合、メモリ管理のためにCopy-On-Writeを利用できます。masterにはすべてのコードが読込まれており、workerはmasterをfork(分岐)して作成されます。masterが変更されない限り、workerはメモリの一部をmasterと共有します。それはworkerがより少ないメモリ量しか使わないことを意味します。workerは自分がmasterとメモリを共有していることを認識しておらず、workerどうしでもやり取りもできません。スレッドのようには機能しません。オペレーティングシステムだけが今のところmasterプロセスのメモリ全体をforkプロセスにコピーする代わりに、それを省略していること(メモリを共有すること)を認識しています。少なくともworkerがメモリから読み込むまで。passengerの説明、またはunicornの説明を確認してください。

しかし、私が注目しても欲しいのはBlogが定数として定義されており、(アプリケーション起動前に)読込まれている点ではありません(これはRailsの前のバージョンから変わっていません)。私はFoo定数が本番環境に読み込まれていないことを注意して欲しいです。

defined?(Blog)
# => "constant"

defined?(Foo)
# => nil

なぜそれが問題なのでしょう?eager loadingの良くない理由として、Fooがeager loadingされないということは、以下を意味します。

  • アプリケーションはHTTPリクエストがきたときにFooを探し終える必要があり、それにより少し遅くなるでしょう。1つのクラスならそこまで遅くはなりませんが。アプリケーションはfoo.rbというファイルを見つけてクラスを読み込むので、もっと遅くなります。
  • (リクエストのときにFooを探すと)全てのworkerはメモリ内でFooが定義されているコードを共有できません。Copy-On-Writeの最適化を利用できなくなってしまいます。

すべてが1つのクラスに関するものであれば、それほど問題にはなりません。しかし、いくつかのレガシーrailsアプリケーションでは、開発者がconfig.autoload_pathsに多くのディレクトリを追加しているのを見ています。そしてこれらのディレクトリにある単一のクラスが本番環境で、実行前に読み込まれていることはありません。デプロイ後にこれらのクラスの動的に読み込む必要があり、初期リクエストのパフォーマンスが低下する可能性があります。あなたが継続的な開発を行う際に、特に痛ましいことでしょう。私たち(開発者)は自分たちのデプロイ(ソースの追加・更新)によって(アプリケーションの)利用者に影響が出ること望んでいません。

どうすれば修正できますか?

config.eager_load_pathsという、あまり知られていないrails設定を使うことで目標を達成できます。

config.eager_load_paths += %W( #{config.root}/extras )

それは本番環境にどのように作用しますか?見てみましょう。

defined?(Blog)
# => "constant"
defined?(Foo)
# => "constant"

extra/foo.rbのクラスFooが自動で読み込まれただけでなく、本番環境でも事前に読み込まれています。これで問題は解決しました。
待って、それではこれからファイルを読込むのに2行書く必要があるという意味ですか?

config.autoload_paths += %W( #{config.root}/extras )
config.eager_load_paths += %W( #{config.root}/extras )

autoloadingはeager_load_pathsも使用しています。

以下の書き方をすれば2行書く必要がありません。

config.eager_load_paths += %W( #{config.root}/extras )

開発環境と本番環境はうまく機能しているようです。 autoloadingはeager_load_pathsをチェックするように設定されているためだと思います。

def _all_autoload_paths
  @_all_autoload_paths ||= (
    config.autoload_paths   +
    config.eager_load_paths +
    config.autoload_once_paths
  ).uniq
end

in Rails::Engine code.

One more thing

残念ながら私たちは多くの人がこのように書いてるのを見ました。

config.autoload_paths += %W( #{config.root}/app/services )
config.autoload_paths += %W( #{config.root}/app/presenters )

app/*は既に追加されているので、これは全く必要ありません。app/に任意ディレクトリを追加して、app/controllersapp/modelsと同じように使うことができます。ただし、コンソール、サーバ、spring サーバ(spring stop)の再起動が必要です。以下はrails 4.1.7 のデフォルトパス構成です。

def paths
  @paths ||= begin
    paths = Rails::Paths::Root.new(@root)

    paths.add "app",                 eager_load: true, glob: "*"
    paths.add "app/assets",          glob: "*"
    paths.add "app/controllers",     eager_load: true
    paths.add "app/helpers",         eager_load: true
    paths.add "app/models",          eager_load: true
    paths.add "app/mailers",         eager_load: true
    paths.add "app/views"

    paths.add "app/controllers/concerns", eager_load: true
    paths.add "app/models/concerns",      eager_load: true

    paths.add "lib",                 load_path: true
    paths.add "lib/assets",          glob: "*"
    paths.add "lib/tasks",           glob: "**/*.rake"

    paths.add "config"
    paths.add "config/environments", glob: "#{Rails.env}.rb"
    paths.add "config/initializers", glob: "**/*.rb"
    paths.add "config/locales",      glob: "*.{rb,yml}"
    paths.add "config/routes.rb"

    paths.add "db"
    paths.add "db/migrate"
    paths.add "db/seeds.rb"

    paths.add "vendor",              load_path: true
    paths.add "vendor/assets",       glob: "*"

    paths
  end
end

paths.add "app", eager_load: true, glob: "*"globに注目してください。これはapp配下のサブディレクトリです。

設定はいつでもコンソールで確認できます。

Rails.configuration.autoload_paths
Rails.configuration.eager_load_paths

念のために。

config.pathsと結論

Rails :: Engine :: Configurationを見ると、これらのメソッドがどのように定義されているか分かります。

def eager_load_paths
  @eager_load_paths ||= paths.eager_load
end

def autoload_once_paths
  @autoload_once_paths ||= paths.autoload_once
end

def autoload_paths
  @autoload_paths ||= paths.autoload_paths
end

それら(* eager_load, autoload_once, autoload)はすべて、最初の呼び出しでRails.configuration.pathspathsを受け取ります。これによりextrasディレクトリをrailsと同じ方法で設定できるという結論に至ります。

config.paths.add "extras", eager_load: true

いい感じじゃない?

注意

eager_loading( 今回の説明で話したpathの話)とActiveRecordのeager_loading( N+1問題を回避するため、事前に必要なデータを読み込む)を混同しないでください。これについても記事があります。それらの処理方法は似ていますが、完全に異なるものです。

もっと学びたいですか?

あなたがこの記事を楽しんだなら、あなたが日々のRailsプログラマーの仕事に役立つ知識を得るために、あなたが常に最新の情報を得れるように私たちのニュースレターを購読してください。 コンテンツは主にRubyRails、Web開発、そしてRailsアプリケーションのリファクタリングに焦点を合わせています。 また、私たちの最新の本Domain-Driven Railsを必ずチェックしてください。 特に、大きくて複雑なRailsアプリケーションを扱う場合は特にそうです。


翻訳は以上です。

補足

  1. defined? メソッド
    式が定義されていなければ、偽を返します。定義されていれば式の種別 を表す文字列を返す。
    クラス/メソッドの定義 (Ruby 1.9.3)

  2. プロセスとスレッドの違い
    プロセスはCPUとメモリを仮想的に作成したもの(copy on write)でしたが、スレッドはメモリ部分だけは共通にCPU部分を分けたもの。
    各々の生き方: プロセスとスレッドの話

参考

rubyの定数が定義されているかをdefined?で確認して三項演算子に格納するときに()の有無でハマった - ツナワタリマイライフ