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.