UP | HOME

TP2 - Un registre comme service restful - Contrôle optimiste de la concurrence

Table of Contents

1 Introduction : registre comme service, service comme registre

Un registre offre deux services, l'un de lecture, l'autre d'écriture. Le serveur associé possède donc un état modifiable. C'est fréquemment le cas qu'un serveur présente un état modifiable par les clients. Cependant, sur le Web, les requêtes les plus fréquentes sont des requêtes en lecture seulement ("GET") et non en écriture ("POST" ou "PUT"). C'est la raison pour laquelle on privilégie un contrôle optimiste de la concurrence.

On traite ci-dessous l'exemple d'un registre ayant pour contenu un entier. C'est l'exemple le plus simple d'un serveur possédant un état modifiable. Le traitement de la concurrence par un contrôle optimiste a cependant une valeur paradigmatique : il peut être appliqué à des serveurs dont l'état est bien plus complexe.

2 Registre : les règles chimiques

Voici la spécification des registres que nous allons implémenter, du plus simple au plus complexe.

2.1 Serveur "stateless" sans contrôle de concurrence

Serveur

  • deux canaux : get et set
  • état : Ressource(x)
- get(rep) & Ressource(x) -> rep(x) & Ressource(x)
- set(rep, y) & Ressource(x) -> rep(y) & Ressource(y)

Un client incrémentant (deux fois) la ressource

  • canaux :
    • lecture pour la réception des get
    • ecriture1 et ecriture2 pour la réception des set
  • état : Affichage(x)
- get(lecture) // Etat initial
- lecture(x) -> set(ecriture1, x + 1)
- ecriture1(x) -> Affichage(x) & set(ecriture2, x + 1)
- ecriture2(x) -> Affichage(x) 

(double incrémentation avec affichage des valeurs écrites)

L'exécution de deux tels clients ne produit pas les incrémentations attendues : des pertes en écriture sont possibles. La propriété de sérialisabilité n'est pas vérifiée : toute exécution n'est pas équivalente à une exécution séquentielle des clients.

2.2 Serveur avec contrôle optimiste de la concurrence

Pour éviter les pertes en écriture, on munit la ressource d'une version. L'écriture n'est possible que si le client connaît la version courante de la ressource.

Serveur

  • deux canaux : get et set
  • état : Ressource(x), Version(n)
- get(rep) & Ressource(x) & Version(n) 
    -> rep(x, n) & Ressource(x) & Version(n)
// Ecriture réussie
- set(rep, y, n) & Ressource(x) & Version(n) 
    -> rep(OK, y, n + 1) & Ressource(y) & Version(n + 1) 
// Ecriture annulée
- set(rep, y, m) & Ressource(x) & Version(n) & (n != m)
    -> rep(KO, x, n) & Ressource(x) & Version(n)

Une requête set est accompagnée de la dernière version connue par le client. Si elle correspond à la version courante du serveur, le serveur réalise l'écriture, sinon il envoie la valeur courante de la ressource accompagnée de sa version. Les pertes en écriture sont ainsi évitées.

2.3 Serveur avec ajout d'un cache côté client

On peut améliorer l'efficacité de l'interaction en introduisant un cache côté client. Lorsque le client demande à lire la valeur de la ressource (via une requête get), le serveur peut lui répondre de regarder dans son cache s'il connaît la version courante.

Serveur

// Lecture à réaliser en cache
- get(rep, n) & Version(n) 
    -> rep(CACHE, n) & Version(n)
// Lecture de la ressource
- get(rep, m) & Ressource(x) & Version(n) & (n != m)
    -> rep(x, n) & Ressource(x) & Version(n) 
// Ecriture réussie
- set(rep, y, n) & Ressource(x) & Version(n) 
    -> rep(OK, y, n + 1) & Ressource(y) & Version(n + 1)
