Les middlewares, fondations de Rails

Publié le 21 mars 2012 par Nicolas Cavigneaux | back

Cet article est publié sous licence CC BY-NC-SA

Les middlewares ? C’est quoi ?

Dans un article précédent, nous avons vu comment créer un middleware pour Rails. Pour vous rafraîchir la mémoire, voici un petit rappel à propos des middlewares.

Un middleware est un morceau de code qui agit de façon indépendante, qui est appelé et fait ses traitements avant que le code de Rails à proprement parlé soit appelé. Utiliser un middleware a pour gros avantage de ne pas passer par toute la stack de Rails ce qui résulte donc en un gain considérable de performance.

Les fondations de Rails

Rails est une application compatible Rack et n’hésite pas à en tirer partie. De nombreuses choses dans une application Rails sont gérées par des applications Rack connexes, les middlewares.

Les middlewares sont particulièrement adaptés au traitement d’actions simples et récurrentes, déconnectées de la logique de l’application Rails. Ils sont donc très utiles pour des tâches tels que le logging, les remontées d’exceptions, la gestion des cookies, de la session, des messages flash, …

Les middlewares par défaut

Comme nous l’avons vu, Rails dans un souci de modularité, d’évolutivité et de performance fait largement usage des middlewares pour ses fondations. Voyons quels sont ces middlewares ainsi que leur rôle au sein d’une application Rails.

Nous sommes ici dans le cas d’une application Rails 3.2.2 avec uniquement les middlewares activés par défaut (en mode développement puis production).

Pour avoir une liste des middlewares activés sur une application donnée, voici la commande a exécuter :

$ rake middleware

use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fb93c60d748>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
run MyApp::Application.routes
$ rake middleware RAILS_ENV=production

use Rack::Cache
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f9bbb596330>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
use Sass::Plugin::Rack
use Airbrake::Rack
run MyApp::Application.routes

ActionDispatch::Static

ActionDispatch::Static comme vous pouvez l’imaginer est utilisé pour servir les fichiers statiques sans passer par la stack. Il serait tout à fait inutile de demander à Rails de traiter la requête, l’analyser pour finalement servir un simple fichier statique.

Il faut garder à l’esprit que les middlewares sont appelés dans le sens de chargement. La toute première action lors d’une requête sera donc de vérifier si le chemin demandé correspond à un fichier statique. Si c’est le cas il sera servit, sinon on passe au middleware suivant.

La méthode call des middlewares correspond à l’action exécutée à chaque appel, voici son code :

def call(env)
  path   = env['PATH_INFO'].chomp('/')
  method = env['REQUEST_METHOD']

  if FILE_METHODS.include?(method)
    if file_exist?(path)
      return @file_server.call(env)
    else
      cached_path = directory_exist?(path) ? "#{path}/index" : path
      cached_path += ::ActionController::Base.page_cache_extension

      if file_exist?(cached_path)
        env['PATH_INFO'] = cached_path
        return @file_server.call(env)
      end
    end
  end

  @app.call(env)
end

On voit donc que si un fichier correspondant au path est trouvé (dans public/) alors il est servi, sinon on continue notre chemin.

Voici comment, en quelques lignes, sont gérés les fichiers statiques et les caches de page. Tout cela sans charger la stack complète de Rails, ce qui rend l’action très rapide.

Rack::Lock

Rack::Lock permet de forcer tout ce qui est chargé après à utiliser un unique thread. Tout ce qui sera appelé après ce middleware aura automatiquement un lock et sera synchronisé.

Si on fait, par exemple :

use Rack::Cache
use Rack::Lock
run myapp

Ici le middleware de cache pourra utiliser plusieurs threads mais pas myapp.

use Rack::Lock
use Rack::Cache
run myapp

Dans ce cas, le cache serait lui aussi cantonné à l’utilisation d’un seul thread.

On peut donc facilement contrôler à quel niveau l’app doit se synchroniser, tout simplement en déplaçant l’appel à Rack::Lock.

ActiveSupport::Cache::Strategy::LocalCache::Middleware

Rails propose un système de cache basé sur des couples de clé / valeur :

Rails.cache.write "foo", "bar"
Rails.cache.read "foo"

Ce cache se repose normalement sur le système de fichier mais le middleware permet à Rails.cache d’utiliser un stockage en mémoire pour améliorer les performances

Rack::Runtime

Rack::Runtime permet d’ajouter à la volée une entête “X-Runtime” à la réponse. Cette valeur représente le temps nécessaire à la rêquete pour être traitée. Cette valeur est exprimée en secondes.

Vous pouvez déplacer ce middleware vers le bas, par exemple juste avant l’appel à votre application ce qui aura pour effet de ne prendre en compte que le temps de rendu de votre app, en occultant les middlewares appelés avant.

$ curl -I http://host.dev

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-UA-Compatible: IE=Edge
ETag: "0d48246875a0f4e63c8925f0b3fd5941"
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: _host_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE4ODViMjZiNTI1NDI2ODE1NDNjOTY0OTcwNDcyYzJkBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMWp5Qk80K2JXWENkZmJZbE5NbDVEQXlhUzAvMzJSZVdobFFhK3ltNnpkVnM9BjsARg%3D%3D--6e2645898c73ec7d118b91bda6f742318a326aaf; path=/; HttpOnly
X-Request-Id: 25bc69615b363e77c02d0801dcb07969
X-Runtime: 0.485419
Connection: close

Très pratique en phase de développement ou d’optimisation pour connaitre rapidement les temps de traitement d’actions données.

Rack::MethodOverride

Vous avez déjà surement remarqué, dans vos formulaires Rails, la présence d’un

<input name="_method" type="hidden" value="put" />

ajouté automatiquement pour le peu que vous utilisiez les helpers. Ce paramètre caché permet de préciser à Rails le verbe (GET / POST / PUT / DELETE) avec lequel a été appelée l’action.

Pourquoi ajouter ça puisque ce sont des verbes standards de la norme HTTP ? Parceque malheureusement tout n’est pas rose et peu de serveurs web comprennent ces quatres verbes. Pour la plupart ils ne comprennent que GET et POST.

Rails a donc pris le parti de n’utiliser que GET et POST et de simuler les autres verbes. C’est donc un moyen de s’assurer que la requête pourra être identifiée avec le bon verbe.

Le middleware Rack::MethodOverride a pour but de de vérifier les appels passés en POST à la recherche d’un paramétre _method qui permettra de redéfinir correctement la méthode HTTP (verbe) dans la requête reçue.

ActionDispatch::RequestId

ActionDispatch::RequestId permet d’ajouter un identifiant unique à chaque requête et une en-tête X-Request-Id à la réponse.

Cet id a pour but de faciliter le traçage d’une requête, dans les logs par exemple.

Vous pouvez vous référer à l’exemple précédent (Curl) qui contient un X-Request-Id dans sa réponse.

Rails::Rack::Logger

Rails::Rack::Logger permet de signaler dans les logs le démarrage d’une requête en précisant la méthode utilisée, le chemin, l’ip du client ainsi que l’heure actuelle.

Les logs sont ensuite flushés en fin de requête pour s’assurer que tous les messages soient affichés.

ActionDispatch::ShowExceptions

ActionDispatch::ShowExceptions se charge d’attraper les exceptions à la volée et d’appeler une app dédiée à les afficher (dans les logs et le navigateur) dans un format adapté aux développeurs.

ActionDispatch::DebugExceptions

ActionDispatch::DebugExceptions est l’app dédiée à l’affichage et au log des exceptions. C’est ce middleware qui affiche les pages de debug (erreur 500 par exemple) en mode développement et qui s’assure de reporter ces informations dans les logs.

ActionDispatch::RemoteIp

ActionDispatch::RemoteIp sert à stocker l’ip du client dans l’environnement de la requête ce qui permet d’y faire référence ensuite dans l’application.

Au passage ce middleware fait aussi quelques vérifications d’usage pour protéger l’app contre le spoofing.

ActionDispatch::Reloader

ActionDispatch::Reloader permet de recharger automatiquement les classes avant chaque requête lorsque vous êtes en mode développement. C’est ce qui fait (en partie) que votre application est plus lente en mode développement qu’en mode production mais c’est aussi ce qui fait que vous n’avez pas à relancer votre serveur quand vous modifiez un modèle.

ActionDispatch::Callbacks

ActionDispatch::Callbacks donne accès à des callbacks de requête (before / after). Ces callbacks seront appelés à chaque requête :

