Les microservices permettent de composer facilement un site web.
Chaque microservices aura son propre cycle de vie (rythme de développement et de déploiement), ses choix techniques et même ses équipes de développement.
Pour ne pas avoir de surprises, il est indispensable de s'appuyer sur des normes éprouvées, comme le sont REST pour exposer des API, et JWT pour l'authentification.
Exemple de microservice composé à partir d'API distantes
Internet regorge d'API plus ou moins exotique permettant de composer simplement une application spécifique à partir de services génériques et experts.
Le principal souci des API distantes sont les latences, plus ou moins longues selon l'humeur et la popularité du service. Pour les technologies synchrones (PHP, Rails, Django…) ces temps d'attente peuvent être catastrophiques. Ces applications ont un pool de workers réduits, avec un contrat tacite de temps d'exécution court, très court même, une requête doit être traitée en moins de 300 ms pour rester raisonnable.
Une fois tous les workers bloqués en attente, il ne reste plus personne pour répondre, le Nginx va faire poireauter, puis commencer à balancer les tant redoutés 503 ou 504 .
1/3 de seconde, sur une même machine, c'est tout à fait faisable, par contre, pour un appel distant, sur une machine quelque part sur Internet, c'est trop court.
Pour gérer comme il faut les attentes, le plus simple est d'utiliser une technologie asynchrone. Il existe des bibliothèques pour gérer ça dans tous les langages ( python asyncio , ruby EventMachine , reactphp …) mais ce ne sera que très rarement intégré à vos frameworks adorés.
Le plus simple est d'utiliser Javascript , vous savez, le langage asynchrone depuis toujours. Et comme vous avez déjà des pages plus lourdes en javascript qu'en HTML sur vos sites, vous savez déjà comment ça fonctionne.
Node.js a ses petits défauts, mais ça fait longtemps qu'il est utilisé en production, et le javascript contemporain n'est plus le machin des débuts. Node.js embarque un V8 , le moteur utilisé par Chrome, et gère les dernières spécifications ECMA , dont l'indispensable async/await
qui permet d'avoir un code lisible et élégant, sans devoir traverser les 9 cercles de l'enfer des callbacks.
Service REST en Node.js
L'écosystème Node est fasciné par la prolifération des bibliothèques. Il faut faire avec. Pour ne pas perdre de temps, je vais ne vais choisir que des bibliothèques très largement utilisées.
- Axios comme client HTTP, pour interroger les services REST distants.
- Jest pour les tests unitaires.
- Express pour gérer le serveur Http, et créer une API REST.
Le µservice qui nous servira d'exemple, fantaisiste et arbitraire, va proposer une illustration à partir d'un article. Il va demander à Dandelion de trouver les entités dans le texte (personne, lieu…) et à Giphy un GIF correspondant à cette entité. Sans validation, en prenant le risque d'avoir un GIF ringard ou craignos, par ce que yolo, c'est pour un exemple.
const axios = require('axios'); // Entity extraction with Dandelion async function dandelion(txt) { const resp = await axios .get("https://api.dandelion.eu/datatxt/nex/v1", { params: { token: process.env.DANDELION_TOKEN, html_fragment: txt, lang: "fr", "social.hashtag": "true", include: "types,categories", }, }); return resp.data; } exports.dandelion = dandelion;
Le gros avantage des APIs REST, c'est qu'on peut les utiliser tout de suite, avec un bon gros curl
, ou là, l'équivalent en Node.js.
La fonction est asynchrone (non bloquante), elle fait un simple GET, en passant un token issu d'une variable d'environnement, ainsi que les arguments.
// Find an ugly moving picture async function giphy(query) { const resp = await axios.get("https://api.giphy.com/v1/gifs/search", { params: { api_key: process.env.GIPHY_TOKEN, q: query, limit: "32", offset: "0", //rating: "g", lang: "fr", }, }); return resp.data; } exports.giphy = giphy;
Même approche pour giphy : même simplicité et lisibilité.
Les tests qui vont bien, pour ne jamais se perdre dans son code et valider que chaque unité de code fonctionne comme attendu :
const mashup = require("./mashup"); test("dandelion knows Stallman", () => { return mashup .dandelion( 'Richard Stallman : "il faut éliminer Facebook qui entrave les libertés"' ) .then((d) => { expect(d.annotations[0].categories).toContain("Hacker"); }); }); test("giphy rulez", () => { return mashup.giphy('rulez').then((g) => { expect(g.data[0].images.downsized_small).toBeDefined(); }); });
Un peu de code métier pour extraire une seule entité, à partir d'un texte et d'une liste ordonnée de types :
// Extract one entity from a text, with type priorities async function the_entity(txt, types) { const entities = await dandelion(txt); for (let type of types) { for (let entity of entities.annotations) { if (entity.types.includes(`http://dbpedia.org/ontology/${type}`)) { return entity; } } } // ok, there no cute entity, lets use the first, yolo return annotations[0]; }
L'assemblage de tout ça reste simple :
// Get a pict from a text async function illustrate_that_for_me(txt) { const entity = await the_entity(txt, ["Organisation", "Person", "Place", "Location"]); const gif = await giphy(entity.label); return { label: entity.label, gif: gif.data[0].images.downsized_small, }; } exports.illustrate_that_for_me = illustrate_that_for_me;
ES2016 apporte les async
utilisés pour définir les fonctions asynchrones, et les await
pour les appels de fonctions asynchrones. C'est un emballage élégant des promesses .
On a dans cet exemple un assemblage de fonctions asynchrones sans utiliser aucun callback.
Les lecteurs les plus attentifs auront noté l'exception dans les tests, où l'on a un appel synchrone de fonctions asynchrones, avec la syntaxe "pas si pire" avec un .then((g) => {})
qui chaine une Promise
avec une arrow function
définissant le tant craint callback.
Les sources sont disponibles sur Github dans le projet Demo API mashup Node .
Dans l'épisode suivant, on verra l'utilisation de REST et JWT, ainsi que l'UI élégante que verra l'utilisateur.
Crédit photo : freethoughtblogs