// Ecriture annulée 
- set(rep, y, m) & Ressource(x) & Version(n) & (n != m) 
  -> rep(KO, x, n) & Ressource(x) & Version(n) 

Une requête get est accompagnée de la dernière version connue par le client. Si elle correspond à la version courante du serveur, le serveur répond que la valeur n'a pas changé et doit donc être trouvée dans le cache du client, sinon il envoie la valeur courante de la ressource accompagnée de sa version. La charge de travail du serveur diminue ainsi.

2.4 Client avec cache

Version répartie (simple)

Approche modulaire avec l'ajout d'un intercepteur : le client communique avec l'intercepteur qui communique avec le serveur. L'intercepteur gère les versions.

2.4.1 Client

Le client communique avec l'intercepteur. Lorsqu'il réalise une écriture, sa requête peut échouer : il reçoit alors une réponse KO au lieu de OK.

  • canaux fournis :
    • lecture pour la réception des get
    • ecriture1 et ecriture2 pour la réception des set
  • canaux requis :
    • getI et setI (fournis par l'intercepteur)
  • état : Affichage(x)
// Etat initial 
- getI(lecture)

// Première incrémentation
- lecture(x) -> setI(ecriture1, x + 1)
// Echec avec reprise
- ecriture1(KO, x) -> setI(ecriture1, x + 1)
// Succès et seconde incrémentation
- ecriture1(OK, x) -> Affichage(x) & setI(ecriture2, x + 1)
// Echec avec reprise
- ecriture2(KO, x) -> setI(ecriture2, x + 1)
// Succès
- ecriture2(OK, x) -> Affichage(x) 

Le client gère la reprise de la transaction.

2.4.2 Intercepteur

L'intercepteur intercepte les requêtes du client qu'il transmet au serveur. Il gère le cache et annote les messages avec la version du cache.

  • canaux fournis :
    • lecture[k] pour la réception des get (k étant un canal du client)
    • ecriture[k] pour la réception des set (k étant un canal du client)
    • getI et setI (interception)
  • canaux requis :
    • get et set (serveur)
  • état : Cache(x, n)
// Etat initial 
- Cache(NON_DEFINI, NON_DEFINI)

// Interception d'une requête en lecture
- getI(k) & Cache(x, n) 
    -> get(lecture[k], n) & Cache(x, n)
// Réponse demandant la lecture en cache
- lecture[k](CACHE, n) & Cache(x, n) 
    -> k(x) & Cache(x, n)
// Réponse transmettant le résultat et mise en cache
- lecture[k](y, m) & Cache(x, n) 
    -> k(y) & Cache(y, m)

// Interception d'une requête en écriture 
- setI(k, y) & Cache(x, n) 
    -> set(ecriture[k], y, n) & Cache(x, n)
// Ecriture réussie avec mise à jour du cache 
- ecriture[k](OK, y, m) & Cache(x, n)
    -> k(OK, y) & Cache(y, m)
// Ecriture annulée avec mise à jour du cache
- ecriture[k](KO, y, m) & Cache(x, n) 
    -> k(KO, y) & Cache(y, m)

3 Exercice pratique : implémentation du registre

On cherche à implémenter le registre précédent par un service Web Restful.

3.1 Modèle objet

Le modèle objet est fourni.

  • Interface ServiceRegistre déclarant deux méthodes, get et set
  • Classe Registre implémentant l'interface ServiceRegistre
  • Classe Ressource utilisée par ServiceRegistre et Registre

3.2 Serveur : registre sans contrôle de la concurrence ni cache

Créer un nouveau projet, de type Dynamic Web Project. Ajouter la bibliothèque CXF Runtime si elle n'est pas présente. Importer l'archive tp2_registre_serveur.zip à partir de la racine. Trois paquets sont importés :

  • modele : le modèle objet
  • infrastructure.jaxrs : filtres utilisés pour le contrôle de la concurrence et le cache
  • serveur : une classe principale permettant de lancer un serveur déployant le service

3.2.1 Annotations JAX-RS

Annoter les méthodes de l'interface ServiceRegistre de manière à vérifier les propriétés suivantes.

  1. Chaque méthode est qualifiée par le type idoine de méthode HTTP.
  2. Les chemins relatifs d'accès aux ressources correspondant aux deux méthodes sont les suivants :
    • set : registre,
    • get : registre.

    Ainsi, si l'URI de base est http://localhost:8080/Projet, une URI sera par exemple : http://localhost:8080/Projet/registre. (Pour indiquer un chemin C, on utilise l'annotation @Path("C").)

  3. Les données sont produites ou consommées au format application/xml. (Pour indiquer qu'un résultat est sérialisé suivant un certain format javax.ws.rs.core.MediaType.F, on utilise l'annotation @Produces(MediaType.F) ou @Consumes(MediaType.F) après importation de MediaType.)

