Projet Javanaise :

un cache d’objets répartis en Java

 

F. Boyer, Master M2GI,

Fabienne Boyer, Laboratoire LIG,

 Université Grenoble Alpes

 

Les fichiers source fournis (sous forme de projet Eclipse) peuvent être récupérés dans le paquet DONNE.zip.

Les transparents de présentation du projet sont accessibles ici. 

Les transparents du cours associé sont accessibles ici..

Accès aux transparents des autres présentations : dynamic proxies, annotations.

 

1. Objectifs

L'objectif du projet Javanaise est de réaliser en Java un cache d'objets répartis. Les applications Java utilisant Javanaise peuvent créer et accéder à des objets répartis cachés localement.

2. Principes de mise en oeuvre

Le service Javanaise est mis en œuvre au travers d’un serveur centralisé qui gère les objets répartis (ce serveur est appelé coordinateur) et d’une librairie (jar) permettant à toute application d’utiliser le cache d’objets répartis.  Pour utilisr le service Javanaise, une application doit instancier une classe particulière fournie dans cette librairie (cette classe est nommée JvnServer). L’instanciation de la classe JvnServer crée un serveur local à l’application qui lui permettra de créer et d’accéder à des objets répartis (interface JvnLocalServer).

Pour gérer la cohérence des différentes copies des objets répartis, tout accès à un objet réparti est intercepté. Autrement dit, tout objet caché localement possède un objet d’indirection (appelé également intercepteur), qui intercepte les invocations  pour exécuter les actions de contrôle nécessaires à la gestion de la cohérence de la copie locale (mise à jour, invalidations, etc).

Les différents serveurs Javanaise communiquent avec le coordinateur via l’interface JvnRemoteCoord. Inversement, le coordinateur peut faire appel aux serveurs au travers de leurs interfaces distantes (JvnRemoteServer). Toutes les communications distantes sont basées sur l’usage de Java/RMI. La figure suivante montre l’architecture globale de Javanaise.

 

 

.

3. API Javanaise

3.1 Version 1

L'API Javanaise (jvn dans la suite du document) définit comment une application doit utiliser le service Javanaise. Le préfixe jvn est utilisé pour toute classe ou interface apparaissant dans l'API Javanaise. Cette API est composée de l’interface JvnLocalServer.

 

L’interface JvnLocalServer est fournie par tout serveur Javanaise, au travers de la classe JvnServerImpl qui implémente en fait deux interfaces :

·      l’interface JvnLocalServer, destinée à être utilisée par l'application à laquelle le serveur est associé,

·      l’interface JvnRemoteServer destinée à être utilisée par le coordinateur global.

 

public class JvnServerImpl extends UnicastRemoteObject implements JvnRemoteServer, JvnLocalServer {

// récupère la référence du serveur pour une application cliente

      public static jvnServer jvnGetServer();

 

// Fin d'utilisation du service JVN

public void jvnTerminate();

 

// creation d'un objet JVN

public JvnObject jvnCreateObject(Serializable jos);

 

// association d'un nom symbolique à un objet JVN

public void jvnRegisterObject(String jon, JvnObject jo);

 

// récupération d'une référence sur un objet JVN à partir de son nom symbolique

public JvnObject jvnLookupObject(String jon);

}

 

Au niveau applicatif, l’accès à un objet réparti se fait toujours avec, préalablement, un verrouillage de l’objet en lecture ou en écriture, en fonction du type de la méthode invoquée.  L’interface JvnObject est fournie par tout objet JVN. Cette interface définit entres autres des méthodes qui permettent à une application de verrouiller l’objet en lecture ou en écriture.

 

Interface JvnObject {

// Verrouillage de l'objet en lecture

      public void jvnLockRead();

// Verrouillage de l'objet en écriture

      public void jvnLockWrite();

// Déverrouillage de l'objet

      public void jvnUnLock();

}

 

L’interface JvnObject est en fait implémentée par l’intercepteur (ou objet d’indirection) associé à un objet réparti. Tout accès à un objet réparti doit passer par l’objet d’indirection qui lui est attribué (de la même manière qu ‘un accès à un objet réparti avec RMI passe par le talon associé à cet objet).

 

La manière d'accéder à un objet réparti depuis une application cliente dépend de la version de Javanaise. Dans la version 1, l'accès et la manipulation d'un objet réparti est moins transparente pour le programmeur que dans la version 2. Plus précisément, dans la version 1, l'objet d'indirection est explicitement invoqué lors d’un appel de méthode applicative.

 

         // Version 1 de Javanaise :

         JvnServer js = JvnServer.jvnGetServer() ;

 

         // création d’un objet partagé de classe C1

         C1 o = new C1(..);

         JvnObject jvnO = js.jvnCreateObject((..)o);

         

          // appel d'une méthode applicative

         jvnO.jvnLockRead();

         jvnO.jvnGetObjectState().<method>(<params>);

         jvnO.jvnUnLock();

        

 

 

