JeuWeb - Crée ton jeu par navigateur
Le fameux problème de mon appli JCJ - 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 : Le fameux problème de mon appli JCJ (/showthread.php?tid=8086)



Le fameux problème de mon appli JCJ - L'Omniscient - 27-04-2020

Hello les gens,
Voilà je me lance enfin sur l'intégration des combats JCJ dans mon jeu.
Et je dois régler ce fameux problème du joueur X qui, influencé par plusieurs conditions inconnues aussi rares que de pluies d'étoiles filantes (peut-être un peu moins quand même) n'arrive pas à inscrire son attaque dans la base de données.

Voici donc les codes. Je ne copie que ceux nécessaires à la résolution du bug (puisque le joueur qui a le problème ne peut pas aller plus loin).


function search_attaque() {
global $bdd;
global $qui;

$bdd->beginTransaction();

if ($qui['IDCombattantActif'] == $_SESSION['ID']) {

$attaque=$bdd->query('SELECT AttaquePassif FROM z_erentis_combats_jcj WHERE IDCombattantActif = "'.$_SESSION['ID'].'" ');
} else if ($qui['IDCombattantPassif'] == $_SESSION['ID']) {
$attaque=$bdd->query('SELECT AttaqueActif FROM z_erentis_combats_jcj WHERE IDCombattantPassif = "'.$_SESSION['ID'].'" ');
}

$bdd->commit();

$attaque = $attaque->fetch();

return $attaque[0];
}


/* Trop tard pour l'attaque de l'adversaire */
function cherche_et_toolate() {
global $bdd;
global $qui;

$time = 0;
$attaque = search_attaque();
/* On cherche l'attaque du joueur adverse jusqu'à ce qu'on la trouve */
while ($attaque == "?" AND $time < 30) {
$attaque = search_attaque();
if ($attaque == "?") {
sleep(2);
}
$time = $time + 2;
}

if ($time >= 30) {
$attaque = "X";
if ($qui['IDCombattantActif'] == $_SESSION['ID']) {
$use = $bdd->prepare('UPDATE z_erentis_combats_jcj SET AttaquePassif = "X", AttaqueActif = "?", TooLatePassif = "Yes", Tour = Tour + 0.5 WHERE IDCombattantActif = ?');
} else if ($qui['IDCombattantPassif'] == $_SESSION['ID']) {
$use = $bdd->prepare('UPDATE z_erentis_combats_jcj SET AttaqueActif = "X", AttaquePassif = "?", TooLateActif = "Yes", Tour = Tour + 0.5 WHERE IDCombattantPassif = ?');
}
$use->execute(array($_SESSION['ID']));
$aten['Nom'] = "X";
}

return $attaque;
}

if ($_POST['passe'] == "yes") {
/* RECUPERATION DES DONNEES DE L'ATTAQUE DU JOUEUR */
$recup_attaque = $bdd->prepare('SELECT * FROM z_erentis_attaques WHERE Element LIKE ? AND Hand = ? AND Moove = ?');
$recup_attaque->execute(array("%".$_POST['element']."%", $_POST['nhand'], $_POST['nmoove']));
$at = $recup_attaque->fetch();

/* Si l'attaque n'existe pas on la nomme X */
if (empty($at['Nom'])) {
$at['Nom'] = "X";
$at['ID'] = "X";
} else {
$vitesse_jo = $at['Vitesse'];
}
} else {
$at['ID'] = "0";
$at['Nom'] = "Concentration";
}

/* On insère l'attaque du joueur et on augmente le tour */

if ($qui['IDCombattantActif'] == $ID) {
$cherche_attaque = $bdd->prepare('SELECT AttaqueActif, TooLateActif FROM z_erentis_combats_jcj WHERE IDCombattantActif = ?');
$cherche_attaque->execute(array($ID));
$attaque_avant_choix = $cherche_attaque->fetch();

if ($attaque_avant_choix[0] == "?") {
$use = $bdd->prepare('UPDATE z_erentis_combats_jcj SET AttaqueActif = ?, TooLateActif = "No", Tour = Tour + 0.5 WHERE IDCombattantActif = ?');
$use->execute(array($at['ID'], $ID));
}
} else if ($qui['IDCombattantPassif'] == $ID) {
$cherche_attaque = $bdd->prepare('SELECT AttaquePassif, TooLatePassif FROM z_erentis_combats_jcj WHERE IDCombattantPassif = ?');
$cherche_attaque->execute(array($ID));
$attaque_avant_choix = $cherche_attaque->fetch();

if ($attaque_avant_choix[0] == "?") {
$use = $bdd->prepare('UPDATE z_erentis_combats_jcj SET AttaquePassif = ?, TooLatePassif = "No", Tour = Tour + 0.5 WHERE IDCombattantPassif = ?');
$use->execute(array($at['ID'], $ID));
}
}

