Introduction aux acteurs Akka en Java

1. Introduction

Akka est une bibliothèque open source qui permet de développer facilement des applications simultanées et distribuées à l' aide de Java ou Scala en exploitant le modèle d'acteur.

Dans ce didacticiel, nous présenterons les fonctionnalités de base telles que la définition des acteurs, la manière dont ils communiquent et comment nous pouvons les tuer . Dans les notes finales, nous noterons également quelques bonnes pratiques lors de l'utilisation d'Akka.

2. Le modèle de l'acteur

Le modèle d'acteur n'est pas nouveau dans la communauté informatique. Il a été introduit pour la première fois par Carl Eddie Hewitt en 1973, en tant que modèle théorique pour gérer le calcul simultané.

Il a commencé à montrer son applicabilité pratique lorsque l'industrie du logiciel a commencé à réaliser les pièges de la mise en œuvre d'applications concurrentes et distribuées.

Un acteur représente une unité de calcul indépendante. Certaines caractéristiques importantes sont:

  • un acteur encapsule son état et une partie de la logique applicative
  • les acteurs interagissent uniquement via des messages asynchrones et jamais via des appels de méthode directs
  • chaque acteur a une adresse unique et une boîte aux lettres dans laquelle d'autres acteurs peuvent délivrer des messages
  • l'acteur traitera tous les messages de la boîte aux lettres dans un ordre séquentiel (l'implémentation par défaut de la boîte aux lettres étant une file d'attente FIFO)
  • le système d'acteurs est organisé selon une hiérarchie arborescente
  • un acteur peut créer d'autres acteurs, peut envoyer des messages à n'importe quel autre acteur et s'arrêter ou tout acteur est créé

2.1. Avantages

Le développement d'applications simultanées est difficile car nous devons gérer la synchronisation, les verrous et la mémoire partagée. En utilisant les acteurs Akka, nous pouvons facilement écrire du code asynchrone sans avoir besoin de verrous et de synchronisation.

L'un des avantages de l'utilisation de message au lieu d'appels de méthode est que le thread expéditeur ne bloquera pas l'attente d'une valeur de retour lorsqu'il envoie un message à un autre acteur . L'acteur récepteur répondra avec le résultat en envoyant un message de réponse à l'expéditeur.

Un autre grand avantage de l'utilisation des messages est que nous n'avons pas à nous soucier de la synchronisation dans un environnement multi-thread. Ceci est dû au fait que tous les messages sont traités séquentiellement .

Un autre avantage du modèle d'acteur Akka est la gestion des erreurs. En organisant les acteurs dans une hiérarchie, chaque acteur peut notifier son parent de l'échec, afin qu'il puisse agir en conséquence. L'acteur parent peut décider d'arrêter ou de redémarrer les acteurs enfants.

3. Configuration

Pour tirer parti des acteurs Akka, nous devons ajouter la dépendance suivante de Maven Central:

 com.typesafe.akka akka-actor_2.12 2.5.11  

4. Créer un acteur

Comme mentionné, les acteurs sont définis dans un système hiérarchique. Tous les acteurs partageant une configuration commune seront définis par un ActorSystem.

Pour l'instant, nous allons simplement définir un ActorSystem avec la configuration par défaut et un nom personnalisé:

ActorSystem system = ActorSystem.create("test-system"); 

Même si nous n'avons pas encore créé d'acteurs, le système contiendra déjà 3 acteurs principaux:

  • l'acteur gardien racine ayant l'adresse «/» qui, comme le nom l'indique, représente la racine de la hiérarchie du système d'acteurs
  • l'utilisateur tuteur acteur ayant l'adresse «/ utilisateur». Ce sera le parent de tous les acteurs que nous définissons
  • l'acteur gardien du système ayant l'adresse «/ system». Ce sera le parent de tous les acteurs définis en interne par le système Akka

Tout acteur Akka étendra la classe abstraite AbstractActor et implémentera la méthode createReceive () pour gérer les messages entrants d'autres acteurs:

public class MyActor extends AbstractActor { public Receive createReceive() { return receiveBuilder().build(); } }

C'est l'acteur le plus basique que nous puissions créer. Il peut recevoir des messages d'autres acteurs et les rejetter car aucun modèle de message correspondant n'est défini dans ReceiveBuilder. Nous parlerons de la correspondance des modèles de message plus loin dans cet article.

Maintenant que nous avons créé notre premier acteur, nous devons l'inclure dans ActorSystem :

ActorRef readingActorRef = system.actorOf(Props.create(MyActor.class), "my-actor");

4.1. Configuration des acteurs

La classe Props contient la configuration de l'acteur. Nous pouvons configurer des éléments tels que le répartiteur, la boîte aux lettres ou la configuration de déploiement. Cette classe est immuable, donc thread-safe, elle peut donc être partagée lors de la création de nouveaux acteurs.

Il est fortement recommandé et considéré comme une meilleure pratique de définir les méthodes de fabrique à l'intérieur de l'objet acteur qui gèreront la création de l' objet Props .

Pour illustrer, définissons un acteur qui effectuera un traitement de texte. L'acteur recevra un objet String sur lequel il effectuera le traitement:

