
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 iluvrailsThe 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.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.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 funReturn 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.
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.
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, ARGVRails 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
endWhat's most interesting for us here is that it:
Rails: :Server Which is a subclass of Rack: :ServerAPP_PATH, which points to our . /config/application.rbquires rails.application.root#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.
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
endFirst 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
endThis 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
endOk, 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
}
endTL; DR: Rake is told to charge . /config.ru
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.applicationOk, 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
endIt'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} .rbload_config_initializers Who is charging . /config/initializers/ *.rbThe 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:
APP_PATH To . /config/application.rbENV ['BUNDLE_GEMFILE'] To . /GemfileRails: :Server (subclass of Rack: :Server)enforcement Which is a subclass of Rails: :Application.Rails: :Server initialized previously.We love Rails for all the magic it does for us, but it's best to understand how that magic works.