if (isset($attaque_avant_choix[1]) AND $attaque_avant_choix[1] == "Yes") {
$attaque_lancee_jo = "reload";
if ($qui['IDCombattantActif'] == $_SESSION['ID']) {
$use = $bdd->prepare('UPDATE z_erentis_combats_jcj SET AttaqueActif = "?", TooLateActif = "No" WHERE IDCombattantActif = ?');
} else if ($qui['IDCombattantPassif'] == $_SESSION['ID']) {
$use = $bdd->prepare('UPDATE z_erentis_combats_jcj SET AttaquePassif = "?", TooLatePassif = "No" WHERE IDCombattantPassif = ?');
}
$use->execute(array($_SESSION['ID']));
} else {
$attaque = cherche_et_toolate();
/* Suite du code */
}

Le problème :
Le code fonctionne très bien SAUF avec une certaine configuration assez précise mais difficilement identifiables :
- Il ne touche que certain joueurs, TOUJOURS les mêmes.
- Il n'affecte ces joueurs QUE contre certain joueurs, et TOUJOURS les mêmes opposants
- Il pose problème QUE lorsque le joueur touché attaque en deuxième.

Dans ces conditions, le joueur qui a des soucis n'arrive pas à inscrire son attaque dans la table. Le joueur qui n'arrive pas à inscrire son attaque finit par entrer dans la condition qui le "reload". Donc j'ai l'impression qu'il est bloqué jusqu'à ce que l'autre joueur ait finit sa boucle a lui.

De ce fait, et après une petite analyse, j'ai pu remarquer que les joueurs touchés étaient généralement ceux avec de mauvaises connexions internet.
Et ça se produit contre ceux qui semblent avoir d'excellentes connexions.
Je me suis posé la question suivante : est-ce dû à la différence de connexion ?

Du coup le joueur 2 n'arrive pas à UPDATE son attaque. L'éciture de la table serait-elle lock par le joueur à connexion trop rapide qui la lit en boucle ?

C'est hyper énigmatique cette histoire. J'ai essayé de regarder ce qu'il se passait dans ma table durant la lecture en boucle, et la case "?" du deuxième joueur ne s'UPDATE pas sauf quand le joueur 1 l'UPDATE au bout de 30 secondes, ensuite seulement le deuxième joueur peut poursuivre son chemin, trouver le "Yes" et dire au client qu'il a été déco (ce qui est faux du coup :x). On dirait qu'il est bloqué à un endroit et ne peut poursuivre son chemin que lorsque le premier joueur a fini le sien.

Le PHP finit par revenir vers le client, donc pas d'erreur dans le code PHP.

C'est hyper compliqué à débuguer Confused Ma solution serait d'écrire la boucle côté JS et pas côté Serveur, en espérant que le joueur quitte bien la table pour laisser l'autre y entrer comme il veut.

