Archives pour la catégorie NodeJS

Écrire des scripts npm multi-plateforme

npm c’est très bien, j’ai complètement abandonné gulp, grunt, et consors, et je commence même à embrasser ma condition d’allergique au fichier de conf (le package.json sera mon unique tolérance). Bref, si comme moi votre « task runner » est npm, alors comme moi vous avez du vous heurter au cas des « autres » plateformes, celles qui n’ont pas les mêmes commandes que vous, voire pas de commande du tout pour ce que vous voulez faire… c’est-à-dire Windows et son shell de Néandertal. Voici donc une série de modules node qui résolvent ces questions.

Commandes de base

COPY vs cp, MKDIR vs mkdir -p, etc… les commandes de base ne sont pas acquises du tout, passez par des modules npm faits pour ça :

  • rm -rf filesrimraf files
  • cp -r source targetcpx source target (qui a en plus une option --watch pour maintenir la cible à jour)
  • mkdir -p a/b/cmkdirp a/b/c

Variables d’environnement

La syntaxe ENV_VAR=value commande sous Windows ne fonctionne pas.

  • Utilisez plutôt cross-env ENV_VAR=val command

Exécuter plusieurs commandes

  • Plusieurs scripts npm en parallèle :
    • npm run a & npm run b & npm run c & waitnpm-run-all --parallel a b c (plus court, plus solide, plus universel)
  • Plusieurs scripts npm en série :
    • npm run a && npm run b && npm run c fonctionne très bien, pas besoin d’outil sur ce coup
    • npm-run-all a b c sera plus concis
  • Plusieurs commandes hors scripts npm, en parallèle : aggregate-commands (mais autant mettre ces commandes en script npm et utiliser npm-run-all)

Commandes de « watch »

Aucune excuse !

Vous n’avez maintenant plus aucune excuse pour ne pas écrire de scripts de « build » qui tournent partout, sans pour autant vous encombrer de task-runners obèses. Vous en connaissez d’autres ? Commentez !

Node 4.0.0

Si vous l’aviez raté, sachez que Node vient de passer un cap important :) 4.0.0 c’est un peu l’album de la maturité. Passons en revue le quoi, le pourquoi et le comment 😉

Pourquoi 4.0.0 ?

Alors non ce n’est pas une lubie commerciale, ou un trait d’humour « parce que JavaScript ». La blague (voire le troll) est facile, mais la réalité est bien plus respectable.

À la base : io.js, un fork de Node qui décide de respecter le semantic versioning (*), et de suivre au plus près les mises à jour de V8. Comme mettre à jour V8 implique parfois des ruptures de compatibilités, io.js est vite passé de la 1.x à la 2.x et récemment la 3.x.

Puis, la fusion : on décide de fusionner les deux projets (en très gros,  renommer « io.js » en « node » et passer un coup de polish), il faut bien donner un numéro de version :

  • Passer « node » de 0.x à 1.x ? Ça a du sens, mais « io.js » utilisait le nom « node » pour son binaire et de nombreuses références internes (peut-être une erreur, mais ça a aussi largement simplifié la fusion au retour) donc on aurait risqué moultes confusions à l’avenir.
  • Passer « node » de 0.x à 4.x ? Ça fait rire, mais ça règle toutes les questions techniques.

Les nouveautés

API

J’avoue n’avoir pas suivi en détail les évolutions dans io.js (surtout à partir du moment où une fusion à court terme a été confirmée), mais voici ce que j’ai noté d’important :

Il manque encore 2-3 choses pour être au top 😉 personnellement j’utilise Babel pour ces fonctionnalités :

Du coup on n’est quand-même pas loin de pouvoir faire de l’ES6 tout en pouvant se passer de Babel. Mais on n’y est pas encore tout-à-fait (les modules restant le gros morceau pour moi).

La nouvelle orientation de Babel prend tout son sens (merci Bruno pour le lien :*).

(*) Semantic Versionning

Finies les 0.x, node adopte sans ambiguités le « semantic versioning« . Vous pourrez donc être sûr que vos modules écrits pour la version 4 fonctionneront pour toutes les 4.* suivantes.

