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" ``` pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy