UP | HOME

TP3 - Partie 2 - Une bibliothèque - Mobilité des canaux - Vers l'approche réactive

Table of Contents

1 Introduction - Un portail comme réservoir de liens

Dans cette seconde partie, on considère deux sortes de bibliothèques :

  • des archives, qui stockent des livres,
  • des portails, qui référencent des archives et ne stockent pas de livres.

Ces bibliothèques ne présentent pas les mêmes services. Toutes permettent la recherche d'un livre et l'obtention d'un catalogue, mais seules les archives permettent d'ajouter un livre ou d'interroger un livre. Un portail manipule non seulement les livres mais aussi les archives comme des hyperliens.

Après l'implémentation des archives dans la première partie de ce TP, cette seconde partie s'intéresse à l'implémentation des portails. Le but est de proposer plusieurs implémentations des méthodes de recherche, en recourant au parallélisme et à la communication asynchrone. Par parallélisme, on entend que plusieurs fils d'exécution sont possibles simultanément ; par communication asynchrone, on entend que l'appel d'un service n'est pas bloquant, autrement dit, que la latence apparente (temps logique écoulé entre la requête et la réponse) n'est pas nulle, du point de vue du client. Ces notions s'opposent respectivement à la séquentialité, caractérisée par l'unicité du fil d'exécution, et à la communication synchrone, caractérisée par une latence apparente nulle (du point de vue du client). Lorsque l'exécution est purement parallèle (chaque appel d'un service possédant son propre fil d'exécution), il ne peut pas y avoir de blocage, si bien que la communication peut être considérée comme asynchrone.

2 Portail : les règles chimiques

On s'intéresse au serveur et à la seule méthode de recherche d'un livre, pour aller à l'essentiel. On distingue les différents cas, suivant la nature de la communication et celle de l'exécution.

2.1 Serveur portail : canaux et état

Pour simplifier, on s'intéresse à la description d'une unique session, correspondant à une seule requête.

Canaux fournis (réalisés concrètement par une méthode HTTP et un chemin, accompagnés de leurs arguments) :

  • recherche(livre, canalDeRetour) : recherche du livre livre renvoyant sur canalDeRetour un canal pointant vers le livre ou une erreur (notée KO)
  • ret[h] : canal de retour utilisé pour la recherche dans une archive, indexé par le canal h utilisé pour la requête de recherche dans l'archive

Etat du serveur :

  • Liens(archives) : liste des canaux pointant vers le service de recherche des archives
  • Diffusion(archives, livre, canalDeRetour) : diffusion de la recherche de livre aux archives restantes, canalDeRetour étant le canal à utiliser pour la réponse finale
  • Attente(h, canalDeRetour) : attente de la réponse de la requête sur le canal h, canalDeRetour étant le canal à utiliser pour la réponse finale
  • Reponse(canalDeRetour) : drapeau indiquant que la réponse finale a déjà été envoyée sur canalDeRetour

Type de données : listes fonctionnelles (pour représenter la liste de canaux pointant vers les archives)

  • vide : liste vide
  • h::ar : liste de tête h et de reste ar

2.2 Règles chimiques

// 1. Réception d'une requête et initialisation des diffusions
- Liens(ar) & recherche(l, rep) -> Liens(ar) & Diffusion(ar, l, rep)
// 2. Portail non vide
// 2.1 Diffusion des requêtes
- Diffusion(h::ar, l, rep) 
  -> h(l, ret[h]) & Attente(h, rep) & Diffusion(ar, l, rep)
// 2.2 Réponses
// 2.2.1 Réponse positive  
  // Première réponse
- Attente(h, rep) & ret[h](url) & (Reponse(rep) inactive)-> rep(url) & Reponse(rep)
  // Réponses suivantes
- Attente(h, rep) & ret[h](url) & Reponse(rep) -> Reponse(rep)
// 2.2.2 Réponse négative 
- Attente(h, rep) & ret[h](KO) -> 
// 3. Portail vide ou diffusion terminée sans succès
- Diffusion(vide, l, rep) & (Attente(_, rep) inactive) & (Reponse(rep) inactive) -> rep(KO)

Le modèle chimique correspond à une communication asynchrone (ou synchrone point-à-point mais pas agent-à-agent) et une exécution parallèle.

  • Exécution séquentielle

Lorsque l'exécution est séquentielle, les règles s'effectuent dans un ordre prédéterminé.

Communication synchrone : chaque requête 2.1 est suivie par la réponse associée 2.2., soit 2.2.1 ou 2.2.2.

  • 1 ; (2.1 ; 2.2)* ; 3.

