Dans ce sujet, nous mettons en œuvre l'algorithme de Naimi-Tréhel qui assure un mécanisme d'exclusion mutuelle dans un système réparti. Pour un système de N éléments, la demande de la ressource critique est gérée avec en moyenne Log(N) messages. C'est un algorithme à jeton qui suppose aucune défaillance (aucune perte de message, aucune défaillance des N éléments du système). Pour écrire ce programme, vous n'êtes pas obligé de comprendre les détails de l'algorithme (l'énoncé détaille chacune des étapes de l'algorithme lorsqu'il demande de les mettre en œuvre), mais une compréhension générale de l'architecture globale du système est très utile.
Les N éléments forment deux structures qui vont évoluer au cours des échanges de messages :
Toutes les fois qu'un nœud veut obtenir la ressource en exclusion mutuelle, l'algorithme transforme l'arbre et la liste de façon à ce que ce nœud devienne la nouvelle racine de l'arbre ainsi que le dernier élément de la liste. Pour illustrer le principe de l'algorithme, regardons ce qui se passe lorsqu'un nœud réclame la ressource à partir de la situation initiale de la figure 1 ci-dessous :
ResourceRequest
        (« RRQ(4) » sur la figure) vers son
        père et se détache de l'arbre pour se préparer à devenir la
        nouvelle racine. L'algorithme remonte le
        message ResourceRequest jusqu'à la racine et tous
        les nœuds sur ce chemin prennent pour père l'émetteur
        du ResourceRequest (ici le nœud 4). Sur le
        deuxième schéma le RRQ(4) est arrivé dans la file
        d'attente TCP du nœud 2, mais n'est pas encore
        traité par le nœud 2 ;
      ResourceRequest à son ancien père, et
        ensuite a pris le nœud 4 pour père ;
      ResourceRequest est arrivé à la racine,
        qui est aussi le dernier de la liste.  Ce nœud cède ces deux
        rôles en prenant pour père le nœud 4 et en se
        positionnant l'avant dernier dans la liste chaînée (il ajoute
        le nœud 4 à la fin de la liste) ;
      ResourceRequest.
      
    Donc, en résumé, la demande de ressource émise par le nœud 4 a placé ce dernier à la racine de l'arbre et en dernier sur la liste. Le jeton n'a pas changé de propriétaire. Voyons maintenant la partie de l'algorithme qui gère ce changement du propriétaire du jeton.
