NO LEA ESTE ARCHIVO EN GITHUB, LAS GUÍAS ESTÁN PUBLICADAS EN https://guides.rubyonrails.org.

El Proceso de Inicialización de Rails

Esta guía explica los aspectos internos del proceso de inicialización en Rails. Es una guía extremadamente detallada y se recomienda para desarrolladores avanzados de Rails.

Después de leer esta guía, sabrás:


Esta guía repasa cada llamada de método que es necesaria para iniciar la pila de Ruby on Rails para una aplicación Rails por defecto, explicando cada parte en detalle a lo largo del camino. Para esta guía, nos enfocaremos en lo que sucede cuando ejecutas bin/rails server para iniciar tu aplicación.

NOTA: Las rutas en esta guía son relativas a Rails o a una aplicación Rails a menos que se especifique lo contrario.

CONSEJO: Si deseas seguir mientras navegas por el código fuente de Rails, te recomendamos que uses la tecla t para abrir el buscador de archivos dentro de GitHub y encontrar archivos rápidamente.

1 ¡Despegue!

Comencemos a iniciar e inicializar la aplicación. Una aplicación Rails generalmente se inicia ejecutando bin/rails console o bin/rails server.

1.1 bin/rails

Este archivo es el siguiente:

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative "../config/boot"
require "rails/commands"

La constante APP_PATH se usará más adelante en rails/commands. El archivo config/boot referenciado aquí es el archivo config/boot.rb en nuestra aplicación que es responsable de cargar Bundler y configurarlo.

1.2 config/boot.rb

config/boot.rb contiene:

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require "bundler/setup" # Configura las gemas listadas en el Gemfile.

En una aplicación Rails estándar, hay un Gemfile que declara todas las dependencias de la aplicación. config/boot.rb establece ENV['BUNDLE_GEMFILE'] en la ubicación de este archivo. Si el Gemfile existe, entonces se requiere bundler/setup. El require es usado por Bundler para configurar la ruta de carga para las dependencias de tu Gemfile.

1.3 rails/commands.rb

Una vez que config/boot.rb ha terminado, el siguiente archivo que se requiere es rails/commands, que ayuda a expandir alias. En el caso actual, el array ARGV simplemente contiene server, que será pasado:

require "rails/command"

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

Rails::Command.invoke command, ARGV

Si hubiéramos usado s en lugar de server, Rails habría usado los aliases definidos aquí para encontrar el comando correspondiente.

1.4 rails/command.rb

Cuando uno escribe un comando de Rails, invoke intenta buscar un comando para el espacio de nombres dado y ejecuta el comando si se encuentra.

Si Rails no reconoce el comando, le pasa el control a Rake para ejecutar una tarea con el mismo nombre.

Como se muestra, Rails::Command muestra automáticamente la salida de ayuda si el namespace está vacío.

module Rails
  module Command
    class << self
      def invoke(full_namespace, args = [], **config)
        namespace = full_namespace = full_namespace.to_s

        if char = namespace =~ /:(\w+)$/
          command_name, namespace = $1, namespace.slice(0, char)
        else
          command_name = namespace
        end

        command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
        command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

        command = find_by_namespace(namespace, command_name)
        if command && command.all_commands[command_name]
          command.perform(command_name, args, config)
        else
          find_by_namespace("rake").perform(full_namespace, args, config)
        end
      end
    end
  end
end

Con el comando server, Rails ejecutará el siguiente código:

module Rails
  module Command
    class ServerCommand < Base # :nodoc:
      def perform
        extract_environment_option_from_argument
        set_application_directory!
        prepare_restart

        Rails::Server.new(server_options).tap do |server|
          # Requerir la aplicación después de que el servidor establezca el entorno para propagar
          # la opción --environment.
          require APP_PATH
          Dir.chdir(Rails.application.root)

          if server.serveable?
            print_boot_information(server.server, server.served_url)
            after_stop_callback = -> { say "Exiting" unless options[:daemon] }
            server.start(after_stop_callback)
          else
            say rack_server_suggestion(using)
          end
        end
      end
    end
  end
end

