Rails 6 boot sequence

Younes Serraj
Younes SerrajJun 4, 2019

Have you ever wondered how your Rails application boots? I mean, when you execute rails server, what happens?

Rails 6 boot sequence


To answer this question, we’re going to start by generating a new Rails 6 application (I’m currently running 6.0.0.rc1).

$ rails new iluvrails
$ cd iluvrails


./bin/rails

The starting point of the boot sequence is the rails executable. To simplify this blog, we’ll start our journey in ./bin/rails.

By the way, what is the rails gem? It’s a packaging for all the following:

$ gem dependency rails -v 6.0.0.rc1
Gem rails-6.0.0.rc1
  actioncable (= 6.0.0.rc1)
  actionmailbox (= 6.0.0.rc1)
  actionmailer (= 6.0.0.rc1)
  actionpack (= 6.0.0.rc1)
  actiontext (= 6.0.0.rc1)
  actionview (= 6.0.0.rc1)
  activejob (= 6.0.0.rc1)
  activemodel (= 6.0.0.rc1)
  activerecord (= 6.0.0.rc1)
  activestorage (= 6.0.0.rc1)
  activesupport (= 6.0.0.rc1)
  bundler (>= 1.3.0)
  railties (= 6.0.0.rc1)
  sprockets-rails (>= 2.0.0)

The core of this is railties.

Quick reminder:

  • Rails::Railtie is the core of the Rails framework. It provides a set of hooks (such as after_initializeadd_routing_paths or set_load_path) to extend Rails and/or modify the initialization process.

  • A railtie is a subclass of Rails::Railtie that's going to extend Rails. It uses the hooks provided by Railties to plug itself to Rails. Said differently, it's not Rails that knows of other components beforehand and requires them but rather the components that each implement a railtie and include themselves into Rails, letting Rails know that they're here.

  • An engine is a railtie with some initializers already set.

  • Rails::Application is an engine.

If you want to learn more about this, there’s no better way than to read the source code:

$ cd `bundle show railties`
$ ls
$ # have fun

Back to ./bin/rails. What's in it?


#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'


The part about Spring is out of the scope of this blog so we’re just going to skip it. In case you don’t know what it is:

Spring is a Rails application preloader. It speeds up development by keeping your application running in the background so you don’t need to boot it every time you run a test, rake task or migration.

APP_PATH = File.expand_path('../config/application', __dir__)

This finds the absolute path to ./config/application.rb which defines Iluvrails::Application (which inherits from Rails::Application).

This is when we start connecting the dots: your Rails application is a railtie. It doesn’t just include Rails: it plugs itself to Rails.

The last two instructions of ./bin/rails are:

require_relative '../config/boot'
require 'rails/commands'

We’re going to look at each one of them.


./config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

First, we set ENV['BUNDLE_GEMFILE'] (if not already set) to the absolute path of our Gemfile. This is for bundler to load the required gems later on. Then we require bundler/setup and bootsnap/setup.

bundler/setup checks your ruby version, which plateform you're running, ensures your Gemfile and Gemfile.lock match, etc. It basically does preliminary checks but does not require gems yet.

Bootsnap is a tool that helps speed up the boot time of an app thanks to caching operations. As for Spring, this is out of the scope of this blog so I’m skipping that part.


rails/commands

At this point, Rails is going to run the command you asked it to run: server. Here's an overview of how it goes.


# frozen_string_literal: true
require "rails/command"
aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}
command = ARGV.shift
command = aliases[command] || command
Rails::Command.invoke command, ARGV

Rails will require rails/command then run Rails::Command.invoke command, ARGV which will end up calling Rails::Command::ServerCommand.perform. Take a look at its code:

def perform
  extract_environment_option_from_argument
  set_application_directory!
  prepare_restart
  Rails::Server.new(server_options).tap do |server|
    # Require application after server sets environment to propagate
    # the --environment option.
    require APP_PATH
    Dir.chdir(Rails.application.root)
    if server.serveable?
      print_boot_information(server.server, server.served_url)
      after_stop_callback = -> { say "Exiting" unless options[:daemon] }
      server.start(after_stop_callback)
    else
      say rack_server_suggestion(using)
    end
  end
end


What’s most interesting for us here is that it:

  • creates a new instance of Rails::Server which is a subclass of Rack::Server

  • requires APP_PATH, which points to our ./config/application.rb

  • changes current directory to Rails.application.root

  • then basically calls #start on the Rails::Server instance.

Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.

At this point, if everything went well, the application boots and you can access it from your web browser.

That’s it. Thank you for reading!

Humm.. not so fast. Haven’t we read some require APP_PATH statement? Well, let's see what happens there.


./config/application.rb

require_relative 'boot'
require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Iluvrails
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