Ce qui implique qu’on n’aura plus peur de toutes les mises à jour. Seulement les majeures :) La suite logique étant…

Le support à long terme !

Il y a un vrai plan de maintenance des versions de node qui fixe la durée de vie d’une version « LTS » à 30 mois : 18 mois de support actif, et 12 mois de maintenance, au-delà desquels ladite version ne sera plus du tout supportée.

En théorie, une nouvelle version « LTS » devrait sortir tous les 12 mois.

Concrètement donc, mettre à jour sa version de Node tous les 12 à 18 mois assurera d’être toujours dans le cadre du support long terme actif.

Alors on fait quoi ?

Et bien on met à jour maintenant :) Si vous utilisiez un gestionnaire de versions du genre « nvm« , ce serait mieux. Et si ce n’est pas le cas, il est temps de le faire.

$ nvm install v4
$ node -v
v4.0.0

Et puis on met à jour son appli avant de tester :)

$ nvm use 4
$ rm -rf node_modules
$ npm install
$ npm start # whatever start command your project uses

Évidemment si vous vous contentez de mettre à jour et de lancer vos applis directement avec la nouvelle version il y a des chances que vous ayez des « Segmentation Faults ». Il est normal que les modules binaires doivent être recompilés en cas de changement de version de v8, arrêtez donc de râler.

Si vous êtes auteur d’un module binaire qui ne fonctionne plus avec la nouvelle version, mettez-le rapidement à jour ! Vous ne voulez pas que la nouvelle version de node voit son adoption ralentie à cause de vous hein :) Idéalement, utilisez les helpers de Nan pour vous simplifier la vie à l’avenir. En attendant, mettez donc à jour votre « package.json » pour éviter les mauvaises surprises (et mettez vite à jour pour que l’humiliation soit moins longue :P) :

{
  "engines": {
    "node": "< 4.0"
  }
}

Si votre application dépend d’un module binaire cassé avec la nouvelle version… Désolé :( prévenez l’auteur, vérifiez qu’il réagit vite sinon envisagez de modifier cette dépendance afin de pouvoir passer à la 4.0 rapidement. Idéalement une application devrait toujours pouvoir tourner avec la LTS en cours. Donc être passé à la 4.0 avant avril 2016.

Un peu de lecture en vrac

Enjoy!

Ma boite à outil Node.js

Voilà le genre d’article qui peut servir à tout le monde, et qui peut devenir une formidable opportunité d’échange pour moi si vous venez partager vos propres « batbelt » en commentaire. Donc n’hésitez pas :)

Je travaille au quotidien avec Node.js (à tel point que j’ai presque oublié comment ça marchait côté client ;)), et vous imaginez donc que j’ai des millions d’outils super bien gaulés pour me faciliter la vie au quotidien. Raté ! Il y en a finalement assez peu, et ce sont les grands classiques que vous connaissez déjà probablement. Mais qui sait, si ce n’est pas le cas, ce post pourrait changer un petit bout de votre vie !

Séance bookmarks, c’est parti.

Lire la suite

L’agrégation sur un serveur MongoDB mono-instance : NON

J’ai eu récemment de gros problèmes de performances sur un serveur, lié à MongoDB. Je vais donc vous présenter le problème et sa solution :)

TL;DR : Tout le code est ici pour faire le test vous-même.

Le contexte général : on a une collection assez large de documents comportant un code de regroupement et une valeur numérique. On cherche à faire une agrégation très classique consistant à prendre l’item dont la valeur numérique est la plus haute, regroupé par le code de regroupement.

Par exemple : on a une collection d’articles (auteur, date, texte) et on veut sortir la liste des derniers articles postés par auteur. Pour l’exemple j’ai préparé une collection de 50’000 documents à valeurs aléatoires (auteur parmi une liste pré-définie, date dans les 3 derniers mois).

Si on connaît bien MongoDB, on va utiliser le framework d’agrégation. Mais dans mon cas avec un MongoDB 2.2, impossible.

La map/reduce

Du coup, on se rabat logiquement sur un map/reduce :

// Article grouped by author
function map () {
  emit(this.author, this);
}
 
// Keep max date
function reduce (key, articles) {
  return articles.sort(function (a1, a2) { return a2.date - a1.date })[0]
}
 
…
.then(mapReduce(map, reduce))

C’est lent. Sur ma machine avec un mongodb en 2.4 ça tourne à ~900 ms. Sur le serveur dont je parlais au début, en 2.2, ça tape dans les 5 secondes (ouch).

Le map/reduce avec béquille

J’ai donc essayé de simplifier le map/reduce, me disant que « sort » devait être trop coûteux. Je sors donc juste la date max par auteur, puis je fais un find :

// Date grouped by author
function map () {
  emit(this.author, this.date);
}
 
// Keep max date
function reduce (key, dates) {
  return Math.max.apply(null, dates)
}
 
…
.then(mapReduce(map, reduce))
// Build query: big $or
.then(function (results) {
  return {$or: results.map(function (result) {
    return {author: result._id, date: new Date(result.value)}
  })})
.then(find)

On y gagne, mais ça reste lent. Sur ma machine concrètement on passe de ~900 ms à ~700 ms, c’est quand-même un beau gain de plus de 20%.

On abandonne le map/reduce

Du coup j’ai essayé la méthode débile. J’ai sorti la liste des clés possibles (la liste des auteurs ici), puis pour chacun fait une requête avec un tri sur la date. Dans notre exemple ça donnerait quelque-chose comme ça :

…
.then(find({}, {author: 1}) // [{author: "Bob"}, {author: "John"}]
.then(pluck('author'))      // ["Bob", "John"]
.then(function (authors) {
  // N queries in concurrency
  return Q.all(authors.map(function (author) {
    // max date's article for this author
    return find({author: author}).sort({date: -1}).limit(1).nextObject()
  }))
})

Le résultat est sans appel : le premier « find » prend en moyenne ~270 ms, les N « find » suivant au total ~60 ms pour un total de ~330 ms soit un gain supérieur à 60%. Et comme c’est la première requête qui prend l’essentiel du temps, si on a la possibilité d’avoir la liste des auteurs de manière moins coûteuse (par exemple une valeur en configuration, ou simplement les listes d’une collection tierce, ce qui dans notre exemple aurait été plus logique) on peut vite diviser par 10 le temps de réponse initial.

Ce fut mon cas, je suis passé de 5 secondes à 0.5 secondes sur mon serveur, en passant de 1 à N+1 requête (une quinzaine dans ce cas).

Conclusion

Comparaison des méthodes

Pour l’anecdote, le problème ne s’était pas posé sur notre premier serveur de recette monté un peu à l’arrache et surtout… branché sur un server mongodb dans le cloud (Clever Cloud en l’occurrence). Et dans ce cas le map/reduce fonctionnait très bien.

Tout ça pour dire : N’oubliez donc pas que MongoDB est mono-threadé, en plus d’avoir un runtime plutôt lent, et que donc s’il n’est pas capable de distribuer les calculs… il vaut mieux ne pas lui en demander :) N’oubliez pas de jeter un œil au TL;DR : code des tests.

Gérer ses dépendances optionnelles

On a parfois des dépendances qui améliorent le fonctionnement de notre module, sans être pour autant obligatoires.

Par exemple, le module redis peut être accéléré par l’utilisation de hiredis (un parseur écrit en C, donc compilé lors du npm install). Pour gérer ça, il est indiqué dans le README qu’on peut installer hiredis si on veut que ça aille plus vite, mais que ça marchera sans.

Concrètement dans le code du module reposant sur une dépendance optionnelle, on aura ce genre de lignes :

// Load optional dependency
function require_optional (name) {
  try {
    // Try normal require
    return require(name);
  } catch (e) {
    // Something went wrong :(
    if (e.code == 'MODULE_NOT_FOUND') {
      // Special case "module not found": we just ignore this error
      return undefined;
    } else {
      // This is an other unexpected error
      throw e
    }
  }
}
 
// Somewhere in my module
var the_module = require_optional('the_module');
// …
if (the_module) {
  // use "the_module" to enhance my code
}

Mais pourquoi avoir des dépendances optionnelles ? Si c’est mieux avec autant l’inclure d’office ! deux bonnes raisons :

  • Le module peut ne pas compiler (par exemple hiredis ou msgpack ne se compilent pas très bien d’office sous Windows…) ;
  • C’est quelqu’un d’autre qui gère ce module, s’il lui prend l’envie de le renommer ou de le supprimer et que mon installation par conséquent peut échouer du jour au lendemain… autant pour une dépendance sans laquelle mon application ne peut pas marcher, je peux tolérer que ce soit fatal, autant pour une dépendance dont je pourrais clairement me passer c’est dommage de planter toute l’installation pour ça ;

Dans le cas de redis ils gèrent ça manuellement en nous disant simplement d’installer hiredis si on veut. Une autre manière aurait été d’utiliser les dépendances optionnelles du package.json. Pour ajouter une dépendance optionelle à votre module, ça se passe donc dans la section optionalDependencies, qu’on peut enrichir comme on s’en doute directement avec npm :

$ npm install --save-optional hiredis
 
$ git diff
…
+  "optionalDependencies": {
+    "hiredis": "~0.1.15"
+  }

Si le prochain qui fait le npm install est sous une machine sous Windows inutilisable où la compilation de hiredis échoue, ce n’est pas grave : il en sera notifié mais le reste de l’installation continuera.

La prochaine fois que vous vous dites que vous pouvez optimiser votre sérialisation avec msgpack, pensez à vos amis sous Windows, utilisez optionalDependencies et un petit require_optional() :)

La mise en production d’un serveur Node.js : l’option « Phusion Passenger » (update)

TL ; DR : Passenger ce n’est pas indispensable, mais ça fait vraiment bien son job. C’est bon, mangez-en :) (et non, ce n’est pas un article sponsorisé, ça se saurait si j’étais foutu de négocier ce genre de truc).

Bon déjà, on va faire comme si c’était normal de n’avoir rien posté pendant près d’un an, et ne pas aborder le sujet 😛 Pas de bonne excuse à l’horizon, je vous ferai un bilan de mon 2013 l’année prochaine, vous allez voir qu’elle était sympa cette année 😉

Le sujet du jour est la mise en production de vos applications Node.js. On a pour ça plusieurs objectifs à atteindre :

  • Démarrage automatique de l’application au démarrage du serveur ;
  • Utilisation de plusieurs cores de CPU ;
  • Redémarrage automatique de l’application en cas de crash ;
  • Notification lors d’un crash ;
  • Monitoring de l’activité de l’application ;
  • Protection de l’application contre les attaques courantes HTTP ;
  • Optimisation du rendu des fichiers statiques ;
  • Une solution pour déployer les mises à jour ;

J’ai assisté lundi au dotJS 2013 qui était plutôt cool, même si ça manquait de conférences « inspirantes » comme le disait justement François Zaninotto. Je vous promettrais bien de faire un article sur mes impressions, mais d’une part vous en trouverez des dizaines déjà faits, et d’autre part c’est encore un article que je finirais par ne pas écrire. Donc non, aucune promesse 😛

Il y a quand-même deux talks qui m’ont particulièrement parlé (ah ah, jeu de mot bilingue, trop classe), et pas de bol tous deux étaient des lightning talks ! Le premier de David Bruant autour de la recherche des fuites mémoire dans nos applications JS et la présentation d’un outil auquel je vais m’empresser de contribuer (et non David, même si je sais que tu aimes ça, je ne te jetterai pas de pierres :P). Le deuxième par Hongli Lai présentait la solution Phusion Passenger pour simplifier la mise en production d’applications. Hop, parlons donc de Passenger, après un tour d’horizon de l’existant.

Lire la suite

*Breaking New* Streams & 0.9.8: objectMode!

C’est tout frais ça vient de sortir dans la 0.9.8, le support des objets autre que des String ou des Buffer dans les streams !

Tiens pour changer je vais mettre les exemples en coffee-script 😉 (je n’aime pas particulièrement, mais c’est utile d’en voir de temps en temps, vue sa popularité il faut bien s’habituer).

Rappelez-vous, notre classe ArrayReadStream :

class ArrayReadStream extends stream.Readable
        constructor: (array) ->
                super {objectMode: true}
                @_array = array
                @_index = 0
        _read: (n, cb) ->
                process.nextTick =>
                        if @_index < @_array.length
                                cb null, @_array[@_index++]
                        else
                                cb null, null

