Search
⌘K
    to navigateEnterto select Escto close

    Loading behavior of Ruby on Rails in depth

    Why Rails does not have many import statements?

    In a normal Ruby program, dependencies need to be loaded by hand.

    For example, the following controller uses classes ApplicationController and Post, and normally you'd need to put require calls for them:

    1# DO NOT DO THIS.
    2require "application_controller"
    3require "post"
    4# DO NOT DO THIS.
    5
    6class PostsController < ApplicationController
    7  def index
    8    @posts = Post.all
    9  end
    10end

    But in a Rails application, we can write the above controller as follows and it will still work:

    1class PostsController < ApplicationController
    2  def index
    3    @posts = Post.all
    4  end
    5end

    To understand more about why we don't need to explicitly import modules into Rails, we need to learn more about the concept of "Autoloading".

    What is autoloading?

    Autoloading means we don't ever have to write require statements, or worry too much about load paths.

    Our classes are accessible from anywhere, so we can refer to them directly and they'll appear, as if by magic.

    There was a "classic" mode for Autoloading and related features, which was previously used by Rails.

    But due to it's limitations, as of Rails 6, the default mode is the modern Zeitwerk mode.

    Issues with classic mode

    The classic loader in Rails is fundamentally limited because it is based on a const_missing method and that callback does not get the nested or qualified constants.

    This mode of autoloading also has issues in test environment if the tests involve multithreading.

    When Module#const_missing is invoked, it doesn’t provide enough information to reliably determine which constant should have been returned.

    This means that autoloading, too, will sometimes return the wrong value for a constant reference.

    Conventions that help with autoloading

    In a Rails application file names have to match the constants they define, with directories acting as namespaces.

    For example, the file app/helpers/users_helper.rb should define UsersHelper and the file app/controllers/admin/payments_controller.rb should define Admin::PaymentsController.

    By default, Rails configures Zeitwerk to inflect file names with String#camelize method.

    For example, it expects that app/controllers/users_controller.rb defines the constant UsersController because:

    1"users_controller".camelize # => UsersController

    Another example is where we specify the routes and it maps automatically to the corresponding controller.

    Consider the following route from granite application:

    1resources :tasks

    So from Rails conventions perspective it expects a file named tasks_controller.rb to be available within the app/controllers directory and it should be defining the TasksController class.

    In the above example the convention that Rails follows is that, it takes the route tasks and suffixes with a _controller.rb and checks for such a controller file.

    So if we update the routes name like so, then our server will fail:

    1resources :tasks2

    The reason for failure is that the route expects a tasks2_controller.rb file, but we haven't created that.

    Without using convention, in order to fix the current issue, we can explicitly specify the expected controller name by passing it as an option to the resources. But that is not recommended way to fix this issue. We should always try to follow the Rails conventions.

    Autoload paths

    We refer to the list of application directories whose contents are to be autoloaded as autoload paths. For example, app/models. Such directories represent the root namespace called Object.

    Within an autoload path, file names must match the constants they define as we had discussed in previous section.

    By default, the autoload paths of an application consist of all the subdirectories of an app that exist when the application boots, plus the autoload paths of engines it might depend on.

    Some directories like assets, javascript etc, are excluded by default.

    For example, if UsersHelper is implemented in app/helpers/users_helper.rb, the module is autoloadable. Which means we do not need (and should not write) a require call for it.

    Autoload paths automatically pick any custom directories under app. For example, if your application has app/presenters, or app/services, etc, they are added to autoload paths.

    Concept of reloading

    Rails automatically reloads classes and modules if application files change.

    More precisely, if the web server is running and application files have been modified, Rails unloads all autoloaded constants just before the next request is processed.

    That way, application classes or modules used during that request are going to be autoloaded, thus picking up their current implementation in the file system.

    There are cases like when using a Rails console and wanting to load file changes. In such cases we could use the reload! method and run it in the console.

    Eager loading

    Autoloading is not thread-safe and hence we need to make sure that all constants are loaded when application boots.

    The concept of loading all the constants even before they are actually needed is called "Eager loading".

    In a way it is opposite of "Autoloading". In the case of "Autoloading" the application does not load the constant until it is needed.

    Once a class is needed and it is missing then the application starts looking in "autoloading paths" to load the missing class.

    eager_load_paths contains a list of directories. When application boots in production then the application loads all constants found in all directories listed in eager_load_paths.

    Rails doesn't autoload in production environment.

    In production environment Rails will load all the constants from eager_load_paths but if a constant is missing then it will not look in autoload_paths and will not attempt to load the missing constant.

    Inclusion of custom files

    We cannot depend on autoloading in all scenarios. There are cases where we'd have to load custom directory or files in reference to a particular file being worked on.

    An prime example of this case is adding the test/support directory where we add all the support files for the test suite.

    What we can do in these cases is to manually require the file in the necessary files and then include the corresponding module or use the methods defined within it.

    In case of test support files we usually add the following line in our test_helper.rb file:

    1Dir[Rails.root.join("test/support/**/*.rb")].each { |f| require f }

    The above code will automatically require all support files. But this doesn't fully do what autoloading does since we still have to include the corresponding modules from the required files if we have to utilise their functionality.

    Viewing the autoloaded modules

    We had already mentioned that there is a classic and Zeitwerk mode from autoloading files in Rails.

    Rails 6 and above uses Zeitwerk mode by default. But still if you want to confirm, then you can check whether the following command outputs true:

    1Rails.autoloaders.zeitwerk_enabled?

    Another case might be to view which all modules are getting autoloaded by Rails. In order to check that we can run the following from the Rails console:

    1Rails.autoloaders.main

    In the above command's output we can see that there is a method called root_dirs which is shown in the output.

    If we execute that method, then we can see all the directories which are getting autoloaded:

    1irb(main):002:0> Rails.autoloaders.main.root_dirs

    And gives an output similar to the following one:

    1{
    2 "granite/app/controllers"=>Object,
    3 "granite/app/controllers/concerns"=>Object,
    4 "granite/app/jobs"=>Object,
    5 "granite/app/mailers"=>Object,
    6 "granite/app/models"=>Object,
    7 "granite/app/models/concerns"=>Object,
    8 "granite/app/policies"=>Object,
    9 "granite/app/services"=>Object,
    10 "granite/app/workers"=>Object,
    11 ...
    12}

    Inbuilt Ruby autoload mechanism

    Ruby has a built-in autoload mechanism. This lets us tell Ruby in advance which file will define a particular constant, without going to the expense of loading that file immediately. Only when we refer to that constant for the first time does Ruby actually load the specified file.

    Let's try out an example. Create a file called autoload_test.rb in the root of your project and add the following content to it:

    1CONSTANT_VARIABLE = "Accessed variable after autoloading!"

    Now open a Rails console from the root of the project and run the following:

    1autoload :CONSTANT_VARIABLE, './autoload_test'

    Now executing the following would print the value of the constant:

    1puts CONSTANT_VARIABLE

    The really important difference between this and Module#const_missing is that we can tell Ruby which file defines which constant before the constant is used, and this information is taken into account during normal constant resolution.

    This potentially eliminates both of the key failures of the #const_missing approach, which we had already discussed.

    Rails in depth

    If you wish to dive deep into how Rails, you can refer to the following videos:

    Rails has evolved since these videos were made but the concepts mentioned in these videos still hold true.

    Previous
    Next