af83

Évolution du web et API asynchrones

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) :

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

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) :

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        http.fetch("http://friendfeed-api.com/v2/feed/bret",
                   callback=self.async_callback(self.on_response))

    def on_response(self, response):
        if response.error: raise tornado.web.HTTPError(500)
        json = tornado.escape.json_decode(response.body)
        self.write("Fetched " + str(len(json["entries"])) + " entries "
                   "from the FriendFeed API")
        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 :

c := make(chan int)  // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
    list.Sort()
    c <- 1  // Send a signal; value does not matter. 
}()
doSomethingForAWhile()
<-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.

blog comments powered by Disqus