Le problème commence avec un deuxième joueur.
Déplacer un objet paraît simple : retirer une quantité ici, l’ajouter ailleurs. Mais deux joueurs peuvent ouvrir le même coffre, et une opération peut céder la main pendant un traitement asynchrone. J’ai donc traité le déplacement comme une opération qui mobilise un ensemble de ressources.
Mon ordonnanceur extrait les identifiants des inventaires concernés : source, destination, inventaire du personnage actif et, selon la demande, inventaire du personnage destinataire. Je déduplique et trie ces clés pour disposer d’un ensemble stable. Deux demandes du même personnage peuvent ainsi se retrouver en conflit, même si leurs destinations diffèrent.
Prendre tous les verrous avant de démarrer.
Je vérifie d’abord que toutes les clés sont disponibles, puis je les réserve avant de lancer le traitement. Cette vérification et cette réservation ne cèdent pas la main. Une opération en attente ne conserve donc pas une partie des verrous en espérant obtenir les autres.
J’ai séparé l’acquisition, l’exécution et la libération. Le processeur est appelé dans un traitement protégé ; à sa sortie, l’ordonnanceur libère les clés et décrémente ses compteurs. Cette protection organise le cycle de vie de l’opération. Elle ne constitue pas, à elle seule, un retour arrière de toutes les modifications métier.
PSEUDO-CODE EXPLICATIFclés = inventaires impliqués, uniques et triés
si toutes les clés sont libres :
réserver les clés sans céder la main
lancer le traitement protégé
à sa sortie : libérer les clés01 réserve Alice et le coffre. 02 attend le coffre, mais 03 peut démarrer sur des inventaires indépendants.
Une demande bloquée ne bloque pas toute la file.
À chaque passage, je parcours la file et je démarre les demandes dont les clés sont libres. Lorsqu’une demande rencontre un verrou, je passe à la suivante. Une opération sur un autre coffre peut donc commencer avant elle.
J’ai borné le nombre de traitements actifs à 32 et le nombre d’entrées examinées par passage à 96. Il s’agit de concurrence coopérative, avec des traitements qui peuvent attendre ou céder la main. Ces valeurs ne désignent pas 32 threads CPU exécutant du Lua en parallèle.
Ce choix favorise la progression des opérations indépendantes. Il ne fournit pas un ordre FIFO strict ni une garantie d’équité entre toutes les demandes. Je garde donc un compteur de demandes actives, la longueur de file et l’âge de sa première entrée pour observer les situations d’attente.
Faire participer la sauvegarde à la coordination.
La persistance utilise le même mécanisme de verrouillage lorsqu’elle sauvegarde un inventaire. Si celui-ci est occupé, elle reporte la tentative. J’évite ainsi de traiter la base de données comme un processus indépendant des opérations qui modifient le cache.
Je conserve un état des objets déjà persistés et je construis les écritures à partir des différences : objets modifiés, ajoutés ou retirés. Ces écritures passent dans une transaction SQL pour l’inventaire sauvegardé.
Je capture également une révision avant l’écriture. Après son succès, je ne retire le marqueur de modification que si cette révision est toujours la même. Une modification survenue entre-temps reste donc à sauvegarder. Cela ne revient pas à promettre une transaction SQL unique couvrant tous les inventaires d’un transfert.
Le principePoursuivre l’explorationJe réserve ensemble les inventaires nécessaires à une opération. Si ces ressources sont occupées, je peux faire avancer une autre demande indépendante.