nodzle

Internet pipes

Le web évolue en permanence. Certaines tendances ne sont que des effets de mode passagers, tandis que d'autres transforment de manière profonde le web. En particulier, ces évolutions peuvent amener de nouvelles façons de concevoir et construire les sites web.

En ce moment, nous entendons de plus en plus parler de « web temps réel ». Cette dénomination est assez floue, mais derrière elle, on peut retrouver le fait que l'information navigue rapidement et relativement facilement entre le sites web : google va récupérer dans google buzz vos images en provenance de flickr et picasa, de nombreux sites intègrent des flux twitter, même ce blog récupère de l'information en provenance de github et de delicious.

Cette approche a des conséquences techniques non-négligeables. Les sites web ne sont plus juste un intermédiaire entre une base de données et des utilisateurs, mais sont de plus en plus obligés de se communiquer entre eux. Or, la communication entre deux sites web est un phénomène assez lent à cause de la latence. Cette lenteur est toute relative : en générale, c'est de l'ordre de quelques centièmes de secondes. Pourtant, cela a un impact énorme sur des applications web qui étaient jusque là capable de générer une page web en moins d'un dixième de seconde.

Les frameworks actuels comme Ruby on Rails ou Django reposent leurs capacité à encaisser un trafic conséquent sur cette rapidité, mais quand ils se retrouvent littéralement à piétiner en attendant que d'autres sites web répondent, cela peut rapidement devenir le drame : le site s'engorge puis finit par ne plus répondre.

La solution existe et est bien connue : il faut changer de paradigme de scalabilité au profit d'architectures asynchrones. L'idée est simple : quand un site web attend la réponse d'un autre site web, il va en profiter pour commencer à répondre à un deuxième client, puis à un troisième, un quatrième, etc. Il va alors se retrouver à gérer un certain nombres de requêtes en parallèle.

Ça ne paraît pas très compliqué, mais pour le développeur, c'est l'enfer : il doit savoir quand il peut traiter une requête, quand il doit passer la main, éviter à tout prix de mélanger les données de l'un avec celle de l'autre, etc. À l'heure actuelle, ce style de programmation est vraiment très exigeant, et une erreur est très vite arrivée.

Heureusement, des personnes ont décidées de s'attaquer à ce problème et de proposer des outils pour simplifier ça. Les frameworks Event-Driven commencent à apparaître, mais cela demande de mettre en place de nouvelles API, d'apprendre de nouveaux réflexes aux développeurs.

Par exemple, là où un simple tweets = Twitter::Base.new(credentials).user_timeline aurait suffit pour récupérer les tweets d'un utilisateur (en Ruby avec le gem twitter), il faut utiliser une approche bien plus compliquée dans un monde asynchrone (exemple libre) :

  1. var tweets = []
  2. new Twitter(credentials).addListener('tweet', function(tweet) {
  3.     tweets.push(tweet);
  4. }).addListener('close', function() {
  5.     // Faire quelque chose d'intéressant avec les tweets ici
  6. });

Comme on peut le voir, le code intéressant doit maintenant se trouver à l'intérieur de la fonction de callback passée à close. On va alors avoir tendance à imbriquer des fonctions dans d'autres fonctions, elles-mêmes imbriquées dans d'autres fonctions, et ainsi de suite. Au final, cela le rend le code difficilement lisible, et assez peu flexible.

Les créateurs de frameworks sont à la recherche de solutions pour proposer des API plus agréables à utiliser. EventMachine, coté Ruby, et Tornado, coté Python, essayent d'encapsuler le tout dans des classes. Cela donne un code relativement lisible, mais très verbeux (trop à mon goût) :

  1. class MainHandler(tornado.web.RequestHandler):
  2.     @tornado.web.asynchronous
  3.     def get(self):
  4.         http = tornado.httpclient.AsyncHTTPClient()
  5.         http.fetch("http://friendfeed-api.com/v2/feed/bret",
  6.                    callback=self.async_callback(self.on_response))
  7.  
  8.     def on_response(self, response):
  9.         if response.error: raise tornado.web.HTTPError(500)
  10.         json = tornado.escape.json_decode(response.body)
  11.         self.write("Fetched " + str(len(json["entries"])) + " entries "
  12.                    "from the FriendFeed API")
  13.         self.finish()

D'autres frameworks comme Node.js tâtonnent encore. Cela se voit par exemple sur ce long de fil de commentaires, qui a amené à la suppression des Promises dans node-v0.1.30. L'article "Do" it fast! est également une réflexion très intéressante sur le sujet.

Une des solutions possibles est de s'inspirer du modèle de passage de messages proposé par Erlang ou des chans et goroutines de Go :

  1. c := make(chan int)  // Allocate a channel.
  2. // Start the sort in a goroutine; when it completes, signal on the channel.
  3. go func() {
  4.     list.Sort()
  5.     c <- 1  // Send a signal; value does not matter. 
  6. }()
  7. doSomethingForAWhile()
  8. <-c   // Wait for sort to finish; discard sent value.