Quel est votre avis sur cette histoire ? Demain je refais les tests pour voir si c'est bien le PHP qui renvoie son info au client ou si c'est le JS qui force la récupération de l'info au bout d'un certain temps. (Heureusement que j'ai un ami avec qui ce bugue se produit sur moi, je sais pas comment je ferais sinon XD)

EDIT :
En reregardant un peu ce que vous m'aviez dit, ma table est en MyISAM, apparemment ça pourrait lock la BdD.

Prochains tests à faire :
- passer ma table en InnoDB
- essayer de commit la table d'update du joueur 1

EDIT 2:
Je viens de me rendre compte Xenos, que quand tu m'avais dis de commit la table, j'ai commit la table de lecture, et pas celle d'UPDATE ! Peut-être que je dois commit la table d'update ?


RE: Le fameux problème de mon appli JCJ - Xenos - 27-04-2020

Salut,

Citation :est-ce dû à la différence de connexion ?
Non, c'est dû à un mauvais code Smile

Citation :L'écriture de la table serait-elle lock par le joueur à connexion trop rapide qui la lit en boucle ?
Non, car une fois côté serveur, la connexion client n'a aucun impact. Elle n'en aurait que sur la concurrence de "quel joueur est le 1er à 'exécuter' le script PHP". C'est pour cela que je peux affirmer "Non" à la première question.

Ce que tu décris ensuite, oui, c'est le principe d'une transaction: une session connectée à la DB et qui lance une transaction va faire des modifications en DB qui ne seront pas lisibles par les autres sessions, tant que cette 1ere session n'aura pas dit "ok, j'ai finit" (ce qui s'appelle "commit"). Pour cela, la plupart des implémentations de DB utilisent des "locks": la 1ere session, qui a lancé une transaction et qui fait des actions, peut "locker" des tables/lignes/colonnes pour que d'autres ne puissent y toucher tant que le "commit" n'est pas fait. Ainsi, les autres sessions attendent que la 1ere session ait fini son travail (soit commit, soit "rollback", ie: "j'ai fini mais tout ce que jai fait, t'oublie en fait et tu sauve rien en DB!").

Citation : Il faudra que je vérifie si c'est le JS qui renvoie le joueur vers le PHP, ou si le PHP attend de lui-même pour finalement renvoyer la donnée au joueur (il me semble que c'est cette deuxieme configuration, mais il faut que je vérifie).
Par défaut, les sessions "classiques" de PHP (sur le disque dur) sont bloquantes. Donc, oui, il faut que tu vérifies que ton JS ne fasse pas inutilement plusieurs appels en paralèle, mais logiquement, même si 2 appels du même joueur sont reçus côté serveur, alors le serveur va ouvrir la session PHP pour le 1er appel, et le 2nd sera mis en attente. Toutefois, si jamais tu as un client JS qui fait ce genre d'empilement, cela peut impliquer le scénario suivant:
- JS envoie 1 query à PHP, qui ouvre la session, et mouline
- JS envoie une 2e query à PHP alors que la 1ere est encore en attente: la session PHP est lockée, et cette query poireaute...
- JS envoie une 3e query à PHP qui indique l'action du joueur: la session PHP est lockée, et cette query poireaute...

Si la 1ere & la 2eme query sont dépilées "trop lentement" par le serveur, alors la 3e sera traitée probablement après la fin du combat et l'action du joueur sera ignoré. Néanmoins, ce scénario n'implique rien sur la connexion des joueurs.
Ce problème se règle, je dirai, par une correction d'archi et l'utilisation des paramètres d'ouverture de session:
- JS ne doit faire qu'1 seule query en parallèle (long polling, ou polling répété) pour connaitre les infos envoyées par les autres joueurs, il faudra donc corriger ton client; de plus, la session devra être ouverte "sans lock", c'est à dire avec session_start(array('read_and_close' => true))
- La query envoyée par le JS pour dire l'action choisie par le joueur sera lancée en parallèle du long polling, éventuellement là aussi avec un read_and_close pour la session (y'a pas de raison de garder ladite session lockée côté serveur

La boucle côté JS, je ne sais pas si ce sera une bonne idée. Si elle a un autre intérêt que "planquer un truc que je comprends pas dans mon code", c'est une idée possible. Si c'est juste pour planquer ton bug, c'est une mauvaise idée Wink

Citation :apparemment ça pourrait lock la BdD.
Non, car MyISAM ne gère pas les transactions. Donc, il n'y a aucun lock possible côté DB. Ce n'est en revanche pas une bonne chose car transactionner les appels à la DB permet d'éviter que deux scripts PHP ne se "marchent dessus". Par exemple, sans transaction, un joueur pourrait (en gros) voler un objet à un autre alors que cet autre joueur vend l'objet exactement au même moment. Là, je dirai que tu n'as aucune idée de comment ton code va réagir : ) Autre exemple, si le joueur vend son objet, alors tu vas certainement faire 3 queries SQL (une pour savoir s'il a l'objet, une pour ajouter de l'or au joueur et une pour retirer l'objet): quid si le serveur SQL plante à un moment dans ces 3 queries? sans transaction, le joueur pourrait avoir gagné son or [là le serveur plante] et la query retirant l'objet ne sera jamais exécutée, et le joueur aura donc encore son objet.
Je t'incite donc très très fortement à passer tout ça en InnoDB, et à utiliser UNE transaction pour CHAQUE page appelée.

Citation : - essayer de commit la table d'update du joueur 1
Non, car cela n'a pas de sens: tu commites une transaction, pas une "table". Dans le cas spécifique que tu as ici, je pense que tu n'aura pas "une" transaction pour toute la page: les pages en long polling ne suivent pas ce pattern. En effet, sur un long polling avec MySQL dans lequel tu "demandes en boucle à la DB si il y a du nouveau, sinon, tu sleep(1) si oui tu l'envoie au joueur", alors transactionner le tout ne donnera rien, puisque tes queries MySQL ne verront jamais les modifications faites par les autres scripts PHP (principe d'isolation d'une transaction; bon, ok, tu peux changer ce niveau d'isolation, mais c'est hors sujet on va dire). Donc, tu vas ici entrer dans la boucle, ouvrir la transaction, regarder ce qu'il y a dans la DB, fermer la transaction, si il y a du changement, l'envoyer au joueur, sinon, sleeper.

Citation : Je viens de me rendre compte Xenos, que quand tu m'avais dis de commit la table, j'ai commit la table de lecture, et pas celle d'UPDATE ! Peut-être que je dois commit la table d'update ?
Cette phrase n'a pas de sens, cf réponse d'au-dessus Wink [je n'ai jamais pu te dire de "commit la table"]


RE: Le fameux problème de mon appli JCJ - L'Omniscient - 27-04-2020

Du coup t'es en train de me dire que là ya pas de solution ?

Je confirme que le PHP finit bien par revenir vers le client et que ce n'est pas le JS qui relance s'il n'a pas de réponse.

Je ne comprends pas comment un code peut foirer côté serveur selon quels clients jouent ensemble... Si c'est pas du race condition, c'est quoi selon toi qui peut faire buguer le serveur selon les clients qui interagissent avec ?


RE: Le fameux problème de mon appli JCJ - Xenos - 27-04-2020

Heu, si, c'est un problème totalement solvable, il faut juste que:
- tu fasses ton client JS pour ne pas empiler 50 requêtes si ce n'est pas utile
- utiliser les session en read_and_close pour tes long pollings
- transactionner tes pages pour ne pas faire n'importe quoi en DB, mais ne pas transactionnner les long polling (dans ce cas, ne transactionne que la partie "dans la boucle d'attente active", et PAS le "sleep()")

C'est une "race condition" que tu as, très probablement oui, mais la notion de "ca vient de ceux qui ont une connexion rapide par rapport aux autres", ça sent le flan comme "diagnostique": ca vient en vrai d'un problème de code, qui a plus de chances éventuellement de se produire si les vitesses des connexions des joueurs diffèrent.

Comme j'ai énormément de mal à comprendre ta logique de code, je te conseille de faire un diagramme sur un papier, avec le process décrivant le combat entre les deux joueurs (en termes de codes pas en termes de gameplay). Je pense que tu verras d'ailleurs vite où se situe le vrai soucis en faisant ça


RE: Le fameux problème de mon appli JCJ - L'Omniscient - 27-04-2020

Le client JS n'empile pas les requêtes, il empêche justement qu'on puisse en envoyer plusieurs

Je vais regarder du côté des read_and_close.

Bah du coup les transactions si je suis en MyIsam ça sert à rien ?
(Pour l'instant je vais surtout résoudre mon bug avant de faire d'autres modifs).

Je vais déjà voir le reand_and_close, puis je posterai ensuite mon diagramme papier.

EDIT : Okkkk, le bug que j'avais avec mon adversaire n'existe plus... Je me rappelle pas avoir touché à quelque chose pourtant... L'informatique desfois...
Ou alors j'ai changé ma table qui était en InnoDB et me rappelle plus, ou alors une mise à jour du serveur...