- Notifications
You must be signed in to change notification settings - Fork0
Solving LeetCode problems in the best way. Python, Java, C++, JavaScript, Go, C# and Ruby are supported! Official website👇🏻:
upcse/leetcode-python-java
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
- Part 0: Before reading Rails 5 source code
- Part 1: Your app: an instance of YourProject::Application
- Part 2: config
- Part 3: Every request and response
- Part 4: What does
$ rails server
do?
- 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.
- You need a good IDE which can help for debugging. I useRubyMine.
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.
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
.
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
Imagine we have this route for the home page.
# ./config/routes.rbRails.application.routes.drawdoroot'home#index'# HomeController#indexend
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
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
# 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>
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
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.
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 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
# ./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
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
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.
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.
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.
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"
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
Uh oh!
There was an error while loading.Please reload this page.