Séquence de démarrage de Rails 6

Younes Serraj
Younes Serraj4 juin 2019

Vous êtes-vous déjà demandé comment votre application Rails démarre ? Je veux dire, quand vous exécutez le rails server, que se passe-t-il ?

Séquence de démarrage de Rails 6


Pour répondre à cette question, nous allons commencer par générer une nouvelle application Rails 6 (j'utilise actuellement la version 6.0.0.rc1).

$ rails new iluvrails
$ cd iluvrails


./bin/rails

Le point de départ de la séquence de démarrage est l'exécutable rails. Pour simplifier ce blog, nous commencerons notre voyage dans ./bin/rails. Au fait, qu'est-ce que la gemme rails ? C'est un packaging pour tout ce qui suit :

$ gem dependency rails -v 6.0.0.rc1
Gem rails-6.0.0.rc1
  actioncable (= 6.0.0.rc1)
  actionmailbox (= 6.0.0.rc1)
  actionmailer (= 6.0.0.rc1)
  actionpack (= 6.0.0.rc1)
  actiontext (= 6.0.0.rc1)
  actionview (= 6.0.0.rc1)
  activejob (= 6.0.0.rc1)
  activemodel (= 6.0.0.rc1)
  activerecord (= 6.0.0.rc1)
  activestorage (= 6.0.0.rc1)
  activesupport (= 6.0.0.rc1)
  bundler (>= 1.3.0)
  railties (= 6.0.0.rc1)
  sprockets-rails (>= 2.0.0)

Le noyau de ce système est railties.

Rappel rapide :

  • Rails::Railtie est le noyau du framework Rails. Il fournit un ensemble de hooks (comme after_initialize, add_routing_paths ou set_load_path) pour étendre Rails et/ou modifier le processus d'initialisation.

  • Un railtie est une sous-classe de Rails::Railtie qui va étendre Rails. Elle utilise les crochets fournis par les Railties pour se connecter à Rails. En d'autres termes, ce n'est pas Rails qui connaît les autres composants à l'avance et qui les requiert, mais plutôt les composants qui implémentent chacun un railtie et qui s'intègrent à Rails, en faisant savoir à Rails qu'ils sont là.

  • Un moteur est un railtie avec certains initialisateurs déjà définis.

  • Rails::Application est un moteur.

Si vous voulez en savoir plus, il n'y a pas de meilleur moyen que de lire le code source :

$ cd `bundle show railties`
$ ls
$ # have fun


Retour à ./bin/rails. Qu'est-ce qu'il y a dedans ?


#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'


La partie concernant Spring n'entre pas dans le cadre de ce blog, nous allons donc la passer sous silence. Au cas où vous ne sauriez pas ce que c'est :

Spring est un préchargeur d'applications Rails. Il accélère le développement en maintenant votre application en arrière-plan, de sorte que vous n'avez pas besoin de la démarrer à chaque fois que vous exécutez un test, une tâche rake ou une migration.

APP_PATH = File.expand_path('../config/application', __dir__)

Cela permet de trouver le chemin absolu vers ./config/application.rb qui définit Iluvrails::Application (qui hérite de Rails::Application).c'est à ce moment que nous commençons à relier les points : votre application Rails est une railtie. Elle ne se contente pas d'inclure Rails : elle se branche elle-même sur Rails. Les deux dernières instructions de ./bin/rails sont :

require_relative '../config/boot'
require 'rails/commands'


Nous allons examiner chacun d'entre eux.


./config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

Tout d'abord, nous définissons ENV['BUNDLE_GEMFILE'] (s'il n'est pas déjà défini) au chemin absolu de notre Gemfile. C'est pour que bundler puisse charger les gems nécessaires plus tard. Ensuite, nous avons besoin de bundler/setup et de bootsnap/setup. bundler/setup vérifie votre version de ruby, la plate-forme que vous utilisez, s'assure que votre Gemfile et Gemfile.lock correspondent, etc. Bootsnap est un outil qui permet d'accélérer le temps de démarrage d'une application grâce à des opérations de mise en cache. Quant à Spring, c'est hors du champ de ce blog, donc je passe cette partie.


rails/commands

À ce stade, Rails va exécuter la commande que vous lui avez demandée : server. Voici un aperçu de la façon dont cela se passe.


# frozen_string_literal: true
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


Rails va demander rails/command puis lancer Rails::Command.invoke command, ARGV qui finira par appeler Rails::Command::ServerCommand.perform. Jetez un coup d'oeil à son code :

def perform
  extract_environment_option_from_argument
  set_application_directory!
  prepare_restart
  Rails::Server.new(server_options).tap do |server|
    # Require application after server sets environment to propagate
    # the --environment option.
    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


Ce qui est le plus intéressant pour nous ici est qu'il:

  • crée une nouvelle instance de Rails::Server qui est une sous-classe de Rack::Server

  • requiert APP_PATH, qui pointe vers notre ./config/application.rbquires 

  • change le répertoire courant en Rails.application.root

  • puis appelle #start sur l'instance de Rails::Server.

Rails fournit une interface minimale, modulaire et adaptable pour développer des applications web en Ruby. En enveloppant les requêtes et les réponses HTTP de la manière la plus simple possible, il unifie et distille l'API des serveurs web, des frameworks web et des logiciels intermédiaires (ce qu'on appelle le middleware) en un seul appel de méthode.

À ce stade, si tout s'est bien passé, l'application démarre et vous pouvez y accéder depuis votre navigateur Web.C'est tout. Merci de votre lecture! Humm... pas si vite. N'avons-nous pas lu une déclaration APP_PATH obligatoire ? Eh bien, voyons ce qui se passe ici.

./config/application.rb

require_relative 'boot'
require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Iluvrails
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

Tout d'abord, il y a require_relative 'boot'. Nous avons déjà demandé ce fichier, donc rien ne se passe à ce stade. Ensuite, nous demandons 'rails/all'.


# frozen_string_literal: true
# rubocop:disable Style/RedundantBegin
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
  sprockets/railtie
).each do |railtie|
  begin
    require railtie
  rescue LoadError
  end
end

Ceci requiert le railtie de chaque composant Rails. Si vous n'avez pas besoin de tous ces composants, vous pouvez alléger votre application en supprimant l'instruction require "I want everything" et en ne demandant manuellement que ceux dont vous avez besoin. Vous remplacerez require 'rails/all' par quelque chose comme ceci :

require "rails"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
require "action_mailer/railtie"
require "active_job/railtie"
require "action_cable/engine"
require "action_mailbox/engine"
require "action_text/engine"
# require "rails/test_unit/railtie"
require "sprockets/railtie"

Une fois que vous avez requis les composants de rails, il est temps pour les gems de votre application d'être requis en utilisant bundler :

Bundler.require(*Rails.groups)

Ensuite, nous définissons notre classe d'application :


module Iluvrails
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

Ok, nous avons donc défini un railtie, mais il manque encore une pièce au puzzle. Quand les initialisateurs et ./config/environments/#{Rails.env}.rb sont-ils chargés ? De retour à Rails::Command::ServerCommand.perform, nous voyons que Rails::Server est initialisé de la manière suivante : Rails::Server.new(server_options) et lorsque nous recherchons server_options, nous voyons qu'il s'agit d'un hash avec les valeurs par défaut suivantes :

class_option :config, aliases: "-c", type: :string, default: "config.ru",
  desc: "Uses a custom rackup configuration.", banner: :file

# [...]

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

TL;DR : On dit à Rake de charger  ./config.ru

./config.ru

# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application

Ok, suivons cette piste. Nous chargeons d'abord config/environment.rb:

# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!

Ainsi, après avoir demandé ./config/application.rb (qui est déjà nécessaire à ce stade), #initialize ! est appelé.


# Initialize the application passing the given group. By default, the
# group is :default
def initialize!(group = :default) #:nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end
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

C'est un gros morceau qui mériterait un article de blog à lui tout seul. Sans trop entrer dans les détails, rappelons que Rails est constitué de nombreux hooks. Certains d'entre eux sont liés à l'initialisation. Pendant #run_initializers sera exécuté parmi d'autres hooks :

  • load_environment_config qui charge ./config/environments/#{Rails.env}.rb

load_config_initializers qui charge ./config/initializers/*.rb


Note de clôture

Le code source de Rails::Application nous donne un rapide rappel du déroulement du processus de démarrage :

1) nécessite "config/boot.rb" pour configurer les chemins de chargement.

2) require railties et engines

3) Définissez Rails.application comme "class MyApp::Application < Rails::Application".

4) Exécuter les callbacks config.before_configuration

5) Chargement de config/environments/ENV.rb

6) Exécuter les callbacks config.before_initialize

7) Exécuter Railtie#initializer défini par railties, moteurs et application. Un par un, chaque moteur configure ses chemins de chargement, ses routes et exécute ses fichiers config/initializers/*.

8) Les initialisateurs Railtie# personnalisés ajoutés par les chemins de fer, les moteurs et les applications sont exécutés.

9) Construction de la pile middleware et exécution des callbacks to_prepare.

10) Exécutez config.before_eager_load et eager_load ! si eager_load est vrai.

11) Exécution des callbacks config.after_initializeSaid differently:

  • Défini APP_PATH to ./config/application.rb

  • Défini ENV['BUNDLE_GEMFILE'] to ./Gemfile

  • Configuration de Bundler (sans avoir besoin de gems pour le moment)

  • Initialisation d'un nouveau Rails::Server (sous-classe de Rack::Server)

  • Nécessite tous les composants Rails (ActiveRecord, ActionPack, etc.)

  • Nécessite toutes les gemmes de votre Gemfile

  • Définissez une application qui est une sous-classe de Rails::Application.

  • Changez le répertoire à la racine de votre application Rails.

  • Lancez le Rails::Server initialisé précédemment.

  • Exécutez les hooks Rails de manière ordonnée (chargez la configuration, exécutez les initialisateurs, etc.)

  • Votre serveur est maintenant en attente de requêtes !


Nous aimons Rails pour toute la magie qu'il opère pour nous, mais il est préférable de comprendre comment cette magie fonctionne.

Partager
Younes Serraj
Younes Serraj4 juin 2019

Blog de Capsens

Capsens est une agence spécialisée dans le développement de solutions fintech. Nous aimons les startups, la méthodologie scrum, le Ruby et le React.

Ruby Biscuit

La newsletter française des développeurs Ruby on Rails.
Retrouve du contenu similaire gratuitement tous les mois dans ta boîte mail !
S'inscrire