20-10-2012, 03:58 PM
J'ai fait mes premiers pas avec OTP. Le but est de créer le socle d'une application OTP qu'on peut lancer et qui lance un superviseur principal et tous les processus enfants.
Pour le moment l'application ne fait rien d'autre que démarrer. Le code est sur GitHub.
Derrière le terme d'application OTP se cache en fait 3 composants : un fichier qui décrit l'application, un module qui implémente le comportement
Le tout est organisé en 2 répertoire : un premier nommé
Ce fichier de description contient s'appelle
Le fichier suivant est mon module d'application,
Il définit donc le module
Comme toujours avec OTP, on fournit essentiellement des modules contenant des fonctions qu'on n'utilise pas directement : c'est OTP qui les invoquera et il faut donc respecter ses exigences en terme d'arguments acceptés et de valeur retournées (cf. celles du comportement
La fonction
N'oubliez pas qu'en Erlang, les mots comme
En revanche, le second argument est bien transmis à la fonction dans la variable
La fonction
Le fichier suivant est mon module d'application,
Le superviseur a pour rôle de… superviser. C'est un processus qui surveille un autre processus et de réagir à son cycle de vie (extinction propre, extinction brutale, etc.). C'est la base d'Erlang en matière de tolérance à l'erreur. L'un des mantra d'Erlang est "Let it crash". En effet, Erlang incite à ne pas coder de manière défensive (ce qui complique considérable le code) et de laisser un processus planter s'il rencontre une erreur (un message innatendu, par exemple) afin qu'il soit relancé dans un état sain par un superviseur.
Ici, le fichier définit le module
En Erlang, les epxression comme
La fonction
Le premier tuple indique qu'on va nommer localement le processus (à l'échelle d'un nœud Erlang, et pas de tout le cluster éventuel dans le cas d'une application distribuée sur plusieurs machines). On choisit d'utiliser le nom du module comme nom de processus.
Le second argument indique un nom de module. OTP se chargera d'appeler la fonction
Et il se trouve que — comme par hasard — on définit également cette fonction
Ce second tuple va d'abord indiquer les critères de relancement en cas de crash des processus supervisés. On choisit la stratégie de relancement
Le second élément du tuple est une liste qui décrit les processus qui seront supervisés. Pour le moment il n'en supervise aucun.
Voilà ! Comme vous pouvez le voir, il y a beaucoup de choses à dire sur ces 16 malheureuses lignes de code !
Ma prochaine étape est de créer un module qui va me permettre de placer des vaisseaux (chacun représenté par un processus Erlang) sur mon plateau de jeu. Je devrais également prévoir un superviseur pour tous les vaisseaux. L'étape d'après sera de pouvoir faire bouger ces vaisseaux puis de faire persister leur état à l'extinction et au redémarrage du serveur.
Pour le moment l'application ne fait rien d'autre que démarrer. Le code est sur GitHub.
Derrière le terme d'application OTP se cache en fait 3 composants : un fichier qui décrit l'application, un module qui implémente le comportement
application
et un autre qui implémente le comportement supervisor
.Le tout est organisé en 2 répertoire : un premier nommé
src
qui contient le code, et l'autre nommé ebin
qui contient le code compilé (et le fichier de description de l'application).Ce fichier de description contient s'appelle
battleship.app
, où battleship est le nom de notre application. Il contient un tuple de 3 éléments.
{
application,
battlefleet,
[
{ description, "A game." },
{ vsn, "0.1.0" },
{ modules, [ battlefleet_app, battlefleet_sup ] },
{ registered, [] },
{ applications, [ kernel, stdlib ] },
{ mod, { battlefleet_app, [] } }
]
}.
- Le premier atome application est fixe.
- Le second atome reprend le nom de l'application.
- La liste représente un ensemble de propriétés.
- Les propriétés
description
etvsn
sont explicites.
modules
indique les modules utilisés par l'application (pour le moment je n'ai que mon module d'application et son superviseur).
registered
liste les processus nommés (les autres sont anonymes et ne peuvent pas être appelés directement).
applications
liste les autres applications nécessaires au bon fonctionnement de la mienne, icikernel
etstdlib
(indiquées par commodités, ces deux là sont toujours chargées). Dans un futur proche, je rajouterai probablement le serveur HTTPcowboy
qui me permettra de faire du push.
- Enfin,
mod
indique le module d'application et les arguments à lui transmettre. La fonctionstart
de ce module sera appelée (en lui transmettant ces arguments, ici une liste vide).
- Les propriétés
Le fichier suivant est mon module d'application,
src/battlefleet_app.erl
:
-module(battlefleet_app).
-behaviour(application).
-export([ start/2, stop/1 ]).
start(normal, Args) ->
battlefleet_suptart_link(Args).
stop(_State) ->
ok.
Il définit donc le module
battlefleet_app
, qui implémente le comportement application
. Il exporte deux fonctions start
et stop
(En Erlang, les fonctions qui ne sont pas exportées ne peuvent être appelées qu'au sein du module dans lequel elles sont définies).Comme toujours avec OTP, on fournit essentiellement des modules contenant des fonctions qu'on n'utilise pas directement : c'est OTP qui les invoquera et il faut donc respecter ses exigences en terme d'arguments acceptés et de valeur retournées (cf. celles du comportement
application
)La fonction
start
sert à appeler la fonction start_link
de notre superviseur principal (le module battlefleet_sup
) avec des arguments donnés (la fameuse liste vide qu'on a renseigné dans notre fichier battleship.app
).N'oubliez pas qu'en Erlang, les mots comme
normal
ne sont pas des variables (qui commencent par une majuscule) mais des atomes, des valeurs qui servent juste à donner du sens au code grâce au pattern matching. Ici, si on appelle la fonction start
de ce module (ce que fera OTP) avec autre chose que l'atome normal
en premier argument, la fonction ne sera même pas appelée. Cela nous permet d'écrire différentes implémentation de la fonction selon ce qu'elle reçoit. En quelque sorte, ça évite d'avoir à faire une condition à l'intérieur de la fonction.En revanche, le second argument est bien transmis à la fonction dans la variable
Args
.La fonction
stop
a pour seul effet de retourner l'atome ok
. Notez le underscore qui précède le nom de la variable. Il sert à indiquer au développeur (qui relit le code) que cette variable ne sera pas utilisée. C'est une convention purement sémantique. Le compilateur n'émet pas d'avertissement indiquant que la variable n'etst pas utilisée si elle est préfixée de ce underscore.Le fichier suivant est mon module d'application,
src/battlefleet_sup.erl
:
-module(battlefleet_sup).
-behaviour(supervisor).
-export([ start_link/1, init/1 ]).
start_link(Args) ->
supervisortart_link({ local, ?MODULE }, ?MODULE, Args).
init(_Args) ->
RestartStrategy = { one_for_one, 1, 60 },
ChildrenSpecification = [],
{ ok, { RestartStrategy, ChildrenSpecification } }.
Le superviseur a pour rôle de… superviser. C'est un processus qui surveille un autre processus et de réagir à son cycle de vie (extinction propre, extinction brutale, etc.). C'est la base d'Erlang en matière de tolérance à l'erreur. L'un des mantra d'Erlang est "Let it crash". En effet, Erlang incite à ne pas coder de manière défensive (ce qui complique considérable le code) et de laisser un processus planter s'il rencontre une erreur (un message innatendu, par exemple) afin qu'il soit relancé dans un état sain par un superviseur.
Ici, le fichier définit le module
battlefleet_sup
(qu'on a utilisé dans battlefleet_app
!), qui implémente le comportement supervisor
et exporte les fonctions start_link
et init
.En Erlang, les epxression comme
?MODULE
sont des macros. En l'occurrence, ?MODULE
est une macro native qui représente le nom du module courant. Les macros sont remplacées par leur valeur au moment de la compilation. Ici, la macro ?MODULE
sera remplacée par l'atome battlefleet_sup
.La fonction
start_link
appelle à son tour la fonction start_link
du module supervisor
(fourni par Erlang) en lui transmettant 3 arguments : un tuple, un nom de module et la liste d'arguments qu'elle a elle-même reçu.Le premier tuple indique qu'on va nommer localement le processus (à l'échelle d'un nœud Erlang, et pas de tout le cluster éventuel dans le cas d'une application distribuée sur plusieurs machines). On choisit d'utiliser le nom du module comme nom de processus.
Le second argument indique un nom de module. OTP se chargera d'appeler la fonction
init
de ce module en lui transmettant les arguments Args
.Et il se trouve que — comme par hasard — on définit également cette fonction
init
! Celle-ci a pour but de décrire les processus qui seront supervisés par le processus superviseur. Pour cela, elle doit renvoyer un tuple de 2 éléments : l'atome ok
et un autre tuple de 2 éléments pour décrire la stratégie de relancement des processus qui plantent et la liste de ces processus.Ce second tuple va d'abord indiquer les critères de relancement en cas de crash des processus supervisés. On choisit la stratégie de relancement
lol
(qui consiste à simplement relancer le processus qui plante) avec une On impose au superviseur des limites : le superviseur ne relancera qu'une fois un processus planté toutes les 60 secondes. Le but est bien sûr d'éviter de consommer trop de ressources à relancer un processus qui persiste à planter.Le second élément du tuple est une liste qui décrit les processus qui seront supervisés. Pour le moment il n'en supervise aucun.
Voilà ! Comme vous pouvez le voir, il y a beaucoup de choses à dire sur ces 16 malheureuses lignes de code !
Ma prochaine étape est de créer un module qui va me permettre de placer des vaisseaux (chacun représenté par un processus Erlang) sur mon plateau de jeu. Je devrais également prévoir un superviseur pour tous les vaisseaux. L'étape d'après sera de pouvoir faire bouger ces vaisseaux puis de faire persister leur état à l'extinction et au redémarrage du serveur.