Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Solving LeetCode problems in the best way. Python, Java, C++, JavaScript, Go, C# and Ruby are supported! Official website👇🏻:

NotificationsYou must be signed in to change notification settings

upcse/leetcode-python-java

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 

Repository files navigation

LICENSE996.icu

Table of Contents

Part 0: Before reading Rails 5 source code

  1. I suggest you to learn Rackhttp://rack.github.io/ first.

In Rack, an object withcall method is a Rack app.

So what is the object withcall method in Rails? I will answer this question in Part 1.

  1. You need a good IDE which can help for debugging. I useRubyMine.

What will you learn from this tutorial?

  • How does Rails start your application?

  • How does Rails process every request?

  • How does Rails combine ActionController, ActionView and Routes together?

  • 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's class name isYourProject::Application (defined in./config/application.rb).

First, I will give you a piece of important code.

# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rbmoduleRailsmoduleCommandclassServerCommand <Basedefperform# ...Rails::Server.new(server_options).tapdo |server|# 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'.requireAPP_PATHDir.chdir(Rails.application.root)server.startendendendendclassServer < ::Rack::Serverdefstart#...# '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#performwrapped_appsuper# Will invoke ::Rack::Server#start.endendend

A Rack server need to start with anApp object. TheApp object should have acall method.

config.ru is the conventional entry file for Rack app. So let's look at it.

# ./config.rurequire_relative'config/environment'runRails.application# It seems that this is the app.

Let's test it byRails.application.respond_to?(:call), it returnstrue.

Let's step intoRails.application.

# ./gems/railties-5.2.2/lib/rails.rbmoduleRailsclass <<self@application=@app_class=nilattr_accessor:app_class# 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.defapplication@application ||=(app_class.instanceifapp_class)endendend

BecauseRails.application.respond_to?(:call) returnstrue,app_class.instance has acall method.

When wasapp_class set?

moduleRailsclassApplication <Engineclass <<selfdefinherited(base)# This is a hooked method.Rails.app_class=base# This line set the 'app_class'.endendendend

Rails::Application is inherited byYourProject,

# ./config/application.rbmoduleYourProject# The hooked method `inherited` will be invoked here.classApplication <Rails::Applicationendend

SoYourProject::Application is theRails.app_class here.

You may have a question: When does Rails execute the code in./config/application.rb?

To answer this question, we need to look back toconfig.ru.

# ./config.rurequire_relative'config/environment'# Let's step into this line.runRails.application# It seems that this is the app.
# ./config/environment.rb# Load the Rails application.require_relative'application'# Let's step into this line.# Initialize the Rails application.Rails.application.initialize!
# ./config/application.rbrequire_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)moduleYourProject# The hooked method `inherited` will be invoked here.classApplication <Rails::Applicationconfig.load_defaults5.2config.i18n.default_locale=:zhendend

BecauseYourProject::Application isRails.app_class,app_class.instance isYourProject::Application.instance.

But where is thecall method?

call method should be a method ofYourProject::Application.instance.

Thecall method processes every request. Here it is.

# ./gems/railties/lib/rails/engine.rbmoduleRailsclassEngine <Railtiedefcall(env)# This method will process every request. It is invoked by Rack. So it is very important.req=build_requestenvapp.callreq.env# We will discuss the 'app' object later.endendend# ./gems/railties/lib/rails/application.rbmoduleRailsclassApplication <Engineendend# ./config/application.rbmoduleYourProjectclassApplication <Rails::Applicationendend

Ancestor's chain isYourProject::Application < Rails::Application < Rails::Engine < Rails::Railtie.

SoYourProject::Application.new.respond_to?(:call) returnstrue.

But what doesapp_class.instance really do?

instance is just a method name. What we really expects is something likeapp_class.new.

Let's look at the definition ofinstance.

# ./gems/railties/lib/rails/application.rbmoduleRailsclassApplication <Enginedefinstancesuper.run_load_hooks!# This line confused me at the beginning.endendend

After a deep research, I realized that this code is equal to:

definstancereturn_value=super# Keyword 'super' means call the ancestor's same name method: 'instance'.return_value.run_load_hooks!end
# ./gems/railties/lib/rails/railtie.rbmoduleRailsclassRailtiedefinstance# 'Rails::Railtie' is the top ancestor class.# Now 'app_class.instance' is 'YourProject::Application.new'.@instance ||=newendendend
moduleRailsdefapplication@application ||=(app_class.instanceifapp_class)endend

SoYourProject::Application.new isRails.application.

Rack server will startRails.application in the end.

Rails.application is an important object in Rails.

And you'll only have oneRails.application in one Puma process.

Multiple threads in a Puma process shares theRails.application.

Part 2: config

The first time we see theconfig is in./config/application.rb.

# ./config/application.rb#...moduleYourProjectclassApplication <Rails::Application# Actually, `config` is a method of `YourProject::Application`.# It is defined in it's grandfather's father: `Rails::Railtie`config.load_defaults5.2# Let's step into this line to see what is config.config.i18n.default_locale=:zhendend
moduleRailsclassRailtieclass <<self# 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`definstance# return an instance of `YourProject::Application`.# Call `YourProject::Application.config` will actually call `YourProject::Application.new.config`@instance ||=newendendendclassEngine <RailtieendclassApplication <Engineclass <<selfdefinstance# 'super' will call `:instance` method in `Railtie`,# which will return an instance of `YourProject::Application`.return_value=superreturn_value.run_load_hooks!endenddefrun_load_hooks!returnselfif@ran_load_hooks@ran_load_hooks=true# ...self# `self` is an instance of `YourProject::Application`, and `self` is `Rails.application`.end# This is the method `config`.defconfig# 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))endendend

In the end,YourProject::Application.config === Rails.application.config returnstrue.

Invoke Class'sconfig method become invoke the class's instance'sconfig method.

moduleRailsclass <<selfdefconfigurationapplication.configendendend

SoRails.configuration === Rails.application.config returnstrue.

FYI:

moduleRailsclassApplicationclassConfiguration < ::Rails::Engine::ConfigurationendendclassEngineclassConfiguration < ::Rails::Railtie::Configurationattr_accessor:middlewaredefinitialize(root=nil)super()#...@middleware=Rails::Configuration::MiddlewareStackProxy.newendendendclassRailtieclassConfigurationendendend