Este archivo cambiará al directorio raíz de Rails (una ruta dos directorios arriba de APP_PATH, que apunta a config/application.rb), pero solo si el archivo config.ru no se encuentra. Esto luego inicia la clase Rails::Server.

1.5 actionpack/lib/action_dispatch.rb

Action Dispatch es el componente de enrutamiento del marco de Rails. Añade funcionalidades como enrutamiento, sesión y middlewares comunes.

1.6 rails/commands/server/server_command.rb

La clase Rails::Server se define en este archivo heredando de Rack::Server. Cuando se llama a Rails::Server.new, esto llama al método initialize en rails/commands/server/server_command.rb:

module Rails
  class Server < ::Rack::Server
    def initialize(options = nil)
      @default_options = options || {}
      super(@default_options)
      set_environment
    end
  end
end

Primero, se llama a super, que llama al método initialize en Rack::Server.

1.7 Rack: lib/rack/server.rb

Rack::Server es responsable de proporcionar una interfaz de servidor común para todas las aplicaciones basadas en Rack, de las cuales Rails ahora forma parte.

El método initialize en Rack::Server simplemente establece varias variables:

module Rack
  class Server
    def initialize(options = nil)
      @ignore_options = []

      if options
        @use_default_options = false
        @options = options
        @app = options[:app] if options[:app]
      else
        argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
        @use_default_options = true
        @options = parse_options(argv)
      end
    end
  end
end

En este caso, el valor de retorno de Rails::Command::ServerCommand#server_options se asignará a options. Cuando se evalúan las líneas dentro de la declaración if, se establecerán un par de variables de instancia.

El método server_options en Rails::Command::ServerCommand se define de la siguiente manera:

module Rails
  module Command
    class ServerCommand
      no_commands do
        def server_options
          {
            user_supplied_options: user_supplied_options,
            server:                using,
            log_stdout:            log_to_stdout?,
            Port:                  port,
            Host:                  host,
            DoNotReverseLookup:    true,
            config:                options[:config],
            environment:           environment,
            daemonize:             options[:daemon],
            pid:                   pid,
            caching:               options[:dev_caching],
            restart_cmd:           restart_command,
            early_hints:           early_hints
          }
        end
      end
    end
  end
end

El valor se asignará a la variable de instancia @options.

Después de que super haya terminado en Rack::Server, volvemos a rails/commands/server/server_command.rb. En este punto, se llama a set_environment dentro del contexto del objeto Rails::Server.

module Rails
  module Server
    def set_environment
      ENV["RAILS_ENV"] ||= options[:environment]
    end
  end
end

Después de que initialize haya terminado, volvemos al comando del servidor donde se requiere APP_PATH (que se estableció anteriormente).

1.8 config/application

Cuando se ejecuta require APP_PATH, se carga config/application.rb (recuerda que APP_PATH se define en bin/rails). Este archivo existe en tu aplicación y eres libre de cambiarlo según tus necesidades.

1.9 Rails::Server#start

Después de que se carga config/application, se llama a server.start. Este método se define así:

module Rails
  class Server < ::Rack::Server
    def start(after_stop_callback = nil)
      trap(:INT) { exit }
      create_tmp_directories
      setup_dev_caching
      log_to_stdout if options[:log_stdout]

      super()
      # ...
    end

    private
      def setup_dev_caching
        if options[:environment] == "development"
          Rails::DevCaching.enable_by_argument(options[:caching])
        end
      end

      def create_tmp_directories
        %w(cache pids sockets).each do |dir_to_make|
          FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make))
        end
      end

      def log_to_stdout
        wrapped_app # toca la aplicación para que el logger esté configurado

        console = ActiveSupport::Logger.new(STDOUT)
        console.formatter = Rails.logger.formatter
        console.level = Rails.logger.level

        unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
          Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
        end
      end
  end
end

Este método crea una trampa para señales INT, por lo que si presionas CTRL-C en el servidor, saldrá del proceso. Como podemos ver en el código aquí, creará los directorios tmp/cache, tmp/pids y tmp/sockets. Luego habilita la caché en desarrollo si bin/rails server se llama con --dev-caching. Finalmente, llama a wrapped_app, que es responsable de crear la aplicación Rack, antes de crear y asignar una instancia de ActiveSupport::Logger.

