Latest Posts

Latest Posts

Zero downtime deployment avec Node.js et Express, un première étape …

Zero downtime deployment avec Node.js et Express, un première étape …

Lorsqu’on souhaite stopper ou redémarrer un serveur, différentes solutions s’offrent à nous. Parmi elles, la possibilité d’envoyer un signal de type SIGTERM au processus.

Cette solution est couramment utilisée, malheureusement cela entraîne la coupure des connexions en cours sans permettre au serveur d’honorer les requêtes en cours de traitement.

Dans l’objectif de fournir une meilleure qualité de service, il est important d’honorer toute requête entrante. Comment faire, donc, pour permettre le redémarrage d’un serveur en douceur, sans couper brutalement les connexions en cours ?

Le protocole HTTP permet au serveur de répondre aux requêtes entrantes par un status code 503 qui signifie que le service n’est pas disponible (Service Unavailable). L’idée est donc de renvoyer ce status code dès lors que le process a reçu un signal SIGTERM, tout en laissant le temps au serveur de terminer le traitement des requêtes HTTP en cours, puis de stopper le serveur une fois que les requêtes en cours sont traitées. Il est toujours possible de killer le process du serveur si cela met trop longtemps.

Node.js

Avec Node.js, il est possible d’écouter les signaux reçus par le système et d’y réagir. Il est donc possible de mettre en place une mécanique qui instruit le serveur de répondre aux nouvelles requêtes entrantes par un status code 503, puis de couper le serveur une fois les requêtes en cours traitées.

Cela donne le code suivant:

start = new Date()

# Express
app = express()

gracefullyClosing = false

app.configure ->
    app.set 'port', process.env.PORT or 8000

    app.use (req, res, next) ->
        return next() unless gracefullyClosing
        res.setHeader "Connection", "close"
        res.send 503, "Server is in the process of restarting"

    app.use app.router

app.get '/', (req, res) ->
    res.send 200, 'OK'

httpServer = app.listen app.get('port')

process.on 'SIGTERM', ->
    logger.info "Received kill signal (SIGTERM), shutting down gracefully."
    gracefullyClosing = true
    httpServer.close ->
        logger.info "Closed out remaining connections."
        process.exit()

    setTimeout ->
        console.error "Could not close connections in time, forcefully shutting down"
        process.exit(1)
    , 30 * 1000

L’appel de la fonction close sur l’instance de serveur HTTP renvoyée Express, permet au serveur de terminer le traitement des requête en cours avant de s’arrêter.

Si votre application Node.js est correctement redondée avec un reverse proxy (Nginx, HAProxy) devant les différentes instances, les requêtes entrantes seront redirigées vers d’autres instances en état de traiter les requêtes. Cela sera le cas dès lors que votre application répondra aux requêtes entrantes avec des status code 502 ou 503 par exemple.

Nginx

Si votre application est déployée derrière un reverse proxy tel que Nginx, il suffit de configurer celui-ci avec plusieurs flux upstream vers différentes instances de votre application pour qu’il soit capable de passer la main à une autre instance lorsqu’il reçoit un code erreur 502 ou 503 en réponse à une requête transmise à une des instances.

upstream my_app_upstream {
    server 127.0.0.1:7000;
    server 127.0.0.1:8000;
    server 127.0.0.1:9000;
}

Ensuite, il faut déclarer ce que Nginx doit faire lorsqu’il reçoit une réponse 502 ou 503 et le tour est joué. Ici, nous indiquons à Nginx via la directive proxy_next_upstream de faire suivre la requête au prochain flux upstream lorsqu’il reçoit en réponse une erreur, un timeout ou bien un code HTTP 502 ou 503:

location /app {
...
proxy_next_upstream error timeout http_502 http_503;
...
proxy_pass http://my_app_upstream;
}

Limitations

Ce fonctionnement décrit dans cet article répond bien aux besoins d’applications traitant des requêtes HTTP simples, néanmoins il ne répond pas au problème des configurations ayant activé l’option de keepalive pour les connexions HTTP, ni aux applications utilisant les websockets. Il faudra dès lors trouver une solutions adaptée.

Conclusion

La mise en oeuvre de la notion de Gracefully Closing lors d’un redémarrage pour raison de déploiement d’une nouvelle version, est une première étape importante pour arriver à faire du Zero Downtime Deployment. Cela permet, non seulement d’honorer les requêtes en cours de traitement, mais également à vos reverse proxy de prendre connaissance de l’absence de service et redispatcher les requêtes sur d’autres serveurs avant que le flux upstream soit coupé.

Clusteriser votre application Node.js

Clusteriser votre application Node.js

Les application Node.js sont par nature mono-threadées, or les serveurs, de nos jours, sont presque* toujours multi-core. Pour exploiter l’ensemble des capacités de ces serveurs, il est nécessaire de pouvoir exploiter tous les cores.

Pour cela, il existe principalement 2 techniques:

  • Lancer plusieurs instances d’une application Node.js sur différents avec un reverse proxy pour load balancer les requêtes entrantes
  • Lancer une application Node.js en mode cluster

Dans l’idéal, il faut lancer autant d’instances qu’il y a de cores sur la machine. Cela permet de partager au mieux la puissance de la machine entre les différentes instances sans pour autant dégrader les performances en partager les cores entre plusieurs instances.

Nous allons dans cet article nous intéresser au second cas de figure, c’est à dire le lancement d’application Node.js en mode cluster.

* Le terme “presque” est utilisé ici car de nombreux serveurs cloud d’entrée de gamme restent mono-threadés (VPS et instances EC2 1er prix, …).

Le module cluster

Le module cluster, bien que marqué comme ayant une API expérimental dans la documentation de Node.js, est aujourd’hui largement utilisé.

Son principe est simple, lorsqu’une application est lancée en mode cluster, un premier process est démarré en mode master. Le process master n’a pas pour rôle de traiter les requêtes entrantes à proprement parler, mais plutôt à les dispatcher aux aux process forkés qui eux sont dédiés au traitement des requêtes. Il sont lancés dans le mode worker.

La responsabilité de l’instanciation de forks en mode worker est du ressort du process master, et les règles sont de fork sont laissées à la responsabilité du développeur. Tout au long de la vie du cluster, des événements sont générés aussi bien par le master que par les workers. Il est important de s’y abonner pour être capable de réagir à des changements dans le cluster (Crash d’un worker, par exemple).

Un exemple simple de cluster est proposé par la documentation:

cluster = require("cluster")
http = require("http")
numCPUs = require("os").cpus().length

if cluster.isMaster
  # Fork workers.
  i = 0
  while i < numCPUs
    cluster.fork()
    i++
  cluster.on "exit", (worker, code, signal) ->
    console.log "worker " + worker.process.pid + " died"
else
  # Workers can share any TCP connection
  # In this case its a HTTP server
  http.createServer((req, res) ->
    res.writeHead 200
    res.end "hello world\n"
  ).listen 8000

Bien que les API de clusterisation proposent une API assez simple à gérer, il faut néanmoins s’occuper de plusieurs points de détail, et le risque de mal faire est rapidement arrivé. Il est donc conseillé de s’appuyer sur un module tiers pour traiter ce sujet.

Le module recluster, très intéressant de part sa simplicité et sa maturité, permet de s’affranchir de toute cette complexité de mise en oeuvre.

recluster

Pour l’installer, il suffit de taper la commande suivante:

npm install recluster --save

Pour instancer une application en mode cluster, il suffit d’ajouter le script suivant (cluster.coffee, par exemple) à votre application:

recluster = require("recluster")
path = require("path")

cluster = recluster(path.join(__dirname, "server.js"), { ### Options ### })
cluster.run()

process.on "SIGUSR2", ->
  console.log "Got SIGUSR2, reloading cluster..."
  cluster.reload()

console.log "spawned cluster, kill -s SIGUSR2", process.pid, "to reload"

Le module recluster attend en paramètre le chemin du script. Il suffit ensuite de lancer le fichier cluster.js au lieu du fichier server.js, et le tour est joué ! (Les exemples étant en CoffeeScript, il est nécessaire d’avoir au préalable compilé les fichiers CoffeeScript).

Zero downtime reloading

Le modules recluster permet de mettre à jour une application sans coupure de service, il faut pour cela appeler la fonction reload sur la variable cluster.

Dans l’exemple ci-dessus, le signal SIGUSR2, permet d’indiquer au programme que le cluster doit être rechargé. Celui-ci rechargera les différents workers en se basant sur les options passées en paramètre (timeout de rechargement, etc). Les requêtes en cours de traitement seront honorés sans coupure brutale, dans la limite d’un timeout défini par les options de configuration du module.

Cette fonctionnalité est particulièrement intéressantes lors que l’application doit être mise à jour sans coupure de service. Ainsi, la base de code peut-être mise à jour, puis les workers redémarrés un par un une fois les requêtes en cours traitées.

Il est possible de s’appuyer sur d’autres conditions pour recharger les workers d’un cluster, par exemple, il est possible de s’appuyer sur la modification du fichier package.json pour déclencher un rechargement avec le code suivant:

fs = require 'fs'
fs.watchFile "package.json", (curr, prev) ->
    console.log "Package.json changed, reloading cluster..."
    cluster.reload()

Conclusion

Ne vous laissez pas impressionner par la notion de clusterisation. Elle est très simple à mettre en oeuvre dans le monde Node.js grâce à une API de base faisant parti des core modules, et de nombreux modules s’appuyant dessus.

Gérer les erreurs avec Node.js

Gérer les erreurs avec Node.js

Lorsqu’une exception n’est pas gérée dans un programme Node.js, cela se termine en général par un crash du process de l’application. Il n’y a d’ailleurs pas grand chose à faire pour tenter de rattraper le coup si l’erreur remonte jusqu’à la boucle d’événement. C’est pourquoi, il est nécessaire de traiter les erreurs avec attention.

Si votre programme génère une erreur qui remonte jusqu’à la boucle d’événement comme suit:

process.nextTick () ->
    throw new Error("Some Bad Error")

Vous aurez le droit au message d’erreur qui suit:

Express listening on port: 9000
Started in 0.073 seconds

/Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:30
    throw new Error("Some Bad Error");
          ^
Error: Some Bad Error
    at /Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:30:11
    at process._tickCallback (node.js:415:13)
    at Function.Module.runMain (module.js:499:11)
    at startup (node.js:119:16)
    at node.js:901:3

Process finished with exit code 8

L’événement ‘uncaughtException’

Node.js vous donne une chance d’intercepter les erreurs qui remontent jusqu’à la boucle d’événement grace au dispatch l’événement de type uncaughtExcpetion.

Contrairement à ce qu’on pourrait penser, l’événement n’est pas dispatché par le process Node.js pour catcher l’erreur et permettre de continuer au programme son exécution. C’est principalement pour gérer correctement la libération de resources qui auraient été ouvertes par le programmes, et éventuellement logger de façon plus précise le contexte de l’erreur (Etat de la mémoire, etc…).

Lorsqu’une erreur remonte jusqu’à la boucle d’événement, il ne faut plus considérer l’état du programme comme étant consistant. C’est pour cette raison qu’il ne faut pas tenter de catcher l’exception dans l’idée de permettre au programme de continuer à fonctionner.

Si vous souhaitez logger un message d’erreur dans le cas d’une exception remontée jusqu’à la boucle d’événement, vous pouvevz ajouter le code suivant à votre programme:

process.on 'uncaughtException', (err) ->
    console.log JSON.stringify(process.memoryUsage())
    console.error "An uncaughtException was found, the program will end. #{err}, stacktrace: #{err.stack}"
    process.exit 1

process.nextTick () ->
    throw new Error("Some Bad Error")

Ce qui donne le résultat suivant:

/Users/akinsella/.nvm/v0.10.22/bin/node app-test.js
{"rss":12312576,"heapTotal":4083456,"heapUsed":2153648}
An uncaughtException was found, the program will end. Error: Some Bad Error, stacktrace: Error: Some Bad Error
    at /Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:13:11
    at process._tickCallback (node.js:415:13)
    at Function.Module.runMain (module.js:499:11)
    at startup (node.js:119:16)
    at node.js:901:3

Process finished with exit code 1

Contrairement à la gestion par défaut, nous avons pu retourner un exit code spécifique. Ici le code retour: 1
Le message de log est également différent. Nous sommes donc en mesure de maitriser le log d’erreur en cas de crash.
Par ailleurs, les informations de mémoire rendues disponibles dans les logs participeront à faciliter l’analyse du crash.

Express

Si vous utilisez un framework type Express, vous serez déchargé d’une partie du travail car les erreurs qui interviennent pendant le traitement d’une requête HTTTP sont catchées par le framework qui gérera pour vous l’erreur.

Par défaut Express se contente de logger un crash qui intervient dans le traitement d’une requête HTTP via un simple log retourné dans la réponse HTTP.

Par exemple, en exécutant le programme suivant:

express = require 'express'

app = express()

app.configure ->
    app.set 'port', process.env.PORT or 9000
    app.use app.router

app.get "/", (req, res) ->
    throw new Error("Some Bad Error")

httpServer = app.listen app.get('port')

process.on 'uncaughtException', (err) ->
    console.error "An uncaughtException was found, the program will end. #{err}, stacktrace: #{err.stack}"
    process.exit 1

console.error "Express listening on port: #{app.get('port')}"

Puis en se rendant sur l’url http://localhost:9000, Express renverra dans la réponse HTTP le log suivant:

Error: Some Bad Error
    at /Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:16:11
    at callbacks (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:164:37)
    at param (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:138:11)
    at pass (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:145:5)
    at Router._dispatch (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:173:5)
    at Object.router (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:33:10)
    at next (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at Object.expressInit [as handle] (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/middleware.js:30:5)
    at next (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at Object.query [as handle] (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/node_modules/connect/lib/middleware/query.js:45:5)

Il est également possible d’activer un log plus détaillé, avec une mise en forme HTML, particulièrement utile en mode développement en ajoutant les lignes suivantes:

app.configure 'development', () ->
    app.use express.errorHandler
        dumpExceptions: true,
        showStack: true

Le résultat sera le suivant:

express-error

Express vous permet également de renseigner un middleware qui aura la possibilité d’interagir les erreurs rencontrées dans le traitement des requêtes HTTP. Ce middleware peut être utile pour logger l’erreur rencontrée ou bien encore libérer des resources associées à la requête en cours de traitement.

Il permettra également de renvoyer une réponse adaptée à l’utilisateur en cas d’erreur non gérée. Ce point particulièrement intéressant dans le cas de l’implémentation d’API REST. Le serveur devient capable de renvoyer une erreur interprétable par le client même en cas d’erreur non gérée.

Le middleware prendra la format suivant:

    app.use (err, req, res, next) ->
        console.error "Error: #{err}, Stacktrace: #{err.stack}"
        res.send 500, "Something broke! Error: #{err}, Stacktrace: #{err.stack}"

Les promises

Les promises peuvent vous aider à gérer les erreurs plus efficacement grâce à leur mécanisme de gestion des erreurs.

Un traitement encapsulé dans une promise ne permettra jamais à une erreur de remonter jusqu’à l’event loop, l’erreur sera catchée par la promise qui sera remontée dans la fonction fail ou catch selon la librairie ou bien encore dans le callback d’erreur de la fonction then.

Il est donc intéressant d’encapsuler vos traitement avec des promises, non seulement pour améliorer la lisibilité du code, mais également pour sa capacité à résister aux crashs.

Les domaines

La notion de domain ne sera pas traitée dans cet article, sachez néanmoins que cette notion a été ajoutée à Node.js en version 0.10.

En bref et pour faire simple, l’idée est plus ou moins de containeriser des event emitters en les associant à un domain. En cas d’erreur dans le traitement d’un événement géré par un domain, l’exception ne fera pas crasher le programme directement, c’est le domain qui sera en charge de traiter l’erreur, mais cela ne vous sauvera pas en général d’un redémarrage du process … commen en témoigne la documentation:

Domain error handlers are not a substitute for closing down your process when an error occurs.

By the very nature of how throw works in JavaScript, there is almost never any way to safely « pick up where you left off », without leaking references, or creating some other sort of undefined brittle state.

The safest way to respond to a thrown error is to shut down the process. Of course, in a normal web server, you might have many connections open, and it is not reasonable to abruptly shut those down because an error was triggered by someone else.

The better approach is send an error response to the request that triggered the error, while letting the others finish in their normal time, and stop listening for new requests in that worker.

In this way, domain usage goes hand-in-hand with the cluster module, since the master process can fork a new worker when a worker encounters an error. For node programs that scale to multiple machines, the terminating proxy or service registry can take note of the failure, and react accordingly.

La documentation de Node.js relative aux domains est disponibles à l’url suivante:

http://nodejs.org/api/domain.html

Détecter les versions dépassées de vos dépendances Node.js

Détecter les versions dépassées de vos dépendances Node.js

L’écosystème Node.js est non seulement très jeune, mais également très dynamique. Les versions des librairies que vous utilisez ont tendance à changer très vite. Pour vous économiser la recherche permanente des versions de librairies les plus récentes pour mettre à jour votre fichier package.json, npm met à disposition l’outil npm-outdated qui se charge d’analyser vos dépendances et de vous indiquer celles qui ne sont plus à jour.

npm-outdated

L’outil npm-outdated s’utilise très simplement en l’appelant de la façon suivante:

npm outdated --depth=0

Et produira la sortie ci-dessous:

npm-outdated

Les versions plus anciennes de l’outil ne produiront pas de sortie colorisée, il est donc intéressant de monter de version. La version de npm utilsée ici est la 1.4.9.

npm-outdated analysera aussi bien vos dépendances standards que les dépendances de développement sans faire de distinction.

La sortie retournée par l’outil ne montre que les dépendances ayant une version dépassée. Vous ne verrez donc pas les dépendances ayant une version à jour.

3 versions différentes sont renseignées: Current, Wanted et Latest. Ces versions représentent respectivement la version courante, puis la dernière version à jour correspondant au pattern de version déclaré pour votre dépendance dans le fichier package.json, et enfin la dernière version disponible de la librairie.

Option depth

Le paramètre –depth=0 permet de se limiter aux dépendances directes sans se soucier des dépendances tirées par les librairies elles-même tirées par vos dépendances directes.

Si nous utilisons le paramètre –depth=2, les dépendances indirectes commenceront alors à être matérialisées dans la sortie de l’outil:

npm-outdated-depth-1

Option json

Le paramètre –json permet quant à lui d’obtenir une sortie JSON. Cette option est particulièrement pratique pour exploiter l’information produite dans des rapports de build par exemple, ou bien pour être exploiter par d’autres outils.

En exécutant la ligne de commande suivante:

npm outdated --depth=0 --json

Vous obtiendrez la sortie suivante:

{
  "coffee-script": {
    "current": "1.6.3",
    "wanted": "1.6.3",
    "latest": "1.7.1",
    "location": "node_modules/coffee-script"
  },
  "passport-local": {
    "current": "0.1.6",
    "wanted": "0.1.6",
    "latest": "1.0.0",
    "location": "node_modules/passport-local"
  },
  "uglify-js": {
    "current": "2.4.13",
    "wanted": "2.4.14",
    "latest": "2.4.14",
    "location": "node_modules/uglify-js"
  }, ...
}

npm-update

Maintenant que vous connaissez les dernières versions disponibles, vous souhaitez peut-être en mettre certaines à jour. Pour cela, vous pouvez utiliser l’outil npm update.

Pour mettre à jour la librairie request, il faudrait exécuter la commande suivante:

npm update request

Badges pour votre repository GitHub

Le projet David vous permet de générer des badges indiquant si les versions de vos librairies sont à jour ou bien dépassées.

Ce projet est particulièrement intéressant car, non seulement, il génère des rapports pour votre projet sans que vous ayez à lever le petit doigt, mais il génère également des badges que vous pouvez exposer les pages de votre projet permettant d’indiquer l’état des versions de vos dépendances.

Pour exemple, pour savoir si le projet gtfs-playground a les versions de ses dépendances à jour, vous pouvez vous rendre sur la page suivante: https://david-dm.org/akinsella/gtfs-playground

L’outil fonctionne exclusivement avec les repositories Github. Pour construire un rapport pour votre projet, il suffit de renseigner votre organization et du nom de votre repository dans l’url suivante avant de l’appeler:

https://david-dm.org/<organization>/<repository>

De même, pour obtenir le badge correspondant à votre projet, il suffit de construire la balise img comme suit:

https://david-dm.org/<organization>/<repository>.<extension>

Ce qui donne le résultat suivant pour le format png:

gtfs-playground-png

Et pour le format svg:

gtfs-playground-svg

Conclusion

L’écosystème Node.js évolue rapidement. Les librairies proposent donc régulièrement de nouvelles fonctionnalités ou bien encore des corrections de bug. Il ne faut donc pas hésiter à mettre à jour ses librairies.

Attention cependant à ne pas non plus se précipiter et installer une version de librairie qui ne serait plus compatible avec votre code ou bien encore d’installer une version buggée. Il faut donc penser à faire tourner ses tests pour s’assurer qu’aucune regression n’impacte votre base de code.

Transformez votre code Node.js grâce au module de promises Bluebird

Transformez votre code Node.js grâce au module de promises Bluebird

Lorsqu’on parle de promises dans l’écosystème Node.js, on pense immédiatement à la librairie Q. Toutefois, il existe de nombreux modules de promises proposant chacun des choses différentes. En particulier, le module bluebird se démarque grâce à des fonctionnalités tout à fait intéressantes telles que la “promisification”.

Promisification

Les core modules de Node.js fonctionnent à base de callback. Ainsi pour lire un fichier de façon asynchrone, il faut appeler la fonction readFile du module fs et traiter la réponse depuis le callback passé en dernier paramètre de la fonction lors de son appel:

fs.readFile "file.json", (err, val) ->
    if err
        console.error "unable to read file"
        try
            val = JSON.parse(val);
            console.log val.success
        catch e
            console.error "invalid json in file"

Bluebird permet de transformer le code précédent dans le code suivant:

fs.readFileAsync("file.json").then(JSON.parse).then (val) ->
    console.log val.success
.catch SyntaxError, (e) ->
    console.error "invalid json in file"
.catch (e) ->
    console.error "unable to read file"

promisifyAll

Cette transformation est rendue possible grâce à la promisification du module fs, via l’appel de la fonction promisifyAll qui permet de transformer toutes les fonctions exposées en fonctions renvoyant des promises:

fs = require "fs"
Promise.promisifyAll fs

fs.readFileAsync("file.js", "utf8").then(...)

Selon toute vraisemblance, les fonctions du modules sont proxifiées via un wrapping changeant la signature.
On pourra noter que le chaînage de fonctions catch sur la promise permet de différencier le traitement des erreurs en fonction de leur type. Ici, l’erreur de type SyntaxError est traitée différemment des erreurs typées autrement.

promisify

Il est également possible de ne promisifier qu’une seule fonction grâce à la fonction promisify:

redisGet = Promise.promisify(redisClient.get, redisClient)
redisGet('foo').then () ->
    #...

Il y a tout de même un piège puisque la fonction attend 2 paramètres. Le premier étant la référence de la fonction à promisifier, et le second étant l’objet auquel la fonction est rattachée.

nodeify

La fonction nodeify est également très intéressante car elle permet d’enregistrer un callback sur une promise bluebird et d’appeler celui-ci à la résolution de cette dernière:

getDataFor(input, callback) ->
    dataFromDataBase(input).nodeify(callback)

Cette possibilité est particulièrement intéressante, car elle permet de construire des API qui deviennent utilisables aussi bien par du code qui fonctionne à base de callback, qu’avec du code à base de promise.

Ainsi, si le callback est renseigné, il sera appelé. Sinon, il suffira d’exploiter la promise retournée par la fonction pour obtenir et traiter le résultat de l’appel.

Exemple exploitant le mécanisme de promise:

getDataFor("me").then (dataForMe) ->
    console.log dataForMe

Le même exemple exploitant le mécanisme de callback:

getDataFor "me", (err, dataForMe) ->
    if err
        console.error err
    console.log dataForMe

spread

En temps normal, le code suivant donnera en résultat la tableau : [1, 2, 3].

Promise.resolve([1,2,3]).nodeify (err, result) ->
    # err == null
    # result: [1,2,3]

Toutefois, l’option {spread: true} passée à l’appel de la fonction nodeify, permet de dispatcher les valeurs de résultat sur l’ensemble des arguments de la fonction de callback renseignée:

Promise.resolve([1,2,3]).nodeify (err, a, b, c) ->
    # err == null
    # a == 1
    # b == 2
    # c == 3
, {spread: true}

Conclusion

La librairie bluebird est riche en fonctions pour le moins intéressantes, vous pouvez les retrouver sur la page de documentation du projet GitHub:

Lien: https://github.com/petkaantonov/bluebird/blob/master/API.md

Locker les versions de vos dépendances Node.js

Locker les versions de vos dépendances Node.js

Node.js dispose d’un gestionnaire de dépendances très efficace et incontournable: npm.

Reposant sur les informations de dépendances déclarées dans le fichier package.json, il s’occupera de récupérer les dépendances déclarées et de les installées le dossier node_modules de votre projet, via l’exécution de la commande:

npm install

Pourquoi ?

Contrairement à la mécanique proposée par Maven dans le monde Java, Node.js repose sur une mécanique de dépendances hiérarchiques. C’est à dire que npm va récupérer et installer pour chaque niveau – application, et dépendances elles-mêmes – les librairies associées.

Par exemple, si votre projet, ainsi que les librairies dont il dépend, utilisent la librairie mkdirp, alors npm va charger et installer la dépendance mkdirp à la fois dans le dossier node_modules de votre projet, mais également dans le dossier node_modules de la librairie de votre projet.

Pour chaque librairie, dont vous dépendez, vous devez déclarer un pattern de sélection de version qui indiquera à npm quelle version de dépendance télécharger. Les patterns disponibles sont variés, allant du wildcard, à la version exacte.

Quels risques ?

Très rapidement, vous serez confronté à des problématiques de versions de dépendances qui évoluent.

Cela empêchera, au mieux, vos applications de tourner correctement. Au pire engendrera des bugs subtiles et très difficiles à détecter ou corriger, avec le risque de mettre en péril votre business, ou bien la qualité perçue de vos logiciels.

Quelles solutions ? Quels outils ?

Une technique possible pour se prémunir de ce problème, est de locker les versions de vos dépendances en indiquant des patterns de version plus restrictifs, voir complètement fixés.

Vous ne serez pas sorti d’affaire pour autant. Vous aurez beau fixer les versions de vos dépendances, celles-ci reposent également sur d’autres dépendances, pour lesquelles, leur auteurs respectifs n’appliquent peut-être pas les règles de gestion de versions qui vous arrange.

Ainsi, il est possible qu’une librairie donnée déclare une version de dépendance avec un wildcard. De fait, vous serez amené, à terme, à récupérer une version qui sera, soit incompatible avec votre code, soit tout simplement buggée.

Il faudrait, dans l’idéal, pouvoir locker toute la hiérarchie des versions de dépendances et pouvoir réinstaller ces dépendances de façon répétée dans les versions sélectionnées.

La bonne nouvelle, c’est qu’il existe des solutions pour répondre à ce besoin, dont les outils lockdown et npm-shrinkwrap.

lockdown

Le module lockdown propose de locker les versions des dépendances de votre projet dans le but de vous assurer que le code que vous développez reposera sur les même version de dépendances que ce soit dans votre IDE ou bien pendant vos phases de tests ou bien en production.

L’usage de Lockdown vous permettra de continuer à utiliser la commande npm install, tout en vous assurant d’obtenir le même code à chaque fois que la commande sera exécutée, ainsi qu’en vous évitant d’avoir à copier le code de vos dépendances dans votre gestionnaire de code source ou d’avoir à maintenir un repository privé npm.

Comme expliqué précédemment, même si vous exprimez la version exacte de vos dépendances dans votre fichier projet package.json, vous êtes toujours vulnérable à l’apparition soudaine d’une incompatibilité avec l’une de vos dépendances.

Par exemple, si votre projet dépend d’un package avec une version spécifique, qui, elle même dépend d’un autre package déclaré avec un version range, vous risquez de voir la version de votre dépendance changer lors d’une future exécution de la commande npm install.

Cet exemple n’est hélas pas la seule cause de problème. D’autre actions peuvent accidentellement casser le code de votre application:

  • En poussant une nouvelle version de librairie qui ne supporte plus la version de Node.js que vous utilisez
  • En introduisant un bug dans du code qui fonctionnait bien au préalable

Utilisation

1. Installez une dépendance dans votre projet. Par exemple, en ligne de commande:

npm install <module>@<version> --save

2. Générez le fichier lockdown.json en exécutant la commande lockdown-relock:

node_modules/.bin/lockdown-relock

3. Puis, ajoutez le fichier nouvellement créé à votre gestionnaire de code source.

Installer vos dépendances grâce au fichier lockdown.json

Une fois le fichier lockdown.json généré, il vous suffit d’appeler, de façon tout à fait classique, la commande npm install qui installera l’ensemble des dépendances dans les versions attendues.

Points forts

Lockdown se veut être un outil vous garantissant d’utiliser un code source identique, aussi bien en développement qu’en production. C’est pour cela, qu’en plus de stocker les versions des dépendances utilisées, il stocke également des checksums du code utilisé. Il permet donc de savoir qu’un code source dans une version donnée a été modifié, et vous alerte du problème.

Autre point intéressant: le projet est maintenu par Mozilla, ce qui a tendance à rassurer quant au sérieux et la pérennité de l’outil.

npm-shrinkwrap

Tout comme l’outil lockdown, la commande npm-shrinkwrap propose de figer les versions de dépendances de votre application.

Pas de souci d’installation néanmoins, puisque la commande est directement disponible dans la distribution de npm. Npm venant avec l’installation de Node.js, pas besoin de bouger le petit doigt pour avoir l’outil à disposition.

Le fichier de stockage des informations de version s’appelle quant à lui npm-shrinkwrap.json.

Utilisation

L’utilisation de npm-shrinkwrap est tout à fait similaire à celle de lockdown, il suffit d’utiliser la commande npm install pour installer vos dépendances, puis exécuter la commande npm shrinkwrap pour générer le fichier de version.

Gestion des checksums

Contrairement à lockdown, la commande npm-shrinkwrap ne gère pas de checksum. Néanmoins, il existe des solutions de remplacement, telles que le package npm-seal qui se propose de venir compléter la commande npm-shrinkwrap en proposant la fonctionnalité manquante.

Contrairement à l’utilitaire npm-shrinkwrap, le package npm-seal est un utilitaire 3rd party qui doit être installé en complément avec la commande suivante:

npm install seal -g

Points forts

Nous l’avons déjà vu, la commande est intégrée à la distribution de l’outil npm. Par ailleurs, l’outil est également maintenu par une entreprise gage de sérieux: Uber.

Bon à savoir

Même si les outils lockdown et npm-shrinkwrap vous proposent des solutions différentes, il est tout à fait possible de combiner l’usage de ces deux outils sans que cela pose de problème.

Activer le support JSONP avec Express

Activer le support JSONP avec Express

Si vos services webs sont destinés à être appelés depuis d’autres domaines dans un browser web, il sera nécessaire d’activer le support du JSON Padding pour vos services REST JSON.

Le support du JSON Padding selon le serveur utilisé est très variable. Côté node.JS avec Express, la fonctionnalité est très bien supportée, mais n’est pas activée par défaut. Il faut donc l’activer dans la configuration de votre serveur.

Malheureusement, il faut chercher un peu pour trouver comment faire. Voici donc pour vous faire gagner un peu de temps comment configurer votre serveur:

    app.set 'jsonp callback name', 'callback'

La configuration de la clé: ‘jsonp callback name’ permet de spécifier le nom du paramètre de queryString qui correspondra au callback encapsulant le JSON de retour. Dans notre cas, ici, la variable s’appellera: ‘callback’.

Un appel sans callback donnera le résultat suivant:

akinsella@~$ curl http://localhost:8000/api/v1/conferences
[
  {
    "id": 12,
    "backgroundUrl": "http://blog.xebia.fr/images/devoxxuk-2014-background.png",
    "logoUrl": "http://blog.xebia.fr/images/devoxxuk-2014-logo.png",
    "iconUrl": "http://blog.xebia.fr/images/devoxxuk-2014-icon.png",
    "from": "2014-06-12",
    "name": "DevoxxUK 2014",
    "description": "The Devoxx UK annual event.",
    "location": "Business Design Center",
    "enabled": true,
    "to": "2014-06-13"
  }, ...]

Les en-têtes spécifieront un Content-Type de type ‘application/json’:

akinsella@~$ curl -I http://localhost:8000/api/v1/conferences              
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 4397
Date: Sat, 14 Jun 2014 14:11:36 GMT
Connection: keep-alive

Alors qu’en appellant des resources avec le paramètre de queryString: ‘callback’, le serveur générera des réponses avec un Content-Type de type ‘text/javascript’:

akinsella@~$ curl -I http://localhost:8000/api/v1/conferences\?callback\=cb
HTTP/1.1 200 OK
Content-Type: text/javascript; charset=utf-8
Content-Length: 4430
Date: Sat, 14 Jun 2014 13:56:03 GMT
Connection: keep-alive

Le contenu de la réponse sera le suivant:

akinsella@~$ curl http://localhost:8000/api/v1/conferences\?callback\=cb
typeof cb === 'function' && cb([
  {
    "id": 12,
    "backgroundUrl": "http://blog.xebia.fr/images/devoxxuk-2014-background.png",
    "logoUrl": "http://blog.xebia.fr/images/devoxxuk-2014-logo.png",
    "iconUrl": "http://blog.xebia.fr/images/devoxxuk-2014-icon.png",
    "from": "2014-06-12",
    "name": "DevoxxUK 2014",
    "description": "The Devoxx UK annual event.",
    "location": "Business Design Center",
    "enabled": true,
    "to": "2014-06-13"
  }, ...]);

Cerise sur le gâteau, la génération du résultat est parfaitement gérée: la sortie obtenue intègre les best pratices de codage permettant d’éviter d’être vulnérable à certaines attaques XSS associées à l’utilisation du JSON Padding.

Désactiver l’en-tête de réponse ‘x-powered-by’ avec Express

Désactiver l’en-tête de réponse ‘x-powered-by’ avec Express

Il peut-être jugé embêtant niveau sécurité de dévoiler le type de serveur qui fait tourner vos services web. Il est donc préférable de ne pas envoyer cette information dans les en-tête de réponses HTTP avec Express.

Exemple de réponse HTTP avec l’en-tête ‘X-Powered-By’ activé:

akinsella@~$ curl -I http://localhost:8000/api/v1/conferences
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 4397
Date: Sat, 14 Jun 2014 22:43:15 GMT
Connection: keep-alive

Pour ce faire, il vous suffit de déclarer l’option suivante dans le code de configuration de votre application:

    app.disable "x-powered-by"

Les clients HTTP connectés à vos services ne recevrons ainsi plus cette information:

akinsella@~$ curl -I http://localhost:8000/api/v1/conferences
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 4397
Date: Sat, 14 Jun 2014 22:43:15 GMT
Connection: keep-alive
Créer un middleware de log des requêtes HTTP entrantes pour Express

Créer un middleware de log des requêtes HTTP entrantes pour Express

Vous souhaitez pouvoir logger les connections HTTP rentrantes avec Node.js et Express ? Rien de plus simple! Il suffit de déclarer un middleware de la façon suivante:

util = require 'util'

module.exports = (req, res, next) ->
    console.log  """---------------------------------------------------------
                    Http Request - Pid process: [#{process.pid}]
                    Http Request - Url: #{req.url}
                    Http Request - Query: #{util.inspect(req.query)}
                    Http Request - Method: #{req.method}
                    Http Request - Headers: #{util.inspect(req.headers)}
                    Http Request - Body: #{util.inspect(req.body)}
                    ---------------------------------------------------------"""


    next()

Comme vous pouvez le voir, aucun module externe n’est nécessaire. Il suffit ensuite d’intégrer votre nouveau middleware dans le code de configuration de votre serveur Express, comme suit:

express = require 'express'
requestLogger = require './lib/requestLogger'

app = express()

app.configure ->
    console.log "Environment: #{app.get('env')}"
    app.set 'port', 8000

    ...

    app.use requestLogger

    ...

    app.use app.router

app.listen app.get('port')

Le log résultant d’une requête HTTP prendra la forme suivante:

---------------------------------------------------------
Http Request - Pid process: [26074]
Http Request - Url: /
Http Request - Query: {}
Http Request - Method: GET
Http Request - Headers: { host: 'localhost:9000',
  connection: 'keep-alive',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36',
  'accept-encoding': 'gzip,deflate,sdch',
  'accept-language': 'fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4',
  cookie: '...' }
Http Request - Body: undefined
---------------------------------------------------------
Transformez vos callbacks Node.js en promises Q

Transformez vos callbacks Node.js en promises Q

Le callback Hell en JavaScript, on en a tous entendu parler, voir même un peu trop, mais pas sans raison.

De bonnes règles de codage permettent tout de même de gommer en grande partie ce problème, et l’usage des promises est à mettre en tête de liste des bonnes pratiques pour y parvenir.

Nous allons voir dans cet article différentes techniques proposées par le module Q permettant passer d’une écriture de code à base de callback à une écriture à base de promises.

Un exemple de départ

Partons de l’extrait suivant qui permet d’importer des données de conférences:

En JavaScript:

var importConferences = function(conferences, callback) {
  return conferenceImporter.importData(conferences, function(err, result) {
    if (err) {
      return callback(err);
    } else {
      return doSomethingWithResult(result, function(err, otherResult) {
        return callback(err, otherResult);
      });
    }
  });
};

Puis en CoffeeScript:

importConferences = (conferences, callback) ->
    conferenceImporter.importData conferences, (err, result) ->
        if err
            callback(err)
        else
            doSomethingWithResult result, (err, otherResult) ->
                callback(err, otherResult)

L’usage de CoffeeScript permet déjà de gommer une partie l’effet pyramide des appels de fonctions. Néanmoins, ce n’est pas suffisant, et dans le cas où de nombreux appels sont imbriqués, on s’y perd rapidement dans la compréhension du programme.

Q.defer()

Avec l’usage de la fonction defer() de Q, il sera possible de réécrire le même code de la façon suivante:

importConferences = (conferences) ->
    deferred = Q.defer()
    conferenceImporter.importData conferences, (err, result) ->
        doSomethingWithResult result, (err, result) ->
            if err
                deferred.reject(err)
            else
                deferred.resolve(result)
    deferred.promise

C’est un peu plus verbeux, mais la transformation du callback initial en promise permet maintenant d’appeler la fonction importConferences de la façon suivante:

importConferences(conferences)
.then (result) ->
    console.log "Result: #{util.inspect(result)}"
    someOtherPromise()
.then (otherResult)
    console.log "Result: #{util.inspect(otherResult)}"
.fail (err) ->
    console.log "Error - Message: #{err.message}"

En appliquant également la fonction defer() pour réécrire les fonctions importData et doSomethingWithResult, nous obtenons le code suivant:

importConferences = (conferences) ->
    conferenceImporter.importData(conferences)
    .then (result) ->
        doSomethingWithResult(result)

Plus besoin de la fonction defer() puisque nous travaillons avec des fonctions retournant des promises.

La fonction fail() n’est pas utilisée ici, car nous laissons le soin à la fonction appelante d’ajouter la gestion des erreurs à la promise retournée.

deferred.makeNodeResolver()

Dans certains contextes, vous ne pouvez pas vous baser sur une promise retournée par un appel de fonction. C’est par exemple le cas lorsque vous appelez une fonction d’un module qui ne supporte pas les promises.

Pour éviter d’écrire une résolution manuelle de promise basée sur le résultat d’un callback, Q propose la fonction makeNodeResolver. Elle simplifie l’écriture de conversion comme suit:

importConferences = (conferences) ->
    deferred = Q.defer()
    conferenceImporter.importData conferences, (err, result) ->
        doSomethingWithResult result, deferred.makeNodeResolver()
    deferred.promise

Le code n’est finalement pas plus verbeux que l’original, et vous permet de travailler avec des promises plutôt que des callbacks.

Conclusion

Les techniques de conversion visant à remplacer l’usage de callback par des promises sont variées et permettent en général de s’adapter au besoin.

Liens utiles

Pour en savoir plus sur la librairie Q, vous pouvez visiter la page du projet sur GitHub à l’adresse suivante:

https://github.com/kriskowal/q