All posts in micro-services

LRA Long Running Actions

Principe général

Long Running Actions (LRA) est un module de la stack Eclipse MicroProfile comme peuvent l’être « Health », « Fault Tolerance » ou « GraphQL ». Son objectif est de permettre la coordination des microservices concourant à implémenter une même activité métier. Sur le plan théorique ce pattern est référencé sous le nom de SAGA.

Bien sûr l’idée n’est pas de revenir au mode de fonctionnement centralisé et fortement couplé dont l’architecture microservices essaie de s’éloigner. Il n’est donc pas question d’opérer des transactions SQL distribuées comme on a pu le faire il y a quelques années, chaque microservice reste seul responsable de son système de stockage qui n’est pas partagé.

Le but ici est d’atteindre l’eventual consistency. Attention pour ceux qui ne maîtriseraient pas l’anglais, eventual est un faux amis, il ne s’agit pas de dire que le système sera éventuellement cohérent mais qu’il le sera forcément tôt ou tard.
Ce concept est familier aux utilisateurs des bases de données de nouvelle génération qui l’exploitent pour offrir une bonne montée en charge.

Examinons maintenant plus concrètement comment déployer cela au sein d’une architecture microservices.

Le coordinateur

L’implémentation de Long Running Actions requière la coopération d’un service additionnel à nos microservices métier. Son rôle sera d’orchestrer les transactions, c’est à dire d’informer nos services pour leur demander de compenser (défaire) leurs actions ou au contraire de les valider définitivement.

Ainsi la première étape consiste à lancer ce fameux coordinateur. Nous utiliserons ici l’implémentation issue du projet Narayana, packagée dans un microservice Quarkus:
docker run -d -p 8080:8080 quay.io/jbosstm/lra-coordinator

Pour vérifier qu’il tourne correctement:
curl -i localhost:8080/lra-coordinator
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 2
Narayana-LRA-API-version: 1.1

[]

D’autres alternatives à Narayana existent et il est possible de les utiliser sans impact sur le reste de notre architecture car les interactions s’appuient sur un protocole HTTP REST normalisé.

Sur les microservices

Au niveau du code de nos microservices, les _endpoints_ participant à une transaction métier longue doivent être annotés @LRA, par exemple:

@LRA(value = LRA.Type.REQUIRED, end=false)
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Path("/book")
public Response bookFlight(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId, String destination) {
  if (destination.equals("Toulouse") || destination.equals("Paris")) {
    System.out.println("Succès");
    return Response.ok().build();
  }
  else {
    System.out.println("Echec");
    return Response.serverError().build();
  }
}

L’annotation @LRA présente des similitudes avec une autre annotation largement utilisée : @Transactional. En effet, elles partagent toutes deux des paramètres pour spécifier le périmètre de la transaction, qui se ressemblent beaucoup. Par défaut, l’attribut value est égal à « REQUIRED », ce qui indique que si une transaction est déjà en cours, la méthode sera exécutée dans son contexte. La propagation de l’id de la LRA se fait au travers de l’entête HTTP « Long-Running-Action ». C’est ainsi la présence de cette information dans la requête entrante qui détermine s’il faut créer une LRA ou pas. Dans la réponse HTTP nous retrouverons également cette entête. Elle devra être réutilisée par le client dans les requêtes successives. Pour fixer les idées la valeur de cette entête pourrait être par exemple :

http://localhost:8080/lra-coordinator/0_ffffc0c5100f_f355_6113bebe_22

L’id de la LRA prend la forme de son URL canonique. La création de la LRA se fait sur demande par l’appel de l’API REST du coordinateur depuis notre microservice.
Nous n’avons pas à coder cette interaction, cela se fait automatiquement par la librairie LRA.
Les appels aux microservices suivants, lorsque ceux-ci sont annotés avec @LRA et que l’entête est bien fournie, se voient donc inclus dans la même transaction tant que la propriété end=true n’est pas fixée. Quand ce sera le cas, la transaction LRA sera fermée après l’exécution de la méthode et le coordinateur appellera pour chaque microservice son point de terminaison d’API annoté @Complete.

@Complete
@Path("/complete")
@PUT
public Response completeFlight(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId, String userData) {
  String message = "Flight Booking completed with LRA with id: " + lraId;
  System.out.println(message);
  return Response.ok(ParticipantStatus.Completed.name()).build();
}

Si au contraire, une des méthodes des microservices annotées @LRA avait été en erreur, la LRA dans son ensemble aurait été annulée. Les méthodes callback invoquées auraient alors été celles annotées @Compensate.

