Plusieurs personnes veulent compter leurs contributions à un travail collectif. En AP6, nous avons écrit une boucle qui conserve un nombre : un message arrive, la boucle calcule la nouvelle valeur et se rappelle avec elle. Nous allons donner à cette boucle une petite API, des tests et une place dans une application supervisée.
Ouvrir le projet
Téléchargez le squelette compteur-local,
décompressez-le à la racine de votre dépôt ap-homework, puis ouvrez dans
votre éditeur le dossier compteur-local/, celui qui contient mix.exs.
Le README.md donne les fichiers à compléter et les commandes de test par
palier. Les premiers échecs, « à compléter », indiquent les fonctions encore
à écrire.
Le projet n’a aucune dépendance externe. Il se travaille avec mix test
et iex -S mix ; Livebook peut rester votre brouillon pour essayer une fonction.
D’abord une valeur, puis un serveur
Nous reprenons la différence entre calculer une nouvelle valeur et conserver
cette valeur entre deux appels. Le processus qui la conserve est le
propriétaire de l’état : lui seul la lit et la remplace, et tous les autres
lui envoient des requêtes. Un bref essai avec
Agent, un processus tout fait pour
garder une seule valeur, permet de manipuler cet état sans réécrire la boucle
de réception. Nous passons ensuite à
GenServer pour définir les
messages acceptés par notre compteur.
Un GenServer sépare deux côtés. Côté client, GenServer.call(server, message)
envoie une requête et attend sa réponse. Côté serveur, un callback
handle_call(message, _from, etat) reçoit la requête avec l’état courant et
rend {:reply, reponse, nouvel_etat} : la réponse part vers le client, le
nouvel état est conservé pour la requête suivante. Chaque clause est une
transition : à partir d’un état et d’une requête, elle calcule une réponse
et l’état suivant, sans rien modifier.
Vous complétez les transitions et les réponses du serveur : incrémenter, lire, remettre à zéro. Les signatures de l’API, la lecture côté serveur et le démarrage sont fournis. Retrouvez dans chaque callback les trois éléments de la boucle d’AP6 : le message, l’état reçu et l’état conservé pour la suite.
Plusieurs clients peuvent-ils perdre une contribution ?
Deux clients lisent 0, puis demandent chacun d’écrire 1. Quel est le total ?
Il n’est que 1 : la seconde écriture a recouvert la première, car le serveur
n’a vu que deux requêtes « écris 1 ». En revanche, si chaque client demande
au serveur d’incrémenter, le serveur réalise chaque transition entièrement
avant la suivante ; on dit que la transition est atomique. Nous vérifions
cette propriété avec cent clients lancés par Task.async, un processus
temporaire par client, et nous attendons leurs réponses avec
Task.await_many avant de lire le total.
Ne découpez donc pas une incrémentation en un get puis un set côté client.
La boucle séquentielle du serveur ne rend pas atomique une suite de plusieurs
appels. Le squelette fait répondre increment/2 avec le nouveau total, afin
que le client sache quand sa contribution a été prise en compte.
Que garantit le superviseur ?
Une fois le compteur fonctionnel, nous provoquons son arrêt et retrouvons son remplaçant. L’application le démarre sous un superviseur, un processus qui lance ses enfants et les relance s’ils s’arrêtent. Avant l’expérience, prédisez : le nom répondra-t-il encore, et quel total annoncera-t-il ? Le nom enregistré permet de retrouver le service ; le PID change. La valeur repart à zéro : nous avons obtenu un redémarrage, sans stockage durable.
Pour la reprise, conservez une trace avant/après et expliquez la différence entre disponibilité et conservation des données. En AP8, nous ajouterons plusieurs copies de l’état, pour continuer à compter quand le serveur est inaccessible. La réplication demandera une règle de fusion.
Si une forme de GenServer vous manque en cours de route, le chapitre 10 du poly en donne le gabarit complet avec ses sorties.
