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
- 2. Portail : les règles chimiques
- 3. Implémentation en Java
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 archives mais aussi les livres 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).
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 à 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 // 3.1 Diffusion terminée sans succès - Diffusion(vide, l, rep) & (Attente(_, rep) inactive) & (Reponse(rep) inactive) -> rep(KO) // 3.1 Diffusion terminée avec succès - Diffusion(vide, l, rep) & (Attente(_, rep) inactive) & Reponse(rep) ->
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 émettre 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
- soit par un stream de Java 8,
- soit par un observable de la bibliothèque ReactiveX, qui existe en plusieurs langages.
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)
** Observable initial émettant des canaux vers des archives (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])) appliquant à chaque canal émis h la fonction en argument, produisant un observable formé d'observables h(l, ret[h]). - Un message h(l, ret[h]) peut en effet être considéré comme un observable, émettant 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) appliquant successivement à chaque message reçu la fonction en argument et produisant un observable émettant des 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 // 3.1 Diffusion terminée sans succès - Diffusion(vide, l, rep) & (Attente(_, rep) inactive) & (Reponse(rep) inactive) -> rep(KO) // 3.1 Diffusion terminée avec succès - Diffusion(vide, l, rep) & (Attente(_, rep) inactive) & Reponse(rep) ->
** 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)
Il serait utile de typer les expressions précédentes. La documentation de la bibliothèque ReactiveX n'utilise pas de types mais s'appuie sur des diagrammes représentant les flots. Ainsi les deux points de vue se complèteraient. Cependant, il faudrait recourir à des types enrichis pour exprimer des sessions ou des interactions. Pour les curieux, voir à ce sujet une étude de 2016, Behavioral Types in Programming Languages.
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 NomAlgorithme pour représenter les algorithmes de recherche et leur nom, utilisé pour leur administration.
Une modification a été apportée relativement à la première partie.
- 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 fournies :
- ImplemNomAlgorithme,
- ImplemIdentifiantLivre,
- ImplemLivre.
Une première classe d'implémentation, ImplemBibliotheque, de l'interface BibliothequeArchive, est aussi fournie : c'est celle de la première partie du TP à deux modifications près.
- La méthode de recherche asynchrone est implémentée.
- La recherche est ralentie, en patientant un certain temps, avant de renvoyer le résultat : voir la fonction Outils.patienter du paquet infrastructure.jaxrs. Cette astuce permet d'observer des temps d'exécution significatifs.
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.28</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>2.28</version> </dependency> <dependency> <groupId>org.glassfish.jersey.inject</groupId> <artifactId>jersey-hk2</artifactId> <version>2.28</version> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-grizzly2-http</artifactId> <version>2.28</version> </dependency> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-proxy-client</artifactId> <version>2.28</version> </dependency> <dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-client</artifactId> <version>2.28</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
Reprendre les annotations des méthodes des interfaces Archive et Bibliotheque de la première partie du TP. Le nom des annotations associées aux filtres et aux intercepteurs a pu légèrement changer.
Annoter les nouvelles 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 annotée comme la méthode chercher.
- Le chemin relatif d'accès est async.
- 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 :
- mêmes annotations qu'une méthode utilisant la communication synchrone,
- 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 : elle concerne la recherche d'un livre dans une bibliothèque.
3.2.3 Annotations JAXB
Annoter la classe d'implémentation ImplemNomAlgorithme et l'interface NomAlgorithme de manière à ce qu'un objet de type ImplemNomAlgorithme soit transformé en un document XML de la forme suivante.
<algo nom="..."/>
(algo est une balise munie d'un attribut nom.)
3.2.4 Filtrage des réponses
Trois filtres, AdapterServeurReponsesGETNullEn404, AdapterServeurReponsesPOSTEnCreated et AdapterServeurReponsesPUTOptionEn404OuValeur (du paquet infrastructure.jaxrs) sont fournis. Analyser la transformation des messages que ces classes réalisent.
Vérifier que les méthodes des interfaces Bibliotheque et Archive sont annotées pour utiliser ces filtres. Vérifier aussi que ces filtres sont enregistrés dans les classes de configuration des bibliothèques-archives et des portails (ServiceBibliotheque et ServicePortail respectivement, dans le paquet configuration).
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
- Tous les filtres sont fournis.
Rôle de client pour le portail
- Un premier filtre-intercepteur est fourni : la classe AdpaterClientReponsesPUT404EnOption 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 AdapterClientReponsesPUTEnOption 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 de manière à spécifier que le contenu est de type HyperLien<Livre> ; 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 AdpaterClientReponsesPUTEnOption 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<NomAlgorithme, 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. Après avoir appelé la méthode chercher, elle transmet le résultat à l'objet gérant la réponse asynchrone.
- La méthode repertorier réalise la concaténation 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). On pourra procéder ainsi :
- créer un flot parallèle (de type java.util.stream.Stream) à partir de la liste des hyperliens vers des bibliothèques archives,
- appliquer à chaque hyperlien la fonction calculant le catalogue de l'archive pointée par l'hyperlien, converti en liste puis flot, puis transformer ce flot de flots en un flot d'hyperliens vers des livres,
- convertir ce flot en HyperLiens<Livre> et renvoyer le résultat.
- 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 this.algoRecherche = new RechercheY("nom de l'algo Y"); ... // ajout dans la table, algo et nom étant des variables locales algo = new RechercheZ("nom de l'algo Z"); nom = algo.nom(); tableAlgos.put(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<Optional<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.
La seconde méthode est plus générale que la première : elle permet non seulement d'enregistrer une fonction de rappel ("callback"), mais aussi de récupérer une promesse, comme la première.
3.2.6.3 Cas synchrone
Implémenter les quatre algorithmes en suivant les indications suivantes. Tous héritent de RechercheSynchroneAbstraite et implémentent AlgorithmeRecherche. Ils agrègent un nom de type NomAlgorithme, initialisé par la chaîne de caractères passée en paramètre du constructeur.
- 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). L'observation d'un observable se fait dans une tâche unique, spécifiée par la méthode subscribeOn, qui prend en argument un ordonnanceur. De ce fait, il est impossible de paralléliser l'observation d'un observable. La parallélisation n'est possible qu'en utilisant un observable émettant des observables, chacun émettant dans une tâche dédiée. La méthode Observable.flatMap. permet de transformer un observable émettant des observables émettant des X en un observable de X. Pour un complément, voir la documentation de l'opérateur subscribeOn et les références mentionnées, notamment RxJava- Understanding observeOn() and subscribeOn() et RxJava- Achieving Parallelization, deux articles de Thomas Nield, auteur de Learning RxJava.
3.2.6.4 Cas asynchrone
Implémenter les quatre algorithmes en suivant les indications suivantes. Tous héritent de RechercheAsynchroneAbstraite et implémentent AlgorithmeRecherche. Ils agrègent un nom de type NomAlgorithme, initialisé par la chaîne de caractères passée en paramètre du constructeur.
- 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, à l'intérieur de 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. Etudier le code de la classe ServiceBibliotheque utilisée pour la configuration des bibliothèques-archives.
Tester votre application avec votre navigateur.
Administration
- Requête : PUT
- http://localhost:8080/Projet/portail/admin/recherche
- Content-Type : application/xml
Corps :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><algo nom="recherche async multi"/>
Recherche
- Requête : PUT
- http://localhost:8080/Projet/portail/async ou http://localhost:8080/Projet/portail/
- Accept et Content-Type : application/xml
Corps :
<livre> <titre>Services5.6</titre> </livre>
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.