Pour conclure, je dirais que la seule chose qui est sûr aujourd'hui, c'est que les frameworks de demain seront asynchrones et proposeront des API adaptées à ce nouveau fonctionnement, mais pour le moment, nous n'en sommes encore réduit à explorer les différentes possibilités.

Logo des webhooks

Les webhooks sont un principe tout simple, mais qui offre de nombreuses possibilités pour étendre les fonctionnalités d'un site web. L'idée est vraiment simple : quand quelque chose de particulier se passe sur notre site, on envoie une requête HTTP avec le verbe POST vers une URL donnée. Les webhooks servent donc à notifier des scripts externes à une application web qu'un événement a eu lieu.

Hmmm, ça reste abstrait ? Prenons donc un exemple pour voir pourquoi et en quoi c'est très pratique. Github implémente des webhooks. Vous pouvez configurer pour un de vos dépôts git des URL de callbacks. A chaque fois qu'un commit aura lieu sur le dépôt en question, ces URL seront appelées avec les informations utiles (le nom du dépôt, la révision du commit, etc.). Cela offre de nombreuses possibilités : vous pouvez vous en servir pour déclencher un build sur votre service d'intégration continue (integrity par exemple), mettre à jour la documentation (rdoc.info offre ce service pour les projets ruby) ou encore analyser certaines métriques (Caliper).

Nous voyons que les webhooks peuvent donc servir à notifier très rapidement d'un événement. Mais ce hook peut également déclencher d'autres actions. Par exemple, le service d'intégration continue qui vient de recevoir un hook pourra lancer un build, puis poster le résultat sur un service de microblogging, et celui-ci pourrait alors lancer un autre webhook pour que les utilisateurs qui suivent ce compte soient notifiés, par exemple, en lançant qui script qui enverrait un message jabber, etc. Les webhooks sont cascadables à volonté.

Il existe une autre classe d'utilisation des webhooks : permettre à des script externes d'agir sur le site lui-même. Quand l'URL externe est appelée par un webhook, l'application peut lire le retour et agir en conséquence. Le cas le plus parlant est celui des robots sur Google Wave. Quand un utilisateur écrit dans une wave surveillée par un robot, celui-ci est prévenu par un webhook du contenu qui vient d'être écrit et il peut modifier la wave en réponse. Cela permet d'étendre le fonctionnement de Google wave via l'équivalent de ce que l'on pourrait appeler des plugins, mais avec la grosse différence que le code de ces plugins n'a pas besoin d'être sur le même serveur que la wave elle-même. On peut donc laisser la possibilité à des développeurs externes d'enrichir notre application de manière contrôlée.

Bref, les webhooks me semblent un élément de plus en plus important dans le web temps réel. L'alternative utilisée jusque là, le polling, devient de moins en moins adaptée à des contenus dont le rythme de mise à jour ne cesse d'accélérer. L'exemple de github est d'ailleurs assez symptomatique : il peut se passer plusieurs mois entre 2 commits, mais dès que j'ai commité, je souhaite que l'intégration continue se déclenche au plus vite afin d'être prévenu au plus tôt des regressions que j'aurais pu provoquer avec mon commit.

J'espère que cette introduction aux webhooks vous donnera envie d'appronfondir un peu le sujet, et surtout d'en mettre en place dans vos applications.

Hier, j'ai joué avec l'API de backtype. Elle permet de récupérer des tweets, commentaires et autres contenus en rapport avec une recherche. Dans mon cas, je voulais afficher sur les pages d'un site les tweets avec un lien vers cette page. J'aurais pu passer par l'API de recherche de twitter, mais je serais passé à coté de la plupart des tweets, car ceux-ci contiennent des URL raccourcies. Heureusement, Backtype comble ce problème, et il propose gratuitement une API pour faire ça.

Donc, c'est parti. Je crée un compte en 3 clics, et je jette un coup d'oeil à la doc de l'API. Bien, ça a l'air très simple, je ne devrais pas en avoir pour plus d'une heure. Et pourtant, 3 heures plus tard, j'y étais toujours... Rien n'est jamais aussi simple que cela en a l'air.

Mais pourquoi ? Pourquoi faut-il lire une documentation pour utiliser une API Rest toute simple ?

Douglas McIlroy résume la philosophie UNIX en :
« This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface. »
Source : Wikipedia

Cette philosophie de faire une seule chose mais de la faire bien pourrait être appliquée au web. Il faudrait sûrement arranger certains points : utiliser des flux HTML en UTF-8 plutôt que des flux textes ascii, remplacer le terme programme par application web, etc. Mais fondamentalement, rien ne nous empêche de concevoir le web de cette façon.

Pourtant, j'ai l'impression que le web actuel s'éloigne de plus en plus de cela pour fournir des applications complètes, complexes, mais qui ont du mal à communiquer entre-elles et dont le passage de l'une à l'autre manque de fluidité.

Bref, je pense que l'on aurait beaucoup à gagner à essayer de se ré-approprier la philosophie UNIX et à l'appliquer au web. Pour cela, il faudrait sûrement inventer des concepts similaires aux pipes UNIX (les WebHooks ?), aux flux standard, au principe "tout est fichier" ("tout est accessible par une URL" ?), aux permissions UNIX, etc. A suivre...