@Compensate
@Path("/compensate")
@PUT
public Response compensateFlight(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) String lraId, String userData) {
  String message = "Flight Booking compensated with LRA with id: " + lraId;
  System.out.println(message);
  return Response.ok(ParticipantStatus.Compensated.name()).build();
}

Conclusion

Comme nous venons de le voir, le module Long Running Actions fournit un cadre d’implémentation du pattern SAGA.
Si des points communs avec les transactions JPA (Java Persistence API) peuvent être relevés, le fonctionnement des LRAs est cependant largement plus découplé et repose sur un système de compensation. C’est en effet à nous d’implémenter le rollback en fonction bien sûr de la technologie sous-jacente. Même principe pour la méthode @Complete c’est à nous qu’incombe de coder la logique de « commit« . Ce terme est à considérer avec des guillemets car même si le système de stockage du microservice est construit sur une base traditionnelle, sa transaction SQL locale sera déjà véritablement commitée (il est inenvisageable de conserver des verrous base de données sur plusieurs requêtes HTTP). La plupart du temps la tâche de la méthode @Complete se résume à nettoyer les ressources ou informations conservées dans l’optique de pouvoir compenser la LRA si finalement elle devait être annulée.

Ainsi l’essentiel du framework consiste à mettre en œuvre une série de notifications pour informer les microservices des résultats des opérations des uns des autres. Cette synchronisation se fait avec un décalage qui peut être plus ou moins long, certains microservices pouvant échouer à compenser, mais le coordinateur insistera et les notifiera à nouveau. L’objectif final n’étant pas de garantir une consistence instantanée des données mais juste de permettre d’y arriver à un moment donnée.

Pour plus de détails encore dans le protocole Long Running Actions, voici sa javadoc, bien sûr n’hésitez pas à expérimenter avec Quarkus par exemple:
https://download.eclipse.org/microprofile/microprofile-lra-2.0/apidocs/

Les micro services peuvent-ils remplacer les serveurs d’applications ?

Un concept en fin de vie ?

Durant ces dix dernières années, particulièrement dans l’écosystème Java, le serveur d’applications fut le roi. Tout bout de code « server side » finissait fatalement déployé sur cet élément hautement important du système d’information. Comme nous l’enseignons nous-mêmes en formation son rôle est de fournir les services techniques aux applications (connexions aux bases de données, gestion du cycle de vie des composants, module d’authentification, supervision des transactions…) déchargeant ainsi les développeurs de l’implémentation de cette tuyauterie ; tâche critique et souvent difficile.

Cette séparation des responsabilités est au cœur de la philosophie Java EE, la spécification de la plateforme distingue en permanence les activités incombant aux développeurs d’applications de celles prises en charge par les administrateurs gérant l’infrastructure ou encore justement les fonctionnalités devant être assurées par le serveur d’applications.

Si on peut comprendre cette logique, pensée pour les environnements complexes des grandes organisations où le SI est géré de manière centralisée, cette approche souffre néanmoins d’une rigidité et d’une lourdeur certaine, chaque application nécessite avant d’être déployée que l’on paramètre son environnement technique sur son conteneur.

Les micro services

Les micro services s’inscrivent dans une démarche opposée : la notion de serveur d’applications (conteneur) gouvernant l’exécution des composants d’application déployés en son sein n’existe plus. Chaque micro service est une application « standalone » tournant dans son propre processus. Ces applications communiquent entre elles au travers de web services de type REST sans l’intermédiation d’un middleware devenu encombrant, à la manière des commandes Unix qui s’agrègent avec un simple pipe. Leur périmètre est généralement réduit, elles font une chose et s’attachent à bien le faire, ainsi on pourra les réécrire si nécessaire complètement en limitant les coûts.

REST a montré qu’il était néfaste de chercher à s’abstraire coûte que coûte du protocole HTTP, les micro services nous enseignent qu’il n’est sans doute pas optimal de chercher à effacer le système d’exploitation comme les serveurs d’applications Java se sont évertués à le faire, en voulant recréer un OS dans l’OS.

Les frameworks

Qui dit nouveaux paradigmes dit nouveaux frameworks. Toujours dans l’univers Java, il y a bien sûr le presque déjà vieux Playframework qui adopte une stratégie résolument de rupture en proposant une pile logicielle avant-gardiste et encourageant fortement l’usage du langage Scala, même si Java est également supporté. Un autre framework, bien que moins audacieux, mérite tout autant que l’on s’y intéresse, il s’agit de Dropwizard. Ce framework possède l’avantage de capitaliser sur des technologies matures, bien connues des développeurs Java EE comme Jersey, Jackson, Jetty ou encore Hibernate validator. La courbe d’apprentissage sera donc douce.

Allez bonne année et RDV en 2013 pour mettre tout ceci en pratique !