I work on some fairly large ruby on rails applications and boot up time in development always starts to become an issue, using a preloader like zeus or spring helps but reducing the first boot time is always useful. One of the big slowdowns for booting these large apps usually turns out to be initializers touching constants which then triggers an autoload. There is a popular pattern used where the class definition and configuration are split up into two, you then end up with a file in config/initializers which will run something like: MyClass.configure, which then triggers MyClass to be defined and slows down the boot process.
It would be nice instead to be able to run the configuration code when the constant gets autoloaded, something like this:
ActiveSupport::Dependencies.on_load(‘MyClass’) do
MyClass.configure do |config|
config here
end
end
That way we still have the benefit of splitting up the class definition and the configuration, but we aren’t eagerly loading the class in the initializer, only later when it’s needed.
Autoloading constants in initializers in general is not a good idea because
the initializer only runs when the application boots.
If MyClass.configure stores anything in the class object autoloaded at boot
time, in development mode[*] when code changes all autoloaded constants are
wiped. In particular next time you hit MyClass the constant is going to
store a different class object that has no relationship with the other one.
Why is the application autoloading in initializers?
Xavier, yes, you’re right, usually config.to_prepare would be wrapped around the MyClass.configure to ensure it is run again when the constant is removed and autoloading brings in a new one.
Here’s an actual example from an application i’m working on now. We have a class which handles outages, think feature flips but for services going down. There’s a model which records the list of outages OutageSet. in an initializer we have a list of outages which are setup, so like this:
config/initializers/outages.rb
OutageSet.configure do |config|
config.outage :search, { some: ‘options’ }
end
Many gems follow this same pattern, but the we don’t run into the same issues as the constants are usually eagerly loaded. It only become a problem when the constants in our initialiazer are using autoloading. I think autoloading is GOOD here, as it reduces application boot time, but it would be nice to be able to ensure the configuration runs as soon as the constant is available rather than negating the benefits we get from autoloading.
Also, yes, the code could be re-written so that internally OutageSet knows about how to load its configuration, but I think the seperation of class and configuration is a very good pattern, which many gems already employ, it just needs a small addition to get it working well with autoloading.
I’ve been thinking about this and I am not totally convinced by the configuration use case. Let me explain.
A callback would technically allow that kind of class configuration idiom, but let’s for a moment imagine the initializer (writing on an iPad):
config/initializers/invoice_configuration.rb
ActiveSupport::Dependencies.on_autoload(‘Invoice’) do
Invoice.configure do
self.generator = MagicPDF
end
end
Looks too convoluted to me for the trivial task that configuring a class should be. Also it looks kinda unbalanced to me, there’s that framework level instruction that to me looks out of place intuitively.
Think about the maintainer, what on earth is this code? The code comment to explain that idiom would be longer than the code itself. Smellish.
Also, the maintainer would wonder, therefore this class is only configured if autoloaded? What if eager loaded? More stuff to explain.
In Rails you typically configure stuff in two ways: config files, or class level. That is, it is not idiomatic in Rails to split User like
config/initializers/user_configuration.rb
User.configure do
has_many :posts
end
Rather, you just do it at the class level
class User < ActiveRecord::Base
has_many :posts
end
which happens to play well with autoloading.
Gems do not have to deal with autoloading, they can choose other patterns, but in Rails we have this constraint, and have to choose idioms that look good and work well.