Les domaines dans Node.JS, à quoi ça sert ?

Les domaines sont l’une des grandes nouveautés de Node.JS 0.8.

J’étais un peu circonspect quand je les ai vus arriver, ayant un peu de mal à comprendre le cas pratique auquel ils répondaient. Puis je suis tombé sur un cas dans la vraie vie :) Je vais vous le partager en version simplifiée, et on va voir comment le corriger à l’aide des domaines.

Un module en apparence innocent

En l’occurrence, le module était request. Il peut fonctionner en mode stream, et pour rappel un flux dans Node.JS est un EventEmitter qui peut donc lever des erreurs via l’évènement error.

Une bonne manière par exemple de télécharger un fichier serait donc la suivante:

request.get('http://server/remote_file')
    .on('error', function (err) {
        // Traiter l'erreur
    })
    .pipe(fs.createWriteStream('local_file'));

Hélas, le .on('error') est souvent laissé de côté. On retrouve la facheuse tendance des développeurs à ne jamais attraper leurs exceptions, et encore moins celles des autres 😉

Un évènement n’est pas une exception

Ça semble une évidence, mais il faut en saisir toute la portée: une exception n’a évidemment aucun sens dans un code asynchrone, le programme est passé hors du bloc try/catch depuis longtemps quand l’erreur survient, et retourner d’un coup dans le catch serait très complexe à gérer pour le développeur.

À côté de ça, un évènement ne « remonte » pas dans la stack: si un EventEmitter est utilisé dans une fonction, impossible de récupérer ses évènements.

Le cas qui ruine le moral

Et là on tombe sur le module mal codé. Celui qui vous expose une fonction de ce genre:

function hello() {
    request.get('http://server/remote_file').resume();
}

C’est request mais ça pourrait être n’importe quel EventEmitter. En cas d’erreur, le code se résume en gros à ça:

function hello() {
    var e = new EventEmitter();
    process.nextTick(e.emit.bind(e, 'error', new Error('I/O Error')));
}

Et là c’est mort. De là où on est, au moment où on appelle hello() il n’y a plus rien qu’on puisse faire, on assiste misérablement à un crash lamentable de l’application à chaque fois qu’il y a un problème de DNS par exemple.

events.js:66
        throw arguments[1]; // Unhandled 'error' event
                       ^
Error: I/O Error

Ceux qui ont déjà vu un serveur entier s’écrouler parce qu’un type n’a pas catché une exception sauront ce qu’on ressent. Bon l’application redémarre en quelques millisecondes, mais tout de même.

La solution avant

La seule solution qui s’offre à nous alors est de réécrire la librairie. La modification n’est pas très compliquée il suffit juste d’exposer d’une manière ou d’une autre ce foutu EventEmitter. Dans notre exemple ça aurait pu être un simple return e à la fin de notre fonction, accompagné d’une contribution au développeur en passant :)

Ainsi on aurait pu éviter le crash

hello().on('error', function (err) {
    // handle error
});

Sinon on pouvait toujours utiliser process.on('uncaughtException') mais démêler le bordel qui arrive là-dedans n’est jamais trivial.

Les domaines à la rescousse

Les domaines permettent d’exécuter un callback dans un contexte d’exécution où tous les EventEmitter et les exceptions seront interceptées et rattachées au domaine. Le domaine est lui-même un EventEmitter qui va donc émettre un évènement error quand il a intercepté une erreur :)

On peut alors conserver le code du module tiers intact, et simplement attacher la méthode à un domaine spécifique:

domain.create()
    .on('error', function (err) {
        // handle error
    })
    .run(hello);

En détails

domain.create() crée un domaine. Un domaine permet ensuite deux choses:

  • Intercepter tous les évènements error et les exceptions lancés dans un appel de fonction (via run(), bind(), et intercept()).
  • Écouter et retransmettre l’évènement error de n’importe quel émetteur (via add()).

Je crée un domaine, lui affecte un gestionnaire d’erreur:

var d = domain.create();
d.on('error', function (err) {
    // handle error
});

Je peux alors simplifier le traitement de cas qui d’ordinaire provoque une uncaughtException (dans tous les exemples suivant on considère que e = new EventEmitter()):

d.add(e); // l'émetteur est surveillé!
e.emit('error', 'FU'); // l'évènement est renvoyé à notre handler
d.run(function () { // toutes les erreurs dans ce callback sont surveillées
    e.emit('error', 'FU'); // l'évènement est renvoyé à notre handler
    // peu importe que "e" ait été instancié dans ou en dehors du callback
});
d.run(function () {
    throw 'FU'; // celle là elle est facile, un simple try/catch aurait suffi
});
 
d.run(function () {
    process.nextTick(function () {
        throw 'FU'; // celle là je vous défie de la catcher autrement ;)
    });
});

Particulièrement pratique en théorie pour créer de larges domaines d’exécution afin de contextualiser les erreurs non interceptées par le développeur: le serveur web, le client de base de données, etc. Et côté pratique on voit que ça peut servir à corriger un comportement erratique dans un module tiers sans avoir à le patcher :)

Note: le statut est expérimental, l’API n’est donc pas figée.

2 réflexions au sujet de « Les domaines dans Node.JS, à quoi ça sert ? »

Laisser un commentaire