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/