JeuWeb - Crée ton jeu par navigateur
Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? (/showthread.php?tid=7949)

Pages : 1 2


Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Sephi-Chan - 25-11-2018

Hello,

Quand je développe avec des machines à états, je me pose souvent la question.

Dois-je gérer les cas d'erreur, ou bien laisser le client effectuer des tests (sur l'état de la machine) pour déclencher ou non des événements ?

Par exemple, imaginons une machine d'un jeu en groupe. La partie ne peut pas démarrer tant qu'il n'y a pas 3 joueurs.

La machine dans l'état "waiting_for_players" accepte les événements "player_joins", "player_leaves" et "start_game".

Si on déclenche l'événement "start_game", doit-elle exploser à la gueule de l'appelant, ou juste rester dans son état "waiting_for_players" ?


Même question pour les tests de "sécurité" : la machine dans l'état "wait_for_player_choices" doit-elle exploser quand elle reçoit un événement "player_uses_item" avec un objet qu'il ne possède pas, ou juste ne rien faire ?



Je répondrais à ces questions un peu plus tard pour ne pas orienter la discussion.


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Xenos - 25-11-2018

Salut,

Je suis allé tapper au pif de duckduckgo et je suis tombé sur ça https://docs.spring.io/spring-statemachine/docs/current/reference/html/sm-error-handling.html dont le principe semble dire "on laisse une chance au client de gérer l'erreur et s'il ne la gère pas, alors on la gère sur le serveur".

Maintenant, je vais parler en terme pragmatiques, pour mon cas perso.
Si je fais le parallèle entre une "state machine" et ma petite archi perso pour Variispace, le principe est simple: la "machine" (le jeu) est dans un état (dicté par la BDD) qui lui fait accepter certaines requêtes (messages) ou non.

Une requête n'est pas acceptable dans deux situations: elle est mal formée (l'event est incomplet) ou elle est mal venue (l'event n'est pas acceptable pour l'instant). On pourrait envisager un 3e cas, "l'event n'est pas compris", mais je le dispatch dans les deux autres: la machine *doit* comprendre tout event qu'elle reçoit (toute requête HTTP), et ranger dans "mal formé" les events incompris ("player_balalaika" n'est pas compris par ta machine dans ton exemple: c'est un event mal formé).

Dans le cas d'une malformation, la machine va directement dire au client que le message n'est pas acceptable. Soit dans son ensemble (parce que l'endpoint HTTP n'existe pas, donc en gros, "404, point, j'peux pas t'aider plus") soit en partie (parce qu'il manque un paramètre GET/POST ou parce qu'un de ces paramètres n'est pas du bon type, auquel cas le client est informé que tel élément du message n'est pas bon).
Cela, je l'ai géré simplement: la machine "endpoint" qui traite la requête jette une exception décrivant son problème, qui est attrapée par le serveur (pas par le client, c'est là ou soit je diverge de Spring soit je n'ai pas compris ce que Spring dit dans ce lien), qui la renvoie à un autre endpoint dédié à la gestion de cette erreur (et ce endpoint formatte l'erreur suivant le header Accept, et la renvoie au client, charge à lui de se démerder).

Dans le cas d'un event (requête) valide, alors le serveur web n'a rien à dire: il passe la demande à la BDD (à une procédure stockée, à laquelle il envoie les paramètres de la demande en rajoutant, souvent l'ID du joueur issu de la session) et le serveur de BDD procède pareil: il récupère la demande, et la traite: si elle est mal formée (procédure pas existante ou donnée par valide), MySQL jettera une exception (le serveur MySQL ne la traite pas, elle sera donc renvoyée au "client" qui est ici le serveur web, qui lui, la traitera comme un event "mal formé": il jettera une exception, catchée un peu plus haut dans le PHP, puis passée à un endpoint dédié au formattage de cette erreur, qui renvoie une erreur 500 sans trop d'infos pour ne rien laisser fuiter sur "l'état de la machine" aka du jeu); si elle est bien formée, alors il traite la demande puis renvoie les result sets au serveur web, qui les traite (et les formatte en HMTML, JSON ou autre suivant le HTTP Accept du client).