public class ReadingActor extends AbstractActor { private String text; public static Props props(String text) { return Props.create(ReadingActor.class, text); } // ... }

Maintenant, pour créer une instance de ce type d'acteur, nous utilisons simplement la méthode factory props () pour passer l' argument String au constructeur:

ActorRef readingActorRef = system.actorOf( ReadingActor.props(TEXT), "readingActor");

Maintenant que nous savons comment définir un acteur, voyons comment ils communiquent à l'intérieur du système d'acteurs.

5. Messagerie d'acteur

Pour interagir les uns avec les autres, les acteurs peuvent envoyer et recevoir des messages de tout autre acteur du système. Ces messages peuvent être n'importe quel type d'objet à condition qu'il soit immuable .

Il est recommandé de définir les messages à l'intérieur de la classe d'acteur. Cela permet d'écrire du code facile à comprendre et de savoir quels messages un acteur peut gérer.

5.1. Envoi de messages

À l'intérieur du système d'acteur Akka, les messages sont envoyés à l'aide de méthodes:

  • dire()
  • demander()
  • vers l'avant()

Lorsque nous voulons envoyer un message et que nous n'attendons pas de réponse, nous pouvons utiliser la méthode tell () . C'est la méthode la plus efficace du point de vue des performances:

readingActorRef.tell(new ReadingActor.ReadLines(), ActorRef.noSender()); 

Le premier paramètre représente le message que nous envoyons à l'adresse de l'acteur readingActorRef .

The second parameter specifies who the sender is. This is useful when the actor receiving the message needs to send a response to an actor other than the sender (for example the parent of the sending actor).

Usually, we can set the second parameter to null or ActorRef.noSender(), because we don't expect a reply. When we need a response back from an actor, we can use the ask() method:

CompletableFuture future = ask(wordCounterActorRef, new WordCounterActor.CountWords(line), 1000).toCompletableFuture();

When asking for a response from an actor a CompletionStage object is returned, so the processing remains non-blocking.

A very important fact that we must pay attention to is error handling insider the actor which will respond. To return a Future object that will contain the exception we must send a Status.Failure message to the sender actor.

This is not done automatically when an actor throws an exception while processing a message and the ask() call will timeout and no reference to the exception will be seen in the logs:

@Override public Receive createReceive() { return receiveBuilder() .match(CountWords.class, r -> { try { int numberOfWords = countWordsFromLine(r.line); getSender().tell(numberOfWords, getSelf()); } catch (Exception ex) { getSender().tell( new akka.actor.Status.Failure(ex), getSelf()); throw ex; } }).build(); }

We also have the forward() method which is similar to tell(). The difference is that the original sender of the message is kept when sending the message, so the actor forwarding the message only acts as an intermediary actor:

printerActorRef.forward( new PrinterActor.PrintFinalResult(totalNumberOfWords), getContext());

5.2. Receiving Messages

Each actor will implement the createReceive() method, which handles all incoming messages. The receiveBuilder() acts like a switch statement, trying to match the received message to the type of messages defined:

public Receive createReceive() { return receiveBuilder().matchEquals("printit", p -> { System.out.println("The address of this actor is: " + getSelf()); }).build(); }

When received, a message is put into a FIFO queue, so the messages are handled sequentially.

6. Killing an Actor

When we finished using an actor we can stop it by calling the stop() method from the ActorRefFactory interface:

system.stop(myActorRef);

We can use this method to terminate any child actor or the actor itself. It's important to note stopping is done asynchronously and that the current message processing will finish before the actor is terminated. No more incoming messages will be accepted in the actor mailbox.

By stopping a parent actor, we'll also send a kill signal to all of the child actors that were spawned by it.

When we don't need the actor system anymore, we can terminate it to free up all the resources and prevent any memory leaks:

Future terminateResponse = system.terminate();

This will stop the system guardian actors, hence all the actors defined in this Akka system.

We could also send a PoisonPill message to any actor that we want to kill:

myActorRef.tell(PoisonPill.getInstance(), ActorRef.noSender());

The PoisonPill message will be received by the actor like any other message and put into the queue. The actor will process all the messages until it gets to the PoisonPill one. Only then the actor will begin the termination process.

Another special message used for killing an actor is the Kill message. Unlike the PoisonPill, the actor will throw an ActorKilledException when processing this message:

myActorRef.tell(Kill.getInstance(), ActorRef.noSender());

7. Conclusion

In this article, we presented the basics of the Akka framework. We showed how to define actors, how they communicate with each other and how to terminate them.

We'll conclude with some best practices when working with Akka:

  • utilisez tell () au lieu de ask () lorsque les performances sont un problème
  • lors de l'utilisation de ask (), nous devons toujours gérer les exceptions en envoyant un message d' échec
  • les acteurs ne doivent partager aucun état mutable
  • un acteur ne doit pas être déclaré au sein d'un autre acteur
  • les acteurs ne sont pas arrêtés automatiquement lorsqu'ils ne sont plus référencés. Nous devons détruire explicitement un acteur lorsque nous n'en avons plus besoin pour éviter les fuites de mémoire
  • les messages utilisés par les acteurs doivent toujours être immuables

Comme toujours, le code source de l'article est disponible à l'adresse over sur GitHub.