Dependencies and classes generated at runtime

I've been experiencing a performance issue using Edge and it appears to be related to how Dependencies handles classes that are generated at runtime. I'll give a simplified scenario of my problem for the sake of clarity. These are the migrations:

001_create_publications.rb: class CreatePublications < ActiveRecord::Migration   def self.up     create_table :publications do |t|       t.column :name, :string       t.column :type, :string     end   end

  def self.down     drop_table :publications   end end

These are the current values in the table: 1, 'A Tale of Two Cities', 'Book' 2, '1984', 'Book'

And the models...

app/models/publication.rb: class Publication < ActiveRecord::Base end

Now, Book would be a model that is generated at runtime (i.e., it is not found in a file). While it wouldn't necessarily make sense for this sort of setup, it does in a plugin like acts_as_versioned that generates a, for example, Publication::Version class at runtime.

So this is what I'm running in the console:

Loading development environment.

Object.const_set('Book', Class.new(Publication))

=> Book

Book.find(:all)

=> [#<Book:0x5966dd8 @attributes={"name"=>"A Tale of Two Cities", "type"=>"Book", "id"=>"1"}>, #<Book:0x5965898 @attributes={"name"=>"A Tale of Three Cities", "type"=>"Book", "id"=>"2"}>]

Book.find(:all)

=> [#<Book:0x5961ff4 @attributes={"name"=>"A Tale of Two Cities", "type"=>"Book", "id"=>"1"}>, #<Book:0x5960ab4 @attributes={"name"=>"A Tale of Three Cities", "type"=>"Book", "id"=>"2"}>]

This produces the right results, however a log from the dependencies shows the following:

Book Columns (0.000000) SHOW FIELDS FROM publications   Book Load (0.016000) SELECT * FROM publications WHERE ( (publications.`type` = 'Book' ) ) Dependencies: called require_or_load("/book", nil) Dependencies: loading /book Dependencies: called load_file("/book.rb", ) Dependencies: called new_constants_in() Dependencies: called require_or_load("/book", nil) Dependencies: loading /book Dependencies: called load_file("/book.rb", ) Dependencies: called new_constants_in()   Book Load (0.016000) SELECT * FROM publications WHERE ( (publications.`type` = 'Book' ) ) Dependencies: called require_or_load("/book", nil) Dependencies: loading /book Dependencies: called load_file("/book.rb", ) Dependencies: called new_constants_in() Dependencies: called require_or_load("/book", nil) Dependencies: loading /book Dependencies: called load_file("/book.rb", ) Dependencies: called new_constants_in()

As a result of how things are currently working, it appears as though it will try to look for a book file every single time an instance of Book is instantiated, regardless of whether it's looked for it before. This seems to be what is causing my performance issue.

My question is: Is this behavior expected? That is, should ActiveRecord classes that are generated at runtime be causing Dependencies to constantly look for the file?

A first crack at the issue shows that require_association_class in associations.rb is calling require_association, which in turn will cause the Dependencies to look for a file named 'book'. This is regardless of whether the constant already exists. I'm tempted to add a conditional that only calls require_association if the constant does not already exist (or even going deeper and having associate_with check if the constant exists). However, I haven't yet looked into the issues this could cause with other parts of ActiveRecord.

Thanks to anyone who might have insight into the issue.

Maybe this is too simple an answer, but have you checked what environment the console is running in? The startup message indicates that it’s in development, where automatic reloading of classes is a feature, not a bug…

Indeed, I am running in development :wink: However, this is run from the console and automatic reloading only occurs when the server is reset (i.e. on page load). In this case, Rails is trying to load the (nonexistent) file every single time the class is instantiated. So, when I have 150 Book records instantiated in Development, Rails will try looking through the $LOAD_PATH for book.rb every time an instance of Book is created (even though the constant already exists).

Granted, after the server is reset, I would expect Rails to reload the Book constant, but only once not 150 times :frowning:

This is the change I've made to currently resolve the problem:

Index: activerecord/lib/active_record/associations.rb

Matt Jones wrote:

Maybe this is too simple an answer, but have you checked what environment the console is running in? The startup message indicates that it's in development, where automatic reloading of classes is a feature, not a bug..

Well I thought I'd try it in production just to see what would happen, but still having the same problem:

  Book Columns (0.000000) SHOW FIELDS FROM publications   Book Load (0.015000) SELECT * FROM publications WHERE ( (publications.`type` = 'Book' ) ) Dependencies: called require_or_load("book", nil) Dependencies: requiring book Dependencies: called new_constants_in(Object) Dependencies: New constants: Dependencies: Error during loading, removing partially loaded constants

Dependencies: called require_or_load("book", nil) Dependencies: called clear()   Book Load (0.000000) SELECT * FROM publications WHERE ( (publications.`type` = 'Book' ) ) Dependencies: called require_or_load("book", nil) Dependencies: requiring book Dependencies: called new_constants_in(Object) Dependencies: New constants: Dependencies: Error during loading, removing partially loaded constants

Dependencies: called require_or_load("book", nil)

You'll notice that it attempts to load the file again after a reload (see ...called clear()). Now the error I'm getting is the fault of require_association. Since this is always called when an ActiveRecord class instantiated, it will cause the following error:

no such file to load -- book ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:492 :in `require' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:492 :in `require' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:337 :in `new_constants_in' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:492 :in `require' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:105 :in `require_or_load' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:61: in `depend_on' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:67: in `associate_with' ./script/../config/../config/../vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:440 :in `require_association' ...

Note though that you'll never see that exception because Dependencies.new_constants_in eats it up. Again, the patch I posted before fixes the problem, but I'm not certain this is the real root of the problem.