Related articles

No items found.
The French newsletter for Ruby on Rails developers. Find similar content for free every month in your inbox!
Register
Share:
Blog
>

Rails 6 start sequence

Have you ever wondered how your Rails application gets started? I mean, when you run the Rails Server, what is going on?

To answer this question, we'll start by building a new Rails 6 application (I'm currently using version 6.0.0.rc1).

$ rails new iluvrails
$ cd iluvrails

. /bin/rails

The starting point for the start sequence is the rails executable. To make this blog simple, we'll start our journey in . /bin/rails. By the way, what is the rails gem? It is a packaging for all of 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 system is Railties.

Quick reminder:

  • Rails: :Railtie is the core of the Rails framework. It provides a set of hooks (like after_initialize, add_routing_paths, or set_load_path) to extend Rails and/or change the initialization process.
  • A railtie is a subclass of Rails: :Railtie that will extend Rails. It uses hooks provided by Railties to connect to Rails. In other words, it is not Rails that knows the other components in advance and requires them, but rather the components that each implement a railtie and that integrate with Rails, letting Rails know that they are there.
  • An engine is a railtie with some initializers already defined.
  • Rails: :Application is a motor.

If you want to know more, there is no better way than to read the source code:

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

Return to . /bin/rails. What is inside?

#!/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 not within the scope of this blog, so we will ignore it. In case you don't know what it is:

Spring is a Rails application preloader. It accelerates development by keeping your application in the background, so you don't have to start it every time you run a test, rake, or migration.
APP_PATH = File.expand_path('../config/application', __dir__)

This makes it possible to find the absolute path to . /config/application.rb Who defines Iluvrails: :Application (who inherits Rails: :Application) .that's when we start connecting the dots: your Rails application is a railtie. It doesn't just include Rails: it plugs into Rails itself. The last two instructions from . /bin/rails are:

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

We are going to look at each 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 define ENV ['BUNDLE_GEMFILE'] (if not already set) to the absolute path of our Gemfile. This is so that bundler can load the required gems later. Then we need bundler/setup and bootsnap/setup.bundler/setup checks your version of Ruby, the platform you are using, makes sure that your Gemfile and Gemfile.lock match, etc. Bootsnap is a tool that helps speed up the startup time of an application through caching operations. As for Spring, it's outside the scope of this blog, so I'm skipping that part.

rails/commands

At this point, Rails will execute the command you asked for: server. Here's a look at how that happens.

# 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 is going to ask rails/command then launch Rails: :command.invoke command, ARGV Who 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:

  • create a new instance of Rails: :Server Which is a subclass of Rack: :Server
  • requires APP_PATH, which points to our . /config/application.rbquires
  • Change the current directory to rails.application.root
  • Then call #start on the instance of Rails: :Server.
Rails 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 of web servers, web frameworks, and intermediary software (called middleware) into a single method call.

At this point, if everything went well, the application will start and you can access it from your web browser. That's all. Thanks for reading! Hmm... not so fast. Didn't we read a statement APP_PATH mandatory? Well, let's see what's going on here.

. /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 of all, there is require_relative 'boot'. We already requested this file so nothing is happening at this point. Then we ask '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 the railtie of each Rails component. If you don't need all of these components, you can lighten your application by removing the require “I want everything” statement and manually requesting only the ones you need. You will 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 have required the rails components, it is time for the gems in your application to be required using bundler:

Bundler.require(*Rails.groups)

Next, 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

Ok, so we've defined a railtie, but there's still one piece missing in the puzzle. When initializers and . /config/environments/ # {rails.env} .rb are they loaded? Back to Rails: :Command: :serverCommand.perform, we see that Rails: :Server is initialized in the following way:Rails: :server.new (server_options) and when we search 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 charge . /config.ru

. /config.ru

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

Ok, let's follow this lead. We charge firstconfig/environment.rb:

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

So after asking . /config/application.rb (which is already needed at this stage), #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

It's a big piece that deserves a blog post all by itself. Without going into too much detail, let's remember that Rails is made up of a lot of hooks. Some of these are related to initialization. During #run_initializers will be executed among other hooks:

  • load_environment_config Who is charging . /config/environments/ # {rails.env} .rb
  • load_config_initializers Who is charging . /config/initializers/ *.rb

Closing note

The source code of Rails: :Application Give us a quick reminder of how the startup process went:

1) requires “config/boot.rb” to configure load paths.
2) require railties and engines
3) Define Rails.application as “class MyApp: :Application < Rails: :Application”.
4) Execute config.before_configuration callbacks
5) Loading config/environments/env.rb
6) Execute config.before_initialize callbacks
7) Run Railtie #initializer defined by railties, engines, and application. One by one, each engine configures its load paths, routes, and executes its config/initializers/* files.
8) Custom Railtie# initializers added by railroads, engines, and applications are executed.
9) Building the middleware stack and running to_prepare callbacks.
10) Run config.before_eager_load and eager_load! if eager_load is true.
11) Executing config.after_initializeSaid callbacks differently:
  • Defined APP_PATH To . /config/application.rb
  • Defined ENV ['BUNDLE_GEMFILE'] To . /Gemfile
  • Bundler setup (no need for gems at this time)
  • Initializing a new Rails: :Server (subclass of Rack: :Server)
  • Requires all Rails components (ActiveRecord, ActionPack, etc.)
  • Requires all the gems in your Gemfile
  • Define a enforcement Which is a subclass of Rails: :Application.
  • Change the directory at the root of your Rails application.
  • Start it Rails: :Server initialized previously.
  • Execute 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 best to understand how that magic works.