Part 3: Every request and response

Imagine we have this route for the home page.

# ./config/routes.rbRails.application.routes.drawdoroot'home#index'# HomeController#indexend

Puma

When a request is made from client, Puma will process the request inPuma::Server#process_client.

If you want to know how Puma enter the methodPuma::Server#process_client, please read part 4 or just search 'process_client' in this document.

# ./gems/puma-3.12.0/lib/puma/server.rbrequire'socket'modulePuma# 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.classServerdefinitialize(app,events=Events.stdio,options={})# app: #<Puma::Configuration::ConfigMiddleware:0x00007fcf1612c338#        @app = #<YourProject::Application:0x00007fcf160fb120>#        @config = #<Puma::Configuration:0x00007fcf169a6c98>#       >@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.#defprocess_client(client,buffer)begin# ...whiletrue# Let's step into this line.casehandle_request(client,buffer)# Will return true in this example.whentruereturnunless@queue_requestsbuffer.resetThreadPool.clean_thread_localsifclean_thread_localsunlessclient.reset(@status ==:run)close_socket=falseclient.set_timeout@persistent_timeout@reactor.addclientreturnendendend# ...ensurebuffer.resetclient.closeifclose_socket#...endend# 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+#defhandle_request(req,lines)env=req.env# ...# app: #<Puma::Configuration::ConfigMiddleware:0x00007fcf1612c338#        @app = #<YourProject::Application:0x00007fcf160fb120>#        @config = #<Puma::Configuration:0x00007fcf169a6c98>#       >status,headers,res_body=@app.call(env)# Let's step into this line.# ...returnkeep_aliveendendend
# ./gems/puma-3.12.0/lib/puma/configuration.rbmodulePumaclassConfigurationclassConfigMiddlewaredefinitialize(config,app)@config=config@app=appenddefcall(env)env[Const::PUMA_CONFIG]=@config# @app: #<YourProject::Application:0x00007fb4b1b4bcf8>@app.call(env)endendendend

Rack apps

As we see when Ruby enterPuma::Configuration::ConfigMiddleware#call, the@app isYourProject::Application instance.

It is just theRails.application.

Rack need acall method to process request.

Rails defined thiscall method inRails::Engine#call, so thatYourProject::Application instance will have acall method.

# ./gems/railties/lib/rails/engine.rbmoduleRailsclassEngine <Railtiedefcall(env)# This method will process every request. It is invoked by Rack.req=build_requestenvapp.callreq.env# The 'app' method is blow.enddefapp# FYI,# caller: [# "../gems/railties-5.2.2/lib/rails/application/finisher.rb:47:in `block in <module:Finisher>'",# "../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 `<top (required)>'",# "config.ru:2:in `require_relative'", "config.ru:2:in `block in <main>'",# "../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 `<main>'",# "../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 `<top (required)>'",# "../path/to/your_project/bin/rails:5:in `require'",# "../path/to/your_project/bin/rails:5:in `<main>'"# ]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 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).stack=default_middleware_stack# Let's step into this line# 'middleware' is a 'middleware_stack'!config.middleware=build_middleware.merge_into(stack)# 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: #<Rack::Sendfile:0x00007ff14d905f60#          @app=#<ActionDispatch::Static:0x00007ff14d906168#                 @app=#<ActionDispatch::Executor:0x00007ff14d9061b8#                        ...#                           @app=#<Rack::ETag:0x00007fa1e540c4f8#                                  @app=#<Rack::TempfileReaper:0x00007fa1e540c520#                                         @app=#<ActionDispatch::Routing::RouteSet:0x00007fa1e594cbe8>#                                 >#                         ...#                        >##                 >#         >@append# Defaults to an ActionDispatch::Routing::RouteSet instance.defendpointActionDispatch::Routing::RouteSet.new_with_config(config)endendend
# ./gems/railties/lib/rails/application...moduleRailsclassApplication <Enginedefdefault_middleware_stackdefault_stack=DefaultMiddlewareStack.new(self,config,paths)default_stack.build_stack# Let's step into this line.endclassDefaultMiddlewareStackattr_reader:config,:paths,:appdefinitialize(app,config,paths)@app=app@config=config@paths=pathsenddefbuild_stackActionDispatch::MiddlewareStack.newdo |middleware|ifconfig.force_sslmiddleware.use ::ActionDispatch::SSL,config.ssl_optionsendmiddleware.use ::Rack::Sendfile,config.action_dispatch.x_sendfile_headerifconfig.public_file_server.enabledheaders=config.public_file_server.headers ||{}middleware.use ::ActionDispatch::Static,paths["public"].first,index:config.public_file_server.index_name,headers:headersendifrack_cache=load_rack_cacherequire"action_dispatch/http/rack_cache"middleware.use ::Rack::Cache,rack_cacheendifconfig.allow_concurrency ==false# User has explicitly opted out of concurrent request# handling: presumably their code is not threadsafemiddleware.use ::Rack::Lockendmiddleware.use ::ActionDispatch::Executor,app.executormiddleware.use ::Rack::Runtimemiddleware.use ::Rack::MethodOverrideunlessconfig.api_onlymiddleware.use ::ActionDispatch::RequestIdmiddleware.use ::ActionDispatch::RemoteIp,config.action_dispatch.ip_spoofing_check,config.action_dispatch.trusted_proxiesmiddleware.use ::Rails::Rack::Logger,config.log_tagsmiddleware.use ::ActionDispatch::ShowExceptions,show_exceptions_appmiddleware.use ::ActionDispatch::DebugExceptions,app,config.debug_exception_response_formatunlessconfig.cache_classesmiddleware.use ::ActionDispatch::Reloader,app.reloaderendmiddleware.use ::ActionDispatch::Callbacksmiddleware.use ::ActionDispatch::Cookiesunlessconfig.api_onlyif !config.api_only &&config.session_storeifconfig.force_ssl &&config.ssl_options.fetch(:secure_cookies,true) && !config.session_options.key?(:secure)config.session_options[:secure]=trueendmiddleware.useconfig.session_store,config.session_optionsmiddleware.use ::ActionDispatch::Flashendunlessconfig.api_onlymiddleware.use ::ActionDispatch::ContentSecurityPolicy::Middlewareendmiddleware.use ::Rack::Headmiddleware.use ::Rack::ConditionalGetmiddleware.use ::Rack::ETag,"no-cache"middleware.use ::Rack::TempfileReaperunlessconfig.api_onlyendendendendend
# ./gems/actionpack-5.2.2/lib/action_dispatch/middleware/stack.rbmoduleActionDispatchclassMiddlewareStackdefuse(klass, *args, &block)middlewares.push(build_middleware(klass,args,block))enddefbuild_middleware(klass,args,block)Middleware.new(klass,args,block)enddefbuild(app=Proc.new)# See Enumerable#inject for more information.return_val=middlewares.freeze.reverse.inject(app)do |a,middleware|# a: app, and will be changed when iterating# middleware: #<ActionDispatch::MiddlewareStack::Middleware:0x00007f8a4fada6e8>, 'middleware' will be switched to another instance of ActionDispatch::MiddlewareStack::Middleware when iteratingmiddleware.build(a)# Let's step into this line.endreturn_valendclassMiddlewaredefinitialize(klass,args,block)@klass=klass@args=args@block=blockenddefbuild(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)endendendend

The core app: ActionDispatch::Routing::RouteSet instance

# Paste again FYI.#  @app: #<Rack::Sendfile:0x00007ff14d905f60#          @app=#<ActionDispatch::Static:0x00007ff14d906168#                 @app=#<ActionDispatch::Executor:0x00007ff14d9061b8#                        ...#                           @app=#<Rack::ETag:0x00007fa1e540c4f8#                                  @app=#<Rack::TempfileReaper:0x00007fa1e540c520#                                         @app=#<ActionDispatch::Routing::RouteSet:0x00007fa1e594cbe8>#                                 >#                         ...#                        >##                 >#         >

As we see in the Rack middleware stack, the last @app is

@app=#<ActionDispatch::Routing::RouteSet:0x00007fa1e594cbe8>

# ./gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rbmoduleActionDispatchmoduleRoutingclassRouteSetdefinitialize(config=DEFAULT_CONFIG)@set=Journey::Routes.new@router=Journey::Router.new(@set)enddefcall(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.endendend# ./gems/actionpack5.2.2/lib/action_dispatch/journey/router.rbmoduleJourneyclassRouterclassRoutingError < ::StandardErrorendattr_accessor:routesdefinitialize(routes)@routes=routesenddefserve(req)find_routes(req).eachdo |match,parameters,route|# Let's step into 'find_routes'set_params=req.path_parameterspath_info=req.path_infoscript_name=req.script_nameunlessroute.path.anchoredreq.script_name=(script_name.to_s +match.to_s).chomp("/")req.path_info=match.post_matchreq.path_info="/" +req.path_infounlessreq.path_info.start_with?"/"endparameters=route.defaults.mergeparameters.transform_values{ |val|val.dup.force_encoding(::Encoding::UTF_8)}req.path_parameters=set_params.mergeparameters# '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_namereq.path_info=path_inforeq.path_parameters=set_paramsnextendreturn[status,headers,body]end[404,{"X-Cascade"=>"pass"},["Not Found"]]enddeffind_routes(req)routes=filter_routes(req.path_info).concatcustom_routes.find_all{ |r|r.path.match(req.path_info)}routes=ifreq.head?match_head_routes(routes,req)elsematch_routes(routes,req)endroutes.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)ifval}[match_data,path_parameters,r]}endendendend# ./gems/actionpack-5.2.2/lib/action_dispatch/routing/route_set.rbmoduleActionDispatchmoduleRoutingclassRouteSetclassDispatcher <Routing::Endpointdefserve(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 { |res| res.request = request; }res=controller.make_response!(req)dispatch(controller,params[:action],req,res)# Let's step into this line.rescueActionController::RoutingErrorif@raise_on_name_errorraiseelsereturn[404,{"X-Cascade"=>"pass"},[]]endendprivatedefcontroller(req)req.controller_classrescueNameError=>eraiseActionController::RoutingError,e.message,e.backtraceenddefdispatch(controller,action,req,res)controller.dispatch(action,req,res)# Let's step into this line.endendendendend# ./gems/actionpack-5.2.2/lib/action_controller/metal.rbmoduleActionControllerclassMetal <AbstractController::Baseabstract!defself.controller_name@controller_name ||=name.demodulize.sub(/Controller$/,"").underscoreenddefself.make_response!(request)ActionDispatch::Response.new.tapdo |res|res.request=requestendendclass_attribute:middleware_stack,default:ActionController::MiddlewareStack.newdefself.inherited(base)base.middleware_stack=middleware_stack.dupsuperend# Direct dispatch to the controller. Instantiates the controller, then# executes the action named +name+.defself.dispatch(name,req,res)ifmiddleware_stack.any?middleware_stack.build(name){ |env|new.dispatch(name,req,res)}.callreq.envelse# '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# < 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::Basenew.dispatch(name,req,res)# Let's step into this line.endenddefdispatch(name,request,response)set_request!(request)set_response!(response)process(name)# Let's step into this line.request.commit_flashto_aenddefto_aresponse.to_aendendend# .gems/actionpack-5.2.2/lib/abstract_controller/base.rbmoduleAbstractControllerclassBasedefprocess(action, *args)@_action_name=action.to_sunlessaction_name=_find_action_name(@_action_name)raiseActionNotFound,"The action '#{action}' could not be found for#{self.class.name}"end@_response_body=nil# action_name: 'index'process_action(action_name, *args)# Let's step into this line.endendend# .gems/actionpack-5.2.2/lib/action_controller/metal/instrumentation.rbmoduleActionControllermoduleInstrumentationdefprocess_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: #<HomeController:0x00007fcd3c5dfd48>result=super# Let's step into this line.payload[:status]=response.statusresultensureappend_info_to_payload(payload)endendendendend# .gems/actionpack-5.2.2/lib/action_controller/metal/rescue.rbmoduleActionControllermoduleRescuedefprocess_action(*args)super# Let's step into this line.rescueException=>exceptionrequest.env["action_dispatch.show_detailed_exceptions"] ||=show_detailed_exceptions?rescue_with_handler(exception) ||raiseendendend# .gems/actionpack-5.2.2/lib/abstract_controller/callbacks.rbmoduleAbstractController# = 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:## * <tt>after_action</tt># * <tt>before_action</tt># * <tt>skip_before_action</tt># * ...moduleCallbacksdefprocess_action(*args)run_callbacks(:process_action)do# self: #<HomeController:0x00007fcd3c5dfd48>super# Let's step into this line.endendendend# .gems/actionpack-5.2.2/lib/action_controller/metal/rendering.rbmoduleActionControllermoduleRenderingdefprocess_action(*)self.formats=request.formats.map(&:ref).compactsuper# Let's step into this line.endendend# .gems/actionpack-5.2.2/lib/abstract_controller/base.rbmoduleAbstractControllerclassBasedefprocess_action(method_name, *args)# self: #<HomeController:0x00007fcd3c5dfd48>, method_name: 'index'# In the end, method 'send_action' is method 'send' by `alias send_action send`send_action(method_name, *args)endaliassend_actionsendendend# .gems/actionpack-5.2.2/lib/action_controller/metal/basic_implicit_render.rbmoduleActionControllermoduleBasicImplicitRenderdefsend_action(method, *args)# self: #<HomeController:0x00007fcd3c5dfd48>, method_name: 'index'# 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_renderunlessperformed?}# Let's step into 'default_render' later.endendend# ./your_project/app/controllers/home_controller.rbclassHomeController <ApplicationController# Will go back to BasicImplicitRender#send_action when method 'index' is done.defindex# 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)endend
# ./app/views/home/index.html.erb<divclass="container"><h1class="display-4 font-italic"><%= t('home.banner_title') %><%= @users %></h1></div>

Render view

As we see inActionController::BasicImplicitRender::send_action, the last line isdefault_render.

So afterHomeController#index is done, Ruby will execute methoddefault_render.

# .gems/actionpack-5.2.2/lib/action_controller/metal/implicit_render.rbmoduleActionController# Handles implicit rendering for a controller action that does not# explicitly respond with +render+, +respond_to+, +redirect+, or +head+.moduleImplicitRenderdefdefault_render(*args)# Let's step into template_exists?iftemplate_exists?(action_name.to_s,_prefixes,variants:request.variant)# Rails has found the default template './app/views/home/index.html.erb', so render it.render(*args)# Let's step into this line later#...elselogger.info"No template found for#{self.class.name}\##{action_name}, rendering head :no_content"ifloggersuperendendendend# .gems/actionview-5.2.2/lib/action_view/lookup_context.rbmoduleActionViewclassLookupContextmoduleViewPaths# Rails checks whether the default template exists.defexists?(name,prefixes=[],partial=false,keys=[], **options)@view_paths.exists?(*args_for_lookup(name,prefixes,partial,keys,options))endalias:template_exists?:exists?endendend# .gems/actionpack-5.2.2/lib/action_controller/metal/instrumentation.rbmoduleActionControllermoduleInstrumentationdefrender(*args)render_output=nilself.view_runtime=cleanup_view_runtimedoBenchmark.ms{# self: #<HomeController:0x00007fa7e9c54278>render_output=super# Let's step into super}endrender_outputendendend# .gems/actionpack-5.2.2/lib/action_controller/metal/rendering.rbmoduleActionControllermoduleRendering# Check for double render errors and set the content_type after rendering.defrender(*args)raise ::AbstractController::DoubleRenderErrorifresponse_bodysuper# Let's step into superendendend# .gems/actionpack-5.2.2/lib/abstract_controller/rendering.rbmoduleAbstractControllermoduleRendering# Normalizes arguments, options and then delegates render_to_body and# sticks the result in <tt>self.response_body</tt>.defrender(*args, &block)options=_normalize_render(*args, &block)rendered_body=render_to_body(options)# Let's step into this line.ifoptions[:html]_set_html_content_typeelse_set_rendered_content_typerendered_formatendself.response_body=rendered_bodyendendend# .gems/actionpack-5.2.2/lib/action_controller/metal/renderers.rbmoduleActionControllermoduleRenderersdefrender_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.eachdo |name|ifoptions.key?(name)_process_options(options)method_name=Renderers._render_with_renderer_method_name(name)returnsend(method_name,options.delete(name),options)endendnilendendend# .gems/actionpack-5.2.2/lib/action_controller/metal/renderers.rbmoduleActionControllermoduleRenderingdefrender_to_body(options={})super ||_render_in_priorities(options) ||" "# Let's step into 'super'endendend
# .gems/actionview-5.2.2/lib/action_view/rendering.rbmoduleActionViewmoduleRenderingdefrender_to_body(options={})_process_options(options)_render_template(options)# Let's step into this line.enddef_render_template(options)variant=options.delete(:variant)assigns=options.delete(:assigns)context=view_context# We will step into this line later.context.assignassignsifassignslookup_context.rendered_format=nilifoptions[:formats]lookup_context.variants=variantifvariantview_renderer.render(context,options)# Let's step into this line.endendend# .gems/actionview-5.2.2/lib/action_view/renderer/renderer.rbmoduleActionViewclassRendererdefrender(context,options)ifoptions.key?(:partial)render_partial(context,options)elserender_template(context,options)# Let's step into this line.endend# Direct access to template rendering.defrender_template(context,options)TemplateRenderer.new(@lookup_context).render(context,options)# Let's step into this line.endendend# .gems/actionview-5.2.2/lib/action_view/renderer/template_renderer.rbmoduleActionViewclassTemplateRenderer <AbstractRendererdefrender(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.enddefrender_template(template,layout_name=nil,locals=nil)view,locals=@view,locals ||{}render_with_layout(layout_name,locals)do |layout|# Let's step into this lineinstrument(:template,identifier:template.identifier,layout:layout.try(:virtual_path))do# template: #<ActionView::Template:0x00007f822759cbc0>template.render(view,locals){ |*name|view._layout_for(*name)}# Let's step into this lineendendenddefrender_with_layout(path,locals)layout=path &&find_layout(path,locals.keys,[formats.first])content=yield(layout)iflayoutview=@viewview.view_flow.set(:layout,content)layout.render(view,locals){ |*name|view._layout_for(*name)}elsecontentendendendend# .gems/actionview-5.2.2/lib/action_view/template.rbmoduleActionViewclassTemplatedefrender(view,locals,buffer=nil, &block)instrument_render_templatedo# self: #<ActionView::Template:0x00007f89bab1efb8#           @identifier="/path/to/your/project/app/views/home/index.html.erb"#           @source="<div class='container'\n ..."#        >compile!(view)# method_name: "_app_views_home_index_html_erb___3699380246341444633_70336654511160" (This method is defined in 'def compile(mod)' below)# view: #<#<Class:0x00007ff10d6c9d18>:0x00007ff10ea050a8>, view is an instance of <a subclass of ActionView::Base> which has same instance variables defined in the instance of HomeController.# You will get the result html after invoking 'view.send'.view.send(method_name,locals,buffer, &block)endrescue=>ehandle_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.defcompile!(view)returnif@compiled# Templates can be used concurrently in threaded environments# so compilation and any instance variable modification must# be synchronized@compile_mutex.synchronizedo# Any thread holding this lock will be compiling the template needed# by the threads waiting. So re-check the @compiled flag to avoid# re-compilationreturnif@compiledifview.is_a?(ActionView::CompiledTemplates)mod=ActionView::CompiledTemplateselsemod=view.singleton_classendinstrument("!compile_template")docompile(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=nilif@virtual_path@compiled=trueendenddefcompile(mod)encode!# @handler: #<ActionView::Template::Handlers::ERB:0x00007ff10e1be188>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 codesource=<<-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# ...#  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='<div>#                <h1>#                  '.freeze;#                   @output_buffer.append=( t('home.banner_title') );#                   @output_buffer.append=( @users );#                   @output_buffer.safe_append='#                </h1>#              </div>#              '.freeze;#              @output_buffer.to_s#            ensure#              @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer#            endmod.module_eval(source,identifier,0)# This line will actually define the method '_app_views_home_index_html_erb___1187260686135140546_70244801399180'# mod: ActionView::CompiledTemplatesObjectSpace.define_finalizer(self,Finalizer[method_name,mod])end# .gems/actionview-5.2.2/lib/action_view/template/handler/erb.rbmoduleHandlersclassERBdefcall(template)template_source=template.source.dup.force_encoding(Encoding::ASCII_8BIT)erb=template_source.gsub(ENCODING_TAG,"")encoding= $2erb.force_encodingvalid_encoding(template.source.dup,encoding)# Always make sure we return a String in the default_internalerb.encode!self.class.erb_implementation.new(erb,escape:(self.class.escape_whitelist.include?template.type),trim:(self.class.erb_trim_mode =="-")).srcendendendendend

How can instance variables defined in Controller be accessed in view file?

It's time to answer the question before:

How can instance variables like@users defined inHomeController be accessed in./app/views/home/index.html.erb?

I will answer this question by showing the source code below.

# ./gems/actionview-5.2.2/lib/action_view/rendering.rbmoduleActionViewmoduleRenderingdefview_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,# This line will set the instance variables like '@users' in this example. Let's step into this line.self)enddefview_assigns# self: #<HomeController:0x00007f83ecfed310>protected_vars=_protected_ivars# instance_variables is an instance method of class `Object` and it will return an array. And the array contains @users.variables=instance_variablesvariables.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"]]}retenddefview_context_class# Will return a subclass of ActionView::Base.@_view_context_class ||=self.class.view_context_classend# 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 automatically before method 'included' executed.# https://apidock.com/ruby/v1_9_3_392/Module/append_featuresmoduleClassMethodsdefview_context_class# self: HomeController@view_context_class ||=beginsupports_path=supports_path?routes=respond_to?(:_routes)  &&_routeshelpers=respond_to?(:_helpers) &&_helpersClass.new(ActionView::Base)doifroutesincluderoutes.url_helpers(supports_path)includeroutes.mounted_helpersendifhelpersincludehelpersendendendendendendend# ./gems/actionview-5.2.2/lib/action_view/base.rbmoduleActionViewclassBasedefinitialize(context=nil,assigns={},controller=nil,formats=nil)@_config=ActiveSupport::InheritableOptions.newifcontext.is_a?(ActionView::Renderer)@view_renderer=contextelselookup_context=context.is_a?(ActionView::LookupContext) ?context :ActionView::LookupContext.new(context)lookup_context.formats=formatsifformatslookup_context.prefixes=controller._prefixesifcontroller@view_renderer=ActionView::Renderer.new(lookup_context)end@cache_hit={}assign(assigns)# Let's step into this line.assign_controller(controller)_prepare_contextenddefassign(new_assigns)@_assigns=new_assigns.eachdo |key,value|# This line will set the instance variables (like '@users') in HomeController to itself.instance_variable_set("@#{key}",value)endendendend

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 what does this command do?

The commandrails locates at./bin/.

#!/usr/bin/env rubyAPP_PATH=File.expand_path('../config/application',__dir__)require_relative'../config/boot'require'rails/commands'# Let's look at this file.
# ./railties-5.2.2/lib/rails/commands.rbrequire"rails/command"aliases={"g"=>"generate","d"=>"destroy","c"=>"console","s"=>"server","db"=>"dbconsole","r"=>"runner","t"=>"test"}command=ARGV.shiftcommand=aliases[command] ||command# command is 'server'Rails::Command.invokecommand,ARGV# Let's step into this line.
# ./railties-5.2.2/lib/rails/command.rbmoduleRailsmoduleCommandclass <<selfdefinvoke(full_namespace,args=[], **config)# ...# command_name: 'server'# After calling `find_by_namespace`, we will get this result:# command: Rails::Command::ServerCommandcommand=find_by_namespace(namespace,command_name)# Equals to: Rails::Command::ServerCommand.perform('server', args, config)command.perform(command_name,args,config)endendendend
# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rbmoduleRailsmoduleCommand# There is a class method 'perform' in the Base class.classServerCommand <Baseendendend

Thor

Thor is a toolkit for building powerful command-line interfaces.

https://github.com/erikhuda/thor

Inheritance relationship:Rails::Command::ServerCommand < Rails::Command::Base < Thor