>> ActionDispatch::Callbacks.before { puts "Avant la requete" }
=> [ActionDispatch::Callbacks]

>> ActionDispatch::Callbacks.after { puts "Apres la requete" }
=> [ActionDispatch::Callbacks]

>> app.get "/"
Avant la requete
Post Load (0.3ms)  SELECT `posts`.* FROM `posts`
Apres la requete
=> 200

ActiveRecord::ConnectionAdapters::ConnectionManagement

ActiveRecord::ConnectionAdapters::ConnectionManagement sert à nettoyer les connexions actives à la base de données en fin de requête ce qui évite tout simplement d’avoir à le gérer à la main.

ActiveRecord::QueryCache

ActiveRecord::QueryCache est le middleware qui permet de cacher vos requêtes SQL pour faire en sorte que deux requêtes identiques ne fassent qu’une fois l’objet d’un appel à la base SQL ce qui a pour effet d’améliorer considérablement les performances si vous n’avez pas pris soin de gérer cela à la main. Si vous gériez ça , vous allez gagner beaucoup de code et de temps.

Ce middleware va donc, en début de requête, activer le cache. Il se chargera également de nettoyer ce cache en fin de requête :

Category Load (0.4ms)  SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = 1 LIMIT 1

CACHE (0.0ms)  SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = 1 LIMIT 1

ActionDispatch::Cookies

ActionDispatch::Cookies gère la lecture des cookies envoyés par le client avec la requête puis la mise à disposition de ces cookies dans le code. C’est aussi lui qui va se charger d’envoyer les cookies générés dans l’app au client via la réponse.

ActionDispatch::Flash

ActionDispatch::Flash permet de gérer le stockage et la récupération de messages flash (objet transmis entre deux actions) en session.

ActionDispatch::Flash va donc stocker un objet lors d’une requête, le mettre à disposition de l’action suivante puis le supprimer.

ActionDispatch::ParamsParser

ActionDispatch::ParamsParser se charge de parser les requêtes (xml / json / yaml) pour le convertir en hash (params).

Le code est un peu plus long que d’habitude mais reste très simple à comprendre.

Ce middleware est l’une des pierres angulaires qui permettent de facilement mettre en place des API à l’aide de Rails.

ActionDispatch::Head

ActionDispatch::Head va vérifier si la requête est de type “HEAD”. Si c’est le cas, seul le status et les en-têtes seront envoyées dans la réponse. On peut donc améliorer les temps de réponse des pages en omettant le corps de la réponse (utilisation de cache, …).

Rack::ETag

Rack::ETag permet d’ajouter une en-tête “ETag” à la réponse. Cet ETag est un hashage du corps de la réponse ce qui permet d’identifier de manière unique le contenu. On peut donc savoir si le contenu de la page a changé ou non depuis la dernière requête.

Rack::ConditionalGet

Rack::ConditionalGet permet de mettre en place du “GET” conditionnel.

Lorsque une page est demandée, le middelware génére un hash du contenu de cette page et vérifie si ce ETag est déjà connu par le client.

Si oui alors une réponse avec un corps vide et un statut 304 est envoyé pour dire que la page n’a pas été modifiée. Elle peut être chargée depuis le cache client. Le client a donc une réponse plus rapide et le serveur une charge plus faible.

Si cet ETag n’est pas connu par le client alors la réponse est envoyée normalement.

ActionDispatch::BestStandardsSupport

ActionDispatch::BestStandardsSupport est un middleware des plus simples, il ne fait qu’ajouter une en-tête supplémentaire à la réponse pour demander aux clients Internet Explorer d’utiliser le plugin Chrome Frame ou son moteur de rendu natif le plus récent.

Rack::Cache

Rack::Cache est un gem qui permet de mettre en place du cache HTTP basé sur le standard RFC 2616.

Pour conclure

Les middlewares peuvent donc s’avérer très utile pour accomplir des tâches simples mais récurrentes qui nécessitent un temps de réponse le plus court possible. Les middlewares ont pour gros avantage de court-circuiter la stack Rails qui n’est pas toujours nécessaire au regard de besoins précis.

Il est donc très intéressant de connaitre les middlewares utilisés par défaut par Rails pour notamment désactiver ceux dont vous n’avez pas besoin, en faire un meilleur usage ou encore pour vous inspirer dans le développement du votre.