From fc3117478ae5c50c6122e9ad1cb156671c5d6889 Mon Sep 17 00:00:00 2001 From: ZhangJian Date: Sun, 3 Feb 2019 10:13:44 +0800 Subject: [PATCH 01/16] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee6e21f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Learn-Rails-by-Reading-Source-Code \ No newline at end of file From f355a59c7a810bbf8b436a8e7b8864f7a325afe0 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Sun, 3 Feb 2019 10:18:04 +0800 Subject: [PATCH 02/16] Add the first chapter --- .gitignore | 4 ++ README.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1247fbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/temp +/tmp +.DS_Store diff --git a/README.md b/README.md index ee6e21f..4e0854e 100644 --- a/README.md +++ b/README.md @@ -1 +1,132 @@ -# Learn-Rails-by-Reading-Source-Code \ No newline at end of file +# Learn-Rails-by-Reading-Source-Code + +### Before you research Rails 5 source code +1) I suggest you learn Rack [http://rack.github.io/](http://rack.github.io/) first. You need to know that an object respond to `call` method is the most important convention. + +So which is the object with `call` method in Rails? + +I will answer this question later. + +2) You need a good IDE with debugging function. I use [RubyMine](https://www.jetbrains.com/). + +### Follow the process of Rails when starting +As Rack described, `config.ru` is the entry file. +```ruby +# ./config.ru +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application # We can guess 'Rails.application' has a 'call' method. + +puts Rails.application.respond_to?(:call) # Returned 'true'. Bingo! +``` + +Let's dig deeper for `Rails.application`. +```ruby +module Rails + class << self + @application = @app_class = nil + + attr_accessor :app_class + def application # Oh, 'application' is a class method for module 'Rails'. It is not an object. + # But it returns an object which is an instance of 'app_class'. + # So it is important for us to know what class 'app_class' is. + @application ||= (app_class.instance if app_class) + end + end +end +``` + +Because `Rails.application.respond_to?(:call) # Returned 'true'.`, `app_class.instance` has a `call` method. + +When was `app_class` set? +```ruby +module Rails + class Application < Engine + class << self + def inherited(base) # This is a hooked method. + Rails.app_class = base # This line set the 'app_class'. + end + end + end +end +``` + +When `Rails::Application` is inherited like below, +```ruby +# ./config/application.rb +module YourProject + class Application < Rails::Application # Here the hooked method `inherited` defined in eigenclass of 'Rails::Application' is invoked. + end +end +``` +`YourProject::Application` will become the `Rails.app_class`. Let's replace `app_class.instance` to `YourProject::Application.instance`. + +But where is the `call` method? `call` method should be a method of `YourProject::Application.instance`. + +Then Rack can `run YourProject::Application.new` (equal to `run Rails.application`). + +The `call` method processes every request. Here it is. +```ruby +# ../gems/railties/lib/rails/engine.rb +module Rails + class Engine < Railtie + def call(env) # This method will process every request. It is invoked by Rack. So it is very important. + req = build_request env + app.call req.env + end + end +end + +# ../gems/railties/lib/rails/application.rb +module Rails + class Application < Engine + end +end + +# ./config/application.rb +module YourProject + class Application < Rails::Application + end +end + +``` + +Ancestor's chain is `YourProject::Application < Rails::Application < Rails::Engine < Rails::Railtie`. + +So `YourProject::Application.new.respond_to?(:call) # Will return 'true'`. + +But what does `app_class.instance` really do? + +`instance` is just a method name. What we really need is `app_class.new`. + +When I was reading these code +```ruby +# ../gems/railties/lib/rails/application.rb +module Rails + class Application < Engine + def instance + super.run_load_hooks! # This line confused me. + end + end +end +``` +After a deep research, I realized that this code is equal to +```ruby +def instance + a_returned_value = super # Keyword 'super' will call the ancestor's same name method: 'instance'. + a_returned_value.run_load_hooks! +end +``` + +```ruby +# ../gems/railties/lib/rails/railtie.rb +module Rails + class Railtie + def instance + @instance ||= new # 'Rails::Railtie' is the top ancestor. Now 'app_class.instance' is 'YourProject::Application.new'. + end + end +end +``` \ No newline at end of file From 2b5f4d6a6b9d60b7f2eda44a2fdbb2382a70dd82 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Tue, 26 Feb 2019 21:50:41 +0800 Subject: [PATCH 03/16] Finish version 1. --- README.md | 1526 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1495 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 4e0854e..43bb7d3 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,85 @@ # Learn-Rails-by-Reading-Source-Code -### Before you research Rails 5 source code -1) I suggest you learn Rack [http://rack.github.io/](http://rack.github.io/) first. You need to know that an object respond to `call` method is the most important convention. +## Part 0: Before you research Rails 5 source code +1) I suggest you learn Rack [http://rack.github.io/](http://rack.github.io/) first. -So which is the object with `call` method in Rails? +You need to know that an object respond to `call` method is the most important convention. -I will answer this question later. +So which is the object with `call` method in Rails App? I will answer this question in Part 1. 2) You need a good IDE with debugging function. I use [RubyMine](https://www.jetbrains.com/). -### Follow the process of Rails when starting -As Rack described, `config.ru` is the entry file. + +### What you will learn from this tutorial? +* How rails start your application? + +* How rails process every request? + +* How rails combine ActionController, ActionView and Routes? + +I should start with the command `$ rails server`. But I put this to Part 4. Because it's not interesting. + +## Part 1: Your app: an instance of YourProject::Application. +First, I will give you a piece of important code. ```ruby -# ./config.ru -# This file is used by Rack-based servers to start the application. +# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb +module Rails + module Command + class ServerCommand < Base + def perform + # ... + Rails::Server.new(server_options).tap do |server| + # APP_PATH is '/Users/your_name/your-project/config/application'. + # require APP_PATH will create the 'Rails.application' object. + # 'Rails.application' is 'YourProject::Application.new'. + # Rack server will start 'Rails.application'. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start + end + end + end + end + + class Server < ::Rack::Server + def start + #... + # 'wrapped_app' is invoked in method 'log_to_stdout'. + # It will get an well prepared app from './config.ru' file. + # It will use the app created at the 'perform' method in Rails::Command::ServerCommand. + wrapped_app -require_relative 'config/environment' + super # Will invoke ::Rack::Server#start. + end + end +end +``` +A rack server need to start with an App. The App should have a `call` method. -run Rails.application # We can guess 'Rails.application' has a 'call' method. +`config.ru` is the conventional entry file for rack app. So let's view it. +```ruby +# ./config.ru +require_relative 'config/environment' -puts Rails.application.respond_to?(:call) # Returned 'true'. Bingo! +run Rails.application # It seems that this is the app. ``` -Let's dig deeper for `Rails.application`. +Let's test it by `Rails.application.respond_to?(:call) # Returned 'true'`. + +Let's step into `Rails.application`. + ```ruby +# ./gems/railties-5.2.2/lib/rails.rb module Rails class << self @application = @app_class = nil attr_accessor :app_class - def application # Oh, 'application' is a class method for module 'Rails'. It is not an object. - # But it returns an object which is an instance of 'app_class'. - # So it is important for us to know what class 'app_class' is. + + # Oh, 'application' is a class method for module 'Rails'. It is not an object. + # But it returns an object which is an instance of 'app_class'. + # So it is important for us to know what class 'app_class' is. + def application @application ||= (app_class.instance if app_class) end end @@ -53,33 +101,65 @@ module Rails end ``` -When `Rails::Application` is inherited like below, +`Rails::Application` is inherited like below, ```ruby # ./config/application.rb module YourProject - class Application < Rails::Application # Here the hooked method `inherited` defined in eigenclass of 'Rails::Application' is invoked. + # The hooked method `inherited` defined in eigenclass of 'Rails::Application' is invoked. + class Application < Rails::Application end end ``` -`YourProject::Application` will become the `Rails.app_class`. Let's replace `app_class.instance` to `YourProject::Application.instance`. +`YourProject::Application` will become the `Rails.app_class`. -But where is the `call` method? `call` method should be a method of `YourProject::Application.instance`. +You may have a question: how we reach this file (`./config/application.rb`)? + +Let's look back to `config.ru` to see the first line of this file `require_relative 'config/environment'`. -Then Rack can `run YourProject::Application.new` (equal to `run Rails.application`). +```ruby +# ./config/environment.rb +# Load the Rails application. +require_relative 'application' # Let's step into this line. + +# Initialize the Rails application. +Rails.application.initialize! +``` + +```ruby +# ./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 YourProject + # The hooked method `inherited` defined in eigenclass of 'Rails::Application' is invoked. + class Application < Rails::Application + config.load_defaults 5.2 + config.i18n.default_locale = :zh + end +end +``` +Let's replace `app_class.instance` to `YourProject::Application.instance`. + +But where is the `call` method? `call` method should be a method of `YourProject::Application.instance`. The `call` method processes every request. Here it is. ```ruby -# ../gems/railties/lib/rails/engine.rb +# ./gems/railties/lib/rails/engine.rb module Rails class Engine < Railtie def call(env) # This method will process every request. It is invoked by Rack. So it is very important. req = build_request env - app.call req.env + app.call req.env # The 'app' object we will discuss later. end end end -# ../gems/railties/lib/rails/application.rb +# ./gems/railties/lib/rails/application.rb module Rails class Application < Engine end @@ -90,7 +170,6 @@ module YourProject class Application < Rails::Application end end - ``` Ancestor's chain is `YourProject::Application < Rails::Application < Rails::Engine < Rails::Railtie`. @@ -101,9 +180,9 @@ But what does `app_class.instance` really do? `instance` is just a method name. What we really need is `app_class.new`. -When I was reading these code +Let's look at the definition of instance. ```ruby -# ../gems/railties/lib/rails/application.rb +# ./gems/railties/lib/rails/application.rb module Rails class Application < Engine def instance @@ -115,18 +194,1403 @@ end After a deep research, I realized that this code is equal to ```ruby def instance - a_returned_value = super # Keyword 'super' will call the ancestor's same name method: 'instance'. - a_returned_value.run_load_hooks! + return_value = super # Keyword 'super' will call the ancestor's same name method: 'instance'. + return_value.run_load_hooks! end ``` ```ruby -# ../gems/railties/lib/rails/railtie.rb +# ./gems/railties/lib/rails/railtie.rb module Rails class Railtie def instance - @instance ||= new # 'Rails::Railtie' is the top ancestor. Now 'app_class.instance' is 'YourProject::Application.new'. + # 'Rails::Railtie' is the top ancestor. + # Now 'app_class.instance' is 'YourProject::Application.new'. + @instance ||= new + end + end +end +``` +And `YourProject::Application.new` is `Rails.application`. +```ruby +module Rails + def application + @application ||= (app_class.instance if app_class) + end +end +``` +Rack server will start `Rails.application` in the end. + +It is the most important object in the whole Rails object. + +And you'll only have one `Rails.application` in one process. Multiple thread shared only one `Rails.application`. + +## Part 2: config +We first time see the config is in `./config/application.rb`. +```ruby +# ./config/application.rb +#... +module YourProject + class Application < Rails::Application + # Actually, config is a method of YourProject::Application. + # It is defined in it's grandfather's father: Rails::Railtie + config.load_defaults 5.2 # Let's go to see what is config + config.i18n.default_locale = :zh + end +end +``` + +```ruby +module Rails + class Railtie + class << self + delegate :config, to: :instance # Method :config is defined here. + + def instance + @instance ||= new # return an instance of YourProject::Application. + end + end + end + + class Engine < Railtie + end + + class Application < Engine + class << self + def instance + # This line is equal to: + # return_value = super # 'super' will call :instance method in Railtie, which will return an instance of YourProject::Application. + # return_value.run_load_hooks! + super.run_load_hooks! + end + end + + def run_load_hooks! + return self if @ran_load_hooks + @ran_load_hooks = true + + # ... + self # return self! self is an instance of YourProject::Application. And it is Rails.application. + end + + # This is the method config. + def config + # It is an instance of class Rails::Application::Configuration. + # Please notice that Rails::Application is father of YourProject::Application (self's class). + @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from)) + end + end +end +``` +In the end, `YourProject::Application.config` will become `Rails.application.config`. + +`YourProject::Application.config === Rails.application.config # return ture.` + +Invoke Class's 'config' method become invoke the class's instance's 'config' method. + +```ruby +module Rails + class << self + def configuration + application.config + end + end +end +``` +So `Rails.configuration === Rails.application.config # return ture.`. + +```ruby +module Rails + class Application + class Configuration < ::Rails::Engine::Configuration + + end + end + + class Engine + class Configuration < ::Rails::Railtie::Configuration + attr_accessor :middleware + + def initialize(root = nil) + super() + #... + @middleware = Rails::Configuration::MiddlewareStackProxy.new + end + end + end + + class Railtie + class Configuration + end + end +end +``` + +## Part 3: Every request and response. +Imagine we have this route for the home page. +```ruby +# ./config/routes.rb +Rails.application.routes.draw do + root 'home#index' # HomeController#index +end +``` + +Rack need a `call` method to process request. + +Rails provide this call method in `Rails::Engine#call`. + +```ruby +# ./gems/railties/lib/rails/engine.rb +module Rails + class Engine < Railtie + def call(env) # This method will process every request. It is invoked by Rack. + req = build_request env + app.call req.env # The 'app' method is blow. + end + + def app + # You may want to know when does the @app first time initialized. + # It is initialized when 'config.ru' is load by rack server. + # Please look at Rack::Server#build_app_and_options_from_config for more information. + # When Rails.application.initialize! (in ./config/environment.rb), @app is initialized. + @app || @app_build_lock.synchronize { # '@app_build_lock = Mutex.new', so multiple threads share one '@app'. + @app ||= begin + # In the end, config.middleware will be an instance of ActionDispatch::MiddlewareStack with preset instance variable @middlewares (which is an Array). + stack = default_middleware_stack # Let's step into this line + # 'middleware' is a 'middleware_stack'! + config.middleware = build_middleware.merge_into(stack) + config.middleware.build(endpoint) # look at this endpoint below + end + } + +#@app is # +# > +# ... +# > +# +# > +# > + @app + end + + # Defaults to an ActionDispatch::Routing::RouteSet. + def endpoint + ActionDispatch::Routing::RouteSet.new_with_config(config) + end + end + + class Application < Engine + def default_middleware_stack + default_stack = DefaultMiddlewareStack.new(self, config, paths) + default_stack.build_stack # Let's step into this line. + end + + class DefaultMiddlewareStack + attr_reader :config, :paths, :app + + def initialize(app, config, paths) + @app = app + @config = config + @paths = paths + end + + def build_stack + ActionDispatch::MiddlewareStack.new do |middleware| + if config.force_ssl + middleware.use ::ActionDispatch::SSL, config.ssl_options + end + + middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header + + if config.public_file_server.enabled + headers = config.public_file_server.headers || {} + + middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers + end + + if rack_cache = load_rack_cache + require "action_dispatch/http/rack_cache" + middleware.use ::Rack::Cache, rack_cache + end + + if config.allow_concurrency == false + # User has explicitly opted out of concurrent request + # handling: presumably their code is not threadsafe + + middleware.use ::Rack::Lock + end + + middleware.use ::ActionDispatch::Executor, app.executor + + middleware.use ::Rack::Runtime + middleware.use ::Rack::MethodOverride unless config.api_only + middleware.use ::ActionDispatch::RequestId + middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies + + middleware.use ::Rails::Rack::Logger, config.log_tags + middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app + middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format + + unless config.cache_classes + middleware.use ::ActionDispatch::Reloader, app.reloader + end + + middleware.use ::ActionDispatch::Callbacks + middleware.use ::ActionDispatch::Cookies unless config.api_only + + if !config.api_only && config.session_store + if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure) + config.session_options[:secure] = true + end + middleware.use config.session_store, config.session_options + middleware.use ::ActionDispatch::Flash + end + + unless config.api_only + middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware + end + + middleware.use ::Rack::Head + middleware.use ::Rack::ConditionalGet + middleware.use ::Rack::ETag, "no-cache" + + middleware.use ::Rack::TempfileReaper unless config.api_only + end + end + end + end +end +``` + +As we see in the Rack middleware stack, the last one is `@app=#` +```ruby +# ./gems/actionpack5.2.2/lib/action_dispatch/routing/route_set.rb +module ActionDispatch + module Routing + class RouteSet + def initialize(config = DEFAULT_CONFIG) + @set = Journey::Routes.new + @router = Journey::Router.new(@set) + end + + def call(env) + req = make_request(env) # return ActionDispatch::Request.new(env) + req.path_info = Journey::Router::Utils.normalize_path(req.path_info) + @router.serve(req) # Let's step into this line. + end + end + end + +# ./gems/actionpack5.2.2/lib/action_dispatch/journey/router.rb + module Journey + class Router + class RoutingError < ::StandardError + end + + attr_accessor :routes + + def initialize(routes) + @routes = routes + end + + def serve(req) + find_routes(req).each do |match, parameters, route| # Let's step into 'find_routes' + set_params = req.path_parameters + path_info = req.path_info + script_name = req.script_name + + unless route.path.anchored + req.script_name = (script_name.to_s + match.to_s).chomp("/") + req.path_info = match.post_match + req.path_info = "/" + req.path_info unless req.path_info.start_with? "/" + end + + parameters = route.defaults.merge parameters.transform_values { |val| + val.dup.force_encoding(::Encoding::UTF_8) + } + + req.path_parameters = set_params.merge parameters + + # route is an instance of ActionDispatch::Journey::Route. + # route.app is an instance of ActionDispatch::Routing::RouteSet::Dispatcher. + status, headers, body = route.app.serve(req) # Let's step into method 'serve' + + if "pass" == headers["X-Cascade"] + req.script_name = script_name + req.path_info = path_info + req.path_parameters = set_params + next + end + + return [status, headers, body] + end + + [404, { "X-Cascade" => "pass" }, ["Not Found"]] + end + + def find_routes(req) + routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| + r.path.match(req.path_info) + } + + routes = + if req.head? + match_head_routes(routes, req) + else + match_routes(routes, req) + end + + routes.sort_by!(&:precedence) + + routes.map! { |r| + match_data = r.path.match(req.path_info) + path_parameters = {} + match_data.names.zip(match_data.captures) { |name, val| + path_parameters[name.to_sym] = Utils.unescape_uri(val) if val + } + [match_data, path_parameters, r] + } + end + + end + end +end + +# ./gems/actionpack5.2.2/lib/action_dispatch/routing/route_set.rb +module ActionDispatch + module Routing + class RouteSet + class Dispatcher < Routing::Endpoint + def serve(req) + params = req.path_parameters # params: { action: 'index', controller: 'home' } + controller = controller(req) # controller: HomeController + res = controller.make_response!(req) # The definition of make_response! is ActionDispatch::Response.create.tap do |res| res.request = request; end + dispatch(controller, params[:action], req, res) # Let's step into this line. + rescue ActionController::RoutingError + if @raise_on_name_error + raise + else + return [404, { "X-Cascade" => "pass" }, []] + end + end + + private + + def controller(req) + req.controller_class + rescue NameError => e + raise ActionController::RoutingError, e.message, e.backtrace + end + + def dispatch(controller, action, req, res) + controller.dispatch(action, req, res) # Let's step into this line. + end + end + end + end +end + +# ./gems/actionpack-5.2.2/lib/action_controller/metal.rb +module ActionController + class Metal < AbstractController::Base + abstract! + + def self.controller_name + @controller_name ||= name.demodulize.sub(/Controller$/, "").underscore + end + + def self.make_response!(request) + ActionDispatch::Response.new.tap do |res| + res.request = request + end + end + + class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new + + def self.inherited(base) + base.middleware_stack = middleware_stack.dup + super + end + + # Direct dispatch to the controller. Instantiates the controller, then + # executes the action named +name+. + def self.dispatch(name, req, res) + if middleware_stack.any? + middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env + else + # self is HomeController, so in this line Rails will new a HomeController instance. + # See `HomeController.ancestors`, you can find many parents classes. + # These are some typical ancestors of HomeController. + # HomeController + # < ApplicationController + # < ActionController::Base + # < ActiveRecord::Railties::ControllerRuntime (module included) + # < ActionController::Instrumentation (module included) + # < ActionController::Rescue (module included) + # < AbstractController::Callbacks (module included) + # < ActionController::ImplicitRender (module included) + # < ActionController::BasicImplicitRender (module included) + # < ActionController::Renderers (module included) + # < ActionController::Rendering (module included) + # < ActionView::Layouts (module included) + # < ActionView::Rendering (module included) + # < ActionDispatch::Routing::UrlFor (module included) + # < AbstractController::Rendering (module included) + # < ActionController::Metal + # < AbstractController::Base + new.dispatch(name, req, res) # Let's step into this line. + end + end + + def dispatch(name, request, response) + set_request!(request) + set_response!(response) + process(name) # Let's step into this line. + request.commit_flash + to_a + end + + def to_a + response.to_a + end + end +end + +# .gems/actionpack-5.2.2/lib/abstract_controller/base.rb +module AbstractController + class Base + def process(action, *args) + @_action_name = action.to_s + + unless action_name = _find_action_name(@_action_name) + raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" + end + + @_response_body = nil + + process_action(action_name, *args) # Let's step into this line. + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/instrumentation.rb +module ActionController + module Instrumentation + def process_action(*args) + raw_payload = { + controller: self.class.name, + action: action_name, + params: request.filtered_parameters, + headers: request.headers, + format: request.format.ref, + method: request.request_method, + path: request.fullpath + } + + ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) + + ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload| + begin + # self: # + result = super # Let's step into this line. + payload[:status] = response.status + result + ensure + append_info_to_payload(payload) + end + end + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/rescue.rb +module ActionController + module Rescue + def process_action(*args) + super # Let's step into this line. + rescue Exception => exception + request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions? + rescue_with_handler(exception) || raise + end + end +end + +# .gems/actionpack-5.2.2/lib/abstract_controller/callbacks.rb +module AbstractController + # = Abstract Controller Callbacks + # + # Abstract Controller provides hooks during the life cycle of a controller action. + # Callbacks allow you to trigger logic during this cycle. Available callbacks are: + # + # * after_action + # * before_action + # * skip_before_action + # * ... + module Callbacks + def process_action(*args) + run_callbacks(:process_action) do + # self: # + super # Let's step into this line. + end + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/rendering.rb +module ActionController + module Rendering + def process_action(*) + self.formats = request.formats.map(&:ref).compact + super # Let's step into this line. + end + end +end + +# .gems/actionpack-5.2.2/lib/abstract_controller/base.rb +module AbstractController + class Base + def process_action(method_name, *args) + # self: #, method_name: 'index' + send_action(method_name, *args) # In the end, method 'send_action' is method 'send' as the below line shown. + end + + alias send_action send + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/basic_implicit_render.rb +module ActionController + module BasicImplicitRender + def send_action(method, *args) + # self: #, method_name: 'index' + # Because 'send_action' is an alias of 'send', so + # self.send('index', *args) will goto HomeController#index. + x = super + x.tap { default_render unless performed? } # Let's step into 'default_render' later. + end + end +end + +# ./your_project/app/controllers/home_controller.rb +class HomeController < ApplicationController + # Will go back to BasicImplicitRender#send_action when method 'index' is done. + def index + # Question: How does this instance variable '@users' in HomeController can be accessed in './app/views/home/index.html.erb' ? + # Will answer this question later. + @users = User.all.pluck(:id, :name) + end +end +``` + +```html +# ./app/views/home/index.html.erb +
+

