La Compensation de Latence

Aparté 7.5

Traduction complétée à

Au programme de ce chapitre :

  • Comprendre la compensation de latence.
  • Ralentissez votre application et voyez ce qu'il se passe.
  • Apprendre comment les méthodes Meteor interagissent entre elles.
  • Dans le dernier chapitre, nous avons présenté un nouveau concept du monde Meteor : les Méthodes.

    Sans compensation de latence
    Sans compensation de latence

    Une Méthode Meteor est un moyen d'exécuter une série de commandes sur le serveur d'une façon structurée. Dans notre exemple, nous avons utilisé une Méthode car nous voulions nous assurer que les nouveaux articles seraient tagués avec le nom et l'id de leur auteur ainsi que l'heure du serveur.

    Cependant, si Meteor exécute des Methodes dans sa plus simple façon, nous aurions un problème. Considérez la séquence suivante d’événements (note : les horodatages sont des valeurs aléatoires choisies seulement pour l'exemple) :

    • +0ms: L'utilisateur clique sur un bouton et le navigateur renvoie un appel de Méthode.
    • +200ms: Le serveur fait des changements dans la base de données Mongo
    • +500ms: Le client reçoit ces changements, met à jour l'interface utilisateur.

    Si Meteor opérait de cette façon, il y aurait alors un petit décalage entre la réalisation de ces actions et l'affichage des résultats (ce décalage étant plus ou moins visible selon votre proximité avec le serveur). Nous ne pouvons nous le permettre dans une application web moderne !

    Compensation de latence

    Avec compensation de latence
    Avec compensation de latence

    Pour éviter ce problème, Meteor introduit un concept appelé Compensation de latence. Quand nous avons définit notre Méthode post, nous l'avons placée à l'intérieur d'un fichier dans le répertoire collections/. Ceci signifie qu'elle est disponible pour le serveur et le client – et elle s'exécutera sur les deux en même temps !

    Quand nous faisons un appel de Méthode, le client envoie l'appel au serveur, mais simule également simultanément l'action de la Méthode sur ses collections côté client. Notre workflow devient :

    • +0ms: L'utilisateur clique sur un bouton et le navigateur renvoie un appel de Méthode.
    • +0ms: Le client simule l'action de l'appel de Méthode sur les collections côté client et met à jour l'interface utilisateur pour refléter ceci.
    • +200ms: Le serveur fait les changements dans la base de données Mongo.
    • +500ms: Le client reçoit ces changements et annule ses changements simulés, en les remplaçant par les changements du serveur (qui sont généralement les mêmes). L'interface utilisateur change pour refléter ceci.

    Le résultat pour l'utilisateur est de voir les changements instantanément. Quand la réponse du serveur revient un peu plus tard, il peut y avoir (ou non) des changements visibles à mesure que les documents canoniques du serveur arrivent. Une chose à retenir est donc que nous devons nous assurer que nous simulons les documents aussi proches de la réalité que possible.

    Observer une compensation de latence

    Nous pouvons faire un petit changement à l'appel de la méthode post pour voir cela en action. Pour ce faire, nous utiliserons la fonction bien pratique Meteor._sleepForMs() pour retarder l'appel de la méthode de 5 secondes, mais (c'est un point crucial) seulement sur le serveur.

    Nous utiliserons isServer pour demander à Meteor si la Méthode est actuellement invoquée sur le client (comme une “ébauche”) ou sur le serveur. Une ébauche est la simulation d'une Méthode que Meteor exécute sur le client en parallèle, pendant que la “vraie” Méthode est en cours d'exécution sur le serveur.

    Nous allons donc demander à Meteor si le code est en cours d'exécution sur le serveur. Si c'est le cas, nous allons retarder l'avancement des choses de 5 secondes et ajouter la chaîne de caractère (server) à la fin du titre de notre article. Sinon, nous ajouterons la chaîne de caractère (client) :

    Posts = new Mongo.Collection('posts');
    
    Meteor.methods({
        postInsert: function(postAttributes) {
            check(this.userId, String);
            check(postAttributes, {
                title: String,
                url: String
            });
    
          if (Meteor.isServer) {
                postAttributes.title += postAttributes.title + "(server)";
                // attente de 5 secondes
                Meteor._sleepForMs(5000);
            } else {
                postAttributes.title += "(client)";
            }
    
            var postWithSameLink = Posts.findOne({url: postAttributes.url});
            if (postWithSameLink) {
                return {
                    postExists: true,
                    _id: postWithSameLink._id
                }
            }
    
            var user = Meteor.user();
            var post = _.extend(postAttributes, {
                userId: user._id, 
                author: user.username, 
                submitted: new Date()
            });
    
            var postId = Posts.insert(post);
    
            return {
                _id: postId
            };
        }
    });
    
    collections/posts.js

    Si nous nous arrêtions ici, la démonstration ne serait pas concluante. Dans l'état des choses, on dirait que le formulaire de soumission d'article se met en pause pendant 5 secondes avant de rediriger vers la liste des posts principaux, et pas grand chose d'autre se passe.

    Pour comprendre cela, revenons au manager de l'événement de soumission de post :

    Template.postSubmit.events({
        'submit form': function(e) {
          e.preventDefault();
    
          var post = {
              url: $(e.target).find('[name=url]').val(),
              title: $(e.target).find('[name=title]').val()
            };
    
          Meteor.call('postInsert', post, function(error, result) {
            // affiche l'erreur à l'utilisateur et s'interrompt
              if (error)
                  return alert(error.reason);
    
              // affiche ce résultat mais 'route' tout de même
                if (result.postExists)
                    alert('This link has already been posted');
    
                Router.go('postPage', {_id: result._id});
            });
        }
    });
    
    client/templates/posts/post_submit.js

    Nous avons placé notre appel de routeur Router.go() à l'intérieur du callback de l'appel de la méthode. Ce qui signifie que le formulaire attend que cette méthode réussisse avant de rediriger.

    C'est normalement la bonne manière de faire les choses. Après tout, vous ne pouvez pas rediriger l'utilisateur avant de savoir si la soumission de son post est valide ou pas, parce que ce serait extrêmement déroutant d'être redirigé une première fois et ensuite d'être redirigé une nouvelle fois, en arrière, vers la page de soumission de post pour corriger vos données, tout cela en quelques secondes.

    Mais pour le bien de cet exemple, nous voulons voir les résultats de nos actions immédiatement. Ainsi, nous allons changer l'appel du routeur pour rediriger vers la route postsList (nous ne pouvons pas diriger vers le post car nous ne connaissons pas son _id en dehors de la méthode), pour ensuite l'extraire du callback, et voir ce qui se passe :

    Template.postSubmit.events({
      'submit form': function(event) {
        event.preventDefault();
    
        var post = {
            url: $(e.target).find('[name=url]').val(),
            title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
            // affiche l'erreur à l'utilisateur et s'interrompt
          if (error)
            return alert(error.reason);
    
          // affiche se résultat mais 'route' tout de même
          if (result.postExists)
            alert('This link has already been posted');
        });
    
        Router.go('postsList');
    
      }
    });
    
    client/templates/posts/post_submit.js

    Commit 7-5-1

    Démonstration de l'ordre d'apparition des articles en uti…

    Si nous créons un article maintenant, nous verrons clairement la compensation de latence en action. Premièrement, un article est inséré avec (client) dans le titre (le premier article dans la liste, relié à Github) :

    Notre article comme initialement stocké dans la collection côté client
    Notre article comme initialement stocké dans la collection côté client

    Ensuite, cinq secondes plus tard, il est proprement remplacé avec le vrai document qui a été inséré par le serveur :

    Notre article une fois que le client reçoit la mise à jour de la collection côté serveur
    Notre article une fois que le client reçoit la mise à jour de la collection côté serveur

    Méthodes des collections sur le client

    Vous pourriez penser que les Méthodes sont compliquées après cela, mais en fait elles peuvent être plutôt simples. En fait, nous avons déjà vu trois méthodes très simples : les Méthodes de mutation de collection, insert, update et remove.

    Quand vous définissez une collection serveur appelée 'posts', vous êtes implicitement en train de définir trois Méthodes : posts/insert, posts/update et posts/delete. En d'autres mots, quand vous appelez Posts.insert() sur votre collection client, vous appelez une Méthode de compensation de latence qui fait deux choses :

    1. Des vérifications pour voir si vous faites la mutation en appelant des callbacks allow et deny (cependant il n'est pas nécessaire que cela arrive dans la simulation).
    2. La modification vers le stockage de données sous-jacent.

    Des méthodes qui appellent des Méthodes

    Si vous suivez bien, vous venez peut-être juste de réaliser que notre Méthode post appelle une autre Méthode (posts/insert) quand nous insérons notre article. Comment ça marche ?

    Quand la simulation (version côté client de la Méthode) est en cours d'exécution, nous effectuons la simulation d'un insert (nous insérons donc dans notre collection cliente), mais nous ne faisons pas le vrai, insert côté serveur, nous attendons que la version côté serveur de post le fasse.

    Par conséquent, quand la Méthode post côté serveur appelle insert il n'y a pas besoin de s'inquiéter de la simulation, et l'insertion continue sans encombre.

    N'oubliez pas comme précédemment de supprimer les changements effectués au cours de ce chapitre avant de passer au suivant.