Merge pull request #37037 from Sean0628/update-initialization-page

Update The Rails Initialization Process page[ci skip]
This commit is contained in:
Rafael Mendonça França 2019-12-17 19:51:14 -03:00
commit b67785a476
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
1 changed files with 276 additions and 242 deletions

View File

@ -133,7 +133,7 @@ Once `config/boot.rb` has finished, the next file that is required is
`ARGV` array simply contains `server` which will be passed over:
```ruby
require_relative "command"
require "rails/command"
aliases = {
"g" => "generate",
@ -166,17 +166,27 @@ As shown, `Rails::Command` displays the help output automatically if the `namesp
is empty.
```ruby
module Rails::Command
class << self
def invoke(namespace, args = [], **config)
namespace = namespace.to_s
namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace)
namespace = "version" if %w( -v --version ).include? namespace
module Rails
module Command
class << self
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s
if command = find_by_namespace(namespace)
command.perform(namespace, args, config)
else
find_by_namespace("rake").perform(namespace, args, config)
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
command_name = namespace
end
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
command = find_by_namespace(namespace, command_name)
if command && command.all_commands[command_name]
command.perform(command_name, args, config)
else
find_by_namespace("rake").perform(full_namespace, args, config)
end
end
end
end
@ -190,14 +200,23 @@ module Rails
module Command
class ServerCommand < Base # :nodoc:
def perform
extract_environment_option_from_argument
set_application_directory!
prepare_restart
Rails::Server.new.tap do |server|
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)
server.start
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
end
@ -221,9 +240,14 @@ The `Rails::Server` class is defined in this file by inheriting from
method in `rails/commands/server/server_command.rb`:
```ruby
def initialize(*)
super
set_environment
module Rails
class Server < ::Rack::Server
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
end
end
```
@ -233,97 +257,79 @@ Firstly, `super` is called which calls the `initialize` method on `Rack::Server`
`Rack::Server` is responsible for providing a common server interface for all Rack-based applications, which Rails is now a part of.
The `initialize` method in `Rack::Server` simply sets a couple of variables:
The `initialize` method in `Rack::Server` simply sets several variables:
```ruby
def initialize(options = nil)
@options = options
@app = options[:app] if options && options[:app]
module Rack
class Server
def initialize(options = nil)
@ignore_options = []
if options
@use_default_options = false
@options = options
@app = options[:app] if options[:app]
else
argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
@use_default_options = true
@options = parse_options(argv)
end
end
end
end
```
In this case, `options` will be `nil` so nothing happens in this method.
In this case, return value of `Rails::Command::ServerCommand#server_options` will be assigned to `options`.
When lines inside if statement is evaluated, a couple of instance variables will be set.
`server_options` method in `Rails::Command::ServerCommand` is defined as follows:
```ruby
module Rails
module Command
class ServerCommand
no_commands do
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
end
end
end
end
```
The value will be assigned to instance variable `@options`.
After `super` has finished in `Rack::Server`, we jump back to
`rails/commands/server/server_command.rb`. At this point, `set_environment`
is called within the context of the `Rails::Server` object and this method
doesn't appear to do much at first glance:
is called within the context of the `Rails::Server` object.
```ruby
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
module Rails
module Server
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
end
end
```
In fact, the `options` method here does quite a lot. This method is defined in `Rack::Server` like this:
```ruby
def options
@options ||= parse_options(ARGV)
end
```
Then `parse_options` is defined like this:
```ruby
def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
args.clear if ENV.include?("REQUEST_METHOD")
options.merge! opt_parser.parse!(args)
options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
options
end
```
With the `default_options` set to this:
```ruby
def default_options
super.merge(
Port: ENV.fetch("PORT", 3000).to_i,
Host: ENV.fetch("HOST", "localhost").dup,
DoNotReverseLookup: true,
environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup,
daemonize: false,
caching: nil,
pid: ENV.fetch("PIDFILE", Options::DEFAULT_PIDFILE).dup,
restart_cmd: restart_command)
end
```
There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`:
```ruby
def opt_parser
Options.new
end
```
The class **is** defined in `Rack::Server`, but is overwritten in
`Rails::Server` to take different arguments. Its `parse!` method looks
like this:
```ruby
def parse!(args)
args, options = args.dup, {}
option_parser(options).parse! args
options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
options[:server] = args.shift
options
end
```
This method will set up keys for the `options` which Rails will then be
able to use to determine how its server should run. After `initialize`
has finished, we jump back into the server command where `APP_PATH` (which was
set earlier) is required.
After `initialize` has finished, we jump back into the server command
where `APP_PATH` (which was set earlier) is required.
### `config/application`
@ -337,92 +343,103 @@ After `config/application` is loaded, `server.start` is called. This method is
defined like this:
```ruby
def start
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
log_to_stdout if options[:log_stdout]
module Rails
class Server < ::Rack::Server
def start(after_stop_callback = nil)
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
log_to_stdout if options[:log_stdout]
super
...
super()
...
>>>>>>> c5102225a36d254dc067c6d5a606856233d46e99
end
private
def setup_dev_caching
if options[:environment] == "development"
Rails::DevCaching.enable_by_argument(options[:caching])
end
end
def create_tmp_directories
%w(cache pids sockets).each do |dir_to_make|
FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make))
end
end
def log_to_stdout
wrapped_app # touch the app so the logger is set up
console = ActiveSupport::Logger.new(STDOUT)
console.formatter = Rails.logger.formatter
console.level = Rails.logger.level
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
end
end
end
private
def setup_dev_caching
if options[:environment] == "development"
Rails::DevCaching.enable_by_argument(options[:caching])
end
end
def create_tmp_directories
%w(cache pids sockets).each do |dir_to_make|
FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
end
end
def log_to_stdout
wrapped_app # touch the app so the logger is set up
console = ActiveSupport::Logger.new(STDOUT)
console.formatter = Rails.logger.formatter
console.level = Rails.logger.level
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
end
```
This is where the first output of the Rails initialization happens. This method
creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the
process. As we can see from the code here, it will create the `tmp/cache`,
This method creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the process.
As we can see from the code here, it will create the `tmp/cache`,
`tmp/pids`, and `tmp/sockets` directories. It then enables caching in development
if `rails server` is called with `--dev-caching`. Finally, it calls `wrapped_app` which is
responsible for creating the Rack app, before creating and assigning an instance
of `ActiveSupport::Logger`.
The `super` method will call `Rack::Server.start` which begins its definition like this:
The `super` method will call `Rack::Server.start` which begins its definition as follows:
```ruby
def start &blk
if options[:warn]
$-w = true
end
module Rack
class Server
def start &blk
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
if library = options[:require]
require library
end
if library = options[:require]
require library
end
if options[:debug]
$DEBUG = true
p options[:server]
pp wrapped_app
pp app
end
if options[:debug]
$DEBUG = true
require 'pp'
p options[:server]
pp wrapped_app
pp app
end
check_pid! if options[:pid]
check_pid! if options[:pid]
# Touch the wrapped app, so that the config.ru is loaded before
# daemonization (i.e. before chdir, etc).
wrapped_app
# Touch the wrapped app, so that the config.ru is loaded before
# daemonization (i.e. before chdir, etc).
handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
wrapped_app
end
daemonize_app if options[:daemonize]
daemonize_app if options[:daemonize]
write_pid if options[:pid]
write_pid if options[:pid]
trap(:INT) do
if server.respond_to?(:shutdown)
server.shutdown
else
exit
trap(:INT) do
if server.respond_to?(:shutdown)
server.shutdown
else
exit
end
end
server.run wrapped_app, options, &blk
end
end
server.run wrapped_app, options, &blk
end
```
@ -431,30 +448,42 @@ we're going to explore more (even though it was executed before, and
thus memoized by now).
```ruby
@wrapped_app ||= build_app app
module Rack
class Server
def wrapped_app
@wrapped_app ||= build_app app
end
end
end
```
The `app` method here is defined like so:
```ruby
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
...
private
def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
module Rack
class Server
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
...
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
self.options.merge! options
app
end
private
def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
@options.merge!(options) { |key, old, new| old }
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
end
```
The `options[:config]` value defaults to `config.ru` which contains this:
@ -463,24 +492,35 @@ The `options[:config]` value defaults to `config.ru` which contains this:
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run <%= app_const %>
run Rails.application
```
The `Rack::Builder.parse_file` method here takes the content from this `config.ru` file and parses it using this code:
```ruby
app = new_from_string cfgfile, config
module Rack
class Builder
def self.load_file(path, opts = Server::Options.new)
...
app = new_from_string cfgfile, config
...
end
...
...
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
end
end
```
The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`. This is where the majority of the initialization process of Rails happens. The `require` line for `config/environment.rb` in `config.ru` is the first to run:
The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`.
This is where the majority of the initialization process of Rails happens.
The `require` line for `config/environment.rb` in `config.ru` is the first to run:
```ruby
require_relative 'config/environment'
@ -567,7 +607,7 @@ defined in `rails/application.rb`.
The `initialize!` method looks like this:
```ruby
def initialize!(group=:default) #:nodoc:
def initialize!(group = :default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
@ -579,7 +619,7 @@ As you can see, you can only initialize an app once. The initializers are run th
the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`:
```ruby
def run_initializers(group=:default, *args)
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)
@ -608,42 +648,53 @@ After this is done we go back to `Rack::Server`.
Last time we left when the `app` method was being defined:
```ruby
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
...
private
def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
module Rack
class Server
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
...
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
self.options.merge! options
app
end
private
def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
@options.merge!(options) { |key, old, new| old }
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
end
```
At this point `app` is the Rails app itself (a middleware), and what
happens next is Rack will call all the provided middlewares:
```ruby
def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass = middleware.shift
app = klass.new(app, *middleware)
module Rack
class Server
private
def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass, *args = middleware
app = klass.new(app, *args)
end
app
end
end
app
end
```
Remember, `build_app` was called (by `wrapped_app`) in the last line of `Server#start`.
Remember, `build_app` was called (by `wrapped_app`) in the last line of `Rack::Server#start`.
Here's how it looked like when we left:
```ruby
@ -655,46 +706,29 @@ server you're using. For example, if you were using Puma, here's what
the `run` method would look like:
```ruby
...
DEFAULT_OPTIONS = {
:Host => '0.0.0.0',
:Port => 8080,
:Threads => '0:16',
:Verbose => false
}
module Rack
module Handler
module Puma
...
def self.run(app, options = {})
conf = self.config(app, options)
def self.run(app, options = {})
options = DEFAULT_OPTIONS.merge(options)
events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio
if options[:Verbose]
app = Rack::CommonLogger.new(app, STDOUT)
launcher = ::Puma::Launcher.new(conf, :events => events)
yield launcher if block_given?
begin
launcher.run
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
launcher.stop
puts "* Goodbye!"
end
end
...
end
end
if options[:environment]
ENV['RACK_ENV'] = options[:environment].to_s
end
server = ::Puma::Server.new(app)
min, max = options[:Threads].split(':', 2)
puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
puts "* Min threads: #{min}, max threads: #{max}"
puts "* Environment: #{ENV['RACK_ENV']}"
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
server.add_tcp_listener options[:Host], options[:Port]
server.min_threads = min
server.max_threads = max
yield server if block_given?
begin
server.run.join
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
server.stop(true)
puts "* Goodbye!"
end
end
```