À dire : Bonne année à toutes et à tous ! Nous ouvrons aujourd'hui la quatrième partie du cours, celle que j'appelle « le parallélisme ». Jusqu'ici, chacun de nos programmes faisait un seul calcul, du début à la fin ; or un central téléphonique, un serveur web ou le tableau de contributions que nous construirons d'ici AP8 servent beaucoup de monde à la fois. Pendant trois séances, nous allons donc faire coopérer plusieurs calculs : comment ils se parlent, où ils gardent leurs données, et ce qui se passe quand l'un d'eux tombe en panne. J'introduirai chaque idée par un court exposé, juste avant la partie du carnet qui la fait pratiquer. Voyons d'abord comment la séance s'organise. Repères : avant le lancement, quiz d'ouverture sur AP5 (quiz/quiz-6-apprentissage, chapitre 8 du poly), dix minutes de réponse et cinq de correction, pris sur la marge du créneau. Vérifier que le terminal et l'éditeur ont été préparés avec la page « Pré cours 6 » : on ne s'en sert qu'en fin de séance, mais un poste sans mix se répare pendant les ateliers. Ouverture et premier exposé : 20 min en tout.
À dire : Le carnet est notre support principal : il reprend en détail ce que je vais dire, et vous pourrez le relire seul après la séance. Pour chacune des cinq parties, je présente l'idée en quelques minutes, vous travaillez dans les sections indiquées, puis nous reprenons ensemble vos essais. Nous restons dans Livebook presque toute la séance ; les vingt dernières minutes se passent dans le terminal et l'éditeur. Avant chaque exécution, écrivez votre prédiction dans une cellule de texte : c'est elle qui nourrira nos reprises. Ouvrez le carnet maintenant, et partons d'une question simple : pourquoi voudrait-on plusieurs calculs à la fois ? Repères : 2 min. Le carnet est autonome, sans bibliothèque à installer ni carnet précédent à recopier. Annoncer dès maintenant la précaution Livebook : un échange complet (adresse de retour, spawn, send, receive) tient dans une seule cellule, et les cellules de test arrêtent leurs processus dans try ... after. Si une cellule reste bloquée, arrêter son évaluation ou redémarrer le runtime.
À dire : Pensez au central téléphonique de la keynote d'AP1 : des milliers de conversations sont en cours, et aucune ne doit attendre qu'une autre se termine. Un serveur web est dans la même situation, tout comme le tableau de contributions auquel toute la classe écrira. Si le programme traite les demandes une par une, du début à la fin, un seul client lent, ou une seule panne, bloque tous les autres. Il nous faut donc plusieurs activités qui avancent en même temps. Avant de voir comment Elixir s'y prend, distinguons deux mots que l'on confond souvent. Repères : 2 min ; l'exposé de la partie 1 dure 18 min jusqu'à la consigne. Faire citer par la classe un service utilisé par beaucoup de monde à la fois (messagerie, ENT, jeu en ligne). Aucun chiffre de performance ici.
À dire : Le temps se lit de gauche à droite. Sur la première ligne, la machine n'a qu'un cœur : elle fait avancer A, puis B, puis de nouveau A, si bien que les deux activités sont en cours sur la même période sans jamais s'exécuter au même instant. C'est la concurrence, une façon d'organiser le programme. En bas, deux cœurs exécutent réellement A et B en même temps : c'est le parallélisme, une propriété de l'exécution. Un programme concurrent bien découpé profite du parallélisme quand la machine en offre, mais il doit rester juste sur un seul cœur. Dans ce cours, c'est la concurrence qui nous occupe ; la machine virtuelle d'Elixir répartit elle-même le travail sur les cœurs. Voyons maintenant ce qui arrive quand deux activités touchent la même donnée. Repères : 2 min. Les longueurs des segments sont des possibilités d'ordonnancement, pas des mesures. Ne rien promettre sur l'accélération : nos ateliers montreront la correction, pas la vitesse. Le chapitre 9 du poly reprend ces deux définitions.
À dire : Prenons un exemple en Python. Deux fils d'exécution, c'est-à-dire deux exécutions du même programme qui partagent sa mémoire, appellent chacun contribuer une fois. Nous attendons 2 à la fin. Or la ligne compteur += 1 n'est pas une action unique : le fil lit la valeur, calcule la somme, puis écrit le résultat. Entre la lecture et l'écriture, rien n'empêche l'autre fil d'agir. Regardons un ordre possible. Repères : 2 min. Ne pas lancer de démonstration Python en direct : selon la version et la machine, la perte est rare et difficile à reproduire. C'est l'existence d'un entrelacement possible qui rend le programme faux.
À dire : Ce tableau se lit de haut en bas, une ligne par instant et une colonne par fil. A lit 0, puis B lit aussi 0 avant que A ait écrit ; chacun écrit ensuite 0 + 1, et la seconde écriture écrase la première. Deux contributions ont eu lieu, et le compteur n'en garde qu'une ! On appelle condition de course, race condition en anglais, un résultat qui dépend de l'ordre dans lequel les fils s'entrelacent. Le plus traître est que cet ordre fautif est rare : le programme passe la plupart des essais, puis se trompe un jour en production. Les langages à mémoire partagée proposent des verrous, mais il faut penser à les poser partout. Erlang, et donc Elixir, ont fait un autre choix. Repères : 3 min. Gardez ce tableau en tête : la même forme reviendra en AP7, avec des messages au lieu d'une variable partagée. Une phrase suffit sur les verrous (et le risque de deux verrous qui s'attendent) ; ils ne servent pas dans nos ateliers.
À dire : Dans la machine virtuelle d'Erlang, la BEAM, un programme se découpe en processus. Chacun a sa propre mémoire, que personne d'autre ne peut lire ni modifier ; il a un identifiant, le PID, qui sert d'adresse, et une boîte aux lettres où s'accumulent les messages reçus. Sur le schéma, A ne peut pas toucher l'état de B : il dépose un message dans sa boîte, et c'est B qui décidera quoi en faire quand il le lira. Ces processus ne sont pas ceux du système d'exploitation : ils sont si légers que la machine virtuelle en fait tourner des milliers, répartis sur ses cœurs. Vous retrouvez les principes d'Erlang de la keynote d'AP1 : isolation, messages, et une panne qui reste locale. Voyons les primitives qui créent un processus et lui parlent. Repères : 2 min. L'isolation empêche deux processus de modifier la même variable ; elle n'empêche pas toutes les courses entre messages, ce qui sera le sujet d'AP7. Aucun dictionnaire de processus dans ce parcours.
À dire : spawn prend une fonction, la lance dans un nouveau processus et rend aussitôt son PID au créateur ; le nouveau processus s'arrête quand sa fonction se termine. send(pid, message) dépose un message dans la boîte du destinataire et continue sans attendre. receive attend, dans la boîte du processus qui l'exécute, un message qui correspond à l'un de ses motifs, avec la syntaxe du case ; after borne cette attente, en millisecondes. Dans cet exemple, qui exécute chacun des deux self() ? Le premier est exécuté par la cellule, le second par l'enfant : c'est pourquoi nous prenons parent avant spawn. make_ref crée une référence unique, et ^ref exige cette valeur précise dans le motif ; nous verrons bientôt pourquoi c'est utile. Pour suivre qui fait quoi, dessinons cet échange. Repères : 2 min. C'est la cellule de la section 1, simplifiée ; le carnet vérifie en plus les deux PID. Faire prédire le résultat, true, avant d'exécuter. Les numéros d'un PID changent d'un essai à l'autre : on compare des PID, on ne lit pas leurs numéros.
À dire : Ce dessin s'appelle un diagramme de séquence, et nous en lirons beaucoup jusqu'à AP8. Il reprend l'idée du tableau des deux fils : chaque processus a sa colonne, sa ligne de vie, et le temps descend. La flèche marquée spawn crée l'enfant, dont la colonne commence à cet instant. Une flèche horizontale est un message : elle part de l'expéditeur au moment du send et arrive dans la boîte du destinataire. La barre dorée est l'attente dans receive, qui se termine quand le bon message arrive ; la croix marque la fin de l'enfant, dont la fonction est terminée. Le dessin ne dit rien des durées, seulement de l'ordre des événements. Quand un échange vous paraît confus, dessinez-en un au brouillon : c'est le meilleur moyen de savoir qui exécute quoi. Repères : 2 min. Faire relire le diagramme à voix haute par un élève, de haut en bas. Deux événements sur deux colonnes différentes ne sont ordonnés que si une flèche les relie. Le poly, chapitre 9, emploie la même convention.
À dire : Un service est un processus qui attend des demandes et y répond ; le premier que vous écrirez calcule un double. Or le service ne connaît pas ses clients : la demande doit contenir le PID du client, faute de quoi il ne saurait pas à qui répondre. Elle contient aussi une référence, que la réponse recopie, pour que le client reconnaisse la réponse à sa demande parmi d'autres messages. Remarquez enfin que send rend simplement la valeur envoyée : il ne prouve pas que le service a lu le message, ni même qu'il existe encore. Seule la réponse le prouve. Voici les formes dont vous aurez besoin. Repères : 2 min. Le service de la section 2 est à usage unique : il répond une fois puis se termine, avec un after qui rend :idle si rien n'arrive. Faire prédire ce que rend send avant d'exécuter.
À dire : Ces cinq lignes suffisent pour les deux premières sections. Ne confondez pas les deux after : celui du receive borne une attente, celui du try est un nettoyage, comme un finally en Python, qui arrête les processus créés par la cellule même si une assertion échoue. assert_receive est la version « test » de receive : il échoue si le motif n'arrive pas à temps. À vous maintenant. Repères : 1 min. Repères du carnet : chapitre 9 du poly. La fiche « Processus et messages » se consulte après coup, jamais comme prérequis.
À dire : Vous avez vingt-cinq minutes pour les deux premières sections, puis nous ferons une courte reprise. Avant chaque cellule, écrivez votre prédiction, puis exécutez. Pour DoubleServer, partez du tableau du protocole, et dessinez l'échange au brouillon si vous hésitez. Je passe dans les rangs. Repères : 25 min d'atelier, puis 5 min de reprise. Résultat attendu : les assert de la section 1, puis le test de DoubleServer avec 42. Pièges : répondre à self() au lieu de client ; oublier l'after qui rend :idle ; réutiliser le processus déjà terminé pour le second essai.
À dire : Seule la réponse portant notre référence prouve que le service a reçu et traité la demande ; la valeur rendue par send ne dit rien de tel. Le service à usage unique s'est terminé après sa réponse : un second essai demande donc un nouveau processus, et nous verrons dans la troisième partie comment le faire durer. Enfin, un service qui répondrait à self() s'enverrait la réponse à lui-même, et le client attendrait jusqu'au délai. Nos réponses sont maintenant identifiées par une référence ; mais que se passe-t-il quand plusieurs messages attendent dans la boîte ? Repères : 5 min, comptées avec l'atelier. Faire montrer une prédiction juste et une prédiction fausse par deux élèves ; ne pas projeter de corrigé.
À dire : La boîte aux lettres garde les messages dans leur ordre d'arrivée, mais receive ne prend pas forcément le premier. Il parcourt les messages dans l'ordre et retire le premier qui correspond à l'une de ses clauses ; les autres restent en place pour un prochain receive. C'est la réception sélective. Ici, un receive qui attend la réponse saute l'annonce et retire la réponse. Conséquence surprenante : un receive peut attendre alors que la boîte n'est pas vide, simplement parce qu'aucun message ne correspond encore ! Avec une clause attrape-tout, en revanche, il aurait retiré l'annonce. Repères : exposé de la partie 2 : 5 min en tout, dont 2 pour cette slide. Le carnet s'envoie les deux messages à soi-même, dans la même cellule, pour rendre l'ordre d'arrivée certain. L'ordre d'arrivée n'est garanti qu'entre un même expéditeur et un même destinataire ; il n'existe pas d'ordre global entre plusieurs expéditeurs.
À dire : Supposons que la réponse n'arrive pas dans le délai. Le client cesse d'attendre, mais sa demande est peut-être déjà traitée, ou le sera bientôt : rien ne l'a annulée. La réponse tardive arrivera alors dans la boîte, où elle restera si personne ne l'attend plus. Le danger serait de relancer automatiquement : pour une lecture, ce n'est pas grave, mais un ajout risque d'être compté deux fois. Gardez cette idée, elle reviendra en AP7 avec GenServer et en AP8 avec les doublons. Voici les formes de votre deuxième exercice. Repères : 2 min. Pas de relance automatique dans nos protocoles. Le test de timeout du carnet utilise une référence neuve, à laquelle aucune réponse ne peut correspondre : pas de sleep.
À dire : Replies.await reçoit une référence et un délai. Le motif doit épingler la référence demandée : c'est ce qui laisse en place les réponses destinées aux autres demandes. Le délai est un argument, et after accepte une variable aussi bien qu'un nombre écrit en dur. Enfin, la fonction distingue ses deux issues par un tuple étiqueté, comme Map.fetch, rencontrée en AP2 et en AP3. Repères : 1 min. Ne pas écrire la fonction au tableau ; renvoyer au contrat du carnet.
À dire : Un peu plus de dix minutes pour la section 3. Replies.await doit attendre la réponse d'une référence donnée sans consommer celles des autres. Gardez précieusement cette fonction : vous l'appellerez dans la troisième partie. Nous ferons le point juste avant la pause. Repères : 12 min d'atelier, puis 3 min de reprise. Résultat attendu : les trois assert de la cellule de test. Sans ^, le motif accepte n'importe quelle référence : l'attente de first retire la réponse de second, déjà arrivée la première, et le premier assert échoue. Un délai dépassé n'annule pas le travail du service.
À dire : Dix minutes de pause. Avant de partir, sauvegardez votre carnet. Au retour, nous ferons durer notre service et lui confierons un compteur. Repères : 10 min. Profiter de la pause pour vérifier, sur les postes en difficulté, que mix --version répond dans le terminal.
À dire : Nos valeurs sont immuables : aucune variable ne change de valeur entre deux messages. Comment un compteur peut-il alors se souvenir de son total ? Rappelez-vous la somme avec accumulateur d'AP1 : la fonction ne modifiait rien, elle se rappelait avec le nouvel accumulateur en argument. Un processus fait exactement la même chose, à une différence près : au lieu de prendre la tête d'une liste, il attend le prochain message dans sa boîte. Tant qu'il se rappelle, il reste en vie et garde son état ; quand sa fonction se termine, l'état disparaît avec lui. Repères : exposé de la partie 3 : 10 min en tout, dont 3 pour cette slide. Faire dire par un élève quel argument porte l'état. La récursion terminale n'empile pas d'appels ; elle ne borne pas toute la mémoire (messages en attente, taille de l'état).
À dire : Voici une trace du compteur que vous allez écrire, démarré à 10. Il reçoit un ajout de 3, répond 13 au client, puis se rappelle avec 13 : c'est ce nouvel appel qui garde la valeur. Une lecture répond la valeur courante et se rappelle avec la même valeur. Le rappel est la dernière opération de chaque branche ; cette récursion terminale n'empile pas les tours précédents, si bien que le processus peut tourner indéfiniment. Et pendant le receive, il n'occupe pas le processeur : il attend le prochain message. À votre avis, que se passerait-il si la branche d'ajout répondait 13 mais se rappelait avec 10 ? Repères : 3 min. Réponse attendue : la réponse serait juste, mais la lecture suivante rendrait encore 10. C'est le piège principal de la section 4, et le test suivant le détecte. Trace du contrat, pas code à recopier.
À dire : Revenons aux deux fils qui perdaient une contribution. Ici, deux clients envoient chacun un ajout au même compteur, à peu près en même temps. Les deux messages attendent dans la boîte du compteur, qui les traite l'un après l'autre : sur ce diagramme, l'ajout de 2 passe en premier, le compteur répond 2, puis l'ajout de 5 donne 7. L'ordre inverse est tout aussi possible, avec les réponses 5 puis 7 ; le total final vaut 7 dans les deux cas. Aucune contribution ne se perd, car seul le compteur lit et modifie son nombre : les clients envoient des commandes, pas des valeurs à écrire. C'est l'idée qui organisera tout AP7. Repères : 3 min. Le carnet fait exactement cette expérience, avec les montants 2 et 5, et accepte les deux ordres. On attend les messages :done des clients, jamais un délai arbitraire.
À dire : L'interface publique, get et add, s'exécute chez le client : elle crée une référence, envoie la demande avec self() et attend avec votre Replies.await. La boucle, elle, s'exécute dans le processus du compteur ; elle est privée, avec defp, car personne ne doit l'appeler de l'extérieur. Une branche qui ne rappelle pas loop termine le processus : c'est ainsi que vous écrirez :stop. Repères : 1 min. La branche :get est fournie dans le carnet : elle montre la forme, les élèves écrivent les cas analogues.
À dire : Vingt-cinq minutes pour la section 4, qui est le cœur de la séance. Commencez par la boucle, puis écrivez les deux fonctions clientes ; les tests suivent l'ordre du protocole, faites-les passer un par un. Pour les deux clients, écrivez les deux ordres possibles avant d'exécuter. Repères : 25 min d'atelier, puis 5 min de reprise. Résultat attendu : les trois cellules de test passent. Pièges : rappeler loop(value) dans :add (réponse juste, état perdu) ; répondre à self() au lieu de client ; oublier de rappeler loop après la réponse. Variante : un ajout nul ; un ajout négatif est permis ici, AP7 le retirera du contrat.
À dire : Dans le tableau des deux fils, chacun lisait la valeur puis écrivait son propre résultat, et l'écriture de l'un écrasait celle de l'autre. Ici, les clients n'écrivent aucun nombre : ils demandent un ajout, et c'est le compteur qui calcule à partir de son état courant, une demande après l'autre. Deux compteurs, eux, sont deux processus, donc deux états indépendants, même s'ils exécutent le même module. Reste une question : que se passe-t-il si ce processus s'arrête, volontairement ou par accident ? Repères : 5 min, comptées avec l'atelier. Faire lire un diagramme de séquence dessiné par un élève pour l'un des deux ordres.
À dire : Dans un programme Python, une exception que personne ne rattrape arrête tout le programme. Sur la BEAM, une panne termine seulement le processus où elle se produit : c'est l'isolation dont nous parlions au début. Erlang en a fait une philosophie : plutôt que de prévoir chaque erreur possible, on laisse le processus fautif s'arrêter, et un autre processus décide de la suite. Mais comment savoir qu'un processus s'est arrêté ? Son silence ne suffit pas : un service peut ne pas répondre parce qu'il est occupé, bloqué, ou déjà terminé. Repères : exposé de la partie 4 : 12 min en tout, dont 3 pour cette slide. « Laisser planter » ne veut pas dire ignorer les erreurs : on les traite dans un autre processus, qui a une vue d'ensemble. Ne pas développer les stratégies de supervision au-delà de one_for_one.
À dire : Un monitor est une observation : Process.monitor(pid) rend une référence, et quand le processus observé se termine, nous recevons ce message :DOWN, comme n'importe quel autre message. Il contient la référence de l'observation, le PID et la raison de l'arrêt : :normal quand la fonction s'est terminée sans erreur, autre chose pour une panne. spawn_monitor crée le processus et l'observation d'un seul coup, ce qui évite de commencer à observer un processus déjà mort. Le monitor nous informe, mais il ne relance rien. Repères : 3 min. Après :DOWN, Process.alive? confirme la fin ; aucun sleep n'est utile. Un envoi vers un PID mort ne lève pas d'erreur et n'accuse aucune réception.
À dire : Il existe une seconde relation entre processus, le lien. Un lien relie les destins de deux processus : si l'un s'arrête anormalement, l'autre est arrêté aussi, sauf s'il a demandé à recevoir ce signal comme un message. C'est utile pour des processus qui n'ont pas de sens l'un sans l'autre, et c'est sur les liens que reposent les superviseurs. Mais dans le carnet, un lien entre la cellule et un processus qui plante ferait tomber la cellule ! Pour observer une panne sans la subir, nous utilisons donc un monitor. Repères : 2 min. Le mécanisme qui transforme le signal en message (trap_exit) se nomme seulement si un élève le demande. Les start_link d'Agent et de GenServer créent un lien avec l'appelant : c'est pourquoi la démonstration de supervision tue l'enfant, jamais la cellule.
À dire : Un superviseur est un processus dont le métier est de démarrer d'autres processus, ses enfants, et de les relancer quand ils s'arrêtent ; il leur est lié, et c'est ainsi qu'il apprend leur fin. Pour relancer un enfant, il refait l'appel décrit dans sa recette de démarrage, sa child spec : un module, une fonction, des arguments. Or cette recette ne contient que l'état initial, pas la valeur atteinte avant la panne ! Dans la démonstration du carnet, un Agent, c'est-à-dire un petit processus fourni par OTP qui garde une valeur, monte à 7, est tué, puis renaît avec un nouveau PID et la valeur 0. Prédisez-le avant d'exécuter : la supervision remet le service en marche, elle ne sauvegarde pas ses données. Repères : 3 min. Stratégie one_for_one : seul l'enfant tombé est relancé, dans un budget de redémarrages ; un superviseur ne répare pas une erreur qui se reproduit à chaque démarrage. Agent et GenServer seront étudiés en AP7 : aucun catalogue de callbacks ici. La démonstration attend la notification de démarrage, sans sonder which_children ni dormir.
À dire : Pour Failures.observe, vous lancez la panne dans un processus observé, vous attendez son :DOWN avec un délai, et vous nettoyez dans un try ... after, sur le modèle de la cellule qui précède. La démonstration de la section 6 est fournie : lisez-la avant de l'exécuter, et prédisez le PID et la valeur après la panne. Repères : 1 min. La panne volontaire reste dans l'enfant non lié ; jamais directement dans le processus de la cellule.
À dire : Un peu plus de vingt minutes pour les sections 5 et 6. Failures.observe est le seul code à écrire ; le reste consiste à prédire, exécuter et expliquer. Gardez dans votre carnet trois phrases : où vit le nombre, ce que garantit une réponse, et ce que le superviseur reconstruit après une panne. Repères : 23 min d'atelier, puis 5 min de reprise. Résultat attendu : {:stopped, :simulated_failure}, puis send rend :hello et Counter.get rend {:error, :timeout}, puis un nouveau PID avec la valeur 0. Si le groupe manque de temps, écrire Failures.observe/0 collectivement et garder la démonstration de supervision pour tous.
À dire : Le superviseur a reconstruit un processus neuf à partir de sa recette ; l'ancien PID ne sert plus à rien, et la valeur 7 a disparu avec lui. Il faut donc distinguer deux besoins : remettre un service en marche, ce que fait la supervision, et conserver des données, ce qu'elle ne fait pas. Relisez vos trois phrases : elles préparent le quiz d'AP7. Pour finir, sortons du carnet, car une vraie application démarre elle-même son superviseur. Repères : 5 min, comptées avec l'atelier. Ne jamais présenter un redémarrage comme une réparation de tout bug, ni comme une récupération de l'état.
À dire : Jusqu'ici, Livebook démarrait et arrêtait nos processus pour nous. Un vrai programme Elixir est un projet Mix, que la machine virtuelle charge et démarre comme une application OTP, c'est-à-dire un ensemble de modules avec son arbre de supervision. L'application n'est pas elle-même un processus : elle démarre un superviseur, qui démarre les enfants de sa liste children. Nous allons créer ce projet, ajouter l'Agent de la démonstration à cette liste, puis le lire depuis IEx, la console d'Elixir. Nous écrivons toujours de l'Elixir : le mot OTP désigne l'organisation du programme, pas un autre langage. Repères : partie 5 : 20 min en tout, dont 3 pour cet exposé, 15 dans le terminal et 2 de bilan. Sous PowerShell, iex.bat -S mix si iex ne démarre pas. Pas de release, d'interface web, de nœud distant ni de nouveau devoir à introduire.
À dire : Suivez la section 7 du carnet, dans l'ordre : création, premier mix test, modification de children, formatage, tests, puis console. Dans IEx, retrouvez le PID de l'Agent avec which_children et lisez sa valeur. Avant de relancer IEx, prédisez ce que vous lirez. Repères : 15 min. Résultat attendu : 0, puis 3 après l'update, puis 0 après redémarrage, faute de sauvegarde sur disque. Un poste sans mix travaille en binôme ; ne pas transformer ce temps en dépannage collectif. Le checker crée le même projet et vérifie ces lectures.
À dire : Nous avons écrit à la main tout ce qu'il faut pour un service : des processus, des messages, une référence par demande, un état dans une boucle, et un superviseur qui relance. En AP7, nous ne réécrirons plus cette boucle : OTP la fournit sous le nom de GenServer, et nous construirons un compteur de contributions dans un projet Mix comme process_lab. Pensez à sauvegarder votre carnet sous AP6/ap6-processus.livemd et à garder le dossier process_lab. Le quiz d'ouverture d'AP7 portera sur cette séance : relisez le chapitre 9 du poly. Repères : 2 min. Les consignes de rendu sont précisées en cours ; aucune date n'est fixée par ce support, et les anciennes échéances de mini-blockchain ne concernent pas ce parcours.