Zeitwerk integration in Rails 6 (Beta 2)
Posted by fxn, February 22, 2019 @ 8:59 am in Releases
The second beta of Rails 6 is about to ship, and includes Zeitwerk integration.
While Rails 6 final is going to have proper documentation about it, this post will help understanding this feature meanwhile.
Highlights
-
You can now robustly use constant paths in class and module definitions:
# Autoloading in this class' body matches Ruby semantics now. class Admin::UsersController < ApplicationController # ... end
-
All known use cases of
require_dependency
have been eliminated. -
Edge cases in which the autoloaded constant depended on execution order are gone.
-
Autoloading is thread-safe in general, not just for the currently supported use cases with explicit locks like web requests. For example, you can now write multi-threaded scripts to be run by
bin/rails runner
and they will autoload just fine.
Also, applications get some performance benefits for the same price:
-
Autoloading a constant no longer involves walking autoload paths looking for a relative match in the file system. Zeitwerk does one single pass and autoloads everything using absolute file names. In addition, that single pass descends into subdirectories lazily, only when the namespaces are used.
-
When an application is reloaded due to changes in the file system, code in the autoload paths of engines that were loaded as gems won’t be reloaded anymore.
-
Eager loading eager loads not only the application, but also the code of any gem that is managed by Zeitwerk.
Autoloading modes
Rails 6 ships with two autoloading modes: :zeitwerk
and :classic
. They are set using the new configuration point config.autoloader
.
Zeitwerk mode is the default in Rails 6 for CRuby, automatically enabled by
load_defaults "6.0"
in config/application.rb
.
Applications can opt-out putting
config.autoloader = :classic
after the line that loads the defaults.
State of the API
While a first API for Zeitwerk mode is converging, this is still a bit exploratory right now. Please, check the current documentation if your version of Rails is newer than 6.0.0.beta2
.
Autoload paths
The configuration point for autoload paths remains config.autoload_paths
, and if you push by hand to ActiveSupport::Dependencies.autoload_paths
during application initialization, that will also work.
require_dependency
All known use cases of require_dependency
have been eliminated. In principle, you should just delete all these calls in the code base. See also the next section about STIs.
STIs
Active Record needs to have STI hierarchies fully loaded in order to generate correct SQL. Preloading in Zeitwerk was designed for this use case:
# config/initializers/preload_vehicle_sti.rb
autoloader = Rails.autoloaders.main
sti_leaves = %w(car motorbike truck)
sti_leaves.each do |leaf|
autoloader.preload("#{Rails.root}/app/models/#{leaf}.rb")
end
By preloading the leaves of the tree, autoloading will take care of the entire hierarchy upwards following superclasses.
These files are going to be preloaded on boot, and on each reload.
Rails.autoloaders
In Zeitwerk mode, Rails.autoloaders
is an enumerable that has two Zeitwerk instances called main
, and once
. The former is the one managing your application, and the latter manages engines loaded as gems, as well as anything in the somewhat unknown config.autoload_once_paths
(whose future is not bright). Rails reloads with main
, and once
is there just for autoloading and eager loading, but does not reload.
Those instances are accessible respectively as
Rails.autoloaders.main
Rails.autoloaders.once
but since Rails.autoloaders
is an enumerable, there won’t be too many use cases for direct access, probably.
Inspecting autoloaders activity
If you want to see the autoloaders working, you can throw
Rails.autoloaders.logger = method(:puts)
to config/application.rb
after the framework defaults have been set. In addition to a callable, Rails.autoloaders.logger=
accepts also anything that responds to debug
with arity 1, like regular loggers do.
If you want to see the activity of Zeitwerk for all instances in memory (both Rails’ and others that might be managing gems), you can set
Zeitwerk::Loader.default_logger = method(:puts)
at the top of config/application.rb
, before Bundle.require
.
Backwards incompatibility
-
For files below the standard
concerns
directories (likeapp/models/concerns
),Concerns
cannot be a namespace. That is,app/models/concerns/geolocatable.rb
is expected to defineGeolocatable
, notConcerns::Geolocatable
. -
Once the application has booted, autoload paths are frozen.
-
Directories in
ActiveSupport::Dependencies.autoload_paths
that do not exist on boot are ignored. We are referring here to the actual elements of the array only, not to their subdirectories. New subdirectories of autoload paths that existed at boot are picked up just fine as always. (This may change for final.) -
A file that defines a class or module that acts as a namespace, needs to define the class or module using the
class
andmodule
keywords. For example, if you haveapp/models/hotel.rb
defining theHotel
class, andapp/models/hotel/pricing.rb
defining a mixin for hotels, theHotel
class must be defined withclass
, you cannot doHotel = Class.new { ... }
orHotel = Struct.new { ... }
or anything like that. Those idioms are fine in classes and modules that do not act as namespaces. -
A file should define only one constant in its namespace (but can define inner ones). So, if
app/models/foo.rb
definesFoo
and alsoBar
,Bar
won’t be reloaded, but reopened whenFoo
is reloaded. This is strongly discouraged in classic mode anyway though, the convention is to have one single main constant matching the file name. You can have inner constants, soFoo
may define an auxiliary internal classFoo::Woo
.