Lorsque le nœud possédant le jeton n'a plus besoin de la ressource, il transmet le jeton au suivant de la liste chaînée et quitte la liste chaînée (cf. figure 2) :
Token) au suivant sur la liste. Il perd
        donc aussi l'étiquette T.  Après cette action, le
        nœud 2 n'est plus dans la liste chaînée, c'est-à-dire
        qu'il ne réclame plus la ressource. Notez aussi que plus
        aucun nœud ne possède le jeton (pas d'étiquette T),
        car il est actuellement dans le message Token
        dans le file d'attente TCP du nœud 3 ;
      Token.
      
    
    Enfin, regardons le cas particulier où la liste est réduite à un
    seul nœud : il est donc à la fois le premier de la liste (il
    possède le jeton), et le dernier de la liste (il est la racine de
    l'arbre).  Lorsque ce nœud libère la ressource, il n'y a plus de
    nœud suivant sur la liste à qui transmettre le jeton. Dans ce cas,
    le nœud conserve le jeton pour le transmettre au nœud émetteur du
    premier message ResourceRequest qu'il recevra.  Cela
    maintient la troisième propriété énoncée plus haut : si
    aucun nœud n'utilise la ressource, c'est le nœud racine qui
    conserve le jeton.
 
Vous disposer de 3h pour réaliser ce TP. Vous avez le droit aux documents suivants :
JAVA ≥ 17.
      Pour installer le sujet :
En fin de TP :
    La classe représentant un nœud est la
    classe tsp.csc4509.tpnote.Node. Dans la suite de ce
    sujet, nous désignons les nœuds par le
    terme Node.  Chaque Node
    du réseau est un élément de l'arbre, et éventuellement, s'il
    demande la ressource, un élément de la liste chaînée.  Du point de
    vue de la programmation réseau JAVA NIO,
    un Node est un serveur TCP de tous
    ses fils dans l'arbre et un serveur TCP de son
    prédécesseur dans la liste chaînée.  Et, par symétrie, il est un
    client TCP de son père dans l'arbre et un
    client TCP du suivant dans la liste chaînée.
  
    Chaque Node est piloté par
    une Console, une différente pour
    chaque Node. Elle sert à le contrôler (quand
    réclame-t-il la ressource et pour combien de temps ?). Du
    point de vue du réseau, la Console est un
    client TCP du Node.
  
    Enfin, pour simuler la ressource, un gestionnaire de
    ressource TaskServer (unique) est prêt à
    accepter la connexion des Nodes (mais une
    seule à un instant donné). Le TaskServer
    réalise une tâche qui, pour ce TP, se résume à l'appel
    de Thread.sleep(). Le TaskServer
    est donc un serveur TCP des Nodes.
  
    Tous les messages échangés entre ces éléments sont des objets Java
    sérialisés, et nous utilisons la
    classe FullDuplexMsgWorker écrite et utilisée durant
    les TPs pour faire ces échanges.
  
La liste des classes composants cette architecture réseau est donc :
tsp.csc4509.tpnote.Node : classe représentant un
        nœud. L'essentiel du code à écrire pour ce TP se trouve dans
        cette classe ;
      tsp.csc4509.tpnote.TaskServer : classe
        représentant la ressource critique à partager. Cette classe
        doit être écrite en quasi totalité ;
      tsp.csc4509.appli.Console : classe
        contrôlant chaque nœud pour
        réaliser les tests d'intégration. Le code de cette classe est
        totalement fourni.
      
    La liste des types des messages se trouve dans
    l'énumération tsp.csc4509.tpnote.MessType. On y
    trouve :
    
TASKREQUESTTYPE : le type des messages
        transportant un objet TaskRequest qui est
        envoyé par la Console
        au Node, et ensuite, lorsque
        le Node obtient le jeton,
        du Node
        au TaskServer ;
      RESOURCEREQUESTTYPE : le type des messages
        transportant un objet ResourceRequest qui
        est transmis de Node
        en Node jusqu'au nœud racine, comme nous
        l'expliquons dans l'introduction de ce sujet ;
      TOKENTYPE : le type des messages
        transportant un objet Token envoyé par
        un Node vers le Node
        suivant de la liste chaînée quand il libère la
        ressource ;
      TASKRESPONSETYPE : le type des messages
        transportant un objet TaskResponse émis
        par le TaskServer vers
        un Node une fois la tâche terminée, et qui
        est ensuite transmis par le Node à
        sa Console.
      
    Chaque Node de l'arbre est identifié grace à
    un NodeId (unique donc pour
    chaque Node). Ce NodeId est aussi
    le paramètre des ResourceRequests pour
    identifier le Node émetteur des requêtes. La
    classe NodeId possède deux attributs :
    
int num : le numéro
        du Node ;
      InetSocketAddress addr :
        l'adresse TCP/IP
        du Node. Lorsqu'un ResourceRequest
        est reçu par un Node, il doit utiliser
        cette adresse pour se connecter au Node
        émetteur.
      Le sujet offre les classes d'application déjà écrites :
AppliNode : cette classe démarre
        un Node. Elle a deux usages, un pour
        le Node racine, et un autre pour tous les
        autres Nodes ;
      AppliTaskServer : cette classe démarre
        le TaskServer ;
      Console : cette classe est sa propre classe
      d'application.
      Le sujet ne vous demande pas de les utiliser directement. Mais ces trois classes sont utilisées en fin de sujet dans les tests d'intégrations déjà écrits.
Il y a 9 questions :
TaskServer ;
      Node.
      Le barème est donné à titre d'indication.
Vous pouvez faire les questions dans l'ordre de votre choix, avec la contrainte que la dernière intègre toutes les autres et ne peut être testée après les autres testées comme correctes.
Cependant, certaines questions possèdent des thématiques communes :
Node ;
      Attention : ce sujet porte sur des échanges réseaux, et le comportement des échanges ne seront corrects que si vous pensez à fermer les connexions quand elles ne servent plus. La plupart du temps, le sujet n'en fera pas mention, et c'est donc à vous d'être vigilant sur cet aspect.
Des tests unitaires sont fournis pour les questions 1—8. Suivez les consignes qui vous sont données pour chacune de ses questions pour les utiliser.
TaskServer
    Cette classe, qui est dans le
    paquetage tsp.csc4509.tpnote, fournit le code
    du serveur de tâches. Lorsqu'un Node obtient
    le jeton, il se connecte à ce serveur, lui envoie
    un TaskRequest et attend en retour
    un TaskResponse. Le serveur TaskRequest
    ne gère qu'un seul Node à la fois, et donc
    reste en mode synchrone.
  
Cette classe possède deux attributs :
ServerSocketChannel listenChannel :
        le ServerSocketChannel du serveur qui sert à
        accepter les clients ;
      int resquestNumber : le compteur des tâches
        reçues par le serveur. Il sera aussi le résultat à placer dans
        le TaskResponse pour notre simulation de tâches.
      TaskServer
    Le constructeur de TaskServer possède un seul paramètre :
    
int port : le numéro du port occupé par le
        serveur de tâches.
      
    Écrivez le constructeur de TaskServer pour
    initialiser totalement l'attribut listenChannel pour
    qu'il soit prêt pour réaliser des accept().
  
Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant la
    méthode testTaskServer() de la classe de
    test TestTaskServer et lancez ce test.
  
TaskServer::loop()
    Écrivez la méthode TaskServer::loop() qui réalise la
    boucle du serveur de tâches.  Cette boucle (infinie, sauf en cas
    d'exception, que vous ne devez pas gérer) doit :
    
FullDuplexMsgWorker pour cette
        connexion ;
      TaskRequest ;
      resquestNumber ;
      Thread.sleep(1000 *
          taskRequest.getTaskDelay()); (en supposant
          que taskRequest est la référence sur
        le TaskRequest reçu) ;
      TaskResponse construit par
        l'instruction new
        TaskResponse(resquestNumber) ;
      Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant la
    méthode testLoop() de la classe de
    test TestTaskServer et lancez ce test.
  
Node
    La classe Node, qui est dans le
    paquetage tsp.csc4509.tpnote, réalise tout
    l'algorithme de Naimi-Tréhel. Elle a de nombreux attributs :
    
NodeId id : l'identifiant
        du Node qui contient son numéro ainsi que
        son adresse TCP/IP ;
      NodeId father et FullDuplexMsgWorker
          fatherConnexion : l'identifiant
          du Node père et la connexion vers
          ce Node. Ces deux attributs sont
          à null pour le Node
          racine. Ces deux attributs doivent toujours évoluer de pair
          (c'est-à-dire, en même temps) ;
      NodeId next et FullDuplexMsgWorker
          nextConnexion : l'identifiant
          du Node suivant dans la liste d'attente,
          et la connexion vers ce Node. Ces deux
          attributs sont à null si
          le Node n'attend pas la ressource ou
          s'il est le dernier de la liste d'attente. Ces deux
          attributs doivent toujours évoluer de pair
          (c'est-à-dire, en même temps) ;
      boolean havingToken : booléan à vrai pour
        le Node ayant le jeton, et à faux pour tous les
        autres ;
      boolean requesting : booléan à vrai pour
        les Nodes réclamant la ressource et à faux pour
        les autres ;
      ServerSocketChannel listenChannel : canal
        qui sert à accepter toutes les connexions
        (des Node fils, du Node précédent,
        et de la Console). Ce canal est géré en
        asynchrone ;
      Selector selector : Selector
        gérant les évènements asynchrones du Node ;
      Map<SelectionKey, FullDuplexMsgWorker>
          inConnexions : dictionnaire gérant toutes les
          connexions où une lecture asynchrone peut se produire
          (message venant d'un Node fils,
          du Node précédent, de
          la Console ou
          du TaskServer) ;
      TaskRequest taskRequest : attribut pour
        mémoriser le TaskRequest entre le moment où
        le Node le reçoit de
        la Console et le moment où il l'envoie
        au TaskServer ;
      InetSocketAddress serverAddr :
        adresse TCP/IP
        du TaskServer ;
      FullDuplexMsgWorker console : connexion vers
        la Console.
      
    Attention : il faut vous
    rappelez que father et fatherConnexion
    doivent évoluer ensemble. De même, next et
      nextConnexion doivent évoluer ensemble.
    Si, par exemple,
    l'instruction dit father = node i, il faut comprendre
    qu'il faut changer father avec le NodeId
    du Node i mais aussi
    connecter fatherConnexion au Node i.
  
    Les deux constructeurs de la classe sont déjà écrits et ont
    correctement initialisé le Node pour le placer
    dans l'arbre (soit à la racine, pour le premier constructeur, soit
    en fils d'un autre Node pour le second
    constructeur).  Les
    attributs inConnexions, serverAddr
    et selector sont
    initialisés par ces constructeurs.
  
    La classe fournit aussi deux versions de la
    méthode FullDuplexMsgWorker connectTo(), qui ouvre
    une connexion TCP synchrone et construit
    un FullDuplexMsgWorker pour gérer les échanges. La
    première version utilise une adresse TCP/IP
    (InetSocketAddress) déjà initialisée, et la seconde
    fonctionne avec le
    couple (hostName,portNumber). N'hésitez pas les
    utiliser pour les connexions qui ne demandent aucune lecture, et
    où le mode synchrone ne pose donc pas de problème. Pour
    rappel : un émetteur d'un message peut très bien faire
    ses envois en mode synchrone, tandis que le récepteur en face gère
    les lectures en mode asynchrone.
  
Il reste 7 méthodes à écrire, qui forment les 7 questions restantes du sujet. Vous pouvez faire les 6 premières dans l'ordre que vous voulez, elles ne dépendent pas les unes des autres. La 7è intégre tout ce qui a été écrit auparavant et ne peut être testée qu'une fois les précédentes terminées et testées. Vous pouvez cependant décider de l'écrire quand vous le voulez.
Node::sendTaskToServer()Cette méthode doit :
Node::serverAddr ;
      Node::selector (déjà
        initialisé par les constructeurs déjà écrits) ;
      FullDuplexMsgWorker pour cette connexion
        et l'ajouter à l'attribut
        dictionnaire Node::inConnexions (déjà initialisé
        par les constructeurs déjà écrits) pour préparer la réception
        asynchrone du TaskResponse que va envoyer
        le TaskServer ;
      TaskRequest au serveur de tâches
        (c'est l'attribut Node::taskRequest, qui lors de
        l'appel de la méthode Node::sendTaskToServer(), a
        déjà été initialisé).
      Écrivez la méthode sendTaskToServer().
Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant la
    méthode testSendTaskToServer() de la classe de
    test TestNode et lancez ce test.
  
Node::receivingResourceRequest()
    Cette méthode réalise les actions d'un Node lorsqu'il
    reçoit un ResourceRequest d'un Node
    fils.  En voici l'algorithme en langage libre :
  
    Pour rappel, resourceRequest.getNodeId() vous donne
    l'id du Node émetteur
    du ResourceRequest, et
    resourceRequest.getNodeId().getAddr() son
    adresse TCP/IP. De plus, dans l'arbre, aucun message ne
    circule dans le sens père vers fils, donc la connexion vers le
    père ne sert qu'à faire des écritures et peut rester synchrone.
  
    Pour rappel,
    attention : father
    et fatherConnexion doivent évoluer ensemble. De
    même, next et
      nextConnexion doivent évoluer ensemble.  Si, par
    exemple, l'instruction dit « father = node
    i », il faut comprendre qu'il faut
    changer father avec le NodeId
    du Node i mais aussi
    connecter fatherConnexion au Node
    i.
  
    Écrivez la méthode receivingResourceRequest().
  
Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant les
    méthodes testReceivingResourceRequest1(),
    testReceivingResourceRequest2() et testReceivingResourceRequest3()
    de la classe de
    test TestNode et lancez ces tests.
  
Node::requestingResource()
    Cette méthode réalise les actions d'un Node lorsqu'il
    reçoit un TaskRequest de sa Console.  En
    voici l'algorithme en langage libre :
  
    Écrivez la méthode requestingResource().
  
Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant les
    méthodes testRequestingResource1() et
    testRequestingResource2()
    de la classe de
    test TestNode et lancez ces tests.
  
Node::receivingToken()
    Cette méthode réalise les actions d'un Node
    lorsqu'il reçoit le Token du Node
    précédent dans la liste chaînée.  En voici l'algorithme en langage
    libre :
  
    Écrivez la méthode receivingToken().
  
Les tests :
   Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
   retirez l'annotation @Disabled devant la
   méthode testReceivingToken() de la classe de
   test TestNode et lancez ce test.
  
Node::releasingResource()
    Cette méthode réalise les actions du Node qui détient
    le jeton lorsqu'il libère la ressource.  En voici l'algorithme en
    langage libre :
  
    Écrivez la méthode releasingResource().
  
Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant les
    méthodes testReleasingResource1() et
    testReleasingResource2()
    de la classe de
    test TestNode et lancez ces tests.
  
Node::openListenChannel()
    Cette méthode réalise l'ouverture du ServerSocketChannel
      listenChannel du Node.
  
    Écrivez la méthode openListenChannel() qui initialise
    l'attribut listenChannel du Node
    pour que ce ServerSocketChannel soit prêt à faire
    des accept() en asynchrone sur le
    port myPort passé en paramètre. Le nouveau canal
    (listenChannel) est enregistré auprès
    du Selector du Node
    (attribut selector déjà créé et initialisé par les
    constructeurs déjà écrits).
  
Les tests :
     Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disabled devant la
    méthode testOpenListenChannel()
    de la classe de
    test TestNode et lancez ce test.
  
Node::algoNaimiTrehel()Cette méthode réalise l'algorithme de Naimi-Tréhel en intégrant dans la boucle d'un serveur asynchrone les méthodes écrites jusqu'ici.
Écrivez la boucle (infinie, sauf en cas d'exceptions que vous n'avez pas à gérer) d'un serveur asynchrone.
Cette boucle doit :
select() du selector
        (attribut de Node, déjà créé par les
        constructeurs déjà écrits) ;
      inConnexions
            (attribut de Node, déjà créé et
            initialisé par les constructeurs déjà écrits) ;
          RESOURCEREQUESTTYPE,
            appelez la méthode receivingResourceRequest()
            de la question 4 ;
          TOKENTYPE, appelez la
            méthode receivingToken() de la
            question 5 ;
          TASKREQUESTTYPE :
            console avec
                le FullDuplexMsgWorker utilisé pour lire
                ce message ;
              requestingResource()
                de la question 6.
              TASKRESPONSETYPE :
            releasingResource() de
                la question 7 ;
              TASKRESPONSETYPE à
                la Console.
              
    La boucle doit aussi réaliser toutes les actions que l'on attend
    pour une boucle de lectures asynchrones (gestion
    des close(), des clear(), etc.)
  
Les tests :
    Pour réaliser les tests d'intégration, nous ajoutons dans le code
    du Node une sonde qui va envoyer à un
    programme de log de toutes les évolutions de l'état de
    chaque Node. Pour cela il faut ajouter dans votre
    code, dans la boucle juste avant
    l'instruction selector.select() la ligne
    suivante :
  
Ensuite pour passer les tests, lancez depuis la racine du projet (le répertoire où il y a le fichier pom.xml) les scripts suivants :
./TestInteg1.sh
      ./TestInteg2.sh
      ./TestInteg3.sh
      ./TestInteg4.sh
      ./TestInteg5.sh
      ./TestInteg6.sh
      Les tests de terminent par la ligne Test OK lorsqu'ils réussissent.
Sinon, vous pouvez exploiter le log pour comprendre l'erreur. Pour cela, il faut comprendre fonctionnement de ces tests.
Les tests d'intégration :
Nodes ;Si le log correspond au fichier de référence, le test réussit, sinon le test échoue. Si votre code contient des affichages de Log, ils sont affichés durant tout le déroulement, mais dans tous les cas, le test termine son affichage par un résumé, et son statut (échec, ou réussi). Voici par exemple la fin d'un test qui échoue :
Les fichiers de log à :
    La différence à noter entre les deux fichiers est l'absence des
    deux dernières lignes dans le fichier du test en échec (à
    gauche). Pour information, l'erreur vient du fait que,
    dans releasingResource, on a oublié de
    mettre requesting à faux.
  
    Une nouvelle ligne de log est générée à chaque changement
    d'un Node :
    
Node. Les Nodes
        sans père pointent sur X (cela peut concerner
        plusieurs Nodes, car
        lorsqu'un ResourceRequest circule, un nouvel
        arbre est en construction) ;
      Node suivant pour
        chaque Node.
        Les Nodes sans suivant pointent
        sur X ;
      Nodes
        indiquent l'état de leurs booléens.
        T est indiqué pour les Nodes
        avec havingToken à vrai, et
        R est indiqué pour les Nodes
        avec requesting à vrai.
      Pour la lecture de ce log :
Node 1, racine, puis
        le Node 2, fils de la racine) ;
      TaskRequest pour le début du changement de
        l'arbre (le Node 2 se détache de
        l'arbre) ;
      Node
        ne le possède) ;
      Node 2 possède la ressource et
        peut faire sa requête.
      
    Le log s'arrête avec le Node 2, toujours
    étiqueté R, alors que toutes les requêtes devraient avoir
    été traitées. Il y a donc un bug.
  
Lorsque le test échoue, il indique aussi le fichier de référence. Vous pouvez le consulter pour voir où le log diffère de cette référence.
Ce TP noté est terminé et vous avez écrit le code permettant de mettre en œuvre l'algorithme réparti de Naimi-Tréhel.
Félicitations !