Communication asynchrone : les requêtes 2.1 sont effectuées puis les réponses 2.2 sont traitées.

  • 1 ; 2.1* ; 2.2* ; 3

2.3 Comparaison - Méthodes d'implémentation

2.3.1 Communication asynchrone : promesses ou rappels

La communication asynchrone (sans blocage côté client) se fait traditionnellement de deux manières :

  • soit la requête renvoie immédiatement côté client une promesse (de type Future), qui sera réalisée lorsque la réponse arrivera du serveur,
  • soit la requête ne renvoie rien mais à la place enregistre une fonction de rappel de type InvocationCallBack, qui sera appelée lorsque la réponse arrivera du serveur, dans une tâche dédiée.

2.3.2 Complexité en temps

Le temps total d'exécution d'une recherche (soit la complexité en temps) dépend des latences des canaux (délais entre les requêtes et les réponses), et aussi bien sûr des durées propres à chaque calcul local. Supposons qu'on ait \(n\) canaux. On note \((L_i)_i\) les latences relatives aux canaux indexés par \(i\) (des entiers naturels pour simplifier). Dans les évaluations ci-dessous des temps d'exécution, on néglige la durée de tout calcul local. On suppose que la recherche se termine par la réponse positive du canal \(k\).

2.3.2.1 Exécution séquentielle

Dans le cas d'une exécution séquentielle, on procède par une itération sur la liste des canaux pointant vers les archives, simple dans le cas synchrone, double dans le cas asynchrone. Dans le cas synchrone, pour chaque recherche sur un canal, on doit attendre la réponse tout en étant bloqué. Dans le cas asynchrone, on évite ce blocage, si bien qu'on doit attendre uniquement pendant la seconde phase, dans l'attente des réponses.

  • Cas synchrone : \(\Sigma_{0 \leq i \leq k} L_i\)
  • Cas asynchrone : \(\mathrm{Max}_{0 \leq i \leq k} L_i\)
2.3.2.2 Exécution parallèle

Pour obtenir une exécution parallèle, on utilise un programme multi-tâche. La méthode traditionnelle est de lancer autant de tâches que de requêtes ; dans le cas asynchrone, on peut plus simplement itérer sur les requêtes, en enregistrant les fonctions de rappel qui seront exécutées dans des tâches dédiées. Une fois les réponses obtenues, il est nécessaire de les coordonner pour produire le résultat final. On peut par exemple utiliser une barrière de synchronisation de type CountDownLatch. Le temps d'exécution peut dépendre de l'ordonnancement des tâches, celles dédiées aux réponses et celle principale produisant le résultat. Ainsi nous proposons un intervalle pour le temps d'exécution.

  • Cas synchrone : de \(L_k\) à \(\mathrm{Max}_{0 \leq i \leq k} L_i\)
  • Cas asynchrone : de \(L_k\) à \(\mathrm{Max}_{0 \leq i \leq k} L_i\)
2.3.2.3 Conclusion

Dans le cas d'une exécution séquentielle, la communication asynchrone doit être utilisée puisqu'elle permet de modifier la complexité (temps constant contre temps linéaire). Le parallélisme est une optimisation qui ne modifie pas la classe de complexité mais accélère l'exécution (dans le meilleur des cas, avec un ordonnancement efficace). Elle ne dépend pas de la nature de la communication, synchrone ou asynchrone.

2.3.3 Une méthode moderne d'implémentation : la programmation réactive

