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.