El método super llamará a Rack::Server.start, que comienza su definición como sigue:

module Rack
  class Server
    def start(&blk)
      if options[:warn]
        $-w = true
      end

      if includes = options[:include]
        $LOAD_PATH.unshift(*includes)
      end

      if library = options[:require]
        require library
      end

      if options[:debug]
        $DEBUG = true
        require "pp"
        p options[:server]
        pp wrapped_app
        pp app
      end

      check_pid! if options[:pid]

      # Toca la aplicación envuelta, para que config.ru se cargue antes de
      # la demonización (es decir, antes de chdir, etc.).
      handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
        wrapped_app
      end

      daemonize_app if options[:daemonize]

      write_pid if options[:pid]

      trap(:INT) do
        if server.respond_to?(:shutdown)
          server.shutdown
        else
          exit
        end
      end

      server.run wrapped_app, options, &blk
    end
  end
end

La parte interesante para una aplicación Rails es la última línea, server.run. Aquí encontramos nuevamente el método wrapped_app, que esta vez vamos a explorar más (aunque ya se ejecutó antes, y por lo tanto, ya está memorizado).

module Rack
  class Server
    def wrapped_app
      @wrapped_app ||= build_app app
    end
  end
end

El método app aquí se define así:

module Rack
  class Server
    def app
      @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
    end

    # ...

    private
      def build_app_and_options_from_config
        if !::File.exist? options[:config]
          abort "configuration #{options[:config]} not found"
        end

        app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
        @options.merge!(options) { |key, old, new| old }
        app
      end

      def build_app_from_string
        Rack::Builder.new_from_string(self.options[:builder])
      end
  end
end

El valor de options[:config] por defecto es config.ru, que contiene esto:

# Este archivo es usado por servidores basados en Rack para iniciar la aplicación.

require_relative "config/environment"

run Rails.application

El método Rack::Builder.parse_file aquí toma el contenido de este archivo config.ru y lo analiza usando este código:

module Rack
  class Builder
    def self.load_file(path, opts = Server::Options.new)
      # ...
      app = new_from_string cfgfile, config
      # ...
    end

    # ...

    def self.new_from_string(builder_script, file = "(rackup)")
      eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
        TOPLEVEL_BINDING, file, 0
    end
  end
end

El método initialize de Rack::Builder tomará el bloque aquí y lo ejecutará dentro de una instancia de Rack::Builder. Aquí es donde ocurre la mayor parte del proceso de inicialización de Rails. La línea require para config/environment.rb en config.ru es la primera en ejecutarse:

require_relative "config/environment"

1.10 config/environment.rb

Este archivo es el archivo común requerido por config.ru (bin/rails server) y Passenger. Aquí es donde estos dos métodos para ejecutar el servidor se encuentran; todo lo anterior a este punto ha sido configuración de Rack y Rails.

Este archivo comienza requiriendo config/application.rb:

require_relative "application"

1.11 config/application.rb

Este archivo requiere config/boot.rb:

require_relative "boot"

Pero solo si no se ha requerido antes, lo cual sería el caso en bin/rails server pero no sería el caso con Passenger.

¡Entonces comienza la diversión!

2 Cargando Rails

La siguiente línea en config/application.rb es:

require "rails/all"

2.1 railties/lib/rails/all.rb

Este archivo es responsable de requerir todos los marcos individuales de Rails:

require "rails"

%w(
  active_record/railtie
  active_storage/engine
  action_controller/railtie
  action_view/railtie
  action_mailer/railtie
  active_job/railtie
  action_cable/engine
  action_mailbox/engine
  action_text/engine
  rails/test_unit/railtie
).each do |railtie|
  begin
    require railtie
  rescue LoadError
  end
end

Aquí es donde se cargan todos los marcos de Rails y, por lo tanto, se hacen disponibles para la aplicación. No entraremos en detalle sobre lo que sucede dentro de cada uno de esos marcos, pero se te anima a intentar explorarlos por tu cuenta.

Por ahora, solo ten en cuenta que funcionalidades comunes como motores de Rails, I18n y configuración de Rails se están definiendo aquí.

2.2 Volvamos a config/environment.rb

El resto de config/application.rb define la configuración para el Rails::Application, que se usará una vez que la aplicación esté completamente inicializada. Cuando config/application.rb ha terminado de cargar Rails y ha definido el espacio de nombres de la aplicación, volvemos a config/environment.rb. Aquí, la aplicación se inicializa con Rails.application.initialize!, que se define en rails/application.rb.

2.3 railties/lib/rails/application.rb

El método initialize! se ve así:

def initialize!(group = :default) # :nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end

Solo puedes inicializar una aplicación una vez. Los inicializadores de Railtie se ejecutan a través del método run_initializers, que se define en railties/lib/rails/initializable.rb:

def run_initializers(group = :default, *args)
  return if instance_variable_defined?(:@ran)
  initializers.tsort_each do |initializer|
    initializer.run(*args) if initializer.belongs_to?(group)
  end
  @ran = true
end

El código de run_initializers en sí es complicado. Lo que Rails está haciendo aquí es recorrer todos los ancestros de la clase buscando aquellos que respondan a un método initializers. Luego ordena los ancestros por nombre y los ejecuta. Por ejemplo, la clase Engine hará que todos los motores estén disponibles al proporcionar un método initializers en ellos.

La clase Rails::Application, tal como se define en railties/lib/rails/application.rb define los inicializadores bootstrap, railtie y finisher. Los inicializadores bootstrap preparan la aplicación (como inicializar el logger), mientras que los inicializadores finisher (como construir la pila de middleware) se ejecutan al final. Los inicializadores railtie son los inicializadores que se han definido en el propio Rails::Application y se ejecutan entre los bootstrap y los finishers.

NOTA: No confundas los inicializadores de Railtie en general con el initializer instance de inicializadores o sus inicializadores de configuración asociados en config/initializers.

Después de que esto se hace, volvemos a Rack::Server.

2.4 Rack: lib/rack/server.rb

La última vez que nos fuimos, el método app estaba siendo definido:

module Rack
  class Server
    def app
      @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
    end

    # ...

    private
      def build_app_and_options_from_config
        if !::File.exist? options[:config]
          abort "configuration #{options[:config]} not found"
        end

        app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
        @options.merge!(options) { |key, old, new| old }
        app
      end

      def build_app_from_string
        Rack::Builder.new_from_string(self.options[:builder])
      end
  end
end

En este punto, app es la propia aplicación Rails (un middleware), y lo que sucede a continuación es que Rack llamará a todos los middlewares proporcionados:

module Rack
  class Server
    private
      def build_app(app)
        middleware[options[:environment]].reverse_each do |middleware|
          middleware = middleware.call(self) if middleware.respond_to?(:call)
          next unless middleware
          klass, *args = middleware
          app = klass.new(app, *args)
        end
        app
      end
  end
end

Recuerda, build_app fue llamado (por wrapped_app) en la última línea de Rack::Server#start. Así es como se veía cuando nos fuimos:

server.run wrapped_app, options, &blk

En este punto, la implementación de server.run dependerá del servidor que estés usando. Por ejemplo, si estuvieras usando Puma, así es como se vería el método run:

module Rack
  module Handler
    module Puma
      # ...
      def self.run(app, options = {})
        conf   = self.config(app, options)

        events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio

        launcher = ::Puma::Launcher.new(conf, events: events)

        yield launcher if block_given?
        begin
          launcher.run
        rescue Interrupt
          puts "* Deteniendo con gracia, esperando que las solicitudes terminen"
          launcher.stop
          puts "* ¡Adiós!"
        end
      end
      # ...
    end
  end
end

No profundizaremos en la configuración del servidor en sí, pero este es el último paso de nuestro viaje en el proceso de inicialización de Rails.

Esta visión general de alto nivel te ayudará a entender cuándo se ejecuta tu código y cómo, y en general a convertirte en un mejor desarrollador de Rails. Si aún quieres saber más, el propio código fuente de Rails es probablemente el mejor lugar para ir a continuación.


Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.