JAVACT est un intergiciel (ou middleware) Java pour la programmation d'applications concurrentes, réparties et mobiles [AMM01], développé par l'équipe IAM (Ingénierie des Applications Mobiles1) de l'IRIT (Institut de Recherche en Informatique de Toulouse, France) et distribué sous forme de logiciel libre sous licence LGPL.
JAVACT fournit des
mécanismes pour la programmation d'agents mobiles adaptables qui
s'appuient sur les concepts d'acteur et d'implémentation ouverte2. JAVACT étend le paquetage java.rmi (RMI : Remote Method Invocation3)
dont il utilise les mécanismes de passage d'objets en argument et
d'exception, et l'adapte aux communications asynchrones au moyen d'un
mécanisme de type send-receive. Outre l'abstraction par rapport à java.rmi, JAVACT permet au programmeur de s'abstraire de la gestion de mécanismes de bas niveau tel les threads, leur ordonnancement et la synchronisation.
JAVACT est une bibliothèque Java standard : son utilisation ne demande pas de traitement particulier au niveau du code source ou du bytecode, ni de modification de la machine virtuelle. Dans sa version actuelle, JAVACT repose sur le Software Development Kit 1.44 (le JRE 1.3 suffit pour l'exécution).
Ce document explique comment développer une application au moyen de JAVACT (version 0.5.0) et décrit :
Les réseaux à grande échelle, tel l'Internet, et les grilles de calcul ou de stockage donnent accès à des quantités de données, de services, de ressources logicielles et matérielles répartis. Dans un tel contexte, qui touche également l'informatique ominiprésente (ubiquitous computing) et l'informatique nomade, le développement et le déploiement d'applications demande des méthodes et des outils adaptés permettant de traiter les problèmes qui résultent de l'hétérogénéité (des données, du logiciel, du matériel), de la répartition des ressources et des utilisateurs, des volumes de données déplacés sur le réseau, de la concurrence, de la sécurité, etc. En particulier, les applications doivent faire face à de fortes variations des conditions d'exécution (capacités physiques des réseaux et des machines, disponibilité des composants) et il n'est pas possible de faire d'hypothèse sur la qualité des services requis ou rendus. Ainsi, les applications doivent être adaptables, si possible de manière dynamique. Dans ce cadre, les concepts d'agents, de mobilité, et d'implémentation ouverte semblent du plus grand intérêt.
Les intergiciels « classiques » permettent aux
développeurs de s'abstraire de certains aspects techniques, parfois
complexes, liés à la répartition et aux opérations distantes.
Cependant, dans le contexte du calcul réparti à grande échelle et de
l'informatique mobile, il est nécessaire, lors du développement, de
prendre en compte la qualité des services et de s'y adapter. Afin de
limiter la complexité du développement et de la maintenance et de
faciliter la construction d'applications robustes capables de s'adapter
dynamiquement aux conditions d'exécution, nous proposons JAVACT, un intergiciel de haut niveau à base d'agents mobiles adaptables.
La programmation par agents mobiles est un paradigme de programmation
des applications réparties, susceptible de compléter ou de se
substituer à d'autres paradigmes plus classiques tel le passage de
messages, l'appel de procédure à distance, l'invocation d'objet à
distance, l'évaluation à distance [FPV98,BI02]. Elle est d'un grand intérêt pour la mise en uvre
d'applications dont les performances varient en fonction de la
disponibilité et de la qualité des services et des ressources, ainsi
que du volume des données déplacées. Le concept d'agent mobile facilite
en effet la mise en
uvre d'applications dynamiquement adaptables, et il offre un cadre générique [CHK94]
pour le développement des applications réparties sur des réseaux de
grande taille qui recouvrent des domaines administratifs multiples.
Un agent logiciel est une entité autonome capable de communiquer, disposant d'une connaissance partielle de ce qui l'entoure et d'un comportement privés, ainsi que d'une capacité d'exécution propre. Un agent agit pour le compte d'un tiers (un autre agent, un utilisateur) qu'il représente sans être obligatoirement connecté à celui-ci, réagit et interagit avec d'autres agents.
Un agent mobile peut se déplacer d'un site à un autre en cours d'exécution pour accéder à des données ou à des ressources. Il se déplace avec son code et ses données propres, mais aussi avec son état d'exécution. L'agent décide lui-même de manière autonome de ses mouvements. Ainsi, la mobilité est contrôlée par l'application elle-même, et non par le système d'exécution comme dans le cas de la migration de processus dans les systèmes opératoires.
En pratique, la mobilité d'agent permet de rapprocher client et serveur et en conséquence de réduire le nombre et le volume des interactions distantes (en les remplaçant par des interactions locales), de spécialiser des serveurs distants ou de déporter la charge de calcul d'un site à un autre. Une application construite à base d'agents mobiles peut se redéployer dynamiquement suivant un plan pré-établi ou en réaction à une situation particulière, afin par exemple d'améliorer la performance ou de satisfaire la tolérance aux pannes, de réduire le trafic sur le réseau, ou de suivre un composant matériel mobile. La mobilité du code offre un premier niveau de flexibilité aux applications. La décentralisation de la connaissance et du contrôle à travers les agents, et la proximité physique entre les agents et les ressources du système renforce la réactivité et les capacités d'adaptation.
La mobilité ne se substitue pas aux capacités de
communication des agents (la communication distante reste possible)
mais les complète ; afin de satisfaire aux contraintes des réseaux de
grande taille ou sans fil (latence, non permanence des liens de
communication), les agents communiquent par messages asynchrones.
Le déplacement d'une unité en cours d'exécution d'une machine à une autre se heurte aux problèmes d'hétérogénéité des matériels et des logiciels. Les intergiciels destinés au développement d'applications réparties à grande échelle doivent permettre de s'en abstraire. Par ailleurs, ils doivent fournir les mécanismes permettant le déplacement des agents et la communication entre agents. En particulier, la communication doit (a priori) être insensible aux déplacements des agents : les messages à destination ou provenant d'un agent doivent être acheminés indépendamment des mouvements de cet agent.
La capacité de capture et de restauration de l'état d'exécution d'un agent, appelée mobilité forte, est un point critique. La mobilité faible représente la capacité pour un système de déplacer le code des agents accompagné seulement de données d'initialisation (et non de l'état complet). En fait, c'est essentiellement un problème d'abstraction et d'expressivité, lié à la manipulation des processus qui exécutent les agents, qui est posé : dans les systèmes à mobilité faible, c'est au programmeur de gérer lui-même le mécanisme de reprise. Cette propriété est liée (et parfois confondue) avec la capacité pour un agent de se déplacer à n'importe quel point de son exécution (mobilité anytime).
Un frein à l'utilisation réelle de cette technologie réside cependant dans les problèmes de sécurité qu'elle introduit. La mise en place d'une politique de sécurité peut demander, d'une part la protection des ressources et des données des machines hôtes (en limitant les droits d'accès et la consommation des ressources), et d'autre part, la préservation de l'intégrité et de la confidentialité des agents eux-mêmes et de leurs communications.
La large diffusion de l'environnement Java (et l'utilisation courante des applets)
a contribué à l'intérêt porté aux agents mobiles, de par les nombreux
avantages offerts (machine virtuelle, sérialisation, invocation de
méthode à distance, gestionnaires de sécurité, ...). Java ne permet
cependant pas la capture de l'état d'exécution des threads, et
par conséquent, l'état dans lequel le calcul sera repris à distance
doit être explicitement programmé, ce qui réduit l'expressivité : cette
propriété est appelée mobilité faible (il existe des solutions Java
non-standard basées sur la modification de la machine virtuelle, ou un
pré-traitement code source, ou une modification du bytecode).
Les acteurs [Agh86] sont des entités anthropomorphes qui communiquent par messages. Au sein d'une communauté, un acteur est uniquement connu par sa référence. La communication est point à point, asynchrone, unilatérale et supposée sûre. Les messages reçus sont stockés dans une boîte aux lettres et traités en série (en exclusion mutuelle). Le comportement décrit la réaction de l'acteur à un message. Le comportement est privé : il contient les données et les fonctions de l'acteur, et constitue son état. L'acteur encapsule données et fonctions mais également une activité (fil d'exécution) unique qui les manipule. Un comportement d'acteur n'est donc, à un instant donné, traversé que par un fil d'exécution au plus (la synchronisation est implicite).
Lors du traitement d'un message, un acteur peut créer (dynamiquement) de nouveaux acteurs, envoyer des messages aux acteurs qu'il connaît, et changer de comportement c'est-à-dire définir son comportement pour le traitement du prochain message. Le changement de comportement est un mécanisme puissant d'évolution et d'adaptation individuelles. Il permet à un acteur de modifier dynamiquement son interface ; mais, en conséquence, il n'est pas possible de garantir dans tous les cas qu'un message envoyé pourra effectivement être traité par son destinataire. Cependant, pour le programmeur, le changement de comportement offre une alternative à la gestion de variables d'état et à l'utilisation de gardes, qui accroît l'expressivité. Programmer un acteur revient donc à programmer ses comportements, l'enchaînement des comportements étant décrit dans les comportements eux-mêmes.
La capacité de changement de comportement introduit une difficulté particulière dans le contrôle des envois de messages. En effet, il est délicat de déterminer statiquement si un message envoyé à un acteur pourra effectivement être traité. C'est le problème des messages orphelins [DP+00] :
Si on lui envoie deux fois le message , l'un des deux messages sera orphelin de sûreté. Si on lui envoie seulement le message
, alors celui-ci sera orphelin de vivacité ; il ne pourra être traité tant qu'aucun message
n'aura été envoyé à l'acteur.
L'idée de l'utilisation des acteurs pour la programmation d'applications concurrentes et réparties n'est pas neuve. Néanmoins, elle semble particulièrement pertinente dans le cadre du calcul réparti à grande échelle de par les propriétés qui distinguent les acteurs du modèle d'objet classique (encapsulation de l'activité, changement d'interface, communications asynchrones). D'une part, la communication par messages asynchrones est bien adaptée aux réseaux de grande taille, voire sans fil, où les communications synchrones sont trop difficiles et coûteuses à établir. D'autre part, l'autonomie d'exécution et l'autonomie comportementale favorisent l'intégration de mécanismes de mobilité et garantissent un certain niveau d'intégrité.
La mobilité d'acteur peut être naturellement définie en se
basant sur le traitement des messages en série et le changement de
comportement, ainsi que sur la création dynamique et l'envoi de
messages. Lors du changement de comportement (donc, entre deux
messages), l'acteur se réduit d'une part au contenu de sa boîte aux
lettres, et d'autre part au comportement défini pour le traitement du
prochain message. Par nature, le comportement est une entité de
première classe qui matérialise l'état et qui est manipulable par
programme. Il est alors possible de créer à distance un autre acteur
défini à partir du comportement. On obtient ainsi simplement une
évolution de l'acteur d'origine à qui on peut faire suivre tous les
messages qui étaient en attente. L'état est donc entièrement transporté
(via le comportement) et restauré sans que le programmeur n'ait à
développer de code spécifique pour cela. Ainsi, après migration,
l'acteur peut poursuivre son activité à distance en bénéficiant des
acquis résultant des traitements de messages précédents. La mobilité
est ainsi différée au moment où l'acteur change de comportement.
La communication entre acteurs étant supposée sûre, la
mobilité pose le problème de l'acheminement des messages. Pour cela, on
définit le protocole suivant (d'autres sont possibles) : l'acteur
d'origine se transforme lors du déplacement en un proxy
du nouvel acteur (distant) ; il ne traite plus les messages qu'il
reçoit mais les transmet. Ainsi, l'acteur reste connu à son adresse
d'origine qui demeure valide, et l'envoi de message reste possible à
tout instant y compris pendant que l'acteur se déplace (les messages
sont stockés au niveau de l'acteur d'origine, puis transmis quand le
clone distant est opérationnel). D'autre part, les communications
sortantes (émissions de messages) restent valides (pour cela, les
références d'acteur doivent être visibles à travers le réseau). Le
mécanisme de mobilité est donc transparent pour le système de
communication (il reste cependant les problèmes de liaison avec les
ressources et les fichiers ouverts). La multiplication des déplacements
d'un acteur a alors pour effet de construire une chaîne de liens de
poursuite à travers laquelle l'acteur mobile peut être localisé et les
messages acheminés (en pratique, on peut optimiser ce mécanisme).
Tout acteur est donc potentiellement mobile, et, ainsi
définie, la mobilité n'altère pas la sémantique des programmes. On peut
noter que les problèmes de cohérence de copies et de synchronisation
que l'on rencontre quand on introduit la mobilité dans le modèle objet
ne se posent pas ici du fait de l'unicité du fil d'exécution et de
l'exclusion mutuelle sur le traitement des messages.
JAVACT est un logiciel développé en Java, il est donc nécessaire d'avoir un environnement Java correctement installé5. La bibliothèque JAVACT peut être installée et les machines virtuelles JAVACT exécutées avec le JRE 1.3 minimum. Pour compiler des application JAVACT le JSDK 1.4 minimum est nécessaire.
Il faut également avoir récupéré le programme d'installation et l'archive de la bibliothèque : installJavAct.class et JavActv050.jar. Ces fichiers sont disponibles sur le site de JAVACT 6.
java installJavAct <archive javact.jar> <répertoire de destination>où <archive javact.jar> est le chemin complet de l'archive de la bibliothèque (JavActv050.jar),
java installJavAct JavActv050.jar . Installing javact from 'JavActv5.jar' to '/home/leriche/JavActv050' [...] Generating scripts... (platform=Linux) javact is now correctly installed and configured. To run, build, compile applications for javact, you can use the scripts in '/home/leriche/JavActv050/bin' [...]
Lors de cette étape, la bibliothèque est installée dans le répertoire JavActv050. Des scripts de lancement (cf. section suivante) sont générés en fonction de la plate-forme dans le sous-répertoire bin.
JavActv050 bin # contient les scripts d'exécution pour : build # recompiler la bibliothèque javact # lancer une application javactc # compiler une application javactgen # générer les messages de l'application javactvm # lancer un système d'accueil installJavAct.java # installer la bibliothèque sur un autre site doc # documentation 'javadoc' de la bibliothèque [...] examples # exemples d'applications net # fonctionnement en mode réparti [...] local # fonctionnement en local [...] javact # code source de la bibliothèque compiler # générateur de messages lang # code principal local # code spécifique au fonctionnement local net # code spécifique au fonctionnement réparti util # classes utiles, complémentaire de javact.lang awfullPolicy # exemple de politique de sécurité javact.jar # code compilé de la bibliothèque LGPL.TXT # licence LGPL de la bibliothèque places.txt # exemple de fichier de places
Le lancement se fait grâce au script javactvm (dans le sous-répertoire bin), qui s'utilise de la manière suivante :
javactvm [chemin d'accès au fichier places.txt] [port]où [chemin d'accès] est facultatif. S'il est précisé, il doit être impérativement sous forme absolue, sinon le fichier places.txt dans le répertoire courant sera utilisé,
javactvm /home/leriche/JavActv050/places.txt 2010 Starting javact : 2010 javact v0.5.0 [chambord:2010] - 5:25:13 PM
S'il existe un système de fichiers partagés (type nfs), on utilisera par exemple :
-Djava.rmi.server.codebase= "file:/home/leriche/JavActv050/examples/net/rmi/HelloWorld/"(attention au '/' final !), on peut aussi utiliser un serveur web externe, par exemple :
-Djava.rmi.server.codebase="http://noman.zerezo.com/JavAct/HelloWorld/"
/home/leriche/JavActv050/bin/javact -Djava.rmi.server.codebase= "file:/home/leriche/JavActv050/examples/net/rmi/HelloWorld/" HelloWorld caraibe chambord chambord:1100
~/examples>/home/leriche/JavActv050/bin/javact -Djava.rmi.server.codebase="file:/home/leriche/JavActv050/" examples.net.rmi.helloWorld.HelloWorld caraibe chambord chambord:1100
Compiling javact ... Compiling Creator for RMI ... Compiling ReceiveCt for RMI ... Compiling ClientSideAnswerBackImpl for RMI ... Building javact.jar... [...] Done !
Nous allons détailler le processus de développement dans un 1erexemple ; puis, les exemples suivants serviront chacun à expliquer la mise en uvre de certaines caractéristiques de JAVACT.
Nous utilisons trois acteurs qui s'exécutent sur trois places. Le premier se chargera d'imprimer « Hello », puis activera le second qui affichera alors « Wonderful » et activera le troisième, qui lui affichera « World ».
La fig.1 présente l'articulation entre les diverses interfaces à définir. Chaque comportement doit être spécifié par une interface appropriée qui doit hériter de BehaviorProfile, et déclarer:
import javact.util.BehaviorProfile; import javact.util.ActorProfile; import javact.lang.Actor; interface Hello extends BehaviorProfile, ActorProfile { /* Paramètres : les acteurs chargés d'afficher "Wonderful" et "World" */ public void print(Actor wonderful, Actor world); } import javact.util.BehaviorProfile; import javact.util.ActorProfile; import javact.lang.Actor; interface Wonderful extends BehaviorProfile, ActorProfile { /* Paramètre : l'acteur chargé d'afficher "World" */ public void print(Actor world); } import javact.util.BehaviorProfile; import javact.util.ActorProfile; interface World extends BehaviorProfile, ActorProfile { public void print(); }
En fait, on peut spécifier deux sortes de méthodes auxquelles correspondent deux sortes de messages :
Les messages avec retour sont des messages spéciaux, qui restent asynchrones et non bloquants pour l'émetteur mais qui lui permettent d'obtenir une réponse. Quand il en a besoin, l'émetteur doit la demander explicitement en faisant un appel à la méthode getReply() de l'objet message ; alors, il se bloque jusqu'à son obtention. Auparavant il aura pu effectuer d'autres actions.
Pour les messages asynchrones classiques, le préfixe JAM est ajouté devant le nom du message pour obtenir le nom du fichier java correspondant. Pour les messages avec retour, on ajoute le préfixe JSM ainsi que le nom du type de retour, pour distinguer les messages de même nom mais renvoyant des valeurs de types différents12, et la méthode getReply() est engendrée avec le « bon » type de retour.
Pour effectuer la génération automatique, il suffit d'exécuter le script javactgen (dans le sous-répertoire bin) qui se chargera d'extraire toutes les informations nécessaires à la génération. Cela donnera un résultat semblable au suivant:
$ javactgen *.java Found the following actor profile(s): Hello, Wonderful, World. Compiling files defining actor profile interfaces... Extracting messages... Handling the actor profile Hello... This is also a behavior profile. Handling the actor profile Wonderful... This is also a behavior profile. Handling the actor profile World... This is also a behavior profile. Generating QuasiBehaviors... Generating messages... Compiling generated files...
Pour Hello World les fichiers générés seront:
import javact.lang.Actor; class HelloBeh extends HelloQuasiBehavior { public void print(Actor wonderful, Actor world) { System.out.println("<" + myPlace() + "> -- Hello ..."); send(new JAMprint(world), wonderful); suicide(); } }Lorsqu'il traite le message print, l'acteur affiche « Hello » puis transmet le message aux acteurs suivants. Enfin il se suicide puisqu'il a terminé sa tâche.
Nous obtenons finalement la structure présentée en fig.2.
import javact.lang.Actor; import javact.net.rmi.CreateCt; import javact.net.rmi.SendCt; public class HelloWorld { public static void main(String[] args) { if (args.length == 3) { Actor hello = CreateCt.STD.create(args[0], new HelloBeh()); Actor wonderful = CreateCt.STD.create(args[1], new WonderfulBeh()); Actor world = CreateCt.STD.create(args[2], new WorldBeh()); SendCt.STD.send(new JAMprint(wonderful, world), hello); } else { System.out.println("Running on randomly determinated places"); Actor hello = CreateCt.STD.create(new HelloBeh()); Actor wonderful = CreateCt.STD.create(new WonderfulBeh()); Actor world = CreateCt.STD.create(new WorldBeh()); SendCt.STD.send(new JAMprint(wonderful, world), hello); } } }Les trois acteurs sont créés sur trois places dont les noms (String) sont passés en ligne de commande (ou, par défaut, choisies aléatoirement au sein du domaine défini dans le fichier places.txt), puis le premier est activé en lui envoyant un message « print ». C'est ce message qui permet d'amorcer la communication.
La compilation des classes restantes se fait avec le script javactc
(sous-répertoire bin). Il s'utilise exactement comme le
compilateur javac et doit recevoir en paramètre le fichier contenant
la déclaration « public static void main... ».
Pour l'exécution de cet exemple ou pourra se reporter à la section précédente.
Nous allons ici illustrer le changement de comportement avec une cellule alternativement vide et pleine14. Les profils de comportement sont les suivants:
interface Full extends BehaviorProfile { public void get(Actor a); public void become(Empty bp); } interface Empty extends BehaviorProfile { public void put(int v); public void become(Full bp); }
L'acteur correspondant est décrit par le profil suivant :
interface Cell extends Empty, Full, ActorProfile { }
Pour illustrer le fonctionnement de la cellule nous définissons un client qui récupèrera son contenu:
interface Client extends BehaviorProfile, ActorProfile { public void val(int v) ; }
L'annexe B
p. donne un exemple de comportement
automatiquement généré.
L'implantation des comportements sera la suivante:
class EmptyCellBeh extends EmptyQuasiBehavior { public void put(int i) { System.out.println("Empty cell " + ego() + " receives put(" + i + ") request"); become(new FullCellBeh(i, this)); } } class FullCellBeh extends FullQuasiBehavior { int i; Empty empty; FullCellBeh(int i, Empty empty) { this.i = i; this.empty = empty; } public void get(Actor a) { System.out.println("Full cell " + ego() + " receives get request; will return " + i); send(new JAMvalue(i), a); become(empty); } } class ClientBeh extends ClientBehavior { public void value(int i) { System.out.println("Client " + ego() + " receives " + i); } }
Et le programme principal pourra être le suivant :
public static void main(String[] args) { Actor cell = CreateCt.STD.create("localhost", new EmptyCellBeh()); SendCt.STD.send(new JAMput(2004), cell); Actor customer = CreateCt.STD.create("localhost:1100", new ClientBeh()); SendCt.STD.send(new JAMget(customer), cell); }
Ici, la cellule et le client sont respectivement créés sur les places localhost et localhost:1100.
L'exemple présenté dans cette partie concerne le calcul des N premiers nombres premiers par la méthode du Crible d'Eratosthène. Cet exemple illustre en particulier la faculté de création dynamique des acteurs. L'algorithme est proche d'une version parallèle initialement proposée par D. Gries15.
Le calcul repose sur la construction dynamique d'un pipeline d'acteurs,
chacun représentant un nombre premier, les acteurs étant chaînés dans
l'ordre croissant. Chacun des acteurs joue le rôle d'un crible plus
sélectif que le crible précédent. Tout acteur est conçu pour recevoir
et traiter (méthode sift) un message contenant un nombre à cribler (message JAMsift). En terme de communication, tous les acteurs du crible offrent donc la même interface.
Tous les messages sont envoyés au premier acteur du pipeline, un par un et dans l'ordre croissant. Lorsque le nombre reçu est un multiple du nombre (premier) que l'acteur représente, il ne traverse pas le crible et il est abandonné. En revanche, lorsque ce nombre n'est pas multiple, deux cas se présentent, selon que l'acteur qui le traite est à l'extrémité du pipeline ou pas. Chacun de ces cas est implanté par un comportement différent (classe LastBeh pour le comportement de l'acteur extrémité, classe IntermediateBeh dans l'autre cas) :
Quand un acteur du pipeline a pris un comportement d'acteur
intermédiaire, il le conserve définivement. Ceci conduit à spécifier
deux interfaces de comportement qui diffèrent par l'existence de la
méthode become16. Alors, on peut spécifier les profils ainsi :
public interface IntermediateSieve extends BehaviorProfile { public void sift(int i); } public interface LastSieve extends IntermediateSieve { public void become(IntermediateSieve b); } public interface Sieve extends IntermediateSieve, LastSieve, ActorProfile { }
Les comportements peuvent alors s'implémenter comme suit :
class IntermediateBeh extends IntermediateSieveQuasiBehavior { protected int prime; protected Actor next; public IntermediateBeh(int i, Actor a) { super(); prime = i; next = a; } public void sift(int i) { if ((i % prime) != 0) send(new JAMsift(i), next); } } class LastBeh extends LastSieveQuasiBehavior { protected int prime; public LastBeh(int i) { super(); prime = i; System.out.println("*** " + prime + " is a prime number ***"); } public void sift(int i) { if ((i % prime) != 0) { Actor next = create(new LastBeh(i)); become(new IntermediateBeh(prime, next)); } } }
L'application commence par la création de l'acteur pour l'entier 2 auquel sont envoyés les nombres dans l'ordre croissant17.
public class PrimeNumbers { public static void main(String[] args) { if (args.length != 0) { int valint = Integer.parseInt(args[0]); if (valint > 2) { Actor two = CreateCt.STD.create(new LastBeh(2)); for (int i = 3; valint >= i; i++) SendCt.STD.send(new JAMsift(i), two); } else System.out.println("Give an int > 2"); } } }
Les deux captures d'écran (figures 3 et 4) montrent la trace de l'exécution (ici, les acteurs sont créés aléatoirement sur une des places du domaine, ce qui en l'occurrence engendre une inefficacité certaine). Dans la figure 4, on voit en bas à gauche la fenêtre montrant caraibe station Sun sous SunOS, en haut à droite la ligne de commande du crible (notez l'utilisation d'un serveur de classe sur chambord:8080), et en bas à droite le listing des classes demandées au serveur de classe
Nous considérons maintenant une application dans laquelle un agent mobile recherche des informations sur des places qu'il visite18. Par cet exemple, nous illustrons d'une part la mobilité d'agent et ses différents aspects, d'autre part le mécanisme d'envoi de messages avec retour de résultats.
Ici, pour l'exemple, l'information recherchée est le volume
de mémoire disponible. L'agent mobile de recherche est susceptible de
recevoir et de traiter trois types de requêtes :
Selon le type de demande, l'agent opère comme suit :
Pour l'agent mobile, de type browser, les méthodes browseAndJump() et getInfos()
définissent l'ensemble des messages qu'il peut traiter. Le traitement
d'une demande de recherche complexe impose à l'agent de prendre un
comportement particulier dans lequel il va parcourir l'itinéraire et
rechercher l'information, sans traiter les messages en attente.
Toutes les requêtes étant envoyées à l'agent mobile par le
programme principal, nous ne spécifions pas d'agent client. En
revanche, l'application implique un agent superviseur (dont la
référence est connue de l'agent mobile) et qui a pour seul rôle d'être
informé par message lorsqu'un déplacement de l'agent échoue (méthode trace()).
La spécification des profils des comportements et des acteurs conduit donc aux définitions ci-dessous :
public interface OneStepBrowser extends BehaviorProfile { public void browseAndJump(String place); public void browseAndJump(Vector itinerary, String place); public Vector getInfos(); void become(MultiStepBrowser b); } public interface MultiStepBrowser extends BehaviorProfile { void become(OneStepBrowser b); } public interface Browser extends OneStepBrowser, MultiStepBrowser, ActorProfile { } public interface Supervisor extends BehaviorProfile, ActorProfile { public void trace(String s); }
Le prétraitement de ces interfaces (via le script javactgen) permet d'engendrer les classes :
La classe JSMgetInfosVector offre une méthode getReply() qui permet la récupération du résultat de la requête :
public class JSMgetInfosVector extends MessageWithReply { private java.util.Vector result ; ... public java.util.Vector getReply() { ... } }
Pour implanter les comportements, on dispose de la méthode go(String p) qui permet d'indiquer que le prochain message sera traité (avec le prochain comportement) sur la place identifiée par p. Comme c'est le cas pour le changement de comportement, le déplacement n'est pas réellement effectué au moment de l'appel à go() mais avant de traiter le message suivant19. Précisons que :
En revanche, via un mécanisme d'exception interne à la bibliothèque, il est possible pour le programmeur, de contrôler un échec éventuel lors du déplacement : la méthode go(String p, HookInterface h) permet d'associer au déplacement un objet dédié à la reprise du calcul en cas d'echec au déplacement, et dont la méthode void resume() sera alors automatiquement invoquée. On peut programmer la reprise du calcul au sein d'une classe qui implante l'interface HookInterface en implantant la méthode resume() ; si cette classe est interne à la classe comportement, on peut accéder à tous les éléments du comportement courant, et en particulier redéfinir le déplacement et le comportement suivant de l'agent.
Pour que l'agent puisse opérer de manière autonome sur son
itinéraire, le comportement de recherche complexe doit implanter
l'interface javact.util.StandAlone : cette interface spécifie une méthode run()
qui est exécutée automatiquement à chaque fois qu'un agent est installé
(par création ou déplacement) sur une place. Via cette interface, on
peut d'une part créer des agents auto-actifs sans qu'il soit nécéssaire
de les activer par message, d'autre part (c'est ici le cas) des agents
capables de se déplacer de manière autonome sur le réseau 20.
On code alors les comportements comme suit (la structure de donnée est déclarée Serializable afin de pouvoir être déplacée et transmise sans provoquer une exception) :
class Info implements Serializable { private long freeMemory; private String place; public Info(String place, long freeMemory) { this.place = place; this.freeMemory = freeMemory; } long getFreeMemory() { return freeMemory; } String getPlace() { return place; } public String toString() { return place + " : " + freeMemory; } } class SimpleHook implements HookInterface { Actor supervisor; SimpleHook(Actor supervisor) { this.supervisor = supervisor; } public void resume(GoException e) { // Nothing to do : the agent stays in its current place. A message is sent. SendCt.STD.send(new JAMtrace("Unreachable place : " + e.getPlace()), supervisor); } } /** * The behavior for (when activated) local browsing, then moving. */ class OneStepBrowserBeh extends OneStepBrowserQuasiBehavior { Actor supervisor; Vector collectedInfos; class Hook implements HookInterface { Vector itinerary; String place; Hook(Vector itinerary, String place){ this.itinerary = itinerary; this.place = place; } public void resume(GoException e) { String message = ego() + " : " + e.getPlace() + " is unreachable from " + myPlace() + " - Moves to " + itinerary.firstElement(); send(new JAMtrace(message), supervisor); go((String) itinerary.firstElement(), new Hook(itinerary, place)); itinerary.removeElementAt(0); become(new MultiStepBrowserBeh(supervisor, collectedInfos, itinerary, place)); } } // Constructors OneStepBrowserBeh(Actor supervisor) { this.supervisor = supervisor; this.collectedInfos = new Vector(); } OneStepBrowserBeh(Actor supervisor, Vector collectedInfos) { this.supervisor = supervisor; this.collectedInfos = collectedInfos; } // Methods public void browseAndJump(String place) { // local browsing first collectedInfos.addElement(new Info(myPlace(), Runtime.getRuntime().freeMemory())); // then, moving go(place, new SimpleHook(supervisor)); } public void browseAndJump(Vector itinerary, String place) { // local browsing first collectedInfos.addElement(new Info(myPlace(), Runtime.getRuntime().freeMemory())); if (!itinerary.isEmpty()) { // then travelling go((String) itinerary.firstElement(), new Hook(itinerary, place)); itinerary.removeElementAt(0); become(new MultiStepBrowserBeh(supervisor, collectedInfos, itinerary, place)); } else // else moving go(place, new SimpleHook(supervisor)); } public Vector getInfos() { Vector saveCollectedInfos = collectedInfos; collectedInfos = new Vector(); return saveCollectedInfos; } } /** * The behavior for automatic browsing and moving on a list of places */ class MultiStepBrowserBeh extends MultiStepBrowserQuasiBehavior implements StandAlone { Actor supervisor; Vector collectedInfos; Vector itinerary; String place; class Hook implements HookInterface { Vector itinerary; String place; Hook(Vector itinerary, String place){ this.itinerary = itinerary; this.place = place; } public void resume(GoException e) { String message = ego() + " : " + e.getPlace() + " is unreachable from " + myPlace() + " - Moves to "; if (itinerary.size()>0) { message+=itinerary.firstElement(); send(new JAMtrace(message), supervisor); go((String) itinerary.firstElement(), new Hook(itinerary, place)); itinerary.removeElementAt(0); } else { message+=place; send(new JAMtrace(message), supervisor); go(place); } } } // Constructor MultiStepBrowserBeh(Actor supervisor, Vector collectedInfos, Vector itinerary, String place) { this.collectedInfos = collectedInfos; this.itinerary = itinerary; this.place = place; } // Method public void run() { // local browsing first collectedInfos.addElement(new Info(myPlace(), Runtime.getRuntime().freeMemory())); if (!itinerary.isEmpty()) { // then travelling go((String) itinerary.firstElement(), new Hook(itinerary, place)); itinerary.removeElementAt(0); } else { // else moving become(new OneStepBrowserBeh(supervisor, collectedInfos)); go(place, new SimpleHook(supervisor)); } } } class SupervisorBeh extends SupervisorQuasiBehavior { public void trace(String s) { System.out.println(s); } }
Enfin, on écrit le programme principal. Rappelons que l'envoi de messages avec retour de résultat n'est pas bloquant pour l'émetteur de la requête : ici, l'émetteur n'est pas bloqué pendant que l'agent parcours son itinéraire, mais seulement au moment où il demande explicitement le résultat en attendant son arrivée.
public class MobileBrowser { public static void main(String[] args) { if (args.length >= 2) { Actor supervisor = CreateCt.STD.create(new SupervisorBeh()); Actor browser = CreateCt.STD.create(args[0], new OneStepBrowserBeh(supervisor)); for (int i = 1; i < args.length; i++) SendCt.STD.send(new JAMbrowseAndJump(args[i]), browser); SendCt.STD.send(new JAMbrowseAndJump(args[0]), browser); JSMgetInfosVector m1 = new JSMgetInfosVector(); SendCt.STD.send(m1, browser); Vector itinerary = new Vector(); for (int i = 1; i < args.length; i++) itinerary.addElement(args[i]); SendCt.STD.send(new JAMbrowseAndJump(itinerary, args[0]), browser); JSMgetInfosVector m2 = new JSMgetInfosVector(); SendCt.STD.send(m2, browser); System.out.println(m1.getReply()); System.out.println(m2.getReply()); } } }
La capture d'écran (figure 5) montre l'exécution de cet exemple, avec deux machines caraibe et chambord actives. La place XXX n'existe pas, et la trace d'exécution montrée par le superviseur (fenêtre de gauche) indique les actions de reprise sur erreur. La fenêtre de lancement (en haut à droite) contient également les lignes de commande pour la génération des classes intermédiaires ainsi que la compilation du paquetage MobileBrowser.
public class JAMprint implements javact.lang.Message { private int signatureNumber ; private javact.lang.Actor sig0attr0 ; private javact.lang.Actor sig0attr1 ; public JAMprint(javact.lang.Actor _p0, javact.lang.Actor _p1) { signatureNumber = 0 ; sig0attr0 = _p0 ; sig0attr1 = _p1 ; } private javact.lang.Actor sig1attr0 ; public JAMprint(javact.lang.Actor _p0) { signatureNumber = 1 ; sig1attr0 = _p0 ; } public JAMprint() { signatureNumber = 2 ; } public final void handle(javact.lang.QuasiBehavior _behavior) { switch (signatureNumber) { case 0 : if (_behavior instanceof Hello) ((Hello) _behavior).print(sig0attr0, sig0attr1) ; else throw new javact.lang.MessageHandleException() ; break ; case 1 : if (_behavior instanceof Wonderful) ((Wonderful) _behavior).print(sig1attr0) ; else throw new javact.lang.MessageHandleException() ; break ; case 2 : if (_behavior instanceof World) ((World) _behavior).print() ; else throw new javact.lang.MessageHandleException() ; break ; default : throw new javact.lang.MessageHandleException() ; } } }
import javact.lang.QuasiBehavior; public abstract class EmptyQuasiBehavior extends QuasiBehavior implements Empty { public void become(Full b) { try { becomeAny((QuasiBehavior) b); } catch (RuntimeException e) { throw new javact.lang.BecomeException(e);} } }
Actor create(QuasiBehavior b)
Création d'acteur sur la place locale à partir du comportement b.
Actor create(String p, QuasiBehavior b)
Même chose, mais création sur une place p.
Actor create(
String p,
QuasiBehavior b,
MailBoxCtI box,
BecomeCtI bec,
CreateCtI
crt,
LifeCycleCtI lif,
MoveCtI mve,
SendCtI snd)
Même chose, mais en donnant l'ensemble des micro-composants qui régissent le fonctionnement de l'acteur.
void send(Message m, Actor a)
Envoie le message m à l'acteur de référence a de manière asynchrone.
void send(MessageWithReply m, Actor a)
Envoie le message m à l'acteur de référence a. On peut ensuite attendre le résultat avec m.getReply(). En cas d'erreur (ré-émission du même message par exemple), une Runtime exception JSMSendException est levée. En cas de problème côté récepteur, une Runtime exception JavActException est levée (côté récepteur inaccessible à l'émetteur).
void becomeAny(QuasiBehavior b)
Depuis la version 0.5.0 de JAVACT, les primitives typées de changement de comportement
become(XXXQuasiBehavior b) sont générées automatiquement d'après la spécification des comportements d'acteurs (cf. §4.1.2). Néanmoins, il reste possible mais déconseillé de faire des changements de comportements non typés (et donc potentiellement moins sûrs) avec cette primitive.
void suicide()
Permet de terminer l'exécution de l'acteur, qui ne répondra plus à
aucun message et ne traitera pas les éventuels messages en attente.
void go(String p)
Déplacement de l'acteur sur une place p, sans gérer les erreurs.
void go(String p, HookInterface h)
Déplacement de l'acteur sur une place p. Si une GoException est levée, la bibliothèque redonne la main à la méthode resume(GoException e) de l'objet h.
Cet objet peut être l'instance d'une classe interne du comportement,
afin de pouvoir acceder à toutes les information nécessaires au
recouvrement de l'erreur.
Actor ego()
Retourne l'auto-référence de l'acteur.
void with(MailBoxCtI box)
Remplacement du composant boîte aux lettres.
void with(BecomeCtI bec)
Remplacement du composant de changement de comportement.
void with(CreateCtI crt)
Remplacement du composant de création d'acteur.
void with(MoveCtI mve)
Remplacement du composant de mobilité.
void with(SendCtI snd)
Remplacement du composant d'envoi de messages.
void with(LifeCycleCtI lif)
Remplacementdu composant de gestion du cycle de vie.
This document was generated using the LaTeX2HTML translator Version 2002 (1.62)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html -address ' javact@irit.fr ' -split 0 -local_icons -dir tutoriel_0 -mkdir -title 'JavAct v0.5.0' tutoriel.tex
The translation was initiated by Sebastien LERICHE on 2004-02-16