Imaginons qu’on l’utilise pour un tableau d’objet (exemple typique du résultat qu’on aurait voulu avoir avec un client SQL) :

array = [{name: 'John Smith'}, {name: 'John Williams'}, {name: {first: 'John', last: 'Rambo'}}]
 
s = new ArrayReadStream array
 
s.pipe process.stdout

La nouveauté c’est l’option « objectMode«  qui permet d’activer le support des objets directs.

Jusqu’à la 0.9.7 comprise, Ce code ne fait rien. En effet, les données retournée par _read n’étant ni des Buffer ni des String la lecture s’arrête (sans erreur, ce que je trouve un peu moche d’ailleurs…).

En 0.9.8 en revanche ce code va planter :

[…]
TypeError: invalid data
    at WriteStream.Socket.write (net.js:565:11)
[…]

C’est parce que d’un coup process.stdout se prend dans la tête un truc qu’il ne sait pas écrire. Des objets autre qu’un Buffer ou une String ?

Lire la suite

Hangout Google+ FR.Node.js #1

Ce vendredi 18/01/2013 aura lieu le premier Hangout de la communauté Google+ FR ⋅ Node.js.

C’est un essai, j’avoue ne même pas savoir exactement comment ça se passe techniquement, donc on va probablement avoir des ratés mais ce sera marrant 😉

Ça démarre « officiellement » à 12h45. Au programme :

  • Live-coding autour des streams
  • Discussion libre

Si ça marche bien et qu’on s’amuse tous, on essaiera de refaire ça régulièrement, ça peut être un format sympa.

Bonne semaine et à vendredi !

Un point sur les streams

Les streams, c’est l’API de gestion de flux de données de Node.js.

Cette API est très élégante car elle mime le fonctionnement des flux à la Unix, avec la notion de « pipe », permettant de définir des éléments de code unitaires qui se contentent de recevoir et/ou publier un type de données.

Imaginons qu’on souhaite faire une requête SQL sur un annuaire, effectuer quelques transformations (mettre le nom en majuscule), puis écrire le tout dans un fichier CSV.

Avec l’API asynchrone standard on pourrait imaginer quelque chose comme ça (évidemment il ne s’agit pas d’API existantes exactement, c’est juste une idée) :

var csv = CSVFile('fichier.csv');
query('SELECT nom, prenom, telephone FROM annuaire', function (rows) {
  rows
    .map(function (row) {
      row.nom = row.nom.toUpperCase();
      return row;
    })
    .forEach(function (row) {
      csv.write(row);
    });
});

Et maintenant, la même avec les streams :

query('SELECT nom, prenom, telephone FROM annuaire')
  .pipe(streamMap(function (row) {
    row.nom = row.nom.toUpperCase();
    return row;
  }))
  .pipe(CSVWriter('fichier.csv'));

C’est à la fois plus court, et plus lisible.

Ça ressemble furieusement à ce qu’on ferait en shell : `select_annuaire | nom_majuscule | csv_append ‘fichier.csv’`.

Note : les promesses auraient pu représenter un bon compromis entre les deux, mais ce n’est pas le sujet ici 😉

Lire la suite

Authentification WebSocket avec les sessions Express 3.x et Socket.IO

Suite à mon premier article complet sur le sujet (« Authentification et WebSocket, avec Node.JS, Express, et Socket.IO »), beaucoup m’ont signalé que les exemples ne fonctionnent plus avec Express 3.x. C’est pas tout ça mais Express est quand-même officiellement en version 3 depuis un petit moment, donc il était grand temps de mettre à jour le code !

Je vous encourage à consulter les commentaires de l’article original (merci à Laurent et Will pour leurs discussions sur ce thème) pour de plus amples informations sur les petites surprises qu’on peut rencontrer 😉

J’en profite pour vous décrire l’intégralité de la manœuvre pour que vous voyez un cas de migration Express 2→3, avec des petites astuces en passant :) Le code est dispo sur github, et chaque étape est accompagnée d’un lien vers le commit correspondant.

Lire la suite