First there’s require_relative 'boot'. We've already required this file, so at this point nothing happens. Then we require 'rails/all'.


# frozen_string_literal: true
# rubocop:disable Style/RedundantBegin
require "rails"
%w(
  active_record/railtie
  active_storage/engine
  action_controller/railtie
  action_view/railtie
  action_mailer/railtie
  active_job/railtie
  action_cable/engine
  action_mailbox/engine
  action_text/engine
  rails/test_unit/railtie
  sprockets/railtie
).each do |railtie|
  begin
    require railtie
  rescue LoadError
  end
end

This requires each Rails component’s railtie. Now you know how they all get included in your application.

When you don’t need all of them, you can lighten your application by removing this I want everything require statement and manually requiring only the ones you need.

Let’s say you don’t want test_unit to be included. You would replace require 'rails/all' by something like this:

require "rails"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
require "action_mailer/railtie"
require "active_job/railtie"
require "action_cable/engine"
require "action_mailbox/engine"
require "action_text/engine"
# require "rails/test_unit/railtie"
require "sprockets/railtie"

Once you required rails components, it’s time for your application’s gems to be required using bundler:

Bundler.require(*Rails.groups)

Then we define our Application class:



module Iluvrails
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

Okay, so we defined a railtie, but there still is a missing part in the puzzle. When are initializers and ./config/environments/#{Rails.env}.rb loaded?

Back to Rails::Command::ServerCommand.perform, we see that Rails::Server is initialized in the following manner: Rails::Server.new(server_options) and when we look for server_options, we see that it is a hash with the following default values:


class_option :config, aliases: "-c", type: :string, default: "config.ru",
  desc: "Uses a custom rackup configuration.", banner: :file

# [...]

def server_options
  {
    user_supplied_options: user_supplied_options,
    server:                using,
    log_stdout:            log_to_stdout?,
    Port:                  port,
    Host:                  host,
    DoNotReverseLookup:    true,
    config:                options[:config],
    environment:           environment,
    daemonize:             options[:daemon],
    pid:                   pid,
    caching:               options[:dev_caching],
    restart_cmd:           restart_command,
    early_hints:           early_hints
  }
end

TL;DR: Rake is told to load ./config.ru


./config.ru

# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application

Okay, let’s follow this lead. We first load config/environment.rb:


# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!

So after requiring ./config/application.rb (which is already required at this point), #initialize! is called.



# Initialize the application passing the given group. By default, the
# group is :default
def initialize!(group = :default) #:nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end
def run_initializers(group = :default, *args)
  return if instance_variable_defined?(:@ran)
  initializers.tsort_each do |initializer|
    initializer.run(*args) if initializer.belongs_to?(group)
  end
  @ran = true
end

This is a big piece that would deserve a blog post on its own. Without going too much into details, let’s remember that Rails is made of many hooks. Some of them are related to initialization. During #run_initializers will be run among other hooks:

  • load_environment_config which loads ./config/environments/#{Rails.env}.rb

  • load_config_initializers which loads ./config/initializers/*.rb


Closing note

The source code of Rails::Application gives us a quick reminder of how the boot process goes:

1) require “config/boot.rb” to setup load paths
2) require railties and engines
3) Define Rails.application as “class MyApp::Application < Rails::Application”
4) Run config.before_configuration callbacks
5) Load config/environments/ENV.rb
6) Run config.before_initialize callbacks
7) Run Railtie#initializer defined by railties, engines and application. One by one, each engine sets up its load paths, routes and runs its config/initializers/* files.
8) Custom Railtie#initializers added by railties, engines and applications are executed
9) Build the middleware stack and run to_prepare callbacks
10) Run config.before_eager_load and eager_load! if eager_load is true
11) Run config.after_initialize callbacks

Said differently:

  • Set APP_PATH to ./config/application.rb

  • Set ENV['BUNDLE_GEMFILE'] to ./Gemfile

  • Setup Bundler (without requiring gems yet)

  • Initialize a new Rails::Server (subclass of Rack::Server)

  • Require all Rails components (ActiveRecord, ActionPack, etc.)

  • Require all gems from your Gemfile

  • Define an Application that is a subclass of Rails::Application

  • Change directory to the root of your Rails application

  • Start the Rails::Server initialized earlier

  • Run Rails hooks in an orderly manner (load configuration, run initializers, etc.)

  • Your server is now waiting for requests!

We love Rails for all the magic it does for us but it’s better to understand how the magic works.

Partager
Younes Serraj
Younes SerrajJun 4, 2019

Capsens' blog

Capsens is an agency specialized in the development of fintech solutions. We love startups, scrum methodology, Ruby and React.

Ruby Biscuit

The french newsletter for Ruby on Rails developers.
Get similar content for free every month in your mailbox!
Subscribe