Une méthode moderne d'implémentation est d'abstraire la solution chimique (formée de l'état et des messages reçus ou à envoyer) par un observable : un observable possède un état et peut engendrer des événements, correspondant à des messages envoyés ou reçus ou à des changements d'états. Un observable est habituellement implémenté par un flot, une séquence de taille indéfinie (finie ou infinie), représentée

On remarque que les règles chimiques correspondent fréquemment à des opérations classiques sur les flots : application d'une fonction (map), filtrage, réduction, etc. On peut aussi étendre un flot par des fonctionnalités de souscription, qui permettent d'observer les événements engendrés par le flot. C'est le choix fait par la bibliothèque ReactiveX. Pratiquement, dans le domaine des services, c'est la bibliothèque ReactiveX qui est utilisée, puisqu'elle est parfaitement adaptée à ce contexte en permettant de programmer des applications fondées sur la communication asynchrone et la gestion d'événements.

Reprenons les règles chimiques en précisant les opérations à réaliser sur le flot.

L'idée est de transformer l'ensemble des règles par une seule règle, en opérant une transformation de l'état, ici formé de la liste des liens vers les archives.

// Transformation du flot ar formé des canaux pointant vers les archives
//   pour obtenir la réponse 
- recherche(l, rep) & Liens(ar) -> rep(Transfo(l, ar)) & Liens(ar)

Déterminons la transformation Transfo paramétrée par le livre l à rechercher, à partir des règles chimiques.

// 1. Réception d'une requête et initialisation des diffusions
- Liens(ar) & recherche(l, rep) -> Liens(ar) & Diffusion(ar, l, rep)
** Flot initial : archives ar (liste de canaux)

// 2. Portail non vide
// 2.1 Diffusion des requêtes
- Diffusion(h::ar, l, rep) 
    -> h(l, ret[h]) & Attente(h, rep) & Diffusion(ar, l, rep)
** Opération : map(h -> h(l, ret[h]))
   // Un message h(l, ret[h]) peut être considéré comme un observable, 
   //   engendrant un événement correspondant à la réponse 
   //   ret[h](x) envoyée par l'archive distante 
   //   suite à la requête h(l, ret[h]).

// 2.2 Réponses
** Opération : flatmap(ret[h](url) -> url)
   // L'opération flatmap appliquée à un observable dont chaque élément 
   //   est un observable produit un observable contenant tous les évènements 
   //   observables ordonnés suivant le temps, auxquels la fonction passée en argument 
   //   est appliquée. 
   // C'est l'analogue de l'opération flatmap transformant une liste de listes en une liste,
   //   puis appliquant la fonction.  

// 2.2.1 Réponse positive  
  // Première réponse
- Attente(h, rep) & ret[h](url) & (Reponse(rep) inactive)-> rep(url) & Reponse(rep)
  // Réponses suivantes
- Attente(h, rep) & ret[h](url) & Reponse(rep) -> Reponse(rep)
// 2.2.2 Réponse négative 
- Attente(h, rep) & ret[h](KO) -> 
// 3. Portail vide ou diffusion terminée sans succès
- Diffusion(vide, l, rep) & (Attente(_, rep) inactive) & (Reponse(rep) inactive) -> rep(KO)
** Opération : filter(url -> url != KO)
   // Le filtre élimine les réponses négatives.
** Opération : firstOrDefault(KO)
   // Cette sélection correspond au traitement différent 
   //   entre la première réponse positive et les réponses suivantes, 
   //   et au traitement de l'échec de la recherche.

Finalement, on peut exprimer ainsi la transformation Transfo(l, ar), avec une notation pointée et en utilisant des opérations classiques sur les flots.

ar
.map(h -> h(l, ret[h]))
.flatmap(ret[h](url) -> url)
.filter(url -> url != KO)
.firstOrDefault(KO)

3 Implémentation en Java

3.1 Le modèle Objet

Le modèle Objet, fourni, contient les interfaces principales :

  • Bibliothèque, décrivant les services de recherche et de production de catalogue,
  • Archive, décrivant les services permettant de gérer les sous-ressources livresques d'une bibliothèque,
  • BibliothequeArchive, héritière de Bibliotheque et Archive, pour décrire les bibliothèques contenant des livres,
  • AdminAlgo, interface d'administration permettant de choisir l'algorithme de recherche dans les bibliothèques archives répertoriées par un portail,
  • Portail, héritière de Bibliothèque et AdminAlgo, pour décrire les portails répertoriant des bibliothèques archives,
  • Livre, décrivant le type de données représentant les livres de la bibliothèque,
  • IdentifiantLivre, décrivant le type de données permettant d'identifier les livres,
  • AlgorithmeRecherche et AlgorithmeNomme pour représenter les algorithmes de recherche et leur nom, utilisé pour leur administration.

Deux modifications ont été apportées.

  • La méthode HyperLien<Livre> ajouter(Livre l) a été déplacée de Bibliotheque vers Archive, ce qui est plus conforme à la finalité d'une archive : le stockage des livres.
  • Dans l'interface Bibliotheque, deux versions de la méthode de recherche sont fournies : l'une pour la communication synchrone, l'autre pour la communication asynchrone. La méthode de recherche asynchrone est implémentée dans la classe ImplementationAppelsAsynchrones.

Les classes d'implémentation suivantes sont également fournies :

  • ImplemAlgorithmeNomme,
  • ImplemIdentifiantLivre,
  • ImplemLivre.

Une première classe d'implémentation, ImplemBibliotheque, de l'interface BibliothequeArchive, est aussi fournie : elle correspond à la première partie du TP.

Le but de cette seconde partie est d'implémenter l'interface Portail, pour obtenir le schéma suivant. Cette classe d'implémentation, appelée ImplemPortail, utilise le patron de conception "Stratégie", qui permet de changer d'algorithme de recherche. Le but de cette seconde partie est aussi de développer huit implémentations de la recherche d'un livre dans des bibliothèques archives distantes :

  • communication synchrone ou asynchrone,
  • quatre méthodes d'implémentation :
    • séquentiel,
    • multi-tâche classique,
    • flots avec des streams de Java 8,
    • flots avec des observables de ReactiveX.

Voici un schéma récapitulatif.

Enfin, reprises de la première partie, deux classes génériques sont fournies pour représenter les hyperliens :

  • Hyperlien<T> : hyperlien vers une ressource de type T,
  • Hyperliens<T> : liste d'hyperliens vers des ressources de type T.

Le type T ne sert qu'au typage. Ces classes sont des variantes de la classe abstraite Link, qui sert à représenter des hyperliens dans JAX-RS 2.0. En dépendant d'un type, elles fournissent une information utile, le type de la ressource vers laquelle l'hyperlien pointe.

3.2 Serveur

3.2.1 Nouveau projet

Créer un nouveau projet, de type Dynamic Web Projet.

  • Ajouter les dépendances suivantes dans le fichier pom.xml. On importe les bibliothèques de Jersey à la fois côté serveur et client (du fait que le portail est aussi client des archives) et la bibliothèque ReactiveX.

      <dependencies>
          <dependency>
                  <groupId>org.glassfish.jersey.containers</groupId>
                  <artifactId>jersey-container-servlet</artifactId>
                  <version>2.26</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.media</groupId>
                  <artifactId>jersey-media-json-jackson</artifactId>
                  <version>2.26</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.inject</groupId>
                  <artifactId>jersey-hk2</artifactId>
                  <version>2.26</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.containers</groupId>
                  <artifactId>jersey-container-grizzly2-http</artifactId>
                  <version>2.26</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.ext</groupId>
                  <artifactId>jersey-proxy-client</artifactId>
                  <version>2.26</version>
          </dependency>
          <dependency>
                  <groupId>org.glassfish.jersey.core</groupId>
                  <artifactId>jersey-client</artifactId>
                  <version>2.26</version>
          </dependency>
          <dependency>
                  <groupId>io.reactivex.rxjava2</groupId>
                  <artifactId>rxjava</artifactId>
                  <version>2.1.7</version>
          </dependency>
    </dependencies>
    
  • Dans le fichier WebContent/WEB-INF/web.xml, ajouter après l'élément welcome-file-list la référence de la servlet à utiliser.

    <servlet>
      <servlet-name>Jersey</servlet-name>
      <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    </servlet>
    
    <servlet-mapping>
       <servlet-name>Jersey</servlet-name>
       <url-pattern>/*</url-pattern>
    </servlet-mapping>
    

Importer l'archive à partir de la racine.

Les paquets suivants sont importés :

  • modele : le modèle objet,
  • infrastructure : du code lié à JAXB ou JAX-RS, principalement,
  • configuration : l'ensemble des configurations, en particulier toutes les constantes utilisées pour configurer le service web (classe JAXRS),
  • serveur : programme permettant de déployer dix bibliothèques archives, en utilisant le serveur Grizzly.

3.2.2 Annotations JAX-RS des interfaces

Annoter les méthodes des interfaces AdminAlgo et Bibliotheque en suivant les indications suivantes.

Remarque : pour hériter d'annotations JAX-RS, il est nécessaire de respecter certaines règles, décrites dans la spécification de JAX-RS (voir ci-dessous un extrait). En pratique, nous définissons les annotations des méthodes dans les interfaces pour les hériter dans les classes, qui n'en comportent aucune au niveau des méthodes.

Extrait de la spécification de JAX-RS, version 2, section 3.6, Annotation Inheritance

JAX-RS annotations may be used on the methods and method parameters of a super-class or an implemented interface. Such annotations are inherited by a corresponding sub-class or implementation class method provided that the method and its parameters do not have any JAX-RS annotations of their own. Annotations on a super-class take precedence over those on an implemented interface. The precedence over conflicting annotations defined in multiple implemented interfaces is implementation specific. Note that inheritance of class or interface annotations is not supported.

Annoter la méthode changerAlgorithmeRecherche de l'interface AdminAlgo de manière à vérifier les propriétés suivantes. On utilisera les constantes de la classe configuration.JAXRS.

  • La méthode est qualifiée par le type idoine de méthode HTTP.
  • Le chemin relatif d'accès est admin/recherche.
  • Les données sont produites ou consommées au format application/xml.

Annoter la méthode chercherAsynchrone de l'interface Bibliotheque de manière à vérifier les propriétés suivantes.

  • La méthode est qualifiée par le type de méthode HTTP PUT.
  • Le chemin relatif d'accès est async.
  • Les données sont produites ou consommées au format application/xml.
  • Le paramètre supplémentaire de type AsyncResponse, dont la valeur sera fournie par l'implémentation de JAX-RS lors de chaque invocation de la méthode, représente la réponse. Il doit être annoté par @Suspended, du paquet javax.ws.rs.container.

Cette dernière méthode correspond à une communication asynchrone. Pour définir une méthode utilisant une communication asynchrone, on procède donc ainsi :

  • ajout d'un paramètre supplémentaire de type AsyncResponse,
  • annotation par @Suspended,
  • pas de type de retour (void) ou type de retour fictif, ne servant que côté client.

Avec une telle méthode, côté client, l'appel distant n'est pas bloquant mais renvoie une promesse, soit une valeur de type Future<X>, si la réponse est initialisée côté serveur avec une valeur de type X. La promesse contient le canal de retour sur lequel la réponse arrivera. Le serveur reçoit la requête et la traite. Il envoie la réponse au client via le canal de retour. Pendant la durée du traitement, le client peut tester si la promesse est réalisée ou non. Elle l'est lorsque la réponse du serveur parvient au client. Nous avons fait le choix de typer la méthode de manière à ce qu'elle renvoie une valeur de type Future<Optional<HyperLien<Livre>>>, comme du côté client. De fait, ce sera la valeur null, puisque la réponse est transmise par l'argument de type AsyncResponse inséré automatiquement. C'est simplement une indication de typage. La solution retenue par la norme JAX-RS, celle d'un paramètre supplémentaire typé simplement, n'est pas idéale de ce point de vue. Etudier l'implémentation de la recherche asynchrone dans la classe ImplementationAppelsAsynchrones.

3.2.3 Annotations JAXB

Annoter la classe d'implémentation ImplemAlgorithmeNomme et l'interface AlgorithmeNomme de manière à ce qu'un objet de type ImplemAlgorithmeNomme soit transformé en un document XML de la forme suivante.

<algo nom="..."/>

3.2.4 Filtrage des réponses

Deux filtres, AdapterReponsesServeurGETNullEn404 et AdapterReponsesServeurPOSTEnCreated (du paquet infrastructure.jaxrs) sont fournis. Analyser la transformation des messages que ces classes réalisent.

Annoter les méthodes des interfaces Bibliotheque et Archive lorsqu'elles doivent utiliser ces filtres.

Lorsque le portail interroge une archive pour chercher un livre, il peut recevoir une réponse de statut 404, ce qui signifie que la recherche a été infructueuse ; de même, il répond par une réponse 404 lorsqu'une recherche demandée par un client s'avère infructueuse, après interrogation de toutes les archives. Dans le code Java, cette possibilité se traduit par le type de retour Optional<HyperLien<Livre>> pour la méthode chercher. Il est nécessaire de traduire les options par des filtres et des intercepteurs lorsque le portail joue le rôle de serveur lors de l'envoi des réponses aux clients et lorsqu'il joue le rôle de client des archives.

Rappel : il est inutile de définir une annotation côté client pour un filtre (ou un intercepteur). En effet, pour les filtres côté client, la norme JAX-RS ne propose pas de mécanismes utilisant une annotation des méthodes. On doit configurer les cibles de type WebTarget par les filtres utilisés.

Rôle de serveur pour le portail

  • Définir dans le paquet infrastructure.jaxrs.annotations une annotation ReponsesPUTOption comme l'annotation ReponsesGETNullEn404.
  • Annoter les méthodes de recherche dans Bibliotheque par cette annotation.
  • Dans le paquet infrastructure.jaxrs, définir la classe AdapterReponsesServeurPUTOptionEn404OuValeur implémentant ReaderInterceptor et ContainerResponseFilter, et l'annoter par @Provider et ReponsesPUTOption pour établir la liaison entre les méthodes de recherche et ce filtre-intercepteur.
    • Lui donner pour priorité Priorities.HEADER_DECORATOR.
    • Implémenter les méthodes suivant la spécification générale suivante : si l'option est vide, transformer la réponse en 404, sinon, remplacer l'entité par la valeur de l'option. Détaillons.
      • Dans la méthode aroundReadFrom, réaliser la traduction (unmarshalling) du corps du message, pour récupérer un livre, en utilisant la méthode proceed. Mémoriser ce livre dans une propriété du contexte passé en argument. Cette mémorisation permet la communication entre l'intercepteur et le filtre.
      • Dans la méthode filter, si l'entité est de type Optional<?>, alors la convertir en Optional<?>. Si l'option n'est pas présente, convertir la réponse en 404 à l'aide de la méthode convertirEn404, sinon, modifier l'entité en la valeur de l'option.

        private void convertirEn404(
          ContainerRequestContext requete, ContainerResponseContext reponse) {
          System.out.println("recherche : 404 NOT FOUND !");
          String contenu = requete.getUriInfo().getRequestUri().toString();
          // Utilisation de la propriété pour récupérer la description du livre recherché.
          contenu = contenu + " - " + requete.getProperty(LIVRE);
          reponse.setEntity(contenu, null, MediaType.TEXT_PLAIN_TYPE);
          reponse.setStatus(Response.Status.NOT_FOUND.getStatusCode());
        }
        
      • Remarque : la traduction réalisée par le "data binder" utilise le type dynamique de l'entité, ici HyperLien<Livre>. Il n'est pas nécessaire de modifier le contexte de la traduction, par un intercepteur, en précisant le nouveau type de l'entité, comme demandé dans une version précédente du TP. La spécification n'est pas toujours claire pour ce genre de questions.
  • Enregistrer une instance de ce filtre-intercepteur dans ServiceBibliotheque et ServicePortail du paquet configuration.

Rôle de client pour le portail

  • Un premier filtre-intercepteur est fourni : la classe AdapterReponsesClientPUT404EnOption permet de transformer une réponse de statut 404 à une requête PUT en une option vide si le type attendu est Optional. Etudier son code, et en particulier le rôle des propriétés qui permettent de relier les filtres et l'intercepteur. Il reste à définir un intercepteur transformant une valeur en une option contenant cette valeur, lorsque le type attendu est Optional<?>.
  • Définir un intercepteur AdapterReponsesClientPUTEnOption de type ReaderInterceptor, avec la priorité Priorities.HEADER_DECORATOR + 2, ce qui implique que cet intercepteur intervient après le précédent.
    • Dans la méthode aroundReadFrom, si le type de la réponse vaut Optional.class, alors convertir le type générique de la réponse en un ParameterizedType, puis récupérer son seul type argument (qui dans notre cas, sera HyperLien<Livre>) ; modifier le type de la réponse en utilisant la fonction Types.convertirTypeEnClasse du paquet infrastructure.langage; enfin, renvoyer Optional.of(reponse.proceed()). Sinon, renvoyer reponse.proceed(), autrement dit la traduction du document en objet (via la fonction unmarshalling).
  • Pour pouvoir utiliser cet intercepteur lorsque le portail joue le rôle de client (ou d'orchestrateur), enregistrer la classe AdapterReponsesClientPUTEnOption dans la configuration définie dans configuration.Orchestrateur.clientJAXRS.

3.2.5 Implémentation du portail

Définir la classe modele.ImplemPortail en suivant les instructions suivantes.

  • C'est un singleton.
  • Cette ressource est accessible par le chemin relatif portail (constante définie dans la classe JAXRS).
  • Elle implémente Portail.
  • Son état est formé
    • d'une table de type ConcurrentMap<AlgorithmeNomme, AlgorithmeRecherche>,
    • d'un client de type javax.ws.rs.client.Client,
    • d'un algorithme de recherche,
    • d'une liste d'hyperliens vers des bibliothèques archives.
  • Son constructeur initialise ses différents champs ainsi :
    • une table de hachage, initialement vide,
    • un client de valeur Orchestrateur.clientJAXRS(),
    • un algorithme de recherche valant null provisoirement,
    • une liste d'hyperliens valant configuration.Initialisation.bibliotheques().
  • La méthode chercher redirige l'appel vers la méthode chercher de l'algorithme de recherche.
  • La méthode chercherAsynchrone s'implémente en appelant ImplementationAppelsAsynchrones.rechercheAsynchroneBibliotheque. Etudier (si ce n'est déjà fait) cette dernière fonction, qui peut servir de modèle pour implémenter des versions asynchrones de méthodes.
  • La méthode repertorier réalise la somme des catalogues des archives pointées par les hyperliens de la liste. Pour récupérer un proxy, on utilisera la méthode LienVersRessource.proxy (du paquet infrastructure.jaxrs). Pour sommer des listes d'hyperliens (de la classe HyperLiens<Livre>), on utilisera la fonction Outils.sommeHyperLiens.
  • La méthode changerAlgorithmeRecherche(nom) procède ainsi : si la table associe à nom un algorithme de recherche, alors celui-ci devient le nouvel algorithme de recherche, sinon l'algorithme courant est conservé.

Enregistrer la classe ImplemPortail dans le constructeur de configuration.ServicePortail.

Enfin, à chaque fois qu'un nouvel algorithme de recherche est créé, il faudra l'ajouter à la table et au moins une fois initialiser l'algorithme de recherche dans le constructeur.

Exemple

// Initialisation requise au moins une fois
algorRecherche = new RechercheY();
...
// ajout dans la table, algo étant une variable locale
algo = new RechercheX();
table.put(algo.nom(), algo);

3.2.6 Implémentation des algorithmes de recherche

On suit le schéma récapitulatif, en commençant par les classes abstraites puis par les huit classes concrètes. Toutes ces classes implémentent l'interface AlgorithmeRecherche. Pour suivre les exécutions concurrentes (par exemple, par des affichages dans la console), il est nécessaire de déterminer la tâche ("thread") en cours d'exécution : à cet effet, on pourra utiliser la fonction Outils.afficherInfoTache.

Pour la bibliothèque Rx (version 2), il est recommandé de consulter sa documentation, plutôt détaillée.

3.2.6.1 Classe abstraite RechercheSynchroneAbstraite

Implémenter la méthode suivante, réalisant une recherche synchrone dans la bibliothèque (archive) passée en argument.

protected Optional<HyperLien<Livre>> rechercheSync(HyperLien<BibliothequeArchive> h, Livre l, Client client);
3.2.6.2 Classe abstraite RechercheAsynchroneAbstraite

Implémenter les méthodes suivantes, réalisant une recherche asynchrone dans la bibliothèque pointée par le lien. La première utilise une promesse, la seconde une fonction de rappel. A chaque fois, construire à partir de client une cible, puis une requête asynchrone.

protected Future<Optional<HyperLien<Livre>>> rechercheAsync(
  HyperLien<BibliothequeArchive> h, Livre l, Client client);
protected Future<Optional<HyperLien<Livre>>> rechercheAsyncAvecRappel(
  HyperLien<BibliothequeArchive> h, Livre l, Client client,  
  InvocationCallback<HyperLien<Livre>> retour);

Indication : pour déterminer le type de retour de la méthode chercher(Livre l) qui doit être fourni en argument, utiliser Types.typeRetourChercherAsync() du paquet infrastructure.langage.

3.2.6.3 Cas synchrone

Implémenter les quatre algorithmes en suivant les indications suivantes. Tous héritent de RechercheSynchroneAbstraite.

  • Cas séquentiel : classe RechercheSynchroneSequentielle
    • Nom : "recherche sync seq"
    • Une itération sur les hyperliens
  • Cas multi-tâche : classe RechercheSynchroneMultiTaches
    • Nom : "recherche sync multi"
    • Une itération sur les hyperliens
      • A chaque étape, créer une tâche dédiée à la recherche dans la bibliothèque pointée par l'hyperlien en utilisant un ExecutorService, initialisé par Executors.newCachedThreadPool() (du paquet java.util.concurrent).
      • Utiliser une barrière de synchronisation de type CountDownLatch (du paquet java.util.concurrent) : dans la tâche dédiée, utiliser la méthode countDown, dans la tâche principale (celle de la méthode), utiliser await. Dans le cas d'une réponse positive, on peut passer la barrière directement.
  • Cas stream Java 8 : RechercheSynchroneStreamParallele
    • Nom : "recherche sync stream 8"
    • Une définition d'un flot suivie d'une réduction à une option contenant un hyperlien vers un livre
      • Partir de la liste d'hyperliens vers des bibliothèques archives.
      • Créer un flot parallèle.
      • Appliquer la fonction de recherche synchrone.
      • Retirer les options vides.
      • Réduire à n'importe quelle valeur ou à défaut à l'option vide.
  • Cas stream ReactiveX : RechercheSynchroneStreamRx
    • Nom : "recherche sync stream rx"
    • Une définition d'un flot suivi d'une réduction à une option contenant un hyperlien vers un livre
      • Partir d'un observable créé à partir de la liste d'hyperliens.
      • Appliquer en linéarisant (en ordonnant suivant le temps) la fonction (h -> Observable.fromCallable(() -> rechercheSync(h, l, client))). Indication : voir la méthode Observable.flatMap.
      • Pour une exécution multi-tâche, ajouter un appel à subscribeOn(Schedulers.io()) après l'appel à fromCallable.
      • Retirer les options vides.
      • Prendre la première valeur ou à défaut l'option vide. Indication : voir la méthode Observable.blockingFirst.
    • Remarque : pour observer un observable, soit on utilise une souscription (voir les méthodes Observable.subscribe), soit on bloque l'observable (voir les méthodes Obsevable.blockingX).
3.2.6.4 Cas asynchrone

Implémenter les quatre algorithmes en suivant les indications suivantes. Tous héritent de RechercheAsynchroneAbstraite.

  • Cas séquentiel : classe RechercheAsynchroneSequentielle
    • Nom : "recherche async seq"
    • Une première itération sur les hyperliens permettant d'obtenir une liste de promesses
    • Une seconde itération sur les promesses permettant de produire le résultat
  • Cas multi-tâche : classe RechercheAsynchroneMultiTaches
    • Nom : "recherche async multi"
    • Une itération sur les hyperliens
      • A chaque étape, créer une fonction de retour de type InvocationCallback<Optional<HyperLien<Livre>>> puis appeler la méthode de recherche asynchrone.
      • Utiliser une barrière de synchronisation de type CountDownLatch : dans la fonction de retour, utiliser la méthode countDown, dans la tâche principale (celle de la méthode), utiliser await. Dans le cas d'une réponse positive, on peut passer la barrière directement.
  • Cas stream Java 8 : RechercheAsynchroneStreamParallele
    • Nom : "recherche async stream 8"
    • Une définition d'un flot suivie d'une réduction à une option contenant un hyperlien vers un livre
      • Partir de la liste d'hyperliens vers des bibliothèques archives.
      • Créer un flot parallèle.
      • Appliquer la fonction de recherche asynchrone.
      • Appliquer la fonction Outils::remplirPromesse.
      • Retirer les options vides.
      • Réduire à n'importe quelle valeur ou à défaut à l'option vide.
  • Cas stream ReactiveX : RechercheAsynchroneStreamRx
    • Nom : "recherche async stream rx"
    • Une définition d'un flot suivie d'une réduction à une option contenant un hyperlien vers un livre
      • Partir d'un observable créé à partir de la liste d'hyperliens.
      • Appliquer en linéarisant (en ordonnant suivant le temps) la fonction (h -> Observable.fromFuture(rechercheAsync(h, l, client))).
      • Pour une exécution multi-tâche, ajouter un appel à subscribeOn(Schedulers.io()) après l'appel à fromFuture.
      • Retirer les options vides.
      • Prendre la première valeur ou à défaut l'option vide.

3.2.7 Déploiement

Dans le fichier web.xml, sous l'élément servlet, référencer la définition du service dans la classe configuration.ServicePortail. Bien noter aussi l'usage de la balise async-supported permettant la communication asynchrone.

<init-param>
  <param-name>javax.ws.rs.Application</param-name>
  <param-value>configuration.ServicePortail</param-value>
</init-param>
<load-on-startup>1 </load-on-startup>
<async-supported>true</async-supported>

Etudier le code de la classe ServicePortail. Cette configuration est propre à Jersey.

Déployer sur le serveur Tomcat le portail.

Lancer l'application LancementDixArchives. Elle déploie dix bibliothèques archives comptant chacune dix livres, de titre ServicesX.Y (où X et Y varient entre \(0\) et \(9\)), en utilisant le serveur Grizzly.

Tester votre application avec votre navigateur.

Administration

Recherche

3.3 Client

Créer un nouveau projet Java. Réaliser un test automatique du portail.

  • Pour chaque algorithme, faire une ou plusieurs recherches, en mode synchrone et asynchrone. Mesurer le temps en utilisant System.nanoTime().
  • Expliquer les résultats dans un fichier readme.org. Bien noter que le test se fait sur une seule machine, avec plusieurs coeurs. Noter aussi que les réponses des bibliothèques archives sont retardées en cas de recherche infructueuse : voir la fonction Outils.patienter.
  • Reprendre les tests en répartissant l'application :
    • le portail sur sa propre machine,
    • les dix bibliothèques archives sur dix autres machines.
  • Expliquer les résultats dans un fichier readme.org.

3.4 Bonus : le catalogue

Développer une architecture analogue pour la méthode Bibliotheque.repertorier.

Author: Hervé Grall
Version history: v1: 2017-05-02; v2: 2017-12-13[update, *text]; v3: 2018-04-23[update, *text].
Comments or questions: Send a mail.
The webpage content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.