3.2 Version 2

 

Dans la version 2, le service Javanaise se charge de générer des sous-classes de la classe JvnObject pour pouvoir mettre en oeuvre des objets d'indirection spécifiques à l'application. L’accès et la manipulation d'un objet réparti devient totalement transparente pour le programmeur.

 

      Utilisation d’un objet JVN avec la version 1 de Javanaise :

         JvnServer js = JvnServer.jvnGetServer();

        

C1 o1 = new C1(..);

         JvnObject jvnO1 = js.jvnCreateObject(o1);

        

         JvnO1.jvnLockRead();

         // appel d'une méthode applicative

         JvnO1.jvnGetObjectState().meth();

JvnO1.jvnUnLock();

            Utilisation d’un objet JVN avec la version 2 de Javanaise :

            JvnServer js = JvnServer.jvnGetServer();

        

 

JvnC1 o1 = new JvnC1(..);

         // appel d'une méthode applicative

         o1. meth();

 

 

Il y a deux manières de mettre en œuvre cette transparence. Il est possible de générer des classes d’interception spécifiques au moment de la compilation des objets répartis. Ceci impose d’effectuer une analyse de leur interface Java (interface fournie par la classe applicative C1 dans l’exemple), en utilisant les mécanismes d’introspection fournis par le langage Java. Pour la génération des classes d’indirection (sous-classes de la classe JvnObject), l’outil velocity peut être utilisé (http://velocity.apache.org ). Cet outil est très largement exploité pour mettre en œuvre de la génération de code Java. Il est également utilisé pour d’autres types de génération (XML, pages Web, etc). 

Le deuxième moyen possible pour obtenir la transparence dans l’invocation des objets répartis est de générer les classes d’interception spécifiques durant l’exécution (et non pas à la compilation) en utilisant par exemple les dynamic proxies de Java. Cette facilité fournie par Java permet en effet de générer, durant l’exécution et à la demande, des classes implémentant des interfaces spécifiques. C'est cette option que nous allons utiliser.

 

 

4. Principes d'implantation       

 

4.1 Identification d'un objet JVN

Un objet JVN est identifié de manière unique dans le système réparti par un identifiant alloué à la création de l'objet.

 

4.2 Coordination et communication entre serveurs JVN

La coordination et la communication entre différents serveurs JVN fonctionnant pour différentes applications clientes est effectuée au travers d'un serveur central appelé le coordinateur (JvnCoord). Ce coordinateur est lancé préalablement au lancement des applications clientes. Les interactions entre les serveurs et le coordinateur sont illustrées dans la figure ci-après. Dans le prototype que nous allons réaliser, une application qui utilise le service Javanaise est censée être mono-threadée.

 

 

 

4.3 Cohérence des objets

La cohérence des objets JVN repose sur la gestion de verrous. Un objet réparti possède un verrou qui peut être dans l'un des états suivants :

                              NL : no local lock

                              RLC : read lock cached

                              WLC : write lock cached

                              RLT : read lock taken

                              WLT : write lock taken         

                              RLT-WLC : read lock taken – write lock cached     

Un verrou est dit taken s’il est en cours d’utilisation par l’application cliente, c’est à dire s’il y a une méthode applicative en cours d’exécution pour laquelle le verrou a été alloué. L’allocation du verrou est réalisée avant ou au commencement de la méthode applicative au moyen d'un appel à jvnLockRead() ou jvnLockWrite() sur l'objet JVN concerné. L’allocation peut engendrer des communications avec le coordinateur. Un verrou dans l’état taken sera libéré par un appel à  jvnUnlock() normalement réalisé à la fin de (ou juste après) la méthode applicative. Cet appel fera passer le verrou à l’état cached. Un verrou dans l’état cached est un verrou qui est possédé par l’application, mais non utilisé à l’instant courant. Un tel verrou peut être à nouveau utilisé par l’application sans engendrer des communications avec le coordinateur.

L’état RLT-WLC représente le cas dans lequel l’application possède un verrou caché en écriture, qu’elle utilise à l’instant courant en lecture.

En fonction de l’état de l’objet sur le site client, la demande d’un verrou nécessitera (ou pas) de propager un appel au coordinateur. Ce dernier connaît pour chaque objet dupliqué, le site écrivain si l’objet est en écriture ou la liste des sites lecteurs si l’objet est en lecture (afin d’être en mesure de propager des invalidations). Lors d'une demande de verrou, le coordinateur peut demander à un serveur JVN de rendre le verrou qu'il possède ainsi que l'état courant de l'objet. Les méthodes qui peuvent être invoquées par le coordinateur sur un serveur Javanaise sont les suivantes :

      interface JvnRemoteServer (suite) {

           

// permet au coord. de réclamer le passage d’un verrou de l’écriture à la lecture

// pour un objet JVN identifié

            publicjvnInvalidateWriterForReader(int joi);

// permet au coord. de réclamer l’invalidation d’un lecteur

            publicjvnInvalidateReader(int joi);

// permet au coord. de réclamer l’invalidation d’un écrivain

            publicjvnInvalidateWriter(int joi);

      }

 

Inversement, un serveur Javanaise peut invoquer les méthodes suivantes sur le coordinateur :

 

      interface JvnRemoteCoord {

           

// permet de réclamer au coord. un verrou en lecture

            publicjvnLockRead(…);

// permet de réclamer au coord. un verrou en écriture

            publicjvnLockWrite(…);

            }

 

Politique de cohérence

Le coordinateur, ainsi que les serveurs JVN coopèrent pour mettre en œuvre une politique de cohérence pour les objets dupliqués. Cette politique est la cohérence à l'entrée (entry consistency), qui garantit que lors de l'entrée dans un objet dupliqué, le service fournit la dernière version de l'objet. L'entrée dans un objet dupliqué correspond, dans le cadre de Javanaise, à la prise d'un verrou en lecture ou en écriture.

 

 

5. Limitations et/ou  extensions envisageables

Le service Javanaise doit être conçu et mis au point de manière incrémentale afin d’aborder les problèmes par étapes. Pour ce faire, nous allons au départ fixer un certain nombre de limitations qui permettent de simplifier le prototypage initial du service. Ces limitations peuvent correspondre à des contraintes sur l’usage du service JVN. Si le temps le permet, vous chercherez à étendre le prototype réalisé pour  lever certaines de ces limitations.

 

5.1 Gestion de la saturation du cache local

Il est envisageable de limiter le nombre maximum de copies locales d’objets répartis qu’une application peut avoir. Dans ce cas, il faut gérer le vidage du cache lorsque celui-ci sature. Cette extension est intéressante car elle permet à des applications s’exécutant dans des contextes contraints de pouvoir accéder à un grand nombre d’objets sans pour autant avoir à disposer d’une taille mémoire importante.

5.2 Gestion des références

Dans le prototype actuel, les objets répartis doivent être indépendants les uns des autres. Autrement dit, un objet réparti ne doit pas contenir de références à d’autres objets répartis.

Pour lever cette limitation, on peut se baser sur la spécialisation des procédures de sérialisation / désérialisation fournies par Java sur les objets sérialisables. En premier lieu, lorsqu’un objet réparti est transmis à une application par le coordinateur, il est transmis dans une forme sérialisée, et désérialisé  au moment de son arrivée dans l’application.

Si cet objet (soit O) contient une référence (soit r) vers un autre objet réparti, cette référence est stockée sous la forme d’une référence vers un objet de type JvnObject. Etant donné que la classe JvnObject étend la superclasse Serializable, lorsque O est (dé)sérialisé, alors r l’est également. En spécialisant les procédures de sérialisation/désérialisation associées à la classe JvnObject, il est possible de traiter le cas dans lequel des objets répartis se référencent mutuellement.

 

5.3 Coordinateur décentralisé

La mise en œuvre centralisée du coordinateur impose de disposer d’un site dédié à ce rôle, et d’administrer ce site de manière à assurer la disponibilité du coordinateur lorsque des applications souhaitent disposer du service Javanaise.

Une conception plus souple serait de mettre en œuvre un coordinateur décentralisé, basé sur des représentant locaux distribués dans les différentes applications utilisant Javanaise. Pour gérer les communications et la coordination des représentants, l’usage d’un mécanisme de gestion de groupe serait préconisée. En vous basant sur les connaissances acquises dans le cours de Construction d’Applications Réparties, vous pouvez chercher à mettre en œuvre ce coordinateur décentralisé sur la base d’une gestion de groupe à base de diffusion fiable et ordonnée.

6. Tests

On fournit une classe Irc représentant une application cliente de type Chat. Cette classe permet de tester le service Javanaise. Elle propose une fenêtre d’affichage et une fenêtre de saisie de messages, ainsi que deux boutons Read et Write qui déclenchent les lectures et écritures. Les lectures et écritures se font sur un objet JVN appelé Sentence.

 

Une fois les test fonctionnels validés, vous pourrez mettre en place des tests qui stressent le système (vérification du bon fonctionnement sous charge importante) ainsi que des tests de performances.

 

 

 

  

9. Documents à rendre