Toutefois, la requête reçue par MySQL (l'appel de procédure stockée) peut être bien formé, mais pas acceptable dans l'état du jeu (ie: je veux déplacer une flotte pas à moi, ou qui est déjà détruite, ou construire un vaisseau sans avoir les ressources, etc). En ce cas, MySQL jette une exception (SIGNAL) de manière explicite (type IF (NOT EXISTS(SELECT 1 FROM fleet WHERE id_player = idPlayer AND id = idFleet)) THEN SIGNAL... END IF où idPlayer et idFleet sont des paramètres de la procédure stockée désignant le joueur et la flotte à déplacer) car c'est une règle métier, soit il la jette de manière implicite car le modèle de données le rejette (type INSERT INTO player (pseudo, mail) VALUES (paramPseudo, paramMail) qui génère automatiquement une DUPLICATE KEY si le joueur qui veut s'inscrire utilise un mail/pseudo déjà pris).

Dans tous les cas, MySQL "tente" de gérer l'exception (y'a pas de HANDLER généralement, donc il ne fait en fait rien) puis il cascade l'erreur à son client (le serveur web) qui tente alors lui-aussi de traiter le message (via les formatters dédiés aux exceptions).

Au final:
- Si t'exploses la machine, alors tu ne vas pas plus loin, et c'est dangereux: un client peut couper le jeu en forgeant une mauvaise requête, qui stoppe la machine
- Si tu ne fais rien du tout, alors il te seras très très compliqué de déboguer, et sachant que les erreurs en prod ça arrive, les joueurs seront comme deux ronds de flan à ne pas comprendre pourquoi rien ne se passe (et je te paries qu'ils cliqueront alors 30x sur les boutons et ragequiteront)
- Le mieux me semble donc d'être "je reste dans l'etat valide" (pour continuer mon cycle de vie) mais je notifie le client qu'il a fait de la merde et que son message n'est pas accepté (avec ou sans les raisons de cette acceptation, suivant la criticité de cette information).

En espérant ne pas avoir répondu à côté Wink


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Ter Rowan - 26-11-2018

je rejoins Xenos en plus concis :

la machine "serveur" :
"je reste dans l'etat valide" (pour continuer mon cycle de vie) mais je notifie le client qu'il a fait de la merde et que son message n'est pas accepté

la machine "client" :
"je reviens au dernier état valide" en notifiant l'utilisateur du dysfonctionnement (détaillé ou pas)

Après à voir s'il y a vraiment une machine "client" ou si c'est juste un concept :

on peut très bien imaginer que le serveur dans sa réponse renvoie l'état valide du client + le message, le poste client ne faisant que de l'affichage sans réfléchir
ou
le serveur renvoie un refus, et le code du client revient de lui même à un état que lui seul connait / va bien

Mais là c'est plus un sujet d'optimisation / maintenance ([flux de données complet + serveur calcule tout] ou [code client et code serveur et flux de données faible])


bon finalement c 'est pas beaucoup plus concis, mais plus aéré ^^


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Sephi-Chan - 27-11-2018

Merci à vous pour les réponses.

Dans mon cas, le jeu est purement local, écrit en Lua : la machine a état est pilotée par le jeu, elle est autonome sur l'ordinateur du joueur. La machine a état s'assure que les règles sont respectées. Elle plante dans le cas contraire. Quand elle reçoit l'événement "Tel joueur joue telle carte", elle cherche la carte dans la main du joueur donnée, si elle ne l'est pas, une erreur est envoyées. La gestion d'erreur étant minimaliste, on suit plus facilement la logique du jeu. C'est au client d'envoyer les bons paramètres et de s'assurer que le joueur fait ce qu'il a le droit de faire.

Prenons maintenant le cas d'un jeu multijoueurs. Chez moi c'est un process Elixir qui respecte une interface proche d'une machine à états (le module gen_statem d'Erlang, et en l'occurrence le wrapper Elixir GenStateMachine ) : ça se comporte "naturellement" comme une machine à états qui n'accepte que des messages bien précis, le reste est rejeté. Un message inattendu ait planter la machine, qui est relancée par son superviseur. C'est une politique classique en Erlang/Elixir : le "let it crash", ça permet d'éviter beaucoup de code défensif.

Les clients communiqueront par Websocket (pour le client Web en JS) et UDP (pour le client lourd en Lua). L'interface entre le client et le serveur s'assurera que la machine reçoit les bons événements avec de bons arguments, et s'occupera de transmettre au client de "belles" erreurs s'il y en a, ce qui devrait être rare : le client n'envoie pas de données merdiques, mais qui peut arriver (s'il n'a pas pu se synchroniser, etc.).


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Xenos - 27-11-2018

Citation : C'est au client d'envoyer les bons paramètres et de s'assurer que le joueur fait ce qu'il a le droit de faire.
Je ne suis pas sûr d'avoir compris, vu que tu parles de "local" et de "client" (sous-entendu, "serveur" quelque part?!)... D'un point de vue sécu, c'est incongru?! (après, d'un point de vue "bonne pratique", perso, je trouve problématique que le client soit tenu de connaitre le contenu du serveur; car obliger le client a toujours envoyer les bons paramètres, ça oblige vachement le client à connaitre comment le serveur fonctionne en interne)