# ./gems/railties-5.2.2/lib/rails/command/base.rbmoduleRailsmoduleCommandclassBase <Thorclass <<self# command: 'server'defperform(command,args,config)#...dispatch(command,args.dup,nil,config)# Thor.dispatchendendendendend
# ./gems/thor-0.20.3/lib/thor.rbclassThorclass <<self# meth is 'server'defdispatch(meth,given_args,given_opts,config)# ...# Will new a Rails::Command::ServerCommand instance here# because 'self' is Rails::Command::ServerCommand.instance=new(args,opts,config)# ...# Method 'invoke_command' is defined in Thor::Invocation.# command: {Thor::Command}#<struct Thor::Command name="server" ...>instance.invoke_command(command,trailing ||[])endendend# ./gems/thor-0.20.3/lib/thor/invocation.rbclassThor# FYI, this module is included in Thor.# And Thor is grandfather of Rails::Command::ServerCommandmoduleInvocationdefinvoke_command(command, *args)# 'invoke_command' is defined at here.# ...# self: #<Rails::Command::ServerCommand:0x00007fdcc49791b0># command: {Thor::Command}#<struct Thor::Command name="server" ...>command.run(self, *args)endendend# ./gems/thor-0.20.3/lib/thor/command.rbclassThorclassCommand <Struct.new(:name,:description,:long_description,:usage,:options,:ancestor_name)defrun(instance,args=[])# ...# instance: #<Rails::Command::ServerCommand:0x00007fdcc49791b0># 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)endendend
# ./gems/thor-0.20.3/lib/thor.rbclassThor# ...includeThor::Base# Will invoke hooked method 'Thor::Base.included(self)'end# ./gems/thor-0.20.3/lib/thor/base.rbmoduleThormoduleBaseclass <<self# 'included' is a hooked method.# When module 'Thor::Base' is included, method 'included' is executed.defincluded(base)# base: Thor# this line will define `Thor.method_added`.base.extendClassMethods# 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::ServerCommandbase.send:include,Invocation# 'invoke_command' is defined in module Invocationbase.send:include,ShellendendmoduleClassMethods# '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,# `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' in command `$ rails server`.defmethod_added(meth)# ...# self: {Class} Rails::Command::ServerCommandcreate_command(meth)# meth is 'perform'. Let's step into this line.endendendend# ./gems/railties-5.2.2/lib/rails/command/base.rbmoduleRailsmoduleCommand# Rails::Command::Base is superclass of Rails::Command::ServerCommandmoduleBaseclass <<selfdefcreate_command(meth)ifmeth =="perform"# Calling instance method 'server' of Rails::Command::ServerCommand# will be transferred to call instance method 'perform'.alias_method('server',meth)endendendendendend# ./gems/thor-0.20.3/lib/thor/command.rbclassThorclassCommand <Struct.new(:name,:description,:long_description,:usage,:options,:ancestor_name)defrun(instance,args=[])#...# instance: {Rails::Command::ServerCommand}#<Rails::Command::ServerCommand:0x00007fa5f319bf40># 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)endendend# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rbmoduleRailsmoduleCommandclassServerCommand <Base# This is the method will be executed when `$ rails server`.defperform# ...Rails::Server.new(server_options).tapdo |server|# 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'.requireAPP_PATHDir.chdir(Rails.application.root)server.start# Let's step into this line.endendendendend

Rails::Server#start

# ./gems/railties-5.2.2/lib/rails/commands/server/server_command.rbmoduleRailsclassServer < ::Rack::Serverdefstartprint_boot_informationtrap(:INT)doexitendcreate_tmp_directoriessetup_dev_caching# 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.ensureputs"Exiting"unless@options &&options[:daemonize]enddeflog_to_stdout# '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'.# It has been created when `require APP_PATH` in previous code,# just at the 'perform' method in Rails::Command::ServerCommand.wrapped_app# Let's step into this line# ...endendend# ./gems/rack-2.0.6/lib/rack/server.rbmoduleRackclassServerdefwrapped_app@wrapped_app ||=build_app(app# Let's step into this line.)enddefapp@app ||=build_app_and_options_from_config# Let's step into this line.@appenddefbuild_app_and_options_from_config# ...# self.options[:config]: 'config.ru'. Let's step into this line.app,options=Rack::Builder.parse_file(self.options[:config],opt_parser)# ...append# This method is called in Rails::Server#startdefstart(&blk)#...wrapped_app#...# server: {Module} Rack::Handler::Puma# wrapped_app: {YourProject::Application} #<YourProject::Application:0x00007f7fe5523f98>server.run(wrapped_app,options, &blk)# We will step into this line (Rack::Handler::Puma.run) later.endendend# ./gems/rack/lib/rack/builder.rbmoduleRackmoduleBuilderdefself.parse_file(config,opts=Server::Options.new)# config: 'config.ru'cfgfile= ::File.read(config)app=new_from_string(cfgfile,config)returnapp,optionsend# Let's guess what does 'run Rails.application' do in config.ru?# 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 #<Rack::Builder:0x00007f8c861ec278 @warmup=nil, @run=nil, @map=nil, @use=[]>,# '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 for Rack::Builder instance.# enddefself.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,0endendend

Starting Puma

As we see inRack::Server#start, there isRack::Handler::Puma.run(wrapped_app, options, &blk).

