Appels asynchrones avec Play Framework

L’intégration de nouveaux composants permettant la gestion des appels asynchrones dans Play Framework a été récemment annoncé sur la mailing-list par Guillaume Bort. Ils ne sont pour l’instant pas disponible sur la version stable de Play, il faut donc récupérer la branche master pour les utiliser. En voici un rapide aperçu avec un exemple d’utilisation.

En résumé

Intégrés dans la librairie F ( play.libs.F ), ces nouveaux composants offrent, entre autre, la possibilité de mettre en pause une action jusqu’à ce qu’un événement se produise.

Lorsqu’on souhaite implémenter ce type de fonctionnalité, un problème se pose généralement : comme chaque requête est servie par un thread, si on suspend de manière simpliste les threads, on ne pourra pas avoir un nombre de clients supérieur au nombre de threads alloués au serveur. Même si il est possible d’augmenter le nombre de thread du serveur d’application, on ne peut pas le faire à l’infini et on ne peut que repousser le problème, pas l’éliminer.

L’implémentation réalisée dans Play permet de s’affranchir de cette barrière. Lors de la suspension de la requête, le thread est libéré automatiquement par le framework. Lorsque l’événement attendu survient, un thread est automatiquement réaffecté pour poursuivre la requête. Et c’est là que la magie opère : l’action reprend exactement à l’endroit où elle s’était arrêtée, de manière totalement transparente pour le développeur.

En pratique

Prenons l’exemple d’une application permettant de saisir des valeurs dans un tableau à 2 entrées. Cette application possède un écran de visualisation avec une saisie à l’aide d’un éditeur “inplace”. À un instant donné, on peut avoir plusieurs utilisateurs sur cet écran, certains voulant uniquement le consulter et d’autres le modifier. Pour rendre l’expérience utilisateur intéressante, on souhaite que chaque modification effectuée soit immédiatement “poussée” vers tous les utilisateurs.

Pour pouvoir accomplir cette fonctionnalité, il faut dans un premier temps disposer d’une action permettant d’envoyer les données (ici, en JSON) et d’une fonction javascript permettant de mettre à jour l’affichage à partir de ces données.

On définit alors une classe qui va permettre d'”écouter” les modifications et de notifier les abonnés lorsqu’une modification a lieu. Dans notre cas, ce mécanisme est mis en place en se basant sur l’identifiant de l’entité que l’on souhaite suivre.

package models;
import java.util.HashMap;
import play.libs.F.EventStream;
import play.libs.F.Promise;
public class PricingUpdateChannel {
  static HashMap<Long, PricingUpdateChannel> instances = new HashMap<Long, PricingUpdateChannel>();
  final EventStream<Long> pricingEvent = new EventStream<Long>();
  public static PricingUpdateChannel get(Long pricingId) {
    if(!instances.containsKey(pricingId)) {
      instances.put(pricingId, new PricingUpdateChannel());
    }
    return instances.get(pricingId);
  }
  public Promise<Long> nextPricingUpdate() {
    return pricingEvent.nextEvent();
  }
  public void updatePricing(Long pricingId) {
    pricingEvent.publish(pricingId);
  }
}

Cette classe s’appuie sur des EventStream qui permettent de gérer l’abonnement et la notification de manière simple. Grâce à cette classe, nous pouvons créer un contrôleur qui viendra attendre la “promesse” (Promise) en appelant la méthode “nextPricingUpdate()“. L’attente se fait grâce à la méthode await() qui se charge de suspendre et libérer le thread et de le réactiver avec une requête dans le même état qu’avant la suspension.

public class PricingUpdater extends Controller {
	public static void waitUpdate(Long pricingId) {
		PricingUpdateChannel channel = PricingUpdateChannel.get(pricingId);
		// Attente de la Promise : sera suspendu jusqu'à ce que quelqu'un notifie sur l'EventStream
		pricingId = await(channel.nextPricingUpdate());
		// Quand on reprend la main, on envoie les données au client
		Pricing pricing = Pricing.findById(pricingId);
		renderJSON(pricing);
	}
}

Il suffit alors de notifier lors des modifications des données :

class Pricings extends Controller {
	public static void editValue(...) {
		// Modification proprement dite
		...
		// Notification
		updatePricing(pricing);
		// Render habituel
		render(pricing);
	}
	protected static void updatePricing(Pricing pricing) {
		// Notification de la mise à jour
		PricingUpdateChannel.get(pricing.id).updatePricing(pricing.id);
	}
}

C’est tout pour le côté serveur. Le code nécessaire est très léger et plutôt simple à comprendre. Il ne reste plus qu’à s’occuper du client. Pour cet exemple, nous utilisons la technique du Long Polling, mais il serait tout à fait possible d’utiliser un WebSocket à la place.
Le principe est simple, on effectue un appel ajax qui va “attendre” jusqu’à ce qu’une mise à jour des données soit disponible. Une fois que la requête aboutit, on met à jour l’affichage et on relance la requête pour attendre la mise à jour suivante.

var waitUpdates = #{jsAction @PricingUpdater.waitUpdate(pricing.id) /}
// Récupère les nouvelles données
var getUpdate = function() {
    // Appel Ajax qui va "attendre" les nouvelles données
    $.ajax({
        url: waitUpdates(),
        success: function(data) {
            // Mise à jour de l'affichage
            pricingRefresh(data);
            // Petit effet visuel pour montrer à l'utilisateur qu'il s'est passé quelque chose
            $('#pricing').effect('highlight');
            // On relance le requête pour récupérer la prochaine modification
            getUpdate()
        },
        dataType: 'json'
    });
};
// Appel initial
getUpdate();

Démo

Et voilà ce que ça donne côté utilisateur. C’est perfectible mais le fonctionnement de base est là :

httpvh://www.youtube.com/watch?v=_rQ897b8jE0

Perspectives

Play propose une nouvelle API  extrêmement simple pour gérer les appels asynchrones. Tout est pensé pour faciliter au maximum la vie du développeur, en lui évitant de perdre du temps avec les détails d’implémentation. Le code développé est celui d’une action tout ce qu’il y a de plus classique. Les classes et méthodes mises à disposition permettent de conserver un code clair et compréhensible.

Grâce à tous ces éléments, il est possible d’ajouter à une application des fonctionnalités nécessitant des appels asynchrones facilement et sans sacrifier la maintenabilité générale du code.

Développement web , , ,

3 comments


  1. Fredrik Bromee

    Salut Matthieu, thanks for a great write-up of the new Play features! My apologies for writing this comment in english but my high school french is only barely good enough for reading this post…

    I’m wondering about the PricingUpdateChannel.instances HashMap. It’s completely unprotected from concurrent accesses. I assume this is only to make the code in the article easier to read? How would you do this in a more production-like environment, you think? Have the instances be a ConcurrentHashMap? Or replace it with something else?

    Again, thanks for a great article.

    • Matthieu

      Thank you Fredrik for your comment.
      In fact, I wrote this code for a small application that I use as a sandbox. That’s why I didn’t take care of this kind of problems. Thanks for pointing that out !
      In a more robust way, I would probably use a ConcurrentHashMap and replace the “put(…)” with “putIfAbsent(…)”. This seems to be enough to manage concurrent accesses.

Leave a Reply to Fredrik Bromee Cancel reply

Your email address will not be published. Required fields are marked *