Le client peut toujours envoyer des "données merdiques", suffit de les forger Smile


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Sephi-Chan - 27-11-2018

Dans le premier cas (jeu local), le client est juste la partie du code qui n'est pas la state machine et qui appelle celle-ci, à travers une API (ce qui ne t'oblige donc pas à exposer la structure interne).

Dans le cas du client/serveur, les requêtes arrivent sur le endpoint (dans mon cas du WebSocket ou de l'UDP), qui vérifie : si on est dans le cas d'un joueur qui joue une carte, je peux vérifier (via l'API de la state machine) que le joueur donné a bien le droit de jouer, qu'il possède bien la carte qu'il souhaite jouer, etc.

Comme tu le dis, les données merdiques n'arrivent que si on forge : on peut bien se permettre de ne pas tenter de récupérer ou d'afficher de belles erreurs dans ce cas.


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Meraxes - 08-12-2018

Quand vous parlez de "machine à état", vous parlez bien "d'automate fini" ou ça a un sens plus large/différent pour vous ?

En fait, je n'ai pas bien compris dans quel cas tu voudrais faire "planter la machine" côté serveur ? Car si l'action n'est pas valide depuis l'état courant de la machine, on peut juste considérer qu'il n'y a pas de transition entre état, non ? (Ou bien ajouter une transition de l'état vers lui même)

Après on peut très bien ajouter des états d'erreurs dans un automate pour gérer certaines actions abusives ; les deux façons de faire me semblent possibles.


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Argorate - 09-12-2018

pour moi il faut dissocier la machine à état et le client qui interagit avec le serveur.

Le client ne doit pas pouvoir changer la machine à état directement, il doit passer par un truc intermédiaire (middleware) qui reçoit les events, et c'est lui qui gère s'il va ou non changer l'état de la state-machine, quand et comment et le cas échéant ne rien faire et retourner une erreur, mais c'est pas à la machine à état de gérer.

En master on faisait des state machine et le principe était de toujours faire comme si c'était forcément ok, c'est pas le bon endroit pour gérer les erreurs. La machine à état ne doit QUE appliquer le code de son état.


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Sephi-Chan - 09-12-2018

(08-12-2018, 03:26 AM)Meraxes a écrit : Quand vous parlez de "machine à état", vous parlez bien "d'automate fini" ou ça a un sens plus large/différent pour vous ?

En fait, je n'ai pas bien compris dans quel cas tu voudrais faire "planter la machine" côté serveur ? Car si l'action n'est pas valide depuis l'état courant de la machine, on peut juste considérer qu'il n'y a pas de transition entre état, non ? (Ou bien ajouter une transition de l'état vers lui même)

Après on peut très bien ajouter des états d'erreurs dans un automate pour gérer certaines actions abusives ; les deux façons de faire me semblent possibles.

Les states machines d'Erlang/OTP (et Elixir, du coup) plantent quand elles reçoivent un message inattendu. Elles sont immédiatement relancées par leur superviseur. Ça fait partie de la philosophie du truc (peu de code défensif, just let it crash).


(09-12-2018, 10:01 PM)Argorate a écrit : pour moi il faut dissocier la machine à état et le client qui interagit avec le serveur.

Le client ne doit pas pouvoir changer la machine à état directement, il doit passer par un truc intermédiaire (middleware) qui reçoit les events, et c'est lui qui gère s'il va ou non changer l'état de la state-machine, quand et comment et le cas échéant ne rien faire et retourner une erreur, mais c'est pas à la machine à état de gérer.

En master on faisait des state machine et le principe était de toujours faire comme si c'était forcément ok, c'est pas le bon endroit pour gérer les erreurs. La machine à état ne doit QUE appliquer le code de son état.

La question est justement de savoir où faire les essais. Dans mon exemple, il y a un navigateur, un serveur WebSocket et un serveur de jeu. Ce sont donc les appels au serveur WebSocket qui font office de middleware.

J'ai implémenté le jeu du "6 qui prend" (tu connais ? :p). Quand le serveur WebSocket reçoit un message de type : dans la partie 42, le joueur Matrix joue la carte 104. Est-ce que c'est à ce serveur WebSocket de s'assurer que les règles sont respectées ? Que le joueur Matrix en fait bien partie ? Que c'est bien le moment de choisir une carte ? Qu'il possède bien la carte 104 en main ? Ou bien transmet-il le message à la machine a état qui elle va s'assurer de ça ?

Les deux solutions sont possibles et présentent chacune des avantages et des inconvénients.


RE: Machines à états : gérer les cas d'erreurs ou laisser ça a la charge du client ? - Xenos - 09-12-2018

Ah, présenté ainsi, pour moi, le websocket n'a pas les règles métier (les règles du jeu) dans sa base de connaissances. C'est donc nécessairement la machin à état qui gère cela.

Si je fais le parallèle avec ma propre archi, le Websocket est remplacé par mon code PHP de site, et la machine à état par la BDD (au fond, la BDD est une machine à état, donc dans les faits, c'est même exactement le même cas). Tout ce que le PHP (le websocket) se charge alors de faire, c'est de récupérer le message du client (la requête HTTP), de vérifier la structure des données (ie: 104 est bien un integer positif strict) et de le passer à la BDD. Charge alors à elle de vérifier les règles du jeu, et, dans mon cas, si elles sont violées alors la BDD ne change pas d'état (ROLLBACK). Elle remonte une erreur à son client (= au PHP, via SIGNAL + un identifiant de l'erreur, comme "No such card available"), qui va alors transcrire ça en un message d'erreur potable pour le client final ("Une erreur serveur est survenue", ou, si je veux être plus précis, "La carte demandée (104) n'est pas disponible"). Cette transcription est indispensable pour logger les éventuelles erreurs non-attendues, mais aussi (et surtout) pour formatter correctement l'erreur que la BDD a sortie (que ce soit en la "masquant", pour juste dire "HTTP 500", ou en la détaillant, pour dire qu'une planète de Variispace a été détruite entre le moment où le joueur a affiché la page du jeu et le moment où le serveur a reçu la requête de déplacement d'une flotte spatiale par exemple).