# ./gems/puma-3.12.0/lib/rack/handler/puma.rbmoduleRackmoduleHandlermodulePuma# This method is invoked in `Rack::Server#start`:# Rack::Handler::Puma.run(wrapped_app, options, &blk)defself.run(app,options={})conf=self.config(app,options)# ...launcher= ::Puma::Launcher.new(conf,:events=>events)begin# Puma will run your app (instance of YourProject::Application)launcher.run# Let's step into this line.rescueInterruptputs"* Gracefully stopping, waiting for requests to finish"launcher.stopputs"* Goodbye!"endendendendend# .gems/puma-3.12.0/lib/puma/launcher.rbmodulePuma# 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/<env>.rb`.## It is responsible for either launching a cluster of Puma workers or a single# Puma server.classLauncherdefinitialize(conf,launcher_args={})@runner=nil@config=conf# ...ifclustered?# ...@runner=Cluster.new(self,@events)else# For this example, it is Single.new.@runner=Single.new(self,@events)end# ...enddefrun#...# Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id` received.setup_signals# We will discuss this line later.set_process_title@runner.run# We will enter `Single.new(self, @events).run` here.case@statuswhen:haltlog"* Stopping immediately!"when:run,:stopgraceful_stopwhen:restartlog"* Restarting..."ENV.replace(previous_env)@runner.before_restartrestart!when:exit# nothingendendendend
# .gems/puma-3.12.0/lib/puma/single.rbmodulePuma# 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.classSingle <Runnerdefrun# ...# @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 thread execution.# 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.thread.join# 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 thread will exit after all code executed and all the threads not joined will be killed.puts"anything which will never be executed..."endendend
# .gems/puma-3.12.0/lib/puma/runner.rbmodulePuma# 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`.classRunnerdefapp@app ||=@launcher.config.appenddefstart_servermin_t=@options[:min_threads]max_t=@options[:max_threads]server=Puma::Server.new(app,@launcher.events,@options)server.min_threads=min_tserver.max_threads=max_t# ...serverendendend
# .gems/puma-3.12.0/lib/puma/server.rbmodulePumaclassServerdefrun(background=true)#...@status=:runqueue_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 threadThread.current[ThreadLocalKey]=selfprocess_now=falseifqueue_requestsprocess_now=client.eagerly_finishend# ...ifprocess_now# 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)elseclient.set_timeout@first_data_timeout@reactor.addclientendend# ...ifbackground# background: true (for this example)# This part is important.# 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# (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@threadelsehandle_serversendenddefhandle_serverssockets=[check] +@binder.iospool=@thread_poolqueue_requests=@queue_requests# ...# 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 ==:runbegin# This line will cause current thread waiting until a request arrives.# So it will be the entry of every request!# sockets: [#<IO:fd 23>, #<TCPServer:fd 22, AF_INET, 0.0.0.0, 3000>]ios=IO.selectsocketsios.first.eachdo |sock|ifsock ==checkbreakifhandle_checkelseifio=sock.accept_nonblock# You can simply look upon 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.endendendrescueObject=>e@events.unknown_errorself,e,"Listen loop"endendendendend
# .gems/puma-3.12.0/lib/puma/thread_pool.rbmodulePumaclassThreadPool# 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.#definitialize(min,max, *extra, &block)#..@mutex=Mutex.new@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.@workers=[]@reaper=nil@mutex.synchronizedo@min.times{spawn_thread}# Puma spawns @min count threads.endenddefspawn_thread@spawned +=1# 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.3Thread.current.name='puma %03i' %spawnedifThread.current.respond_to?(:name=)block=@blockmutex=@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.whiletruework=nilcontinue=truemutex.synchronizedowhiletodo.empty?if@trim_requested >0@trim_requested -=1continue=falsenot_full.signalbreakendif@shutdowncontinue=falsebreakend@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 waked up thread will process the request.not_empty.waitmutex@waiting -=1end# `work` is the request (in Puma, it's Puma::Client instance) which need to be processed.work=todo.shiftifcontinueendbreakunlesscontinueif@clean_thread_localsThreadPool.clean_thread_localsendbegin# `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)rescueException=>eSTDERR.puts"Error reached top of thread-pool:#{e.message} (#{e.class})"endendmutex.synchronizedo@spawned -=1@workers.deletethendend# end of the Thread.new.@workers <<ththenddefwait_until_not_full@mutex.synchronizedowhiletruereturnif@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.returnif@todo.size -@waiting <@max -@spawned@not_full.wait@mutexendendend# Add +work+ to the todo list for a Thread to pickup and process.def <<(work)@mutex.synchronizedoif@shutdownraise"Unable to add work while shutting down"end# work: #<Puma::Client:0x00007ff114ece6b0># You can look upon Puma::Client instance as a request.@todo <<workif@waiting <@todo.sizeand@spawned <@maxspawn_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.# This code is in `spawn_thread`:# while true#   # ...#   not_empty.wait mutex#   # ...#   block.call(work, *extra) # This line will process the request.# end@not_empty.signalendendendend

Conclusion

In conclusion,$ rails server will executeRails::Command::ServerCommand#perform.

In#perform, callRails::Server#start. Then callRack::Server#start.

Then callRack::Handler::Puma.run(YourProject::Application.new).