+ <%= t('home.banner_title') %> + <%= @users %> +

+
+``` + +```ruby +# .gems/actionpack-5.2.2/lib/action_controller/metal/implicit_render.rb +module ActionController + # Handles implicit rendering for a controller action that does not + # explicitly respond with +render+, +respond_to+, +redirect+, or +head+. + module ImplicitRender + def default_render(*args) + # Let's step into template_exists? + if template_exists?(action_name.to_s, _prefixes, variants: request.variant) + # Rails have found the default template './app/views/home/index.html.erb', so render it. + render(*args) # Let's step into this line later + elsif any_templates?(action_name.to_s, _prefixes) + message = "#{self.class.name}\##{action_name} is missing a template " \ + "for this request format and variant.\n" \ + "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \ + "\nrequest.variant: #{request.variant.inspect}" + + raise ActionController::UnknownFormat, message + elsif interactive_browser_request? + message = "#{self.class.name}\##{action_name} is missing a template " \ + "for this request format and variant.\n\n" \ + "request.formats: #{request.formats.map(&:to_s).inspect}\n" \ + "request.variant: #{request.variant.inspect}\n\n" \ + "NOTE! For XHR/Ajax or API requests, this action would normally " \ + "respond with 204 No Content: an empty white screen. Since you're " \ + "loading it in a web browser, we assume that you expected to " \ + "actually render a template, not nothing, so we're showing an " \ + "error to be extra-clear. If you expect 204 No Content, carry on. " \ + "That's what you'll get from an XHR or API request. Give it a shot." + + raise ActionController::UnknownFormat, message + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + super + end end end end -``` \ No newline at end of file + +# .gems/actionview-5.2.2/lib/action_view/lookup_context.rb +module ActionView + class LookupContext + module ViewPaths + # Rails find out that the default template is './app/views/home/index.html.erb' + def exists?(name, prefixes = [], partial = false, keys = [], **options) + @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) + end + alias :template_exists? :exists? + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/instrumentation.rb +module ActionController + module Instrumentation + def render(*args) + render_output = nil + self.view_runtime = cleanup_view_runtime do + Benchmark.ms { + # self: # + render_output = super # Let's step into super + } + end + render_output + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/rendering.rb +module ActionController + module Rendering + # Check for double render errors and set the content_type after rendering. + def render(*args) + raise ::AbstractController::DoubleRenderError if response_body + super # Let's step into super + end + end +end + +# .gems/actionpack-5.2.2/lib/abstract_controller/rendering.rb +module AbstractController + module Rendering + # Normalizes arguments, options and then delegates render_to_body and + # sticks the result in self.response_body. + def render(*args, &block) + options = _normalize_render(*args, &block) + rendered_body = render_to_body(options) # Let's step into this line. + if options[:html] + _set_html_content_type + else + _set_rendered_content_type rendered_format + end + self.response_body = rendered_body + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/renderers.rb +module ActionController + module Renderers + def render_to_body(options) + _render_to_body_with_renderer(options) || super # Let's step into this line and super later. + end + + # For this example, this method return nil in the end. + def _render_to_body_with_renderer(options) + # The '_renderers' is defined at line 31: class_attribute :_renderers, default: Set.new.freeze. + # '_renderers' is an instance predicate method. For more information, + # see ./gems/activesupport/lib/active_support/core_ext/class/attribute.rb + _renderers.each do |name| + if options.key?(name) + _process_options(options) + method_name = Renderers._render_with_renderer_method_name(name) + return send(method_name, options.delete(name), options) + end + end + nil + end + end +end + +# .gems/actionpack-5.2.2/lib/action_controller/metal/renderers.rb +module ActionController + module Rendering + def render_to_body(options = {}) + super || _render_in_priorities(options) || " " # Let's step into super + end + end +end +``` + +```ruby +# .gems/actionview-5.2.2/lib/action_view/rendering.rb +module ActionView + module Rendering + def render_to_body(options = {}) + _process_options(options) + _render_template(options) # Let's step into this line. + end + + def _render_template(options) + variant = options.delete(:variant) + assigns = options.delete(:assigns) + context = view_context # We will step into this line later. + + context.assign assigns if assigns + lookup_context.rendered_format = nil if options[:formats] + lookup_context.variants = variant if variant + + view_renderer.render(context, options) # Let's step into this line. + end + end +end + +# .gems/actionview-5.2.2/lib/action_view/renderer/renderer.rb +module ActionView + class Renderer + def render(context, options) + if options.key?(:partial) + render_partial(context, options) + else + render_template(context, options) # Let's step into this line. + end + end + + # Direct access to template rendering. + def render_template(context, options) + TemplateRenderer.new(@lookup_context).render(context, options) # Let's step into this line. + end + end +end + +# .gems/actionview-5.2.2/lib/action_view/renderer/template_renderer.rb +module ActionView + class TemplateRenderer < AbstractRenderer + def render(context, options) + @view = context + @details = extract_details(options) + template = determine_template(options) + + prepend_formats(template.formats) + + @lookup_context.rendered_format ||= (template.formats.first || formats.first) + + render_template(template, options[:layout], options[:locals]) # Let's step into this line. + end + + def render_template(template, layout_name = nil, locals = nil) + view, locals = @view, locals || {} + + render_with_layout(layout_name, locals) do |layout| # Let's step into this line + instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do + # template: # + template.render(view, locals) { |*name| view._layout_for(*name) } # Let's step into this line + end + end + end + + def render_with_layout(path, locals) + layout = path && find_layout(path, locals.keys, [formats.first]) + content = yield(layout) + + if layout + view = @view + view.view_flow.set(:layout, content) + layout.render(view, locals) { |*name| view._layout_for(*name) } + else + content + end + end + end +end + +# .gems/actionview-5.2.2/lib/action_view/template.rb +module ActionView + class Template + def render(view, locals, buffer = nil, &block) + instrument_render_template do + # self: # + compile!(view) + # method_name: "_app_views_home_index_html_erb___3699380246341444633_70336654511160" (This method is defined in 'def compile(mod)' below) + # view: #<#:0x00007ff10ea050a8>, view is an instance of which has same instance variables in the instance of HomeController. + # The method 'view.send' will return the result html! + view.send(method_name, locals, buffer, &block) + end + rescue => e + handle_render_error(view, e) + end + + # Compile a template. This method ensures a template is compiled + # just once and removes the source after it is compiled. + def compile!(view) + return if @compiled + + # Templates can be used concurrently in threaded environments + # so compilation and any instance variable modification must + # be synchronized + @compile_mutex.synchronize do + # Any thread holding this lock will be compiling the template needed + # by the threads waiting. So re-check the @compiled flag to avoid + # re-compilation + return if @compiled + + if view.is_a?(ActionView::CompiledTemplates) + mod = ActionView::CompiledTemplates + else + mod = view.singleton_class + end + + instrument("!compile_template") do + compile(mod) # Let's step into this line. + end + + # Just discard the source if we have a virtual path. This + # means we can get the template back. + @source = nil if @virtual_path + @compiled = true + end + end + + def compile(mod) + encode! + # @handler: # + code = @handler.call(self) # Let's step into this line. + + # Make sure that the resulting String to be eval'd is in the + # encoding of the code + source = <<-end_src.dup + def #{method_name}(local_assigns, output_buffer) + _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} + ensure + @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + end + end_src + + # Make sure the source is in the encoding of the returned code + source.force_encoding(code.encoding) + + # In case we get back a String from a handler that is not in + # BINARY or the default_internal, encode it to the default_internal + source.encode! + + # Now, validate that the source we got back from the template + # handler is valid in the default_internal. This is for handlers + # that handle encoding but screw up + unless source.valid_encoding? + raise WrongEncodingError.new(@source, Encoding.default_internal) + end + + # source: def _app_views_home_index_html_erb___1187260686135140546_70244801399180(local_assigns, output_buffer) + # _old_virtual_path, @virtual_path = @virtual_path, "home/index";_old_output_buffer = @output_buffer;; + # @output_buffer = output_buffer || ActionView::OutputBuffer.new; + # @output_buffer.safe_append='
+ #

+ # '.freeze; + # @output_buffer.append=( t('home.banner_title') ); + # @output_buffer.append=( @users ); + # @output_buffer.safe_append=' + #

+ #
+ # '.freeze; + # @output_buffer.to_s + # ensure + # @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + # end + mod.module_eval(source, identifier, 0) # This line will actually define the method '_app_views_home_index_html_erb___1187260686135140546_70244801399180' + # mod: ActionView::CompiledTemplates + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + end + +# .gems/actionview-5.2.2/lib/action_view/template/handler/erb.rb + module Handlers + class ERB + def call(template) + # First, convert to BINARY, so in case the encoding is + # wrong, we can still find an encoding tag + # (<%# encoding %>) inside the String using a regular + # expression + template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) + + erb = template_source.gsub(ENCODING_TAG, "") + encoding = $2 + + erb.force_encoding valid_encoding(template.source.dup, encoding) + + # Always make sure we return a String in the default_internal + erb.encode! + + self.class.erb_implementation.new( + erb, + escape: (self.class.escape_whitelist.include? template.type), + trim: (self.class.erb_trim_mode == "-") + ).src + end + end + end + end +end +``` + +It's time to answer the question before: + +How can this instance variable defined '@users' in HomeController be accessed in './app/views/home/index.html.erb' ? + +```ruby +# ./gems/actionview-5.2.2/lib/action_view/rendering.rb +module ActionView + module Rendering + def view_context + view_context_class.new( # Let's step into this line later. + view_renderer, + view_assigns, # Let's step into this line. + self + ) + end + + def view_assigns + # self: # + protected_vars = _protected_ivars + # instance_variables is an instance method of Object and it will return an array. And the array contains @users. + variables = instance_variables + + variables.reject! { |s| protected_vars.include? s } + ret = variables.each_with_object({}) { |name, hash| + hash[name.slice(1, name.length)] = instance_variable_get(name) + } + # ret: {"marked_for_same_origin_verification"=>true, "users"=>[[1, "Lane"], [2, "John"], [4, "Frank"]]} + ret + end + + def view_context_class + # will return a subclass of ActionView::Base. + @_view_context_class ||= self.class.view_context_class + end + + # How this ClassMethods works? Please look at ActiveSupport::Concern in ./gems/activesupport-5.2.2/lib/active_support/concern.rb + # FYI, the method 'append_features' will be executed before method 'included'. + # https://apidock.com/ruby/v1_9_3_392/Module/append_features + module ClassMethods + def view_context_class + # self: HomeController + @view_context_class ||= begin + supports_path = supports_path? + routes = respond_to?(:_routes) && _routes + helpers = respond_to?(:_helpers) && _helpers + + Class.new(ActionView::Base) do + if routes + include routes.url_helpers(supports_path) + include routes.mounted_helpers + end + + if helpers + include helpers + end + end + end + end + end + end +end + +# ./gems/actionview-5.2.2/lib/action_view/base.rb +module ActionView + class Base + def initialize(context = nil, assigns = {}, controller = nil, formats = nil) + @_config = ActiveSupport::InheritableOptions.new + + if context.is_a?(ActionView::Renderer) + @view_renderer = context + else + lookup_context = context.is_a?(ActionView::LookupContext) ? + context : ActionView::LookupContext.new(context) + lookup_context.formats = formats if formats + lookup_context.prefixes = controller._prefixes if controller + @view_renderer = ActionView::Renderer.new(lookup_context) + end + + @cache_hit = {} + assign(assigns) # Let's step into this line. + assign_controller(controller) + _prepare_context + end + + def assign(new_assigns) + @_assigns = + new_assigns.each do |key, value| + instance_variable_set("@#{key}", value) # This line will set the instance variables in HomeController like '@users' to itself. + end + end + end +end + +``` + +## Part 4: What `$ rails server` do? +Assume your rails project app class name is `YourProject::Application` (defined in `./config/application.rb`). + +First, I will give you a piece of important code. +```ruby +# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb +module Rails + class Server < ::Rack::Server + def start + #... + log_to_stdout + + super # Will invoke ::Rack::Server#start. + ensure + puts "Exiting" unless @options && options[:daemonize] + end + + def log_to_stdout + # 'wrapped_app' will get an well prepared app from './config.ru' file. + # It's the first time invoke 'wrapped_app'. + # The app is an instance of YourProject::Application. + # The app is not created in 'wrapped_app'. + # It has been created when `require APP_PATH` in previous code, + # just at the 'perform' method in Rails::Command::ServerCommand. + wrapped_app + + # ... + end + end +end +``` + +Then, Let's start rails by `rails server`. The command `rails` locates at `./bin/`. +```ruby +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) + +require_relative '../config/boot' +require 'rails/commands' # Let's look at this file. +``` + +```ruby +# ./railties-5.2.2/lib/rails/commands.rb +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 # command is 'server' + +Rails::Command.invoke command, ARGV # Let's step into this line. +``` + +```ruby +# ./railties-5.2.2/lib/rails/command.rb +module Rails + module Command + class << self + def invoke(full_namespace, args = [], **config) + # ... + # In the end, we got this result: {"rails server" => Rails::Command::ServerCommand} + command = find_by_namespace(namespace, command_name) # command value is Rails::Command::ServerCommand + # command_name is 'server' + command.perform(command_name, args, config) # Rails::Command::ServerCommand.perform + end + end + end +end +``` + +```ruby +# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb +module Rails + module Command + class ServerCommand < Base # There is a class method 'perform' in the Base class. + def initialize + end + + # 'perform' here is a instance method. But for Rails::Command::ServerCommand.perform, 'perform' is a class method. + # Where is this 'perform' class method? Answer: In the parent class 'Base'. + def perform + # ... + Rails::Server.new(server_options).tap do |server| + #... + server.start + end + end + end + end +end +``` + +Inheritance relationship: `Rails::Command::ServerCommand < Rails::Command::Base < Thor` + +```ruby +# ./gems/railties-5.2.2/lib/rails/command/base.rb +module Rails + module Command + class Base < Thor # https://github.com/erikhuda/thor Thor is a toolkit for building powerful command-line interfaces. + class << self + # command is 'server' + def perform(command, args, config) + #... + dispatch(command, args.dup, nil, config) # Thor.dispatch + end + end + end + end +end +``` + +```ruby +# ./gems/thor/lib/thor.rb +class Thor + class << self + # meth is 'server' + def dispatch(meth, given_args, given_opts, config) + # ... + instance = new(args, opts, config) # Here will new a Rails::Command::ServerCommand instance. + # ... + # command is {Thor::Command}# + instance.invoke_command(command, trailing || []) # Method 'invoke_command' is in Thor::Invocation. + end + end +end + +# ./gems/thor/lib/thor/invocation.rb +class Thor + module Invocation # This module is included in Thor. Thor is grandfather of Rails::Command::ServerCommand + def invoke_command(command, *args) # 'invoke_command' is defined at here. + # ... + command.run(self, *args) # command is {Thor::Command}# + end + end +end + +# ./gems/thor/lib/thor.rb +class Thor + # ... + include Thor::Base # Will invoke hook method 'Thor::Base.included(self)' +end + +# ./gems/thor/lib/thor/base.rb +module Thor + module Base + class << self + def included(base) # hook method when module 'Thor::Base' included. + base.extend ClassMethods + base.send :include, Invocation # 'Invocation' included in 'Thor'. So 'invoke_command' will be an instance method of Rails::Command::ServerCommand + base.send :include, Shell + end + end + + module ClassMethods + # This is also a hook method. In the end, + # this method will help to "alias_method('server', 'perform')". + # The 'server' is the 'server' for `$ rails server`. + # So it's important. We will discuss it later. + def method_added(meth) + # ... + # here self is {Class} Rails::Command::ServerCommand + create_command(meth) # meth is 'perform'. Let's step into this line. + end + end + end +end + +# ./gems/railties-5.2.2/lib/rails/command/base.rb +module Rails + module Command + module Base # Rails::Command::Base is father of Rails::Command::ServerCommand + class << self + def create_command(meth) + if meth == "perform" + # Instance method 'server' of Rails::Command::ServerCommand will be delegated to 'perform' method now. + alias_method('server', meth) + end + end + end + end + end +end + +# ./gems/thor/lib/thor/command.rb +class Thor + class Command + def run(instance, args = []) + #... + # instance is {Rails::Command::ServerCommand}# + instance.__send__(name, *args) # name is 'server'. Will actually invoke 'instance.perform(*args)'. + end + end +end + +# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb +module Rails + module Command + # In ServerCommand class, there is no instance method 'server' explicitly defined. + # It is defined by a hook method 'method_added' + class ServerCommand < Base + def perform + # ... + Rails::Server.new(server_options).tap do |server| + # APP_PATH is '/Users/your_name/your-project/config/application'. + # require APP_PATH will create the 'Rails.application' object. + # 'Rails.application' is 'YourProject::Application.new'. + # Rack server will start 'Rails.application'. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start # Let's step into this line. + end + end + end + end +end + +# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb +module Rails + class Server < ::Rack::Server + def start + print_boot_information + + # All lines in the block of trap() will not be executed + # unless a signal of terminating the process (like `$ kill -9 process_id`) has been received. + trap(:INT) do + #... + exit + end + + create_tmp_directories + setup_dev_caching + + log_to_stdout # This line is important. Although the method name seems not. Let step into this line. + + super # Will invoke ::Rack::Server#start. I will show you later. + ensure + puts "Exiting" unless @options && options[:daemonize] + end + + def log_to_stdout + # 'wrapped_app' will get an well prepared app from './config.ru' file. + # It's the first time invoke 'wrapped_app'. + # The app is an instance of YourProject::Application. + # The app is not created in 'wrapped_app'. + # It has been created when `require APP_PATH` in previous code, + # just at the 'perform' method in Rails::Command::ServerCommand. + wrapped_app + + # ... + end + end +end + +# ./gems/rack-2.0.6/lib/rack/server.rb +module Rack + class Server + def wrapped_app + @wrapped_app ||= + build_app( + app # Let's step into this line. + ) + end + + def app + @app ||= build_app_and_options_from_config # Let's step into this line. + @app + end + + def build_app_and_options_from_config + # ... + # self.options[:config] is 'config.ru'. Let's step into this line. + app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) + # ... + app + end + + def start(&blk) + #... + wrapped_app + + trap(:INT) do + if server.respond_to?(:shutdown) + server.shutdown + else + exit + end + end + + # server is {Module} Rack::Handler::Puma + # wrapped_app is {YourProject::Application} # + server.run(wrapped_app, options, &blk) # We will step into this line later. + end + end +end + +# ./gems/rack/lib/rack/builder.rb +module Rack + module Builder + def self.parse_file(config, opts = Server::Options.new) + cfgfile = ::File.read(config) # config is 'config.ru' + + app = new_from_string(cfgfile, config) + + return app, options + end + + # Let's guess what will 'run Rails.application' do in config.ru? + # First, we will get an instance of YourProject::Application. + # Then we will run it. But 'run' may isn't what you are thinking about. + # Because the 'self' object in config.ru is an instance of Rack::Builder, + # so 'run' is an instance method of Rack::Builder. + # Let's look at the definition of the 'run' method: + # def run(app) + # @run = app # Just set an instance variable :) + # end + def self.new_from_string(builder_script, file="(rackup)") + # Rack::Builder implements a small DSL to iteratively construct Rack applications. + eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", + TOPLEVEL_BINDING, file, 0 + end + end +end + +# ./gems/puma-3.12.0/lib/rack/handler/puma.rb +module Rack + module Handler + module Puma + def self.run(app, options = {}) + conf = self.config(app, options) + + # ... + launcher = ::Puma::Launcher.new(conf, :events => events) + + begin + # Let's stop our journey here. It's puma's turn now. + # Puma will run your app (instance of YourProject::Application) + launcher.run + rescue Interrupt + puts "* Gracefully stopping, waiting for requests to finish" + launcher.stop + puts "* Goodbye!" + end + end + end + end +end +``` +Now puma has been started successfully running your app (instance of YourProject::Application). + + From 0a9ae20dc1db33db79dfd0c0cf7817dbda45fc64 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Wed, 27 Feb 2019 21:19:25 +0800 Subject: [PATCH 04/16] Refine README.md --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 43bb7d3..c4d0481 100644 --- a/README.md +++ b/README.md @@ -468,7 +468,9 @@ module Rails end ``` -As we see in the Rack middleware stack, the last one is `@app=#` +As we see in the Rack middleware stack, the last one is + +`@app=#` ```ruby # ./gems/actionpack5.2.2/lib/action_dispatch/routing/route_set.rb module ActionDispatch @@ -570,7 +572,8 @@ module ActionDispatch def serve(req) params = req.path_parameters # params: { action: 'index', controller: 'home' } controller = controller(req) # controller: HomeController - res = controller.make_response!(req) # The definition of make_response! is ActionDispatch::Response.create.tap do |res| res.request = request; end + # The definition of make_response! is ActionDispatch::Response.create.tap do |res| res.request = request; end + res = controller.make_response!(req) dispatch(controller, params[:action], req, res) # Let's step into this line. rescue ActionController::RoutingError if @raise_on_name_error @@ -1143,7 +1146,7 @@ end It's time to answer the question before: -How can this instance variable defined '@users' in HomeController be accessed in './app/views/home/index.html.erb' ? +How can instance variable like `@users` defined in `HomeController` be accessed in `./app/views/home/index.html.erb`? ```ruby # ./gems/actionview-5.2.2/lib/action_view/rendering.rb @@ -1236,7 +1239,7 @@ end ``` -## Part 4: What `$ rails server` do? +## Part 4: What does `$ rails server` do? Assume your rails project app class name is `YourProject::Application` (defined in `./config/application.rb`). First, I will give you a piece of important code. @@ -1268,7 +1271,7 @@ module Rails end ``` -Then, Let's start rails by `rails server`. The command `rails` locates at `./bin/`. +Then, let's start rails by `rails server`. The command `rails` locates at `./bin/`. ```ruby #!/usr/bin/env ruby APP_PATH = File.expand_path('../config/application', __dir__) @@ -1591,6 +1594,6 @@ module Rack end end ``` -Now puma has been started successfully running your app (instance of YourProject::Application). +Now puma has been started successfully with your app (instance of YourProject::Application) running. From 4e73daed3af9c15142817ec0ed676371c2b80320 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Mon, 4 Mar 2019 12:42:11 +0800 Subject: [PATCH 05/16] Add more information. --- README.md | 1376 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1177 insertions(+), 199 deletions(-) diff --git a/README.md b/README.md index c4d0481..1872079 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,21 @@ So which is the object with `call` method in Rails App? I will answer this quest ### What you will learn from this tutorial? -* How rails start your application? +* How does Rails start your application? -* How rails process every request? +* How does Rails process every request? -* How rails combine ActionController, ActionView and Routes? +* How does Rails combine ActionController, ActionView and Routes together? -I should start with the command `$ rails server`. But I put this to Part 4. Because it's not interesting. +* How does puma, rack, Rails work together? + +* What's Puma's multiple threads? + +I should start with the command `$ rails server`, but I put this to Part 4. Because it's a little bit complex. + +## Part 1: Your app: an instance of YourProject::Application +Assume your Rails app class name is `YourProject::Application` (defined in `./config/application.rb`). -## Part 1: Your app: an instance of YourProject::Application. First, I will give you a piece of important code. ```ruby # ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb @@ -31,10 +37,12 @@ module Rails Rails::Server.new(server_options).tap do |server| # APP_PATH is '/Users/your_name/your-project/config/application'. # require APP_PATH will create the 'Rails.application' object. - # 'Rails.application' is 'YourProject::Application.new'. + # Actually, 'Rails.application' is an instance of `YourProject::Application`. # Rack server will start 'Rails.application'. require APP_PATH + Dir.chdir(Rails.application.root) + server.start end end @@ -44,9 +52,11 @@ module Rails class Server < ::Rack::Server def start #... - # 'wrapped_app' is invoked in method 'log_to_stdout'. - # It will get an well prepared app from './config.ru' file. - # It will use the app created at the 'perform' method in Rails::Command::ServerCommand. + # 'wrapped_app' will get an well prepared app from `./config.ru` file. + # 'wrapped_app' will return an instance of `YourProject::Application`. + # But the instance of `YourProject::Application` returned is not created in 'wrapped_app'. + # It has been created when `require APP_PATH` in previous code: + # in Rails::Command::ServerCommand#perform wrapped_app super # Will invoke ::Rack::Server#start. @@ -54,9 +64,9 @@ module Rails end end ``` -A rack server need to start with an App. The App should have a `call` method. +A rack server need to start with an `App` object. The `App` object should have a `call` method. -`config.ru` is the conventional entry file for rack app. So let's view it. +`config.ru` is the conventional entry file for rack app. So let's look at it. ```ruby # ./config.ru require_relative 'config/environment' @@ -64,7 +74,7 @@ require_relative 'config/environment' run Rails.application # It seems that this is the app. ``` -Let's test it by `Rails.application.respond_to?(:call) # Returned 'true'`. +Let's test it by `Rails.application.respond_to?(:call)`, it returns true. Let's step into `Rails.application`. @@ -86,7 +96,7 @@ module Rails end ``` -Because `Rails.application.respond_to?(:call) # Returned 'true'.`, `app_class.instance` has a `call` method. +Because `Rails.application.respond_to?(:call)` returns true, `app_class.instance` has a `call` method. When was `app_class` set? ```ruby @@ -112,7 +122,7 @@ end ``` `YourProject::Application` will become the `Rails.app_class`. -You may have a question: how we reach this file (`./config/application.rb`)? +You may have a question: how does rails enter this file (`./config/application.rb`)? Let's look back to `config.ru` to see the first line of this file `require_relative 'config/environment'`. @@ -154,7 +164,7 @@ module Rails class Engine < Railtie def call(env) # This method will process every request. It is invoked by Rack. So it is very important. req = build_request env - app.call req.env # The 'app' object we will discuss later. + app.call req.env # We will discuss the 'app' object later. end end end @@ -174,13 +184,13 @@ end Ancestor's chain is `YourProject::Application < Rails::Application < Rails::Engine < Rails::Railtie`. -So `YourProject::Application.new.respond_to?(:call) # Will return 'true'`. +So `YourProject::Application.new.respond_to?(:call)` will return true. But what does `app_class.instance` really do? `instance` is just a method name. What we really need is `app_class.new`. -Let's look at the definition of instance. +Let's look at the definition of `instance`. ```ruby # ./gems/railties/lib/rails/application.rb module Rails @@ -204,7 +214,7 @@ end module Rails class Railtie def instance - # 'Rails::Railtie' is the top ancestor. + # 'Rails::Railtie' is the top ancestor class. # Now 'app_class.instance' is 'YourProject::Application.new'. @instance ||= new end @@ -221,12 +231,12 @@ end ``` Rack server will start `Rails.application` in the end. -It is the most important object in the whole Rails object. +It is an important object in Rails. -And you'll only have one `Rails.application` in one process. Multiple thread shared only one `Rails.application`. +And you'll only have one `Rails.application` in one process. Multiple threads shared only one `Rails.application`. ## Part 2: config -We first time see the config is in `./config/application.rb`. +First time we see the `config` is in `./config/application.rb`. ```ruby # ./config/application.rb #... @@ -234,7 +244,7 @@ module YourProject class Application < Rails::Application # Actually, config is a method of YourProject::Application. # It is defined in it's grandfather's father: Rails::Railtie - config.load_defaults 5.2 # Let's go to see what is config + config.load_defaults 5.2 # Let's step into this line to see what config is. config.i18n.default_locale = :zh end end @@ -244,10 +254,15 @@ end module Rails class Railtie class << self - delegate :config, to: :instance # Method :config is defined here. + # Method :config is defined here. + # Actually, method :config is delegated to another object `:instance`. + delegate :config, to: :instance + # Call `YourProject::Application.config` will actually call `YourProject::Application.instance.config` def instance - @instance ||= new # return an instance of YourProject::Application. + # return an instance of YourProject::Application. + # Call `YourProject::Application.config` will actually call `YourProject::Application.new.config` + @instance ||= new end end end @@ -258,10 +273,8 @@ module Rails class Application < Engine class << self def instance - # This line is equal to: - # return_value = super # 'super' will call :instance method in Railtie, which will return an instance of YourProject::Application. - # return_value.run_load_hooks! - super.run_load_hooks! + return_value = super # 'super' will call :instance method in Railtie, which will return an instance of YourProject::Application. + return_value.run_load_hooks! end end @@ -270,13 +283,13 @@ module Rails @ran_load_hooks = true # ... - self # return self! self is an instance of YourProject::Application. And it is Rails.application. + self # self is an instance of YourProject::Application. And it is Rails.application. end # This is the method config. def config # It is an instance of class Rails::Application::Configuration. - # Please notice that Rails::Application is father of YourProject::Application (self's class). + # Please notice that Rails::Application is superclass of YourProject::Application (self's class). @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from)) end end @@ -284,9 +297,9 @@ end ``` In the end, `YourProject::Application.config` will become `Rails.application.config`. -`YourProject::Application.config === Rails.application.config # return ture.` +`YourProject::Application.config === Rails.application.config` returns true. -Invoke Class's 'config' method become invoke the class's instance's 'config' method. +Invoke Class's `config` method become invoke the class's instance's `config` method. ```ruby module Rails @@ -326,7 +339,7 @@ module Rails end ``` -## Part 3: Every request and response. +## Part 3: Every request and response Imagine we have this route for the home page. ```ruby # ./config/routes.rb @@ -335,9 +348,118 @@ Rails.application.routes.draw do end ``` +### Puma +When a request is made from client, puma will process the request in `Puma::Server#process_client`. + +If you want to know how puma enter the method `Puma::Server#process_client`, please read part 4 or just search 'process_client' in this document. + +```ruby +# ./gems/puma-3.12.0/lib/puma/server.rb +require 'socket' + +module Puma + # The HTTP Server itself. Serves out a single Rack app. + # + # This class is used by the `Puma::Single` and `Puma::Cluster` classes + # to generate one or more `Puma::Server` instances capable of handling requests. + # Each Puma process will contain one `Puma::Server` instacne. + # + # The `Puma::Server` instance pulls requests from the socket, adds them to a + # `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`. + # + # Each `Puma::Server` will have one reactor and one thread pool. + class Server + def initialize(app, events=Events.stdio, options={}) + # app: # + # @config = # + # > + @app = app + #... + end + + # Given a connection on +client+, handle the incoming requests. + # + # This method support HTTP Keep-Alive so it may, depending on if the client + # indicates that it supports keep alive, wait for another request before + # returning. + # + def process_client(client, buffer) + begin + # ... + while true + # Let's step into this line. + case handle_request(client, buffer) # Will return true in this example. + when true + return unless @queue_requests + buffer.reset + + ThreadPool.clean_thread_locals if clean_thread_locals + + unless client.reset(@status == :run) + close_socket = false + client.set_timeout @persistent_timeout + @reactor.add client + return + end + end + end + # ... + ensure + buffer.reset + client.close if close_socket + #... + end + end + + # Given the request +env+ from +client+ and a partial request body + # in +body+, finish reading the body if there is one and invoke + # the rack app. Then construct the response and write it back to + # +client+ + # + def handle_request(req, lines) + env = req.env + # ... + # app: # + # @config = # + # > + status, headers, res_body = @app.call(env) # Let's step into this line. + + # ... + return keep_alive + end + end +end +``` +```ruby +# ./gems/puma-3.12.0/lib/puma/configuration.rb +module Puma + class Configuration + class ConfigMiddleware + def initialize(config, app) + @config = config + @app = app + end + + def call(env) + env[Const::PUMA_CONFIG] = @config + # @app: # + @app.call(env) + end + end + end +end +``` + +### Rack apps +As we see when Ruby enter `Puma::Configuration::ConfigMiddleware#call`, the `@app` is `YourProject::Application` instance. + +It is just the `Rails.application`. + Rack need a `call` method to process request. -Rails provide this call method in `Rails::Engine#call`. +Rails defined this `call` method in `Rails::Engine#call`, so that `YourProject::Application` instance will have a `call` method. ```ruby # ./gems/railties/lib/rails/engine.rb @@ -359,11 +481,12 @@ module Rails stack = default_middleware_stack # Let's step into this line # 'middleware' is a 'middleware_stack'! config.middleware = build_middleware.merge_into(stack) - config.middleware.build(endpoint) # look at this endpoint below + # FYI, this line is the last line and the result of this line is the return value for @app. + config.middleware.build(endpoint) # look at this endpoint below. We will enter method `build` later. end } -#@app is #, 'middleware' will be switched to another instance of ActionDispatch::MiddlewareStack::Middleware when iterating + middleware.build(a) # Let's step into this line. + end + return_val + end + + class Middleware + def initialize(klass, args, block) + @klass = klass + @args = args + @block = block + end + + def build(app) + # klass is rack middleware like : Rack::TempfileReaper, Rack::ETag, Rack::ConditionalGet or Rack::Head, etc. + # It's typical rack app to use these middlewares. + # See https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib for more information. + klass.new(app, *args, &block) + end + end + end +end +``` +### The core app: ActionDispatch::Routing::RouteSet instance +```ruby +# Paste again FYI. +# @app: # +# > +# ... +# > +# +# > +# > +``` As we see in the Rack middleware stack, the last one is `@app=#` ```ruby -# ./gems/actionpack5.2.2/lib/action_dispatch/routing/route_set.rb +# ./gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rb module ActionDispatch module Routing class RouteSet @@ -564,7 +748,7 @@ module ActionDispatch end end -# ./gems/actionpack5.2.2/lib/action_dispatch/routing/route_set.rb +# ./gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rb module ActionDispatch module Routing class RouteSet @@ -628,11 +812,11 @@ module ActionController middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env else # self is HomeController, so in this line Rails will new a HomeController instance. - # See `HomeController.ancestors`, you can find many parents classes. + # See `HomeController.ancestors`, you can find many superclasses. # These are some typical ancestors of HomeController. - # HomeController + # HomeController # < ApplicationController - # < ActionController::Base + # < ActionController::Base # < ActiveRecord::Railties::ControllerRuntime (module included) # < ActionController::Instrumentation (module included) # < ActionController::Rescue (module included) @@ -677,6 +861,7 @@ module AbstractController @_response_body = nil + # action_name: 'index' process_action(action_name, *args) # Let's step into this line. end end @@ -759,8 +944,9 @@ end module AbstractController class Base def process_action(method_name, *args) - # self: #, method_name: 'index' - send_action(method_name, *args) # In the end, method 'send_action' is method 'send' as the below line shown. + # self: #, method_name: 'index' + # In the end, method 'send_action' is method 'send' as the below line shown. + send_action(method_name, *args) end alias send_action send @@ -772,9 +958,10 @@ module ActionController module BasicImplicitRender def send_action(method, *args) # self: #, method_name: 'index' - # Because 'send_action' is an alias of 'send', so + # Because 'send_action' is an alias of 'send', # self.send('index', *args) will goto HomeController#index. x = super + # performed?: false (for this example) x.tap { default_render unless performed? } # Let's step into 'default_render' later. end end @@ -784,8 +971,8 @@ end class HomeController < ApplicationController # Will go back to BasicImplicitRender#send_action when method 'index' is done. def index - # Question: How does this instance variable '@users' in HomeController can be accessed in './app/views/home/index.html.erb' ? - # Will answer this question later. + # Question: How does the instance variable '@users' defined in HomeController can be accessed in './app/views/home/index.html.erb' ? + # I will answer this question later. @users = User.all.pluck(:id, :name) end end @@ -801,6 +988,11 @@ end ``` +### Render view +As we see in `ActionController::BasicImplicitRender::send_action`, the last line is `default_render`. + +So after `HomeController#index` is done, Ruby will execute method `default_render`. + ```ruby # .gems/actionpack-5.2.2/lib/action_controller/metal/implicit_render.rb module ActionController @@ -810,28 +1002,9 @@ module ActionController def default_render(*args) # Let's step into template_exists? if template_exists?(action_name.to_s, _prefixes, variants: request.variant) - # Rails have found the default template './app/views/home/index.html.erb', so render it. + # Rails has found the default template './app/views/home/index.html.erb', so render it. render(*args) # Let's step into this line later - elsif any_templates?(action_name.to_s, _prefixes) - message = "#{self.class.name}\##{action_name} is missing a template " \ - "for this request format and variant.\n" \ - "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \ - "\nrequest.variant: #{request.variant.inspect}" - - raise ActionController::UnknownFormat, message - elsif interactive_browser_request? - message = "#{self.class.name}\##{action_name} is missing a template " \ - "for this request format and variant.\n\n" \ - "request.formats: #{request.formats.map(&:to_s).inspect}\n" \ - "request.variant: #{request.variant.inspect}\n\n" \ - "NOTE! For XHR/Ajax or API requests, this action would normally " \ - "respond with 204 No Content: an empty white screen. Since you're " \ - "loading it in a web browser, we assume that you expected to " \ - "actually render a template, not nothing, so we're showing an " \ - "error to be extra-clear. If you expect 204 No Content, carry on. " \ - "That's what you'll get from an XHR or API request. Give it a shot." - - raise ActionController::UnknownFormat, message + #... else logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger super @@ -844,7 +1017,7 @@ end module ActionView class LookupContext module ViewPaths - # Rails find out that the default template is './app/views/home/index.html.erb' + # Rails checks whether the default template exists. def exists?(name, prefixes = [], partial = false, keys = [], **options) @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end @@ -887,12 +1060,15 @@ module AbstractController # sticks the result in self.response_body. def render(*args, &block) options = _normalize_render(*args, &block) + rendered_body = render_to_body(options) # Let's step into this line. + if options[:html] _set_html_content_type else _set_rendered_content_type rendered_format end + self.response_body = rendered_body end end @@ -907,7 +1083,7 @@ module ActionController # For this example, this method return nil in the end. def _render_to_body_with_renderer(options) - # The '_renderers' is defined at line 31: class_attribute :_renderers, default: Set.new.freeze. + # The '_renderers' is defined at line 31: `class_attribute :_renderers, default: Set.new.freeze.` # '_renderers' is an instance predicate method. For more information, # see ./gems/activesupport/lib/active_support/core_ext/class/attribute.rb _renderers.each do |name| @@ -938,12 +1114,14 @@ module ActionView module Rendering def render_to_body(options = {}) _process_options(options) + _render_template(options) # Let's step into this line. end def _render_template(options) variant = options.delete(:variant) assigns = options.delete(:assigns) + context = view_context # We will step into this line later. context.assign assigns if assigns @@ -1025,8 +1203,8 @@ module ActionView # > compile!(view) # method_name: "_app_views_home_index_html_erb___3699380246341444633_70336654511160" (This method is defined in 'def compile(mod)' below) - # view: #<#:0x00007ff10ea050a8>, view is an instance of
which has same instance variables in the instance of HomeController. - # The method 'view.send' will return the result html! + # view: #<#:0x00007ff10ea050a8>, view is an instance of which has same instance variables defined in the instance of HomeController. + # You get the result html after invoking 'view.send'. view.send(method_name, locals, buffer, &block) end rescue => e @@ -1079,19 +1257,7 @@ module ActionView end end_src - # Make sure the source is in the encoding of the returned code - source.force_encoding(code.encoding) - - # In case we get back a String from a handler that is not in - # BINARY or the default_internal, encode it to the default_internal - source.encode! - - # Now, validate that the source we got back from the template - # handler is valid in the default_internal. This is for handlers - # that handle encoding but screw up - unless source.valid_encoding? - raise WrongEncodingError.new(@source, Encoding.default_internal) - end + # ... # source: def _app_views_home_index_html_erb___1187260686135140546_70244801399180(local_assigns, output_buffer) # _old_virtual_path, @virtual_path = @virtual_path, "home/index";_old_output_buffer = @output_buffer;; @@ -1118,10 +1284,6 @@ module ActionView module Handlers class ERB def call(template) - # First, convert to BINARY, so in case the encoding is - # wrong, we can still find an encoding tag - # (<%# encoding %>) inside the String using a regular - # expression template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) erb = template_source.gsub(ENCODING_TAG, "") @@ -1144,18 +1306,21 @@ module ActionView end ``` +### How can instance variables defined in Controller be accessed in view file? It's time to answer the question before: -How can instance variable like `@users` defined in `HomeController` be accessed in `./app/views/home/index.html.erb`? +How can instance variables like `@users` defined in `HomeController` be accessed in `./app/views/home/index.html.erb`? +I will answer this question by showing the source code below. ```ruby # ./gems/actionview-5.2.2/lib/action_view/rendering.rb module ActionView module Rendering def view_context + # view_context_class is a subclass of ActionView::Base. view_context_class.new( # Let's step into this line later. view_renderer, - view_assigns, # Let's step into this line. + view_assigns, # This line will set the instance variables like '@users' in this example. Let's step into this line. self ) end @@ -1163,13 +1328,14 @@ module ActionView def view_assigns # self: # protected_vars = _protected_ivars - # instance_variables is an instance method of Object and it will return an array. And the array contains @users. + # instance_variables is an instance method of class `Object` and it will return an array. And the array contains @users. variables = instance_variables variables.reject! { |s| protected_vars.include? s } ret = variables.each_with_object({}) { |name, hash| hash[name.slice(1, name.length)] = instance_variable_get(name) } + # ret: {"marked_for_same_origin_verification"=>true, "users"=>[[1, "Lane"], [2, "John"], [4, "Frank"]]} ret end @@ -1180,7 +1346,7 @@ module ActionView end # How this ClassMethods works? Please look at ActiveSupport::Concern in ./gems/activesupport-5.2.2/lib/active_support/concern.rb - # FYI, the method 'append_features' will be executed before method 'included'. + # FYI, the method 'append_features' will be executed automatically before method 'included' executed. # https://apidock.com/ruby/v1_9_3_392/Module/append_features module ClassMethods def view_context_class @@ -1223,55 +1389,31 @@ module ActionView end @cache_hit = {} + assign(assigns) # Let's step into this line. + assign_controller(controller) _prepare_context end def assign(new_assigns) @_assigns = - new_assigns.each do |key, value| - instance_variable_set("@#{key}", value) # This line will set the instance variables in HomeController like '@users' to itself. + new_assigns.each do |key, value| + # This line will set the instance variables (like '@users') in HomeController to itself. + instance_variable_set("@#{key}", value) end end end end ``` +After all rack apps called, user will get the response. ## Part 4: What does `$ rails server` do? -Assume your rails project app class name is `YourProject::Application` (defined in `./config/application.rb`). - -First, I will give you a piece of important code. -```ruby -# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb -module Rails - class Server < ::Rack::Server - def start - #... - log_to_stdout - - super # Will invoke ::Rack::Server#start. - ensure - puts "Exiting" unless @options && options[:daemonize] - end - - def log_to_stdout - # 'wrapped_app' will get an well prepared app from './config.ru' file. - # It's the first time invoke 'wrapped_app'. - # The app is an instance of YourProject::Application. - # The app is not created in 'wrapped_app'. - # It has been created when `require APP_PATH` in previous code, - # just at the 'perform' method in Rails::Command::ServerCommand. - wrapped_app - # ... - end - end -end -``` +If you start Rails by `$ rails server`. You may want to know how this command can be run? -Then, let's start rails by `rails server`. The command `rails` locates at `./bin/`. +The command `rails` locates at `./bin/`. ```ruby #!/usr/bin/env ruby APP_PATH = File.expand_path('../config/application', __dir__) @@ -1307,13 +1449,16 @@ module Rails class << self def invoke(full_namespace, args = [], **config) # ... - # In the end, we got this result: {"rails server" => Rails::Command::ServerCommand} - command = find_by_namespace(namespace, command_name) # command value is Rails::Command::ServerCommand - # command_name is 'server' - command.perform(command_name, args, config) # Rails::Command::ServerCommand.perform + # command_name: 'server' + # After calling `find_by_namespace`, we will get this result: + # command: Rails::Command::ServerCommand + command = find_by_namespace(namespace, command_name) + + # Equals to: Rails::Command::ServerCommand.perform('server', args, config) + command.perform(command_name, args, config) end end - end + end end ``` @@ -1321,33 +1466,27 @@ end # ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb module Rails module Command - class ServerCommand < Base # There is a class method 'perform' in the Base class. - def initialize - end - - # 'perform' here is a instance method. But for Rails::Command::ServerCommand.perform, 'perform' is a class method. - # Where is this 'perform' class method? Answer: In the parent class 'Base'. - def perform - # ... - Rails::Server.new(server_options).tap do |server| - #... - server.start - end - end + # There is a class method 'perform' in the Base class. + class ServerCommand < Base end end end ``` +### Thor +Thor is a toolkit for building powerful command-line interfaces. + +[https://github.com/erikhuda/thor](https://github.com/erikhuda/thor) + Inheritance relationship: `Rails::Command::ServerCommand < Rails::Command::Base < Thor` ```ruby # ./gems/railties-5.2.2/lib/rails/command/base.rb module Rails module Command - class Base < Thor # https://github.com/erikhuda/thor Thor is a toolkit for building powerful command-line interfaces. + class Base < Thor class << self - # command is 'server' + # command: 'server' def perform(command, args, config) #... dispatch(command, args.dup, nil, config) # Thor.dispatch @@ -1359,55 +1498,87 @@ end ``` ```ruby -# ./gems/thor/lib/thor.rb +# ./gems/thor-0.20.3/lib/thor.rb class Thor class << self # meth is 'server' def dispatch(meth, given_args, given_opts, config) # ... - instance = new(args, opts, config) # Here will new a Rails::Command::ServerCommand instance. + # Will new a Rails::Command::ServerCommand instance here + # because 'self' is Rails::Command::ServerCommand. + instance = new(args, opts, config) # ... - # command is {Thor::Command}# - instance.invoke_command(command, trailing || []) # Method 'invoke_command' is in Thor::Invocation. + # Method 'invoke_command' is defined in Thor::Invocation. + # command: {Thor::Command}# + instance.invoke_command(command, trailing || []) end end end -# ./gems/thor/lib/thor/invocation.rb +# ./gems/thor-0.20.3/lib/thor/invocation.rb class Thor - module Invocation # This module is included in Thor. Thor is grandfather of Rails::Command::ServerCommand + # FYI, this module is included in Thor. + # And Thor is grandfather of Rails::Command::ServerCommand + module Invocation def invoke_command(command, *args) # 'invoke_command' is defined at here. # ... - command.run(self, *args) # command is {Thor::Command}# + # self: # + # command: {Thor::Command}# + command.run(self, *args) end end end -# ./gems/thor/lib/thor.rb +# ./gems/thor-0.20.3/lib/thor/command.rb +class Thor + class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name) + def run(instance, args = []) + # ... + # instance: # + # name: "server" + # This line will invoke Rails::Command::ServerCommand#server, + # the instance method 'server' is defined in Rails::Command::ServerCommand implicitly. + # I will show you how the instance method 'server' is implicitly defined. + instance.__send__(name, *args) + end + end +end +``` + +```ruby +# ./gems/thor-0.20.3/lib/thor.rb class Thor # ... include Thor::Base # Will invoke hook method 'Thor::Base.included(self)' end -# ./gems/thor/lib/thor/base.rb +# ./gems/thor-0.20.3/lib/thor/base.rb module Thor module Base class << self - def included(base) # hook method when module 'Thor::Base' included. + def included(base) # hook method when module 'Thor::Base' is included. + # base: Thor + # this line will define `Thor.method_added`. base.extend ClassMethods - base.send :include, Invocation # 'Invocation' included in 'Thor'. So 'invoke_command' will be an instance method of Rails::Command::ServerCommand + # Here module 'Invocation' is included for class 'Thor'. + # Because Thor is grandfather of Rails::Command::ServerCommand, + # 'invoke_command' will be instance method of Rails::Command::ServerCommand + base.send :include, Invocation base.send :include, Shell end end module ClassMethods - # This is also a hook method. In the end, - # this method will help to "alias_method('server', 'perform')". - # The 'server' is the 'server' for `$ rails server`. - # So it's important. We will discuss it later. + # This is a hook method. + # Whenever a instance method is created in Rails::Command::ServerCommand, + # `method_added` will be executed. + # So, when method `perform` is defined in Rails::Command::ServerCommand, + # create_command('perform') will be executed. + # So in the end, method 'server' will be created by alias_method('server', 'perform'). + # And the method 'server' is for the 'server' command in `$ rails server`. def method_added(meth) # ... - # here self is {Class} Rails::Command::ServerCommand + # self: {Class} Rails::Command::ServerCommand create_command(meth) # meth is 'perform'. Let's step into this line. end end @@ -1417,11 +1588,13 @@ end # ./gems/railties-5.2.2/lib/rails/command/base.rb module Rails module Command - module Base # Rails::Command::Base is father of Rails::Command::ServerCommand + # Rails::Command::Base is superclass of Rails::Command::ServerCommand + module Base class << self def create_command(meth) if meth == "perform" - # Instance method 'server' of Rails::Command::ServerCommand will be delegated to 'perform' method now. + # Calling instance method 'server' of Rails::Command::ServerCommand + # will be transferred to call instance method 'perform' method now. alias_method('server', meth) end end @@ -1430,13 +1603,15 @@ module Rails end end -# ./gems/thor/lib/thor/command.rb +# ./gems/thor-0.20.3/lib/thor/command.rb class Thor - class Command + class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name) def run(instance, args = []) #... - # instance is {Rails::Command::ServerCommand}# - instance.__send__(name, *args) # name is 'server'. Will actually invoke 'instance.perform(*args)'. + # instance is {Rails::Command::ServerCommand}# + # name is 'server'. Will actually invoke 'instance.perform(*args)'. + # Equals to invoke Rails::Command::ServerCommand#perform(*args). Let's step into #perform. + instance.__send__(name, *args) end end end @@ -1444,9 +1619,8 @@ end # ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb module Rails module Command - # In ServerCommand class, there is no instance method 'server' explicitly defined. - # It is defined by a hook method 'method_added' class ServerCommand < Base + # This is the method will be executed when `$ rails server`. def perform # ... Rails::Server.new(server_options).tap do |server| @@ -1454,25 +1628,27 @@ module Rails # require APP_PATH will create the 'Rails.application' object. # 'Rails.application' is 'YourProject::Application.new'. # Rack server will start 'Rails.application'. - require APP_PATH + require APP_PATH + Dir.chdir(Rails.application.root) + server.start # Let's step into this line. end end end end end +``` +### Rails::Server#start +```ruby # ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rb module Rails class Server < ::Rack::Server def start print_boot_information - # All lines in the block of trap() will not be executed - # unless a signal of terminating the process (like `$ kill -9 process_id`) has been received. trap(:INT) do - #... exit end @@ -1490,10 +1666,10 @@ module Rails # 'wrapped_app' will get an well prepared app from './config.ru' file. # It's the first time invoke 'wrapped_app'. # The app is an instance of YourProject::Application. - # The app is not created in 'wrapped_app'. + # But the app is not created in 'wrapped_app'. # It has been created when `require APP_PATH` in previous code, # just at the 'perform' method in Rails::Command::ServerCommand. - wrapped_app + wrapped_app # Let's step into this line # ... end @@ -1517,27 +1693,21 @@ module Rack def build_app_and_options_from_config # ... - # self.options[:config] is 'config.ru'. Let's step into this line. + # self.options[:config]: 'config.ru'. Let's step into this line. app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) # ... app end + # This method is called in Rails::Server#start def start(&blk) #... wrapped_app + #... - trap(:INT) do - if server.respond_to?(:shutdown) - server.shutdown - else - exit - end - end - - # server is {Module} Rack::Handler::Puma - # wrapped_app is {YourProject::Application} # - server.run(wrapped_app, options, &blk) # We will step into this line later. + # server: {Module} Rack::Handler::Puma + # wrapped_app: {YourProject::Application} # + server.run(wrapped_app, options, &blk) # We will step into this line (Rack::Handler::Puma.run) later. end end end @@ -1552,15 +1722,16 @@ module Rack return app, options end - - # Let's guess what will 'run Rails.application' do in config.ru? - # First, we will get an instance of YourProject::Application. - # Then we will run it. But 'run' may isn't what you are thinking about. - # Because the 'self' object in config.ru is an instance of Rack::Builder, - # so 'run' is an instance method of Rack::Builder. + + # Let's guess what does 'run Rails.application' do in config.ru? + # Maybe you may think of that: + # Run the instance of YourProject::Application. + # But 'run' maybe not what you are thinking about. + # Because the 'self' object in config.ru is #, + # 'run' is an instance method of Rack::Builder. # Let's look at the definition of the 'run' method: # def run(app) - # @run = app # Just set an instance variable :) + # @run = app # Just set an instance variable of Rack::Builder instance. # end def self.new_from_string(builder_script, file="(rackup)") # Rack::Builder implements a small DSL to iteratively construct Rack applications. @@ -1569,11 +1740,18 @@ module Rack end end end +``` +### Puma +As we see in `Rack::Server#start`, there is `Rack::Handler::Puma.run(wrapped_app, options, &blk)`. + +```ruby # ./gems/puma-3.12.0/lib/rack/handler/puma.rb module Rack module Handler module Puma + # This method is invoked in `Rack::Server#start` : + # Rack::Handler::Puma.run(wrapped_app, options, &blk) def self.run(app, options = {}) conf = self.config(app, options) @@ -1581,10 +1759,9 @@ module Rack launcher = ::Puma::Launcher.new(conf, :events => events) begin - # Let's stop our journey here. It's puma's turn now. # Puma will run your app (instance of YourProject::Application) - launcher.run - rescue Interrupt + launcher.run # Let's step into this line. + rescue Interrupt # Will enter here when you stop puma by running `$ kill -s SIGTERM rails_process_id` puts "* Gracefully stopping, waiting for requests to finish" launcher.stop puts "* Goodbye!" @@ -1593,7 +1770,808 @@ module Rack end end end + +# .gems/puma-3.12.0/lib/puma/launcher.rb +module Puma + # Puma::Launcher is the single entry point for starting a Puma server based on user + # configuration. It is responsible for taking user supplied arguments and resolving them + # with configuration in `config/puma.rb` or `config/puma/.rb`. + # + # It is responsible for either launching a cluster of Puma workers or a single + # puma server. + class Launcher + def initialize(conf, launcher_args={}) + @runner = nil + @config = conf + + # ... + if clustered? + # ... + @runner = Cluster.new(self, @events) + else + @runner = Single.new(self, @events) + end + + # ... + end + + def run + #... + + # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + setup_signals # We will discuss this line later. + + set_process_title + + @runner.run # We will enter `Single.new(self, @events).run` here. + + case @status + when :halt + log "* Stopping immediately!" + when :run, :stop + graceful_stop + when :restart + log "* Restarting..." + ENV.replace(previous_env) + @runner.before_restart + restart! + when :exit + # nothing + end + end + end +end +``` + +```ruby +# .gems/puma-3.12.0/lib/puma/single.rb +module Puma + # This class is instantiated by the `Puma::Launcher` and used + # to boot and serve a Ruby application when no puma "workers" are needed + # i.e. only using "threaded" mode. For example `$ puma -t 1:5` + # + # At the core of this class is running an instance of `Puma::Server` which + # gets created via the `start_server` method from the `Puma::Runner` class + # that this inherits from. + class Single < Runner + def run + # ... + + # @server: Puma::Server.new(app, @launcher.events, @options) + @server = server = start_server # Let's step into this line. + + # ... + thread = server.run # Let's step into this line later. + + # This line will suspend the main process execution. + # And the `thread`'s block (which is method `handle_servers`) will be executed in main process. + # See `Thread#join` for more information. + # I will show you a simple example for using `thread.join`. + # Please search `test_thread_join.rb` in this document. + thread.join + + # The below line will never be executed because `thread` is always running + # and `thread` has joined to main process. + # When `$ kill -s SIGTERM puma_process_id`, the below line will still not be executed + # because the block of `Signal.trap "SIGTERM"` in `Puma::Launcher#setup_signals` will be executed. + # If you remove the line `thread.join`, the below line will be executed, + # but the main process will exit after all code executed and all the threads not joined will be killed. + puts "anything which will never be executed..." + end + end +end +``` +```ruby +# .gems/puma-3.12.0/lib/puma/runner.rb +module Puma + # Generic class that is used by `Puma::Cluster` and `Puma::Single` to + # serve requests. This class spawns a new instance of `Puma::Server` via + # a call to `start_server`. + class Runner + def app + @app ||= @launcher.config.app + end + + def start_server + min_t = @options[:min_threads] + max_t = @options[:max_threads] + + server = Puma::Server.new(app, @launcher.events, @options) + server.min_threads = min_t + server.max_threads = max_t + # ... + + server + end + end +end +``` + +```ruby +# .gems/puma-3.12.0/lib/puma/server.rb +module Puma + class Server + def run(background=true) + #... + queue_requests = @queue_requests + + # This part is important. + # Remember the block of ThreadPool.new will be called when a request added to the ThreadPool instance. + # And the block will process the request by calling method `process_client`. + # Let's step into this line later to see how puma call the block. + @thread_pool = ThreadPool.new(@min_threads, + @max_threads, + IOBuffer) do |client, buffer| + + # Advertise this server into the thread + Thread.current[ThreadLocalKey] = self + + process_now = false + + if queue_requests + process_now = client.eagerly_finish + end + + # ... + if process_now + # process the request. You can treat `client` as request. + # If you want to know more about 'process_client', please read part 3 + # or search 'process_client' in this document. + process_client(client, buffer) + else + client.set_timeout @first_data_timeout + @reactor.add client + end + end + + # ... + + if background # background: true (for this example) + # It's important part. + # Remember puma created a thread here! + # We will know that the thread's job is waiting for requests. + # When a request comes, the thread will transfer the request processing work to a thread in ThreadPool. + # The method `handle_servers` in thread's block will be executed immediately. + @thread = Thread.new { handle_servers } # Let's step into this line to see what I said. + return @thread + else + handle_servers + end + end + + def handle_servers + sockets = [check] + @binder.ios + pool = @thread_pool + queue_requests = @queue_requests + + # ... + + # The thread is always running! + # Yes, it should always be running to transfer the incoming requests. + while @status == :run + begin + # This line will cause current thread waiting until a request is coming. + # So it will be the entry of every request! + ios = IO.select sockets + + ios.first.each do |sock| + if sock == check + break if handle_check + else + if io = sock.accept_nonblock + # You can simply think a Puma::Client instance as a request. + client = Client.new(io, @binder.env(sock)) + + # ... + + # FYI, the method '<<' is redefined. + # Add the request (client) to thread pool means a thread in the pool will process this request (client). + pool << client # Let's step into this line. + + pool.wait_until_not_full # Let's step into this line later. + end + end + end + rescue Object => e + @events.unknown_error self, e, "Listen loop" + end + end + end + end +end +``` + +```ruby +# .gems/puma-3.12.0/lib/puma/thread_pool.rb +module Puma + class ThreadPool + # Maintain a minimum of +min+ and maximum of +max+ threads + # in the pool. + # + # The block passed is the work that will be performed in each + # thread. + # + def initialize(min, max, *extra, &block) + #.. + @mutex = Mutex.new + @todo = [] # @todo is requests (in puma, it's Puma::Client instance) which need to be processed. + @spawned = 0 # The count of @spawned threads. + @min = Integer(min) # @min threads count + @max = Integer(max) # @max threads count + @block = block # block will be called in method `spawn_thread` to processed a request. + @workers = [] + @reaper = nil + + @mutex.synchronize do + @min.times { spawn_thread } # Puma started @min count threads. + end + end + + def spawn_thread + @spawned += 1 + + # Run a new Thread now. + # The block of the thread will be executed separately from the calling thread. + th = Thread.new(@spawned) do |spawned| + # Thread name is new in Ruby 2.3 + Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=) + block = @block + mutex = @mutex + #... + + extra = @extra.map { |i| i.new } + + # Pay attention to here: + # 'while true' means this part will always be running. + # And there will be @min count threads always running! + # Puma uses these threads to process requests. + # The line: 'not_empty.wait(mutex)' will make current thread waiting. + while true + work = nil + + continue = true + + mutex.synchronize do + while todo.empty? + if @trim_requested > 0 + @trim_requested -= 1 + continue = false + not_full.signal + break + end + + if @shutdown + continue = false + break + end + + @waiting += 1 # `@waiting` is the waiting threads count. + not_full.signal + + # This line will cause current thread waiting + # until `not_empty.signal` executed in some other place to wake it up . + # Actually, `not_empty.signal` is located at `def <<(work)` in the same file. + # You can search `def <<(work)` in this document. + # Method `<<` is used in method `handle_servers`: `pool << client` in Puma::Server#run. + # `pool << client` means add a request to the thread pool, + # and then the thread waked up will process the request. + not_empty.wait mutex + + @waiting -= 1 + end + + # `work` is the request (in puma, it's Puma::Client instance) which need to be processed. + work = todo.shift if continue + end + + break unless continue + + if @clean_thread_locals + ThreadPool.clean_thread_locals + end + + begin + # `block.call` will switch program to the block definition part. + # The Block definition part is in `Puma::Server#run`: + # @thread_pool = ThreadPool.new(@min_threads, + # @max_threads, + # IOBuffer) do |client, buffer| #...; end + # So please search `ThreadPool.new` in this document to look back. + block.call(work, *extra) + rescue Exception => e + STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})" + end + end + + mutex.synchronize do + @spawned -= 1 + @workers.delete th + end + end # end of the Thread.new. + + @workers << th + + th + end + + def wait_until_not_full + @mutex.synchronize do + while true + return if @shutdown + + # If we can still spin up new threads and there + # is work queued that cannot be handled by waiting + # threads, then accept more work until we would + # spin up the max number of threads. + return if @todo.size - @waiting < @max - @spawned + + @not_full.wait @mutex + end + end + end + + # Add +work+ to the todo list for a Thread to pickup and process. + def <<(work) + @mutex.synchronize do + if @shutdown + raise "Unable to add work while shutting down" + end + + # work: # + # You can treat Puma::Client instance as a request. + @todo << work + + if @waiting < @todo.size and @spawned < @max + spawn_thread # Create one more thread to process request. + end + + # Wake up the waiting thread to process the request. + # The waiting thread is defined in the same file: Puma::ThreadPool#spawn_thread. + # There are these code in `spawn_thread`: + # while true + # # ... + # not_empty.wait mutex + # # ... + # block.call(work, *extra) # This line will process the request. + # end + @not_empty.signal + end + end + end +end +``` + +### Conclusion +In conclusion, `$ rails server` will execute `Rails::Command::ServerCommand#perform`. + +In `#perform`, call `Rails::Server#start`. Then call `Rack::Server#start`. + +Then call `Rack::Handler::Puma.run(YourProject::Application.new)`. + +In `.run`, Puma will new a always running Thread to `ios = IO.select(sockets)`. + +Request is created from `ios` object. + +A thread in puma threadPool will process the request. + +The thread will invoke rack apps' `call` to get the response for the request. + +### Stop Puma +When you stop puma by running `$ kill -s SIGTERM puma_process_id`, you will enter `setup_signals` in `Puma::Launcher#run`. +```ruby +# .gems/puma-3.12.0/lib/puma/launcher.rb +module Puma + # Puma::Launcher is the single entry point for starting a Puma server based on user + # configuration. + class Launcher + def run + #... + + # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + setup_signals # Let's step into this line. + + set_process_title + + @runner.run + + # ... + end + + # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + # Signal.list #=> {"EXIT"=>0, "HUP"=>1, "INT"=>2, "QUIT"=>3, "ILL"=>4, "TRAP"=>5, "IOT"=>6, "ABRT"=>6, "FPE"=>8, "KILL"=>9, "BUS"=>7, "SEGV"=>11, "SYS"=>31, "PIPE"=>13, "ALRM"=>14, "TERM"=>15, "URG"=>23, "STOP"=>19, "TSTP"=>20, "CONT"=>18, "CHLD"=>17, "CLD"=>17, "TTIN"=>21, "TTOU"=>22, "IO"=>29, "XCPU"=>24, "XFSZ"=>25, "VTALRM"=>26, "PROF"=>27, "WINCH"=>28, "USR1"=>10, "USR2"=>12, "PWR"=>30, "POLL"=>29} + # Press `Control + C` to quit means 'SIGINT'. + def setup_signals + begin + # After runnning `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`. + Signal.trap "SIGTERM" do + graceful_stop # Let's step into this line. + + raise SignalException, "SIGTERM" + end + rescue Exception + log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!" + end + + begin + Signal.trap "SIGUSR2" do + restart + end + rescue Exception + log "*** SIGUSR2 not implemented, signal based restart unavailable!" + end + + begin + Signal.trap "SIGUSR1" do + phased_restart + end + rescue Exception + log "*** SIGUSR1 not implemented, signal based restart unavailable!" + end + + begin + Signal.trap "SIGINT" do + if Puma.jruby? + @status = :exit + graceful_stop + exit + end + + stop + end + rescue Exception + log "*** SIGINT not implemented, signal based gracefully stopping unavailable!" + end + + begin + Signal.trap "SIGHUP" do + if @runner.redirected_io? + @runner.redirect_io + else + stop + end + end + rescue Exception + log "*** SIGHUP not implemented, signal based logs reopening unavailable!" + end + end + + def graceful_stop + # @runner: instance of Puma::Single (for this example) + @runner.stop_blocked # Let's step into this line. + log "=== puma shutdown: #{Time.now} ===" + log "- Goodbye!" + end + end +end + +# .gems/puma-3.12.0/lib/puma/launcher.rb +module Puma + class Single < Runner + def run + # ... + + # @server: Puma::Server.new(app, @launcher.events, @options) + @server = server = start_server # Let's step into this line. + + # ... + thread = server.run + + # This line will suspend the main process execution. + # And the `thread`'s block (which is method `handle_servers`) will be executed in main process. + thread.join + end + + def stop_blocked + log "- Gracefully stopping, waiting for requests to finish" + @control.stop(true) if @control + # @server: instance of Puma::Server + @server.stop(true) # Let's step into this line + end + end +end + +# .gems/puma-3.12.0/lib/puma/server.rb +module Puma + class Server + def initialize(app, events=Events.stdio, options={}) + # This method returns `IO.pipe`. + @check, @notify = Puma::Util.pipe # @check, @notify is a pair. + + @status = :stop + end + + def run(background=true) + # ... + @thread_pool = ThreadPool.new(@min_threads, + @max_threads, + IOBuffer) do |client, buffer| + + #... + # process the request. + process_client(client, buffer) + #... + end + + # The created @thread is the @thread in `stop` method below. + @thread = Thread.new { handle_servers } + return @thread + end + + # Stops the acceptor thread and then causes the worker threads to finish + # off the request queue before finally exiting. + def stop(sync=false) + # This line which change the :status to :stop. + notify_safely(STOP_COMMAND) # Let's step into this line. + + # The @thread is just the always running Thread created in `Puma::Server#run`. + # Please look at method `Puma::Server#run`. + # `@thread.join` will suspend the main process execution. + # And the @thread's code will continue be executed in main process. + # Because @thread is waiting for incoming request, the next executed code + # will be `ios = IO.select sockets` in method `handle_servers`. + @thread.join if @thread && sync + end + + def notify_safely(message) + @notify << message + end + + def handle_servers + begin + check = @check + sockets = [check] + @binder.ios + pool = @thread_pool + #... + + while @status == :run + # After `@thread.join` in main process, this line will be executed and will return result. + ios = IO.select sockets + + ios.first.each do |sock| + if sock == check + # The @status is updated to :stop for this example in `handle_check`. + break if handle_check # Let's step into this line. + else + if io = sock.accept_nonblock + client = Client.new(io, @binder.env(sock)) + + # ... + pool << client + pool.wait_until_not_full + end + end + end + end + + # Let's step into `graceful_shutdown`. + graceful_shutdown if @status == :stop || @status == :restart + + # ... + ensure + @check.close + @notify.close + + # ... + end + end + + def handle_check + cmd = @check.read(1) + + case cmd + when STOP_COMMAND + @status = :stop # The @status is updated to :stop for this example. + return true + when HALT_COMMAND + @status = :halt + return true + when RESTART_COMMAND + @status = :restart + return true + end + + return false + end + + def graceful_shutdown + if @thread_pool + @thread_pool.shutdown # Let's step into this line. + end + end + end +end +``` + +```ruby +module Puma + class ThreadPool + # Tell all threads in the pool to exit and wait for them to finish. + def shutdown(timeout=-1) + threads = @mutex.synchronize do + @shutdown = true + # `broadcast` will wakes up all threads waiting for this lock. + @not_empty.broadcast + @not_full.broadcast + + # ... + + # dup workers so that we join them all safely + @workers.dup + end + + # Wait for threads to finish without force shutdown. + threads.each do |thread| + # I will use a simple example to show you what `thread.join` do later. + thread.join # I guess `thread.join` means join the executing of thread to the calling (main) process. + end + + @spawned = 0 + @workers = [] + end + + def initialize(min, max, *extra, &block) + #.. + @mutex = Mutex.new + @spawned = 0 # The count of @spawned threads. + @todo = [] # @todo is requests (in puma, it's Puma::Client instance) which need to be processed. + @min = Integer(min) # @min threads count + @block = block # block will be called in method `spawn_thread` to processed a request. + @workers = [] + + @mutex.synchronize do + @min.times { spawn_thread } # Puma started @min count threads. + end + end + + def spawn_thread + @spawned += 1 + + # Run a new Thread now. + # The block of the thread will be executed separately from the calling thread. + th = Thread.new(@spawned) do |spawned| + block = @block + mutex = @mutex + #... + + while true + work = nil + + continue = true + + mutex.synchronize do + while todo.empty? + # ... + + if @shutdown + continue = false + break + end + + # ... + # After `@not_empty.broadcast` in executed in '#shutdown', `not_empty` is waked up. + # Ruby will continue to execute the next line here. + not_empty.wait mutex + + @waiting -= 1 + end + + # ... + end + + break unless continue + + # ... + end + + mutex.synchronize do + @spawned -= 1 + @workers.delete th + end + end # end of the Thread.new. + + @workers << th + + th + end + end +end +``` + +Try to run this `test_thread_join.rb`. + +You will find that if there is no `thread.join`, you can only see `==== I am the main thread.` in console. + +After you added `thread.join`, you can see `~~~~ 1\n~~~~ 2\n ~~~~ 3` in console. + +```ruby +# ./test_thread_join.rb +thread = Thread.new() do + 3.times do |n| + puts "~~~~ " + n.to_s + end +end + +# sleep 1 +puts "==== I am the main thread." + +# thread.join # Try to uncomment these two lines to see the differences. +# puts "==== after thread.join" +``` + +So all the threads in the ThreadPool joined and finished. + +And please look at the caller in block of `Signal.trap "SIGTERM"` below. + +```ruby +# .gems/puma-3.12.0/lib/puma/launcher.rb +module Puma + # Puma::Launcher is the single entry point for starting a Puma server based on user + # configuration. + class Launcher + def run + #... + + # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + setup_signals # Let's step into this line. + + set_process_title + + # Process.pid: 42264 + puts "Process.pid: #{Process.pid}" + + @runner.run + + # ... + end + + def setup_signals + # ... + begin + # After running `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`. + Signal.trap "SIGTERM" do + # I added `caller` to see the calling stack. + # caller: [ + # "../gems/puma-3.12.0/lib/puma/single.rb:118:in `join'", + # "../gems/puma-3.12.0/lib/puma/single.rb:118:in `run'", + # "../gems/puma-3.12.0/lib/puma/launcher.rb:186:in `run'", + # "../gems/puma-3.12.0/lib/rack/handler/puma.rb:70:in `run'", + # "../gems/rack-2.0.6/lib/rack/server.rb:298:in `start'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:55:in `start'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:149:in `block in perform'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:144:in `tap'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:144:in `perform'", + # "../gems/thor-0.20.3/lib/thor/command.rb:27:in `run'", + # "../gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command'", + # "../gems/thor-0.20.3/lib/thor.rb:391:in `dispatch'", + # "../gems/railties-5.2.2/lib/rails/command/base.rb:65:in `perform'", + # "../gems/railties-5.2.2/lib/rails/command.rb:46:in `invoke'", + # "../gems/railties-5.2.2/lib/rails/commands.rb:18:in `'", + # "../path/to/your_project/bin/rails:5:in `require'", + # "../path/to/your_project/bin/rails:5:in `
'" + # ] + puts "caller: #{caller.inspect}" + + # Process.pid: 42264 which is the same as the `Process.pid` in the Puma::Launcher#run. + puts "Process.pid: #{Process.pid}" + + graceful_stop + + # This SignalException is not rescued in the caller stack. + # So in the the caller stack, Ruby will goto the `ensure` part in + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:55:in `start'". + # So the last code executed is `puts "Exiting" unless @options && options[:daemonize]` + # when running `$ kill -s SIGTERM puma_process_id`. + # You can search `puts "Exiting"` in this document to see it. + raise SignalException, "SIGTERM" + end + rescue Exception + # This `rescue` is only for `Signal.trap "SIGTERM"`, not for `raise SignalException, "SIGTERM"`. + log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!" + end + end + end +end ``` -Now puma has been started successfully with your app (instance of YourProject::Application) running. +Welcome to point out the mistakes in this article :) From 745dabb3912bde233789cf2dcb64379e95ec6598 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Mon, 4 Mar 2019 13:51:39 +0800 Subject: [PATCH 06/16] Refine README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1872079..4441779 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # Learn-Rails-by-Reading-Source-Code -## Part 0: Before you research Rails 5 source code +## Part 0: Before reading Rails 5 source code 1) I suggest you learn Rack [http://rack.github.io/](http://rack.github.io/) first. -You need to know that an object respond to `call` method is the most important convention. +In rack, an object with `call` method is a rack app. -So which is the object with `call` method in Rails App? I will answer this question in Part 1. - -2) You need a good IDE with debugging function. I use [RubyMine](https://www.jetbrains.com/). +So what is the object with `call` method in Rails? I will answer this question in Part 1. +2) You need a good IDE which can help for debugging. I use [RubyMine](https://www.jetbrains.com/). ### What you will learn from this tutorial? * How does Rails start your application? @@ -24,7 +23,7 @@ So which is the object with `call` method in Rails App? I will answer this quest I should start with the command `$ rails server`, but I put this to Part 4. Because it's a little bit complex. ## Part 1: Your app: an instance of YourProject::Application -Assume your Rails app class name is `YourProject::Application` (defined in `./config/application.rb`). +Assume your Rails app's class name is `YourProject::Application` (defined in `./config/application.rb`). First, I will give you a piece of important code. ```ruby From 351dc2ace9c9688c15c6313ba398c526d9ac98ea Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Mon, 4 Mar 2019 19:00:24 +0800 Subject: [PATCH 07/16] Add more information. --- README.md | 327 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 221 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 4441779..72de886 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ module Rails def perform # ... Rails::Server.new(server_options).tap do |server| - # APP_PATH is '/Users/your_name/your-project/config/application'. + # APP_PATH is '/Users/your_name/your_project/config/application'. # require APP_PATH will create the 'Rails.application' object. # Actually, 'Rails.application' is an instance of `YourProject::Application`. # Rack server will start 'Rails.application'. @@ -73,7 +73,7 @@ require_relative 'config/environment' run Rails.application # It seems that this is the app. ``` -Let's test it by `Rails.application.respond_to?(:call)`, it returns true. +Let's test it by `Rails.application.respond_to?(:call)`, it returns `true`. Let's step into `Rails.application`. @@ -95,7 +95,7 @@ module Rails end ``` -Because `Rails.application.respond_to?(:call)` returns true, `app_class.instance` has a `call` method. +Because `Rails.application.respond_to?(:call)` returns `true`, `app_class.instance` has a `call` method. When was `app_class` set? ```ruby @@ -110,20 +110,26 @@ module Rails end ``` -`Rails::Application` is inherited like below, +`Rails::Application` is inherited by `YourProject`, ```ruby # ./config/application.rb module YourProject - # The hooked method `inherited` defined in eigenclass of 'Rails::Application' is invoked. + # The hooked method `inherited` will be invoked here. class Application < Rails::Application end end ``` -`YourProject::Application` will become the `Rails.app_class`. +So `YourProject::Application` is the `Rails.app_class` here. -You may have a question: how does rails enter this file (`./config/application.rb`)? +You may have a question: When does Rails execute the code in `./config/application.rb`? -Let's look back to `config.ru` to see the first line of this file `require_relative 'config/environment'`. +To answer this question, we need to look back to `config.ru`. +```ruby +# ./config.ru +require_relative 'config/environment' # Let's step into this line. + +run Rails.application # It seems that this is the app. +``` ```ruby # ./config/environment.rb @@ -145,16 +151,18 @@ require 'rails/all' Bundler.require(*Rails.groups) module YourProject - # The hooked method `inherited` defined in eigenclass of 'Rails::Application' is invoked. + # The hooked method `inherited` will be invoked here. class Application < Rails::Application config.load_defaults 5.2 config.i18n.default_locale = :zh end end ``` -Let's replace `app_class.instance` to `YourProject::Application.instance`. +Because `YourProject::Application` is `Rails.app_class`, `app_class.instance` is `YourProject::Application.instance`. -But where is the `call` method? `call` method should be a method of `YourProject::Application.instance`. +But where is the `call` method? + +`call` method should be a method of `YourProject::Application.instance`. The `call` method processes every request. Here it is. ```ruby @@ -183,11 +191,11 @@ end Ancestor's chain is `YourProject::Application < Rails::Application < Rails::Engine < Rails::Railtie`. -So `YourProject::Application.new.respond_to?(:call)` will return true. +So `YourProject::Application.new.respond_to?(:call)` returns `true`. But what does `app_class.instance` really do? -`instance` is just a method name. What we really need is `app_class.new`. +`instance` is just a method name. What we really expects is something like `app_class.new`. Let's look at the definition of `instance`. ```ruby @@ -195,15 +203,15 @@ Let's look at the definition of `instance`. module Rails class Application < Engine def instance - super.run_load_hooks! # This line confused me. + super.run_load_hooks! # This line confused me at the beginning. end end end ``` -After a deep research, I realized that this code is equal to +After a deep research, I realized that this code is equal to: ```ruby def instance - return_value = super # Keyword 'super' will call the ancestor's same name method: 'instance'. + return_value = super # Keyword 'super' means call the ancestor's same name method: 'instance'. return_value.run_load_hooks! end ``` @@ -220,7 +228,7 @@ module Rails end end ``` -And `YourProject::Application.new` is `Rails.application`. + ```ruby module Rails def application @@ -228,22 +236,27 @@ module Rails end end ``` + +So `YourProject::Application.new` is `Rails.application`. + Rack server will start `Rails.application` in the end. -It is an important object in Rails. +`Rails.application` is an important object in Rails. -And you'll only have one `Rails.application` in one process. Multiple threads shared only one `Rails.application`. +And you'll only have one `Rails.application` in one puma process. + +Multiple threads in a puma process shares the `Rails.application`. ## Part 2: config -First time we see the `config` is in `./config/application.rb`. +The first time we see the `config` is in `./config/application.rb`. ```ruby # ./config/application.rb #... module YourProject class Application < Rails::Application - # Actually, config is a method of YourProject::Application. - # It is defined in it's grandfather's father: Rails::Railtie - config.load_defaults 5.2 # Let's step into this line to see what config is. + # Actually, `config` is a method of `YourProject::Application`. + # It is defined in it's grandfather's father: `Rails::Railtie` + config.load_defaults 5.2 # Let's step into this line to see what is config. config.i18n.default_locale = :zh end end @@ -253,13 +266,13 @@ end module Rails class Railtie class << self - # Method :config is defined here. - # Actually, method :config is delegated to another object `:instance`. + # Method `:config` is defined here. + # Actually, method `:config` is delegated to another object `:instance`. delegate :config, to: :instance # Call `YourProject::Application.config` will actually call `YourProject::Application.instance.config` def instance - # return an instance of YourProject::Application. + # return an instance of `YourProject::Application`. # Call `YourProject::Application.config` will actually call `YourProject::Application.new.config` @instance ||= new end @@ -272,7 +285,9 @@ module Rails class Application < Engine class << self def instance - return_value = super # 'super' will call :instance method in Railtie, which will return an instance of YourProject::Application. + # 'super' will call `:instance` method in `Railtie`, + # which will return an instance of `YourProject::Application`. + return_value = super return_value.run_load_hooks! end end @@ -282,21 +297,19 @@ module Rails @ran_load_hooks = true # ... - self # self is an instance of YourProject::Application. And it is Rails.application. + self # `self` is an instance of `YourProject::Application`, and `self` is `Rails.application`. end - # This is the method config. + # This is the method `config`. def config - # It is an instance of class Rails::Application::Configuration. - # Please notice that Rails::Application is superclass of YourProject::Application (self's class). + # It is an instance of class `Rails::Application::Configuration`. + # Please notice that `Rails::Application` is superclass of `YourProject::Application` (self's class). @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from)) end end end ``` -In the end, `YourProject::Application.config` will become `Rails.application.config`. - -`YourProject::Application.config === Rails.application.config` returns true. +In the end, `YourProject::Application.config === Rails.application.config` returns `true`. Invoke Class's `config` method become invoke the class's instance's `config` method. @@ -309,13 +322,13 @@ module Rails end end ``` -So `Rails.configuration === Rails.application.config # return ture.`. +So `Rails.configuration === Rails.application.config` returns `true`. +FYI: ```ruby module Rails class Application class Configuration < ::Rails::Engine::Configuration - end end @@ -328,7 +341,7 @@ module Rails #... @middleware = Rails::Configuration::MiddlewareStackProxy.new end - end + end end class Railtie @@ -470,10 +483,55 @@ module Rails end def app - # You may want to know when does the @app first time initialized. + # FYI, + # caller: [ + # "../gems/railties-5.2.2/lib/rails/application/finisher.rb:47:in `block in '", + # "../gems/railties-5.2.2/lib/rails/initializable.rb:32:in `instance_exec'", + # "../gems/railties-5.2.2/lib/rails/initializable.rb:32:in `run'", + # "../gems/railties-5.2.2/lib/rails/initializable.rb:63:in `block in run_initializers'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:228:in `block in tsort_each'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:431:in `each_strongly_connected_component_from'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:349:in `block in each_strongly_connected_component'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:347:in `each'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:347:in `call'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:347:in `each_strongly_connected_component'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:226:in `tsort_each'", + # "../ruby-2.6.0/lib/ruby/2.6.0/tsort.rb:205:in `tsort_each'", + # "../gems/railties-5.2.2/lib/rails/initializable.rb:61:in `run_initializers'", + # "../gems/railties-5.2.2/lib/rails/application.rb:361:in `initialize!'", + # "/Users/lanezhang/projects/mine/free-erp/config/environment.rb:5:in `'", + # "config.ru:2:in `require_relative'", "config.ru:2:in `block in
'", + # "../gems/rack-2.0.6/lib/rack/builder.rb:55:in `instance_eval'", + # "../gems/rack-2.0.6/lib/rack/builder.rb:55:in `initialize'", + # "config.ru:in `new'", "config.ru:in `
'", + # "../gems/rack-2.0.6/lib/rack/builder.rb:49:in `eval'", + # "../gems/rack-2.0.6/lib/rack/builder.rb:49:in `new_from_string'", + # "../gems/rack-2.0.6/lib/rack/builder.rb:40:in `parse_file'", + # "../gems/rack-2.0.6/lib/rack/server.rb:320:in `build_app_and_options_from_config'", + # "../gems/rack-2.0.6/lib/rack/server.rb:219:in `app'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:27:in `app'", + # "../gems/rack-2.0.6/lib/rack/server.rb:357:in `wrapped_app'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:92:in `log_to_stdout'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:54:in `start'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:149:in `block in perform'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:144:in `tap'", + # "../gems/railties-5.2.2/lib/rails/commands/server/server_command.rb:144:in `perform'", + # "../gems/thor-0.20.3/lib/thor/command.rb:27:in `run'", + # "../gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command'", + # "../gems/thor-0.20.3/lib/thor.rb:391:in `dispatch'", + # "../gems/railties-5.2.2/lib/rails/command/base.rb:65:in `perform'", + # "../gems/railties-5.2.2/lib/rails/command.rb:46:in `invoke'", + # "../gems/railties-5.2.2/lib/rails/commands.rb:18:in `'", + # "../path/to/your_project/bin/rails:5:in `require'", + # "../path/to/your_project/bin/rails:5:in `
'" + # ] + puts "caller: #{caller.inspect}" + + # You may want to know when is the @app first time initialized. # It is initialized when 'config.ru' is load by rack server. - # Please look at Rack::Server#build_app_and_options_from_config for more information. - # When Rails.application.initialize! (in ./config/environment.rb), @app is initialized. + # Please search `Rack::Server#build_app_and_options_from_config` in this document for more information. + # When `Rails.application.initialize!` (in ./config/environment.rb) executed, @app is initialized. @app || @app_build_lock.synchronize { # '@app_build_lock = Mutex.new', so multiple threads share one '@app'. @app ||= begin # In the end, config.middleware will be an instance of ActionDispatch::MiddlewareStack with preset instance variable @middlewares (which is an Array). @@ -702,8 +760,8 @@ module ActionDispatch req.path_parameters = set_params.merge parameters - # route is an instance of ActionDispatch::Journey::Route. - # route.app is an instance of ActionDispatch::Routing::RouteSet::Dispatcher. + # 'route' is an instance of ActionDispatch::Journey::Route. + # 'route.app' is an instance of ActionDispatch::Routing::RouteSet::Dispatcher. status, headers, body = route.app.serve(req) # Let's step into method 'serve' if "pass" == headers["X-Cascade"] @@ -755,7 +813,8 @@ module ActionDispatch def serve(req) params = req.path_parameters # params: { action: 'index', controller: 'home' } controller = controller(req) # controller: HomeController - # The definition of make_response! is ActionDispatch::Response.create.tap do |res| res.request = request; end + # The definition of 'make_response!' is + # ActionDispatch::Response.create.tap { |res| res.request = request; } res = controller.make_response!(req) dispatch(controller, params[:action], req, res) # Let's step into this line. rescue ActionController::RoutingError @@ -810,9 +869,9 @@ module ActionController if middleware_stack.any? middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env else - # self is HomeController, so in this line Rails will new a HomeController instance. - # See `HomeController.ancestors`, you can find many superclasses. - # These are some typical ancestors of HomeController. + # 'self' is HomeController, so for this line Rails will new a HomeController instance. + # Invoke `HomeController.ancestors`, you can find many superclasses of HomeController. + # These are some typical superclasses of HomeController. # HomeController # < ApplicationController # < ActionController::Base @@ -944,7 +1003,7 @@ module AbstractController class Base def process_action(method_name, *args) # self: #, method_name: 'index' - # In the end, method 'send_action' is method 'send' as the below line shown. + # In the end, method 'send_action' is method 'send' by `alias send_action send` send_action(method_name, *args) end @@ -1077,7 +1136,7 @@ end module ActionController module Renderers def render_to_body(options) - _render_to_body_with_renderer(options) || super # Let's step into this line and super later. + _render_to_body_with_renderer(options) || super # Let's step into this line and 'super' later. end # For this example, this method return nil in the end. @@ -1101,7 +1160,7 @@ end module ActionController module Rendering def render_to_body(options = {}) - super || _render_in_priorities(options) || " " # Let's step into super + super || _render_in_priorities(options) || " " # Let's step into 'super' end end end @@ -1203,7 +1262,7 @@ module ActionView compile!(view) # method_name: "_app_views_home_index_html_erb___3699380246341444633_70336654511160" (This method is defined in 'def compile(mod)' below) # view: #<#:0x00007ff10ea050a8>, view is an instance of which has same instance variables defined in the instance of HomeController. - # You get the result html after invoking 'view.send'. + # You will get the result html after invoking 'view.send'. view.send(method_name, locals, buffer, &block) end rescue => e @@ -1340,7 +1399,7 @@ module ActionView end def view_context_class - # will return a subclass of ActionView::Base. + # Will return a subclass of ActionView::Base. @_view_context_class ||= self.class.view_context_class end @@ -1410,7 +1469,7 @@ After all rack apps called, user will get the response. ## Part 4: What does `$ rails server` do? -If you start Rails by `$ rails server`. You may want to know how this command can be run? +If you start Rails by `$ rails server`. You may want to know what does this command do? The command `rails` locates at `./bin/`. ```ruby @@ -1548,33 +1607,35 @@ end # ./gems/thor-0.20.3/lib/thor.rb class Thor # ... - include Thor::Base # Will invoke hook method 'Thor::Base.included(self)' + include Thor::Base # Will invoke hooked method 'Thor::Base.included(self)' end # ./gems/thor-0.20.3/lib/thor/base.rb module Thor module Base class << self - def included(base) # hook method when module 'Thor::Base' is included. + # 'included' is a hooked method. + # When module 'Thor::Base' is included, method 'included' is executed. + def included(base) # base: Thor # this line will define `Thor.method_added`. base.extend ClassMethods - # Here module 'Invocation' is included for class 'Thor'. + # Module 'Invocation' is included for class 'Thor' here. # Because Thor is grandfather of Rails::Command::ServerCommand, # 'invoke_command' will be instance method of Rails::Command::ServerCommand - base.send :include, Invocation + base.send :include, Invocation # 'invoke_command' is defined in module Invocation base.send :include, Shell end end module ClassMethods - # This is a hook method. - # Whenever a instance method is created in Rails::Command::ServerCommand, + # 'method_added' is a hooked method. + # When an instance method is created in Rails::Command::ServerCommand, # `method_added` will be executed. # So, when method `perform` is defined in Rails::Command::ServerCommand, - # create_command('perform') will be executed. + # `method_added` will be executed and create_command('perform') will be invoked. # So in the end, method 'server' will be created by alias_method('server', 'perform'). - # And the method 'server' is for the 'server' command in `$ rails server`. + # And the method 'server' is for the 'server' in command `$ rails server`. def method_added(meth) # ... # self: {Class} Rails::Command::ServerCommand @@ -1593,7 +1654,7 @@ module Rails def create_command(meth) if meth == "perform" # Calling instance method 'server' of Rails::Command::ServerCommand - # will be transferred to call instance method 'perform' method now. + # will be transferred to call instance method 'perform'. alias_method('server', meth) end end @@ -1607,9 +1668,11 @@ class Thor class Command < Struct.new(:name, :description, :long_description, :usage, :options, :ancestor_name) def run(instance, args = []) #... - # instance is {Rails::Command::ServerCommand}# - # name is 'server'. Will actually invoke 'instance.perform(*args)'. - # Equals to invoke Rails::Command::ServerCommand#perform(*args). Let's step into #perform. + # instance: {Rails::Command::ServerCommand}# + # name: 'server'. + # Will actually invoke 'instance.perform(*args)'. + # Equals to invoke Rails::Command::ServerCommand#perform(*args). + # Let's step into Rails::Command::ServerCommand#perform. instance.__send__(name, *args) end end @@ -1619,11 +1682,11 @@ end module Rails module Command class ServerCommand < Base - # This is the method will be executed when `$ rails server`. + # This is the method will be executed when `$ rails server`. def perform # ... Rails::Server.new(server_options).tap do |server| - # APP_PATH is '/Users/your_name/your-project/config/application'. + # APP_PATH is '/Users/your_name/your_project/config/application'. # require APP_PATH will create the 'Rails.application' object. # 'Rails.application' is 'YourProject::Application.new'. # Rack server will start 'Rails.application'. @@ -1654,7 +1717,8 @@ module Rails create_tmp_directories setup_dev_caching - log_to_stdout # This line is important. Although the method name seems not. Let step into this line. + # This line is important. Although the method name seems not. + log_to_stdout# Let step into this line. super # Will invoke ::Rack::Server#start. I will show you later. ensure @@ -1662,7 +1726,7 @@ module Rails end def log_to_stdout - # 'wrapped_app' will get an well prepared app from './config.ru' file. + # 'wrapped_app' will get an well prepared Rack app from './config.ru' file. # It's the first time invoke 'wrapped_app'. # The app is an instance of YourProject::Application. # But the app is not created in 'wrapped_app'. @@ -1715,7 +1779,8 @@ end module Rack module Builder def self.parse_file(config, opts = Server::Options.new) - cfgfile = ::File.read(config) # config is 'config.ru' + # config: 'config.ru' + cfgfile = ::File.read(config) app = new_from_string(cfgfile, config) @@ -1723,14 +1788,14 @@ module Rack end # Let's guess what does 'run Rails.application' do in config.ru? - # Maybe you may think of that: - # Run the instance of YourProject::Application. + # You may guess that: + # Run YourProject::Application instance. # But 'run' maybe not what you are thinking about. - # Because the 'self' object in config.ru is #, + # Because the 'self' object in 'config.ru' is #, # 'run' is an instance method of Rack::Builder. # Let's look at the definition of the 'run' method: # def run(app) - # @run = app # Just set an instance variable of Rack::Builder instance. + # @run = app # Just set an instance variable for Rack::Builder instance. # end def self.new_from_string(builder_script, file="(rackup)") # Rack::Builder implements a small DSL to iteratively construct Rack applications. @@ -1749,7 +1814,7 @@ As we see in `Rack::Server#start`, there is `Rack::Handler::Puma.run(wrapped_app module Rack module Handler module Puma - # This method is invoked in `Rack::Server#start` : + # This method is invoked in `Rack::Server#start`: # Rack::Handler::Puma.run(wrapped_app, options, &blk) def self.run(app, options = {}) conf = self.config(app, options) @@ -1760,7 +1825,7 @@ module Rack begin # Puma will run your app (instance of YourProject::Application) launcher.run # Let's step into this line. - rescue Interrupt # Will enter here when you stop puma by running `$ kill -s SIGTERM rails_process_id` + rescue Interrupt puts "* Gracefully stopping, waiting for requests to finish" launcher.stop puts "* Goodbye!" @@ -1788,6 +1853,7 @@ module Puma # ... @runner = Cluster.new(self, @events) else + # For this example, it is Single.new. @runner = Single.new(self, @events) end @@ -1797,7 +1863,7 @@ module Puma def run #... - # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM process_id` received. setup_signals # We will discuss this line later. set_process_title @@ -1926,6 +1992,9 @@ module Puma # ... if background # background: true (for this example) + # + puts "#{Thread.current.object_id}" + # It's important part. # Remember puma created a thread here! # We will know that the thread's job is waiting for requests. @@ -2155,7 +2224,43 @@ A thread in puma threadPool will process the request. The thread will invoke rack apps' `call` to get the response for the request. -### Stop Puma +### Exiting Puma +#### Process and Thread +For Puma is multiple threads, we need to have some basic concepts about Process and Thread. + +This link's good for you to obtain the concepts: [Process and Thread](https://stackoverflow.com/questions/4894609/will-a-cpu-process-have-at-least-one-thread) + +In the next part, you will often see `thread.join`. + +I will use a simple example to tell what does `thread.join` do. + +Try to run `test_thread_join.rb`. + +```ruby +# ./test_thread_join.rb +thread = Thread.new() do + 3.times do |n| + puts "~~~~ " + n.to_s + end +end + +# sleep 1 +puts "==== I am the main thread." + +# thread.join # Try to uncomment these two lines to see the differences. +# puts "==== after thread.join" +``` +You will find that if there is no `thread.join`, you can only see `==== I am the main thread.` in console. + +After you added `thread.join`, you can see: +```ruby +~~~~ 1 +~~~~ 2 +~~~~ 3 +```` +in console. + +#### Send `SIGTERM` to Puma When you stop puma by running `$ kill -s SIGTERM puma_process_id`, you will enter `setup_signals` in `Puma::Launcher#run`. ```ruby # .gems/puma-3.12.0/lib/puma/launcher.rb @@ -2181,7 +2286,7 @@ module Puma # Press `Control + C` to quit means 'SIGINT'. def setup_signals begin - # After runnning `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`. + # After running `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`. Signal.trap "SIGTERM" do graceful_stop # Let's step into this line. @@ -2255,8 +2360,8 @@ module Puma # ... thread = server.run - # This line will suspend the main process execution. - # And the `thread`'s block (which is method `handle_servers`) will be executed in main process. + # This line will suspend the main thread execution. + # And the `thread`'s block (which is method `handle_servers`) will be executed. thread.join end @@ -2291,8 +2396,27 @@ module Puma #... end + # 'Thread.current.object_id' returns '70144214949920', + # which is the same as the 'Thread.current.object_id' in Puma::Server#stop. + # Current thread is the main thread here. + puts "#{Thread.current.object_id}" + # The created @thread is the @thread in `stop` method below. - @thread = Thread.new { handle_servers } + @thread = Thread.new { + # 'Thread.current.object_id' returns '70144220123860', + # which is the same as the 'Thread.current.object_id' in 'handle_servers' in Puma::Server#run + # def handle_servers + # begin + # # ... + # ensure + # # FYI, the 'ensure' part is executed after `$ kill -s SIGTERM process_id`. + # puts "#{Thread.current.object_id}" # returns '70144220123860' too. + # end + # end + puts "#{Thread.current.object_id}" # returns '70144220123860' + + handle_servers + } return @thread end @@ -2302,10 +2426,15 @@ module Puma # This line which change the :status to :stop. notify_safely(STOP_COMMAND) # Let's step into this line. + # 'Thread.current.object_id' returns '70144214949920', + # which is the same as the 'Thread.current.object_id' in Puma::Server#run. + # Current thread is exactly the main thread here. + puts "#{Thread.current.object_id}" + # The @thread is just the always running Thread created in `Puma::Server#run`. # Please look at method `Puma::Server#run`. - # `@thread.join` will suspend the main process execution. - # And the @thread's code will continue be executed in main process. + # `@thread.join` will suspend the main thread execution. + # And the @thread's code will continue be executed in main thread. # Because @thread is waiting for incoming request, the next executed code # will be `ios = IO.select sockets` in method `handle_servers`. @thread.join if @thread && sync @@ -2323,7 +2452,7 @@ module Puma #... while @status == :run - # After `@thread.join` in main process, this line will be executed and will return result. + # After `@thread.join` in main thread, this line will be executed and will return result. ios = IO.select sockets ios.first.each do |sock| @@ -2347,6 +2476,14 @@ module Puma # ... ensure + # 'Thread.current.object_id' returns '70144220123860', + # which is the same as the 'Thread.current.object_id' in 'Thread.new block' in Puma::Server#run + # @thread = Thread.new do + # puts "#{Thread.current.object_id}" # returns '70144220123860' + # handle_servers + # end + puts "#{Thread.current.object_id}" + @check.close @notify.close @@ -2400,7 +2537,6 @@ module Puma # Wait for threads to finish without force shutdown. threads.each do |thread| - # I will use a simple example to show you what `thread.join` do later. thread.join # I guess `thread.join` means join the executing of thread to the calling (main) process. end @@ -2476,27 +2612,6 @@ module Puma end ``` -Try to run this `test_thread_join.rb`. - -You will find that if there is no `thread.join`, you can only see `==== I am the main thread.` in console. - -After you added `thread.join`, you can see `~~~~ 1\n~~~~ 2\n ~~~~ 3` in console. - -```ruby -# ./test_thread_join.rb -thread = Thread.new() do - 3.times do |n| - puts "~~~~ " + n.to_s - end -end - -# sleep 1 -puts "==== I am the main thread." - -# thread.join # Try to uncomment these two lines to see the differences. -# puts "==== after thread.join" -``` - So all the threads in the ThreadPool joined and finished. And please look at the caller in block of `Signal.trap "SIGTERM"` below. From bad483c0f63685a0655ee6af4584df0f7944d367 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Mon, 4 Mar 2019 19:05:13 +0800 Subject: [PATCH 08/16] Add more information. --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 72de886..2ac9086 100644 --- a/README.md +++ b/README.md @@ -1908,19 +1908,18 @@ module Puma # ... thread = server.run # Let's step into this line later. - # This line will suspend the main process execution. - # And the `thread`'s block (which is method `handle_servers`) will be executed in main process. + # This line will suspend the main thread execution. + # And the `thread`'s block (which is method `handle_servers`) will be executed in main thread. # See `Thread#join` for more information. # I will show you a simple example for using `thread.join`. # Please search `test_thread_join.rb` in this document. thread.join - # The below line will never be executed because `thread` is always running - # and `thread` has joined to main process. + # The below line will never be executed because `thread` is always running and `thread` has joined. # When `$ kill -s SIGTERM puma_process_id`, the below line will still not be executed # because the block of `Signal.trap "SIGTERM"` in `Puma::Launcher#setup_signals` will be executed. # If you remove the line `thread.join`, the below line will be executed, - # but the main process will exit after all code executed and all the threads not joined will be killed. + # but the main thread will exit after all code executed and all the threads not joined will be killed. puts "anything which will never be executed..." end end @@ -1992,8 +1991,6 @@ module Puma # ... if background # background: true (for this example) - # - puts "#{Thread.current.object_id}" # It's important part. # Remember puma created a thread here! From 8b3882f25c2f5dea76e02b123532b220c6e22e74 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Mon, 4 Mar 2019 19:08:37 +0800 Subject: [PATCH 09/16] Add more information. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2ac9086..e285b8a 100644 --- a/README.md +++ b/README.md @@ -1909,7 +1909,7 @@ module Puma thread = server.run # Let's step into this line later. # This line will suspend the main thread execution. - # And the `thread`'s block (which is method `handle_servers`) will be executed in main thread. + # And the `thread`'s block (which is method `handle_servers`) will be executed. # See `Thread#join` for more information. # I will show you a simple example for using `thread.join`. # Please search `test_thread_join.rb` in this document. @@ -1991,7 +1991,6 @@ module Puma # ... if background # background: true (for this example) - # It's important part. # Remember puma created a thread here! # We will know that the thread's job is waiting for requests. @@ -2431,7 +2430,7 @@ module Puma # The @thread is just the always running Thread created in `Puma::Server#run`. # Please look at method `Puma::Server#run`. # `@thread.join` will suspend the main thread execution. - # And the @thread's code will continue be executed in main thread. + # And the @thread's code will continue be executed. # Because @thread is waiting for incoming request, the next executed code # will be `ios = IO.select sockets` in method `handle_servers`. @thread.join if @thread && sync From 86622fabeb0e8193396eacc95a9d45e5457b9ad4 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Tue, 5 Mar 2019 12:15:21 +0800 Subject: [PATCH 10/16] Add more information. --- README.md | 104 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index e285b8a..2c71bd6 100644 --- a/README.md +++ b/README.md @@ -1957,6 +1957,7 @@ module Puma class Server def run(background=true) #... + @status = :run queue_requests = @queue_requests # This part is important. @@ -1978,7 +1979,7 @@ module Puma # ... if process_now - # process the request. You can treat `client` as request. + # Process the request. You can treat `client` as request. # If you want to know more about 'process_client', please read part 3 # or search 'process_client' in this document. process_client(client, buffer) @@ -1991,11 +1992,12 @@ module Puma # ... if background # background: true (for this example) - # It's important part. + # This part is important. # Remember puma created a thread here! - # We will know that the thread's job is waiting for requests. + # We will know that the newly created thread's job is waiting for requests. # When a request comes, the thread will transfer the request processing work to a thread in ThreadPool. - # The method `handle_servers` in thread's block will be executed immediately. + # The method `handle_servers` in thread's block will be executed immediately + # (executed in the newly created thread, not in the main thread). @thread = Thread.new { handle_servers } # Let's step into this line to see what I said. return @thread else @@ -2010,11 +2012,11 @@ module Puma # ... - # The thread is always running! + # The thread is always running, because @status has been set to :run in Puma::Server#run. # Yes, it should always be running to transfer the incoming requests. while @status == :run begin - # This line will cause current thread waiting until a request is coming. + # This line will cause current thread waiting until a request arrives. # So it will be the entry of every request! ios = IO.select sockets @@ -2029,7 +2031,8 @@ module Puma # ... # FYI, the method '<<' is redefined. - # Add the request (client) to thread pool means a thread in the pool will process this request (client). + # Add the request (client) to thread pool means + # a thread in the pool will process this request (client). pool << client # Let's step into this line. pool.wait_until_not_full # Let's step into this line later. @@ -2058,8 +2061,8 @@ module Puma def initialize(min, max, *extra, &block) #.. @mutex = Mutex.new - @todo = [] # @todo is requests (in puma, it's Puma::Client instance) which need to be processed. - @spawned = 0 # The count of @spawned threads. + @todo = [] # @todo is requests (in puma, they are Puma::Client instances) which need to be processed. + @spawned = 0 # the count of @spawned threads @min = Integer(min) # @min threads count @max = Integer(max) # @max threads count @block = block # block will be called in method `spawn_thread` to processed a request. @@ -2067,15 +2070,15 @@ module Puma @reaper = nil @mutex.synchronize do - @min.times { spawn_thread } # Puma started @min count threads. + @min.times { spawn_thread } # Puma spawned @min count threads. end end def spawn_thread @spawned += 1 - # Run a new Thread now. - # The block of the thread will be executed separately from the calling thread. + # Create a new Thread now. + # The block of the thread will be executed immediately and separately from the calling thread (main thread). th = Thread.new(@spawned) do |spawned| # Thread name is new in Ruby 2.3 Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=) @@ -2118,7 +2121,7 @@ module Puma # You can search `def <<(work)` in this document. # Method `<<` is used in method `handle_servers`: `pool << client` in Puma::Server#run. # `pool << client` means add a request to the thread pool, - # and then the thread waked up will process the request. + # and then the waked up thread will process the request. not_empty.wait mutex @waiting -= 1 @@ -2136,7 +2139,7 @@ module Puma begin # `block.call` will switch program to the block definition part. - # The Block definition part is in `Puma::Server#run`: + # The block definition part is in `Puma::Server#run`: # @thread_pool = ThreadPool.new(@min_threads, # @max_threads, # IOBuffer) do |client, buffer| #...; end @@ -2191,7 +2194,7 @@ module Puma # Wake up the waiting thread to process the request. # The waiting thread is defined in the same file: Puma::ThreadPool#spawn_thread. - # There are these code in `spawn_thread`: + # This code is in `spawn_thread`: # while true # # ... # not_empty.wait mutex @@ -2200,7 +2203,7 @@ module Puma # end @not_empty.signal end - end + end end end ``` @@ -2212,7 +2215,7 @@ In `#perform`, call `Rails::Server#start`. Then call `Rack::Server#start`. Then call `Rack::Handler::Puma.run(YourProject::Application.new)`. -In `.run`, Puma will new a always running Thread to `ios = IO.select(sockets)`. +In `.run`, Puma will new a always running Thread for `ios = IO.select sockets`. Request is created from `ios` object. @@ -2222,14 +2225,15 @@ The thread will invoke rack apps' `call` to get the response for the request. ### Exiting Puma #### Process and Thread -For Puma is multiple threads, we need to have some basic concepts about Process and Thread. +Because Puma is using multiple threads, we need to have some basic concepts about Process and Thread. -This link's good for you to obtain the concepts: [Process and Thread](https://stackoverflow.com/questions/4894609/will-a-cpu-process-have-at-least-one-thread) +This link is good for you to obtain the concepts: [Process and Thread](https://stackoverflow.com/questions/4894609/will-a-cpu-process-have-at-least-one-thread) In the next part, you will often see `thread.join`. -I will use a simple example to tell what does `thread.join` do. +I will use two simple example to tell what does `thread.join` do. +##### Example one Try to run `test_thread_join.rb`. ```ruby @@ -2256,6 +2260,32 @@ After you added `thread.join`, you can see: ```` in console. +##### Example two +```ruby +arr = [ + Thread.new { sleep 1 }, + Thread.new do + sleep 5 + puts 'I am arry[1]' + end, + Thread.new { sleep 8} +] + +puts Thread.list.size # returns 4 (including the main thread) + +sleep 2 + +arr.each { |thread| puts "~~~~~ #{thread}" } + +puts Thread.list.size # returns 3 (because arr[0] is dead) + +# arr[1].join # comment off to see differences + +arr.each { |thread| puts "~~~~~ #{thread}" } + +puts "Exit main thread" +``` + #### Send `SIGTERM` to Puma When you stop puma by running `$ kill -s SIGTERM puma_process_id`, you will enter `setup_signals` in `Puma::Launcher#run`. ```ruby @@ -2267,7 +2297,7 @@ module Puma def run #... - # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM process_id`. setup_signals # Let's step into this line. set_process_title @@ -2277,7 +2307,7 @@ module Puma # ... end - # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM process_id`. # Signal.list #=> {"EXIT"=>0, "HUP"=>1, "INT"=>2, "QUIT"=>3, "ILL"=>4, "TRAP"=>5, "IOT"=>6, "ABRT"=>6, "FPE"=>8, "KILL"=>9, "BUS"=>7, "SEGV"=>11, "SYS"=>31, "PIPE"=>13, "ALRM"=>14, "TERM"=>15, "URG"=>23, "STOP"=>19, "TSTP"=>20, "CONT"=>18, "CHLD"=>17, "CLD"=>17, "TTIN"=>21, "TTOU"=>22, "IO"=>29, "XCPU"=>24, "XFSZ"=>25, "VTALRM"=>26, "PROF"=>27, "WINCH"=>28, "USR1"=>10, "USR2"=>12, "PWR"=>30, "POLL"=>29} # Press `Control + C` to quit means 'SIGINT'. def setup_signals @@ -2357,7 +2387,7 @@ module Puma thread = server.run # This line will suspend the main thread execution. - # And the `thread`'s block (which is method `handle_servers`) will be executed. + # And the `thread`'s block (which is the method `handle_servers`) will be executed. thread.join end @@ -2374,7 +2404,7 @@ end module Puma class Server def initialize(app, events=Events.stdio, options={}) - # This method returns `IO.pipe`. + # 'Puma::Util.pipe' returns `IO.pipe`. @check, @notify = Puma::Util.pipe # @check, @notify is a pair. @status = :stop @@ -2387,7 +2417,7 @@ module Puma IOBuffer) do |client, buffer| #... - # process the request. + # Process the request. process_client(client, buffer) #... end @@ -2419,7 +2449,9 @@ module Puma # Stops the acceptor thread and then causes the worker threads to finish # off the request queue before finally exiting. def stop(sync=false) - # This line which change the :status to :stop. + # This line will set '@status = :stop', + # and cause `ios = IO.select sockets` in method `handle_servers` to return result. + # So the code after `ios = IO.select sockets` will be executed. notify_safely(STOP_COMMAND) # Let's step into this line. # 'Thread.current.object_id' returns '70144214949920', @@ -2430,9 +2462,7 @@ module Puma # The @thread is just the always running Thread created in `Puma::Server#run`. # Please look at method `Puma::Server#run`. # `@thread.join` will suspend the main thread execution. - # And the @thread's code will continue be executed. - # Because @thread is waiting for incoming request, the next executed code - # will be `ios = IO.select sockets` in method `handle_servers`. + # And the code in @thread will continue be executed. @thread.join if @thread && sync end @@ -2443,12 +2473,18 @@ module Puma def handle_servers begin check = @check + # sockets: [#, #] sockets = [check] + @binder.ios pool = @thread_pool #... while @status == :run - # After `@thread.join` in main thread, this line will be executed and will return result. + # After `notify_safely(STOP_COMMAND)` in main thread, this line will be executed and will return result. + # FYI, `@check, @notify = IO.pipe`. + # def notify_safely(message) + # @notify << message + # end + # sockets: [#, #] ios = IO.select sockets ios.first.each do |sock| @@ -2528,6 +2564,10 @@ module Puma # ... # dup workers so that we join them all safely + # @workers is an array. + # @workers.dup will not create new thread. + # @workers is an instance variable and will be changed when shutdown (by `@workers.delete th`). + # So ues dup. @workers.dup end @@ -2539,7 +2579,7 @@ module Puma @spawned = 0 @workers = [] end - + def initialize(min, max, *extra, &block) #.. @mutex = Mutex.new @@ -2621,7 +2661,7 @@ module Puma def run #... - # Set the behavior for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM process_id`. setup_signals # Let's step into this line. set_process_title From 4809af7b40e65aa5bf03cef5e247ee2399ecf3df Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Tue, 5 Mar 2019 14:36:43 +0800 Subject: [PATCH 11/16] Refine README.md --- README.md | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2c71bd6..5da875e 100644 --- a/README.md +++ b/README.md @@ -1863,7 +1863,7 @@ module Puma def run #... - # Set the behaviors for signals like `$ kill -s SIGTERM process_id` received. + # Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id` received. setup_signals # We will discuss this line later. set_process_title @@ -2018,6 +2018,7 @@ module Puma begin # This line will cause current thread waiting until a request arrives. # So it will be the entry of every request! + # sockets: [#, #] ios = IO.select sockets ios.first.each do |sock| @@ -2070,7 +2071,7 @@ module Puma @reaper = nil @mutex.synchronize do - @min.times { spawn_thread } # Puma spawned @min count threads. + @min.times { spawn_thread } # Puma spawns @min count threads. end end @@ -2215,7 +2216,7 @@ In `#perform`, call `Rails::Server#start`. Then call `Rack::Server#start`. Then call `Rack::Handler::Puma.run(YourProject::Application.new)`. -In `.run`, Puma will new a always running Thread for `ios = IO.select sockets`. +In `.run`, Puma will new a always running Thread for `ios = IO.select(#)`. Request is created from `ios` object. @@ -2261,12 +2262,14 @@ After you added `thread.join`, you can see: in console. ##### Example two +Try to run `test_thread_join2.rb`. ```ruby +# ./test_thread_join2.rb arr = [ Thread.new { sleep 1 }, Thread.new do sleep 5 - puts 'I am arry[1]' + puts 'I am arr[1]' end, Thread.new { sleep 8} ] @@ -2279,7 +2282,7 @@ arr.each { |thread| puts "~~~~~ #{thread}" } puts Thread.list.size # returns 3 (because arr[0] is dead) -# arr[1].join # comment off to see differences +# arr[1].join # uncomment to see differences arr.each { |thread| puts "~~~~~ #{thread}" } @@ -2297,7 +2300,7 @@ module Puma def run #... - # Set the behaviors for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id`. setup_signals # Let's step into this line. set_process_title @@ -2307,7 +2310,7 @@ module Puma # ... end - # Set the behaviors for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id`. # Signal.list #=> {"EXIT"=>0, "HUP"=>1, "INT"=>2, "QUIT"=>3, "ILL"=>4, "TRAP"=>5, "IOT"=>6, "ABRT"=>6, "FPE"=>8, "KILL"=>9, "BUS"=>7, "SEGV"=>11, "SYS"=>31, "PIPE"=>13, "ALRM"=>14, "TERM"=>15, "URG"=>23, "STOP"=>19, "TSTP"=>20, "CONT"=>18, "CHLD"=>17, "CLD"=>17, "TTIN"=>21, "TTOU"=>22, "IO"=>29, "XCPU"=>24, "XFSZ"=>25, "VTALRM"=>26, "PROF"=>27, "WINCH"=>28, "USR1"=>10, "USR2"=>12, "PWR"=>30, "POLL"=>29} # Press `Control + C` to quit means 'SIGINT'. def setup_signals @@ -2429,13 +2432,14 @@ module Puma # The created @thread is the @thread in `stop` method below. @thread = Thread.new { + # FYI, this is in the puma starting process. # 'Thread.current.object_id' returns '70144220123860', # which is the same as the 'Thread.current.object_id' in 'handle_servers' in Puma::Server#run # def handle_servers # begin # # ... # ensure - # # FYI, the 'ensure' part is executed after `$ kill -s SIGTERM process_id`. + # # FYI, the 'ensure' part is in the puma stopping process. # puts "#{Thread.current.object_id}" # returns '70144220123860' too. # end # end @@ -2450,8 +2454,8 @@ module Puma # off the request queue before finally exiting. def stop(sync=false) # This line will set '@status = :stop', - # and cause `ios = IO.select sockets` in method `handle_servers` to return result. - # So the code after `ios = IO.select sockets` will be executed. + # and cause `ios = IO.select sockets` (in method `handle_servers`) to return result. + # So that the code after `ios = IO.select sockets` will be executed. notify_safely(STOP_COMMAND) # Let's step into this line. # 'Thread.current.object_id' returns '70144214949920', @@ -2479,7 +2483,7 @@ module Puma #... while @status == :run - # After `notify_safely(STOP_COMMAND)` in main thread, this line will be executed and will return result. + # After `notify_safely(STOP_COMMAND)` in main thread, `ios = IO.select sockets` will return result. # FYI, `@check, @notify = IO.pipe`. # def notify_safely(message) # @notify << message @@ -2508,9 +2512,11 @@ module Puma # ... ensure + # FYI, the 'ensure' part is in the puma stopping process. # 'Thread.current.object_id' returns '70144220123860', # which is the same as the 'Thread.current.object_id' in 'Thread.new block' in Puma::Server#run # @thread = Thread.new do + # # FYI, this is in the puma starting process. # puts "#{Thread.current.object_id}" # returns '70144220123860' # handle_servers # end @@ -2567,13 +2573,13 @@ module Puma # @workers is an array. # @workers.dup will not create new thread. # @workers is an instance variable and will be changed when shutdown (by `@workers.delete th`). - # So ues dup. + # So ues @workers.dup here. @workers.dup end # Wait for threads to finish without force shutdown. threads.each do |thread| - thread.join # I guess `thread.join` means join the executing of thread to the calling (main) process. + thread.join end @spawned = 0 @@ -2586,11 +2592,11 @@ module Puma @spawned = 0 # The count of @spawned threads. @todo = [] # @todo is requests (in puma, it's Puma::Client instance) which need to be processed. @min = Integer(min) # @min threads count - @block = block # block will be called in method `spawn_thread` to processed a request. + @block = block # block will be called in method `spawn_thread` to process a request. @workers = [] @mutex.synchronize do - @min.times { spawn_thread } # Puma started @min count threads. + @min.times { spawn_thread } # Puma spawns @min count threads. end end @@ -2619,7 +2625,7 @@ module Puma end # ... - # After `@not_empty.broadcast` in executed in '#shutdown', `not_empty` is waked up. + # After `@not_empty.broadcast` is executed in '#shutdown', `not_empty` is waked up. # Ruby will continue to execute the next line here. not_empty.wait mutex @@ -2650,7 +2656,7 @@ end So all the threads in the ThreadPool joined and finished. -And please look at the caller in block of `Signal.trap "SIGTERM"` below. +Let's inspect the caller in block of `Signal.trap "SIGTERM"` below. ```ruby # .gems/puma-3.12.0/lib/puma/launcher.rb @@ -2661,7 +2667,7 @@ module Puma def run #... - # Set the behaviors for signals like `$ kill -s SIGTERM process_id`. + # Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id`. setup_signals # Let's step into this line. set_process_title @@ -2679,7 +2685,7 @@ module Puma begin # After running `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`. Signal.trap "SIGTERM" do - # I added `caller` to see the calling stack. + # I inspect `caller` to see the caller stack. # caller: [ # "../gems/puma-3.12.0/lib/puma/single.rb:118:in `join'", # "../gems/puma-3.12.0/lib/puma/single.rb:118:in `run'", From 2275ce1f51b0ff20de5fb035f3f24ccca92b8044 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Tue, 5 Mar 2019 14:44:49 +0800 Subject: [PATCH 12/16] Refine README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 5da875e..661a2bb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ # Learn-Rails-by-Reading-Source-Code +## Table of Contents + + * [Part 0: Before reading Rails 5 source code](#part-0:-Before-reading-Rails-5-source-code) + * [Syntax](#syntax) + * [Naming](#naming) + * [Comments](#comments) + * [Comment Annotations](#comment-annotations) + * [Magic Comments](#magic-comments) + * [Classes & Modules](#classes--modules) + * [Exceptions](#exceptions) + * [Collections](#collections) + * [Numbers](#numbers) + * [Strings](#strings) + * [Date & Time](#date--time) + ## Part 0: Before reading Rails 5 source code 1) I suggest you learn Rack [http://rack.github.io/](http://rack.github.io/) first. From 578cb1242428f11b2c90f5bf46345ca79bf99d31 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Tue, 5 Mar 2019 14:46:08 +0800 Subject: [PATCH 13/16] Refine README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 661a2bb..9460080 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Learn-Rails-by-Reading-Source-Code ## Table of Contents - * [Part 0: Before reading Rails 5 source code](#part-0:-Before-reading-Rails-5-source-code) + * [Part 0 - Before reading Rails 5 source code](#part-0---Before-reading-Rails-5-source-code) * [Syntax](#syntax) * [Naming](#naming) * [Comments](#comments) From 99a5119345e27816c88c5985ac0bf199242d6d91 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Tue, 5 Mar 2019 14:59:43 +0800 Subject: [PATCH 14/16] Add table of content. --- README.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9460080..569df6b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ # Learn-Rails-by-Reading-Source-Code ## Table of Contents - * [Part 0 - Before reading Rails 5 source code](#part-0---Before-reading-Rails-5-source-code) - * [Syntax](#syntax) - * [Naming](#naming) - * [Comments](#comments) - * [Comment Annotations](#comment-annotations) - * [Magic Comments](#magic-comments) - * [Classes & Modules](#classes--modules) - * [Exceptions](#exceptions) - * [Collections](#collections) - * [Numbers](#numbers) - * [Strings](#strings) - * [Date & Time](#date--time) + * [Part 0: Before reading Rails 5 source code](#part-0-before-reading-rails-5-source-code) + * [What will you learn from this tutorial?](#what-will-you-learn-from-this-tutorial) + * [Part 1: Your app: an instance of YourProject::Application](#part-1-your-app-an-instance-of-yourprojectapplication) + * [Part 2: config](#part-2-config) + * [Part 3: Every request and response](#part-3-every-request-and-response) + * [Puma](#puma) + * [Rack apps](#rack-apps) + * [The core app: ActionDispatch::Routing::RouteSet instance](#the-core-app-actiondispatchroutingrouteset-instance) + * [Render view](#render-view) + * [How can instance variables defined in Controller be accessed in view file?](#how-can-instance-variables-defined-in-controller-be-accessed-in-view-file) + * [Part 4: What does `$ rails server` do?](#part-4-what-does--rails-server-do) + * [Thor](#thor) + * [Rails::Server#start](#railsserverstart) + * [Starting Puma](#starting-puma) + * [Conclusion](#conclusion) + * [Exiting Puma](#exiting-puma) + * [Process and Thread](#process-and-thread) + * [Send `SIGTERM` to Puma](#send-sigterm-to-puma) ## Part 0: Before reading Rails 5 source code @@ -24,7 +30,7 @@ So what is the object with `call` method in Rails? I will answer this question i 2) You need a good IDE which can help for debugging. I use [RubyMine](https://www.jetbrains.com/). -### What you will learn from this tutorial? +### What will you learn from this tutorial? * How does Rails start your application? * How does Rails process every request? @@ -1821,7 +1827,7 @@ module Rack end ``` -### Puma +### Starting Puma As we see in `Rack::Server#start`, there is `Rack::Handler::Puma.run(wrapped_app, options, &blk)`. ```ruby From c428249ada86ece7e21d89c53f6c956699ba06c0 Mon Sep 17 00:00:00 2001 From: Lane Zhang Date: Thu, 4 Apr 2019 16:27:44 +0800 Subject: [PATCH 15/16] Add anti-996 --- README.md | 68 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 569df6b..e865d7c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Learn-Rails-by-Reading-Source-Code +[![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) +[![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) ## Table of Contents * [Part 0: Before reading Rails 5 source code](#part-0-before-reading-rails-5-source-code) @@ -22,9 +24,9 @@ ## Part 0: Before reading Rails 5 source code -1) I suggest you learn Rack [http://rack.github.io/](http://rack.github.io/) first. +1) I suggest you to learn Rack [http://rack.github.io/](http://rack.github.io/) first. -In rack, an object with `call` method is a rack app. +In Rack, an object with `call` method is a Rack app. So what is the object with `call` method in Rails? I will answer this question in Part 1. @@ -37,7 +39,7 @@ So what is the object with `call` method in Rails? I will answer this question i * How does Rails combine ActionController, ActionView and Routes together? -* How does puma, rack, Rails work together? +* How does Puma, Rack, Rails work together? * What's Puma's multiple threads? @@ -55,7 +57,7 @@ module Rails def perform # ... Rails::Server.new(server_options).tap do |server| - # APP_PATH is '/Users/your_name/your_project/config/application'. + # APP_PATH is '/path/to/your_project/config/application'. # require APP_PATH will create the 'Rails.application' object. # Actually, 'Rails.application' is an instance of `YourProject::Application`. # Rack server will start 'Rails.application'. @@ -84,9 +86,9 @@ module Rails end end ``` -A rack server need to start with an `App` object. The `App` object should have a `call` method. +A Rack server need to start with an `App` object. The `App` object should have a `call` method. -`config.ru` is the conventional entry file for rack app. So let's look at it. +`config.ru` is the conventional entry file for Rack app. So let's look at it. ```ruby # ./config.ru require_relative 'config/environment' @@ -264,9 +266,9 @@ Rack server will start `Rails.application` in the end. `Rails.application` is an important object in Rails. -And you'll only have one `Rails.application` in one puma process. +And you'll only have one `Rails.application` in one Puma process. -Multiple threads in a puma process shares the `Rails.application`. +Multiple threads in a Puma process shares the `Rails.application`. ## Part 2: config The first time we see the `config` is in `./config/application.rb`. @@ -382,9 +384,9 @@ end ``` ### Puma -When a request is made from client, puma will process the request in `Puma::Server#process_client`. +When a request is made from client, Puma will process the request in `Puma::Server#process_client`. -If you want to know how puma enter the method `Puma::Server#process_client`, please read part 4 or just search 'process_client' in this document. +If you want to know how Puma enter the method `Puma::Server#process_client`, please read part 4 or just search 'process_client' in this document. ```ruby # ./gems/puma-3.12.0/lib/puma/server.rb @@ -447,7 +449,7 @@ module Puma # Given the request +env+ from +client+ and a partial request body # in +body+, finish reading the body if there is one and invoke - # the rack app. Then construct the response and write it back to + # the Rack app. Then construct the response and write it back to # +client+ # def handle_request(req, lines) @@ -550,7 +552,7 @@ module Rails puts "caller: #{caller.inspect}" # You may want to know when is the @app first time initialized. - # It is initialized when 'config.ru' is load by rack server. + # It is initialized when 'config.ru' is load by Rack server. # Please search `Rack::Server#build_app_and_options_from_config` in this document for more information. # When `Rails.application.initialize!` (in ./config/environment.rb) executed, @app is initialized. @app || @app_build_lock.synchronize { # '@app_build_lock = Mutex.new', so multiple threads share one '@app'. @@ -704,8 +706,8 @@ module ActionDispatch end def build(app) - # klass is rack middleware like : Rack::TempfileReaper, Rack::ETag, Rack::ConditionalGet or Rack::Head, etc. - # It's typical rack app to use these middlewares. + # klass is Rack middleware like : Rack::TempfileReaper, Rack::ETag, Rack::ConditionalGet or Rack::Head, etc. + # It's typical Rack app to use these middlewares. # See https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib for more information. klass.new(app, *args, &block) end @@ -730,7 +732,7 @@ end # > # > ``` -As we see in the Rack middleware stack, the last one is +As we see in the Rack middleware stack, the last @app is `@app=#` ```ruby @@ -1486,7 +1488,7 @@ module ActionView end ``` -After all rack apps called, user will get the response. +After all Rack apps called, user will get the response. ## Part 4: What does `$ rails server` do? @@ -1707,7 +1709,7 @@ module Rails def perform # ... Rails::Server.new(server_options).tap do |server| - # APP_PATH is '/Users/your_name/your_project/config/application'. + # APP_PATH is '/path/to/your_project/config/application'. # require APP_PATH will create the 'Rails.application' object. # 'Rails.application' is 'YourProject::Application.new'. # Rack server will start 'Rails.application'. @@ -1863,7 +1865,7 @@ module Puma # with configuration in `config/puma.rb` or `config/puma/.rb`. # # It is responsible for either launching a cluster of Puma workers or a single - # puma server. + # Puma server. class Launcher def initialize(conf, launcher_args={}) @runner = nil @@ -1984,7 +1986,7 @@ module Puma # This part is important. # Remember the block of ThreadPool.new will be called when a request added to the ThreadPool instance. # And the block will process the request by calling method `process_client`. - # Let's step into this line later to see how puma call the block. + # Let's step into this line later to see how Puma call the block. @thread_pool = ThreadPool.new(@min_threads, @max_threads, IOBuffer) do |client, buffer| @@ -2000,7 +2002,7 @@ module Puma # ... if process_now - # Process the request. You can treat `client` as request. + # Process the request. You can look upon `client` as request. # If you want to know more about 'process_client', please read part 3 # or search 'process_client' in this document. process_client(client, buffer) @@ -2014,7 +2016,7 @@ module Puma if background # background: true (for this example) # This part is important. - # Remember puma created a thread here! + # Remember Puma created a thread here! # We will know that the newly created thread's job is waiting for requests. # When a request comes, the thread will transfer the request processing work to a thread in ThreadPool. # The method `handle_servers` in thread's block will be executed immediately @@ -2047,7 +2049,7 @@ module Puma break if handle_check else if io = sock.accept_nonblock - # You can simply think a Puma::Client instance as a request. + # You can simply look upon a Puma::Client instance as a request. client = Client.new(io, @binder.env(sock)) # ... @@ -2083,7 +2085,7 @@ module Puma def initialize(min, max, *extra, &block) #.. @mutex = Mutex.new - @todo = [] # @todo is requests (in puma, they are Puma::Client instances) which need to be processed. + @todo = [] # @todo is requests (in Puma, they are Puma::Client instances) which need to be processed. @spawned = 0 # the count of @spawned threads @min = Integer(min) # @min threads count @max = Integer(max) # @max threads count @@ -2149,7 +2151,7 @@ module Puma @waiting -= 1 end - # `work` is the request (in puma, it's Puma::Client instance) which need to be processed. + # `work` is the request (in Puma, it's Puma::Client instance) which need to be processed. work = todo.shift if continue end @@ -2207,7 +2209,7 @@ module Puma end # work: # - # You can treat Puma::Client instance as a request. + # You can look upon Puma::Client instance as a request. @todo << work if @waiting < @todo.size and @spawned < @max @@ -2241,9 +2243,9 @@ In `.run`, Puma will new a always running Thread for `ios = IO.select(# Date: Fri, 10 Jan 2020 11:38:39 +0800 Subject: [PATCH 16/16] 20200110 --- README.md | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e865d7c..8ab19ce 100644 --- a/README.md +++ b/README.md @@ -2274,13 +2274,23 @@ puts "==== I am the main thread." # thread.join # Try to uncomment these two lines to see the differences. # puts "==== after thread.join" ``` -You will find that if there is no `thread.join`, you can only see `==== I am the main thread.` in console. +You will find that if there is no `thread.join`, you can see +```log +==== I am the main thread. +==== after thread.join +~~~~ 0 +~~~~ 1 +~~~~ 2 +``` +in console. After you added `thread.join`, you can see: -```ruby +```log +==== I am the main thread. +~~~~ 0 ~~~~ 1 ~~~~ 2 -~~~~ 3 +==== after thread.join ```` in console. @@ -2289,26 +2299,36 @@ Try to run `test_thread_join2.rb`. ```ruby # ./test_thread_join2.rb arr = [ - Thread.new { sleep 1 }, Thread.new do - sleep 5 + puts 'I am arr[0]' + sleep 1 + puts 'After arr[0]' + end, + Thread.new do puts 'I am arr[1]' + sleep 5 + puts 'After arr[1]' end, - Thread.new { sleep 8} + Thread.new do + puts 'I am arr[2]' + sleep 8 + puts 'After arr[2]' + end ] -puts Thread.list.size # returns 4 (including the main thread) +puts "Thread.list.size: #{Thread.list.size}" # returns 4 (including the main thread) sleep 2 arr.each { |thread| puts "~~~~~ #{thread}" } -puts Thread.list.size # returns 3 (because arr[0] is dead) +puts "Thread.list.size: #{Thread.list.size}" # returns 3 (because arr[0] is dead) -# arr[1].join # uncomment to see differences +arr[1].join # uncomment to see differences arr.each { |thread| puts "~~~~~ #{thread}" } +sleep 7 puts "Exit main thread" ```