From c5102225a36d254dc067c6d5a606856233d46e99 Mon Sep 17 00:00:00 2001 From: Sho Ito Date: Sun, 25 Aug 2019 21:55:50 +0900 Subject: [PATCH] update initialization.md --- guides/source/initialization.md | 524 +++++++++++++++++--------------- 1 file changed, 275 insertions(+), 249 deletions(-) diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 556c85cc0f..74c050fc22 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -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,99 +343,102 @@ After `config/application` is loaded, `server.start` is called. This method is defined like this: ```ruby -def start - print_boot_information - 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() + ... + 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 print_boot_information - ... - puts "=> Run `rails server -h` for more startup options" - 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 setup_dev_caching - if options[:environment] == "development" - Rails::DevCaching.enable_by_argument(options[:caching]) - 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 - require 'pp' - 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 ``` @@ -438,30 +447,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: @@ -470,24 +491,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' @@ -574,7 +606,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 @@ -586,7 +618,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) @@ -615,42 +647,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 @@ -662,46 +705,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 ```