In.run, Puma will new a always running Thread forios = IO.select(#<TCPServer:fd 22, AF_INET, 0.0.0.0, 3000>).

Request is created fromios object.

A thread in Puma threadPool will process the request.

The thread will invoke Rack apps'call to get the response for the request.

Exiting Puma

Process and Thread

Because Puma is using multiple threads, we need to have some basic concepts about Process and Thread.

This link is good for you to obtain the concepts:Process and Thread

In the next part, you will often seethread.join.

I will use two simple example to tell what doesthread.join do.

Example one

Try to runtest_thread_join.rb.

# ./test_thread_join.rbthread=Thread.new()do3.timesdo |n|puts"~~~~ " +n.to_sendend# sleep 1puts"==== 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 nothread.join, you can see

==== I am the main thread.==== after thread.join~~~~ 0~~~~ 1~~~~ 2

in console.

After you addedthread.join, you can see:

==== I am the main thread.~~~~ 0~~~~ 1~~~~ 2==== after thread.join

in console.

Example two

Try to runtest_thread_join2.rb.

# ./test_thread_join2.rbarr=[Thread.newdoputs'I am arr[0]'sleep1puts'After arr[0]'end,Thread.newdoputs'I am arr[1]'sleep5puts'After arr[1]'end,Thread.newdoputs'I am arr[2]'sleep8puts'After arr[2]'end]puts"Thread.list.size:#{Thread.list.size}"# returns 4 (including the main thread)sleep2arr.each{ |thread|puts"~~~~~#{thread}"}puts"Thread.list.size:#{Thread.list.size}"# returns 3 (because arr[0] is dead)arr[1].join# uncomment to see differencesarr.each{ |thread|puts"~~~~~#{thread}"}sleep7puts"Exit main thread"

SendSIGTERM to Puma

When you stop Puma by running$ kill -s SIGTERM puma_process_id, you will entersetup_signals inPuma::Launcher#run.

# .gems/puma-3.12.0/lib/puma/launcher.rbmodulePuma# Puma::Launcher is the single entry point for starting a Puma server based on user# configuration.classLauncherdefrun#...# Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id`.setup_signals# Let's step into this line.set_process_title@runner.run# ...end# 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'.defsetup_signalsbegin# After running `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`.Signal.trap"SIGTERM"dograceful_stop# Let's step into this line.raiseSignalException,"SIGTERM"endrescueExceptionlog"*** SIGTERM not implemented, signal based gracefully stopping unavailable!"endbeginSignal.trap"SIGUSR2"dorestartendrescueExceptionlog"*** SIGUSR2 not implemented, signal based restart unavailable!"endbeginSignal.trap"SIGUSR1"dophased_restartendrescueExceptionlog"*** SIGUSR1 not implemented, signal based restart unavailable!"endbeginSignal.trap"SIGINT"doifPuma.jruby?@status=:exitgraceful_stopexitendstopendrescueExceptionlog"*** SIGINT not implemented, signal based gracefully stopping unavailable!"endbeginSignal.trap"SIGHUP"doif@runner.redirected_io?@runner.redirect_ioelsestopendendrescueExceptionlog"*** SIGHUP not implemented, signal based logs reopening unavailable!"endenddefgraceful_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!"endendend# .gems/puma-3.12.0/lib/puma/launcher.rbmodulePumaclassSingle <Runnerdefrun# ...# @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 thread execution.# And the `thread`'s block (which is the method `handle_servers`) will be executed.thread.joinenddefstop_blockedlog"- 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 lineendendend# .gems/puma-3.12.0/lib/puma/server.rbmodulePumaclassServerdefinitialize(app,events=Events.stdio,options={})# 'Puma::Util.pipe' returns `IO.pipe`.@check,@notify=Puma::Util.pipe# @check, @notify is a pair.@status=:stopenddefrun(background=true)# ...@thread_pool=ThreadPool.new(@min_threads,@max_threads,IOBuffer)do |client,buffer|#...# Process the request.process_client(client,buffer)#...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{# 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 in the Puma stopping process.#     puts "#{Thread.current.object_id}" # returns '70144220123860' too.#   end# endputs"#{Thread.current.object_id}"# returns '70144220123860'handle_servers}return@threadend# Stops the acceptor thread and then causes the worker threads to finish# off the request queue before finally exiting.defstop(sync=false)# This line will set '@status = :stop',# 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',# 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 thread execution.# And the code in @thread will continue be executed.@thread.joinif@thread &&syncenddefnotify_safely(message)@notify <<messageenddefhandle_serversbegincheck=@check# sockets: [#<IO:fd 23>, #<TCPServer:fd 22, AF_INET, 0.0.0.0, 3000>]sockets=[check] +@binder.iospool=@thread_pool#...while@status ==:run# 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# end# sockets: [#<IO:fd 23>, #<TCPServer:fd 22, AF_INET, 0.0.0.0, 3000>]ios=IO.selectsocketsios.first.eachdo |sock|ifsock ==check# The @status is updated to :stop for this example in `handle_check`.breakifhandle_check# Let's step into this line.elseifio=sock.accept_nonblockclient=Client.new(io,@binder.env(sock))# ...pool <<clientpool.wait_until_not_fullendendendend# Let's step into `graceful_shutdown`.graceful_shutdownif@status ==:stop ||@status ==:restart# ...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# endputs"#{Thread.current.object_id}"@check.close@notify.close# ...endenddefhandle_checkcmd=@check.read(1)casecmdwhenSTOP_COMMAND@status=:stop# The @status is updated to :stop for this example.returntruewhenHALT_COMMAND@status=:haltreturntruewhenRESTART_COMMAND@status=:restartreturntrueendreturnfalseenddefgraceful_shutdownif@thread_pool@thread_pool.shutdown# Let's step into this line.endendendend
modulePumaclassThreadPool# Tell all threads in the pool to exit and wait for them to finish.defshutdown(timeout=-1)threads=@mutex.synchronizedo@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 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 @workers.dup here.@workers.dupend# Wait for threads to finish without force shutdown.threads.eachdo |thread|thread.joinend@spawned=0@workers=[]enddefinitialize(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 process a request.@workers=[]@mutex.synchronizedo@min.times{spawn_thread}# Puma spawns @min count threads.endenddefspawn_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=@blockmutex=@mutex#...whiletruework=nilcontinue=truemutex.synchronizedowhiletodo.empty?# ...if@shutdowncontinue=falsebreakend# ...# After `@not_empty.broadcast` is executed in '#shutdown', `not_empty` is waked up.# Ruby will continue to execute the next line here.not_empty.waitmutex@waiting -=1end# ...endbreakunlesscontinue# ...endmutex.synchronizedo@spawned -=1@workers.deletethendend# end of the Thread.new.@workers <<ththendendend

So all the threads in the ThreadPool joined and finished.

Let's inspect the caller in block ofSignal.trap "SIGTERM" below.

# .gems/puma-3.12.0/lib/puma/launcher.rbmodulePuma# Puma::Launcher is the single entry point for starting a Puma server based on user# configuration.classLauncherdefrun#...# Set the behaviors for signals like `$ kill -s SIGTERM puma_process_id`.setup_signals# Let's step into this line.set_process_title# Process.pid: 42264puts"Process.pid:#{Process.pid}"@runner.run# ...enddefsetup_signals# ...begin# After running `$ kill -s SIGTERM puma_process_id`, Ruby will execute the block of `Signal.trap "SIGTERM"`.Signal.trap"SIGTERM"do# 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'",#   "../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 `<top (required)>'",#   "../path/to/your_project/bin/rails:5:in `require'",#   "../path/to/your_project/bin/rails:5:in `<main>'"# ]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.raiseSignalException,"SIGTERM"endrescueException# This `rescue` is only for `Signal.trap "SIGTERM"`, not for `raise SignalException, "SIGTERM"`.log"*** SIGTERM not implemented, signal based gracefully stopping unavailable!"endendendend

Welcome to point out the mistakes in this article :)

About

Solving LeetCode problems in the best way. Python, Java, C++, JavaScript, Go, C# and Ruby are supported! Official website👇🏻:

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp