J’ai eu à intervenir récemment sur un programme écrit en Play Framework v2.3, dont le rôle est assez simple : faire passe-plat entre un client et un serveur et effectuant notamment des transformations protocolaires (comme REST vers TCP par exemple). Ce qui m’a donné l’occasion d’utiliser les RedeemablePromise de Play Framework, pas du tout documentées à ce jour.
Client asynchrone
Les différents échanges de messages entre les systèmes peuvent être représentés à l’aide du diagramme de séquence suivant :
Le programme en Play Framework est nommé “PassePlat” dans ce diagramme.
Les messages montants sont implémentés sous forme d’une API RESTful ; tandis que le retour du “Serveur”, asynchrone, est implémentée avec des callbacks et une requête REST vers le client d’origine. Voici un exemple de pseudo-code Java sur le traitement du message de retour :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Le système complet fonctionne bien, mais n’est pas très satisfaisant :
le Client et le PassePlat sont très couplés. En effet, le Client doit connaître l’adresse du PassePlat pour lui envoyer les messages montants et le PassePlat doit connaitre l’adresse du Client pour lui renvoyer le message de retour. Bref, une dépendance cyclique ;
Le fonctionnement du Serveur est asynchrone (messages montants et de retours sont décorrélés) et cette implémentation a déporté l’asynchronisme jusqu’au Client, alors que le client (l’humain cette fois-ci :-) voulait plutôt un fonctionnement synchrone du Client, ce qui était plus facile à appréhender pour lui.
Client synchrone
Nous avons donc travaillés sur une implémentation du système plutôt comme ceci :
L’appel du Client vers le PassePlat est donc bloqué tant que l’acquittement et la réponse du Serveur ne sont pas parvenus au PassePlat.
Comment ?
On peut faire cela très simplement avec les RedeemablePromise de Play Framework.
Les RedeemablePromise sont une implémentation du design pattern promise, désormais répandu dans l’informatique pour résoudre le callback hell -l’enfer des callbacks-, problème très fréquent avec la programmation asynchrone. En effet, votre code est exécuté au sein de callbacks en réponse à des évènements : fin de traitement, lecture d’un fichier, arrivée d’un message par Web Socket, …
On trouve des implémentations de ce pattern naturellement en Javascript (plusieurs même), mais aussi en Scala, en Java, …
Voyons comment nous pouvons les utiliser pour répondre à notre nouveau besoin.
Adaptons la classe qui réceptionne les évènements venant du Serveur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
L’élément clé ici est le fait de “résoudre” ou “compléter” la promesse en succès ou en erreur selon le cas. Attention à n’oublier aucun cas de fin de traitement du Serveur, sinon le Client restera bloqué dans ces cas non prévus.
Jetons maintenant un coup d’oeil au contrôleur REST sur le PassePlat qui reçoit les requêtes HTTP depuis le Client et qui “bloque” tant que la réponse du Serveur n’est pas parvenue :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Dans le cas nominal, la Response
sera convertie en Result
au sein de la méthode map
(programmation fonctionnelle).
Et dans le cas où la promesse a été résolue en erreur ?
Par défaut, Play va générer une réponse HTTP avec un code retour 500 et une sérialisation de l’exception renvoyée. Si vous souhaitez définir vous-même votre propre retour, il faut que le traitement de l’erreur génère un Result
standard.
Voici comment adapter la transformation de la promesse pour renvoyer une erreur 500 et une sérialisation de l’exception en cas de promesse résolue en erreur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Au final, nous avons pu rendre l’appel du Client synchrone en à peine quelques lignes de code grâce à l’API riche de Play Framework. Enfin, vous remarquerez que Java 8 améliore significativement la lisibilité du code. Cependant, on reste loin de Scala, de Groovy ou tout simplement de Javascript.