(Toutes ces annotations appartiennent au paquet javax.ws.rs.)

3.2.2 Annotations JAX-B

Annoter la classe Ressource de manière à vérifier les propriétés suivantes.

  1. Une instance de la classe Ressource peut être traduite en un document XML racine.
  2. L'attribut i se traduit par un élément XML de nom x. (On annotera le getter correspondant.)

Cf. les annotations XmlRootElement et XmlElement.

3.2.3 Déploiement

Vérifier que les deux fichiers de configuration web.xml et restful-beans.xml sont bien placés dans le répertoire WebContent/WEB-INF.

Déployer le service de deux manières, sur le serveur Tomcat ou sur un serveur inclus dans CXF (Jetty) en utilisant la classe Lancement (après avoir modifié l'URI pour coïncider avec celle utilisée par Tomcat, *http://localhost:8080/Projet*, où Projet est le nom du projet). Noter qu'on ne peut pas les lancer simultanément.

Cf. http://cxf.apache.org/docs/jaxrs-services-configuration.html pour plus de détails.

Une fois lancé un des deux serveurs, vous pouvez tester votre service (en utilisant Poster dans Firefox).

Address: http://localhost:8080/Projet/registre
Http-Method: GET
--------------------------------------
Response-Code: 200
Content-Type: application/xml
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>0</x></ressource>

Address: http://localhost:8080/TestTP2_restServeurRegistre/registre
Http-Method: PUT
Content-Type: application/xml; charset=UTF-8
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>7</x></ressource>
--------------------------------------
Response-Code: 200
Content-Type: application/xml
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>7</x></ressource>

3.2.4 Premier client

Créer un nouveau projet Java en y incluant la bibliothèque de CXF (Runtime). Y importer à la racine l'archive tp2_registre_client.zip. Trois paquets sont importés :

  • modele : le modèle objet sans l'implémentation du service
  • infrastructure.jaxrs : filtre-intercepteur utilisé pour le contrôle de la concurrence et le cache
  • client : une classe principale permettant de consommer le service

Annoter l'interface ServiceRegistre comme sur le serveur.

Annoter la classe Ressource comme sur le serveur.

Etudier la fonction principale de la classe client.TestRegistre. Configurer correctement le proxy.

Tester votre application cliente.

Que constatez-vous lorsque vous lancez plusieurs clients simultanément ?

Remarque : CXF 2 n'implémente pas complètement la norme JAX-RS 2 côté client. En particulier, la fabrique de proxy est propre à CXF 2. En revanche, la version CXF 3 implémente la norme : cf. la documentation de CXF dédiée à l'API client de JAX-RS.

3.3 Serveur : registre avec contrôle de la concurrence et cache

Bibliographie : cf. Restful Web Services Cookbook, chap. 10.

On contrôle la concurrence à l'aide d'une gestion des versions : un client ne peut modifier la ressource que s'il a lu précédemment la dernière valeur de la ressource. On utilise aussi les versions pour gérer un cache situé chez le client : lorsqu'un client demande la valeur de la ressource alors qu'il connaît la version courante, le serveur peut répondre en envoyant une réponse vide.

3.3.1 Messages HTTP échangés

3.3.1.1 Requête get
Address: http://localhost:8080/Projet/registre
Http-Method: GET
Headers: {... if-none-match=[versionClient] ...}

Le client utilise la champ if-none-match de l'en-tête pour indiquer la version la plus récente qu'il connaît.

Si la version courante du serveur ne correspond pas à versionClient, alors répondre normalement à la requête en envoyant aussi la version courante (via le champ ETag ("Entity Tag")).

Response-Code: 200
Content-Type: application/xml
Headers: {... ETag=[versionCourante]}
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>valeurCourante</x></ressource>

Sinon, répondre que la ressource n'a pas été modifiée.

Response-Code: 304
Headers: {... ETag=[versionClient], Content-Length=[0]}
3.3.1.2 Requête set
Address: http://localhost:8080/Projet/registre
Http-Method: PUT
Content-Type: application/xml
Headers: {... if-match=[versionClient] ...}
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>nouvelleValeur</x></ressource>

Le client utilise la champ if-match de l'en-tête pour indiquer la version la plus récente qu'il connaît.

Si la version du serveur correspond à versionClient, alors exécuter normalement la requête et répondre en renvoyant la valeur écrite et la nouvelle version courante.

Response-Code: 200
Headers: {... ETag=[nouvelleVersion] ...}
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>nouvelleValeur</x></ressource>

Sinon répondre par une erreur ("412", précondition sur les versions invalide) en renvoyant la valeur courante et la version courante.

Response-Code: 412
Content-Type: application/xml
Headers: {... ETag=[versionCourante] ...}
Payload: <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>valeurCourante</x></ressource>

3.3.2 Documentation à engendrer

Voici la documentation à ajouter à l'interface ServiceRegistre (côté serveur) et à compléter (en ajoutant un schéma à engendrer à partir de la classe Ressource, New > Other > JAXB > Schema from Java Classes, cf. le répertoire schemas pour le résultat à obtenir).

/**
 * Service fournissant l'accès en lecture et en écriture à un registre.
 * <P>
 * Après déploiement en <code>htpp://Base</code>, le registre est accessible à
 * l'adresse <code>htpp://Base/registre</code>. Deux requêtes (http) sont
 * possibles :
 * <ul>
 * <li>requête <code>GET</code> pour lire la valeur du registre,
 * <li>requête <code>PUT</code> pour modifier la valeur du registre.
 * </ul>
 * L'état d'un registre, appelé <code>Ressource</code>, est décrit par un
 * document XML conforme au schéma suivant.
 * <PRE>
 * {@code 
 * <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    TODO : schéma à engendrer à partir du code
  </xs:schema>
 * }
 * </PRE>
 * @author hgrall
 *
 */
public interface ServiceRegistre {
  /**
   * Requête <code>PUT</code>.
   * <ul>
   * <li>Address: http://Base/registre
   * <li>Http-Method: PUT
   * <li>Content-Type: application/xml
   * <li>Headers: {... if-match=[versionClient] ...}
   * <li>Payload:
   * {@code <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>nouvelleValeur</x></ressource></pre>}
   * </ul>
   * Le client utilise la champ <code>if-match</code> de l'en-tête pour
   * indiquer la version la plus récente qu'il connaît.
   * 
   * Si la version du serveur correspond à <code>versionClient</code>, alors le
   * serveur répond normalement à la requête.
   * <ul>
   * <li>Response-Code: 204
   * <li>Headers: {... ETag=[nouvelleVersion] ...}
         * <li>Payload:
   * {@code <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>nouvelleValeur</x></ressource></pre>} 
   * </ul>
   * Sinon le serveur répond par une erreur (précondition sur les versions
   * invalide).
   * <ul>
   * <li>Response-Code: 412
   * <li>Content-Type: application/xml
   * <li>Headers: {... ETag=[versionCourante] ...}
   * <li>Payload: 
   * {@code <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>valeurCourante</x></ressource></pre>}
   * </ul>
   * @param n nouvelle valeur du registre
   */
  public Ressource set(Ressource n);

  /**
   * Requête <code>GET</code>.
   * <ul>
   * <li>Address: http://Base/registre
   * <li>Http-Method: GET
   * <li>Headers: {... if-none-match=[versionClient] ...}
   * </ul>
   * Le client utilise la champ <code>if-none-match</code> de l'en-tête pour
   * indiquer la version la plus récente qu'il connaît.
   * 
   * Si la version du serveur ne correspond pas à <code>versionClient</code>, alors
   * le serveur répond normalement à la requête en envoyant aussi la version
   * courante (via le champ <code>ETag</code>).
   * <ul>
   * <li>Response-Code: 200
   * <li>Content-Type: application/xml
   * <li>Headers: {... ETag=[versionCourante]}
   * <li>Payload:
   * {@code <?xml version="1.0" encoding="UTF-8" standalone="yes"?><ressource><x>valeurCourante</x></ressource>}
   * </ul>
   * Sinon, le serveur répond que la ressource n'a pas été modifiée.
   * <ul>
   * <li>Response-Code: 304
   * <li>Headers: {... ETag=[versionClient], Content-Length=[0]}
   * </ul>
   * @return valeur courante du registre
   */
  public Ressource get();
}

Engendrer la documentation (Project > Generate Javadoc).

Avec JAX-RS, il est possible de réaliser ces échanges de messages en utilisant des filtres.

3.3.3 Filtres

Etudier les filtres du paquet infrastructure.jaxrs.

Que fait le filtre Compter ?

Que fait le filtre InteragirAtomiquement ?

Que fait le filtre Cacher ?

Que fait le filtre RealiserTransactionOptimiste ?

Dans quel ordre s'exécutent-ils ?

Commenter toutes ces classes, avec un niveau de détail analogue à celui de la classe infrastructure.jaxrs.Cacher du projet client.

3.3.4 Annotation de l'interface ServiceRegistre

Annoter l'interface ServiceRegistre de manière à appliquer les filtres pour chaque appel de méthode. On utilisera les annotations définies dans le paquet infrastructure.jaxrs.annotations. Cf. la section 6.5.2 de la spécification de JAX-RS 2.0 pour des précisions complémentaires.

3.3.5 Déploiment des services

Mettre à jour la classe serveur.Lancement de manière à enregistrer les filtres.

Indication :

List<Object> filtres = new LinkedList<Object>();
filtres.add(new Filtre());
...
sf.setProviders(filtres);

Mettre à jour le fichier WebContent/WEB-INF/restful-beans.xml de manière à enregistrer les filtres.

Indication :

<jaxrs:server id="testServeurRestRegistre" address="/">
  ...
  <jaxrs:providers>
    <ref bean="filtre" />
  </jaxrs:providers>
  ...
</jaxrs:server>
...
<bean id="filtre" class="infrastructure.jaxrs.Filtre" />

3.4 Client avec cache

Etudier le filtre infrastructure.jaxrs.Cacher en suivant les commentaires.

Annoter l'interface ServiceRegistre de manière à utiliser le filtre.

Dans la classe principale client.TestRegistre, enregistrer le filtre comme pour le serveur.

Tester votre application, dans un contexte concurrent. Rajouter la reprise d'écriture en cas d'erreur "412".

do{
  s.setI(s.getI() + 1);
  s = proxyRegistre.set(s);
}while(cache.erreur412());

Afficher finalement le nombre total de reprises.

System.out.println("Reprises de transaction : " + cache.getReprises());

Last Updated 2016-04-08T11:10+0200. Comments or questions: Send a mail.