ProfileService & session locking : guide avancé (autosave, :Reconcile(), :Release(), dead locks)
TL;DR
ProfileService est un ModuleScript créé par loleris (Mad Studio) qui résout un problème fondamental des DataStores Roblox : la race condition entre plusieurs serveurs tentant de lire/écrire les données d'un même joueur simultanément. Si vous avez déjà vu des doublons d'items ou des pertes de données lors de téléportations, c'est exactement ce que le session locking corrige.
⚠️ ProfileService est stable mais n'est plus maintenu activement. Pour les nouveaux projets, Mad Studio recommande ProfileStore, son successeur direct. Ce guide reste pertinent car les deux modules partagent la même logique.
Pourquoi le session locking existe
Sans mécanisme de verrouillage, rien n'empêche deux serveurs Roblox de charger le profil d'un même joueur en parallèle :
- Le joueur quitte le Serveur A — la sauvegarde
UpdateAsyncest en cours. - Le joueur rejoint le Serveur B — il lit les données avant que A ait fini d'écrire.
- Résultat : écrasement de données ou copies divergentes → duplication ou perte d'items.
Le session locking résout ça : il garde la trace du serveur qui met actuellement les données en cache et transfère proprement la propriété d'un serveur à l'autre sans faire échouer les nouvelles requêtes de session.
Architecture d'un profil
Un profil est un ensemble de données destiné à être chargé une seule fois dans un serveur Roblox, puis lu et modifié localement — sans délai lié aux appels DataStore à chaque changement — tout en étant sauvegardé périodiquement et immédiatement une dernière fois quand le serveur a fini de l'utiliser.
Profile.Data est une table Lua classique que vous lisez et écrivez directement. ProfileService ne vous impose ni getters ni setters.
Structure minimale
local ProfileService = require(game.ServerScriptService.ProfileService)
local Players = game:GetService("Players")
local PROFILE_TEMPLATE = {
Coins = 0,
Level = 1,
Inventory = {},
}
local ProfileStore = ProfileService.GetProfileStore("PlayerData_v1", PROFILE_TEMPLATE)
local Profiles = {} -- [Player] = Profile
Le flux de chargement complet
local function PlayerAdded(player)
local profile = ProfileStore:LoadProfileAsync(
"Player_" .. player.UserId,
"ForceLoad" -- comportement si le profil est déjà verrouillé
)
if profile ~= nil then
profile:AddUserId(player.UserId) -- conformité RGPD
profile:Reconcile() -- remplir les nouvelles clés du template
profile:ListenToRelease(function()
Profiles[player] = nil
player:Kick("Profil chargé sur un autre serveur.")
end)
if player:IsDescendantOf(Players) then
Profiles[player] = profile
else
profile:Release() -- joueur parti pendant le chargement
end
else
player:Kick("Impossible de charger votre profil, réessayez.")
end
end
local function PlayerRemoving(player)
local profile = Profiles[player]
if profile then profile:Release() end
end
Players.PlayerAdded:Connect(PlayerAdded)
Players.PlayerRemoving:Connect(PlayerRemoving)
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(PlayerAdded, player)
end
Décryptage des méthodes clés
profile:Reconcile()
Par défaut, le profile_template n'est copié dans Profile.Data que pour les nouveaux profils. Les modifications apportées au template peuvent être appliquées aux profils existants en appelant Profile:Reconcile().
Concrètement : si vous ajoutez XP = 0 à votre template après une mise à jour, les anciens joueurs auront Profile.Data.XP == nil à la connexion. :Reconcile() remplit automatiquement ces champs manquants. Indispensable pour la compatibilité entre versions.
profile:Release()
Profile:Release() supprime le verrou de session pour ce serveur. Appelez cette méthode quand vous avez fini de travailler avec le profil. Les données seront sauvegardées immédiatement une dernière fois.
Oublier :Release() dans PlayerRemoving laisse un dead lock — le prochain serveur qui tente de charger le profil devra attendre le timeout de ForceLoad, soit potentiellement plusieurs dizaines de secondes.
profile:ListenToRelease()
Se déclenche quand un autre serveur prend la main sur le profil. Dans le callback : retirez le joueur de Profiles et kickez-le. Ne jamais ignorer cet événement.
Autosave
ProfileService répartit automatiquement les appels DataStore de façon uniforme dans la fenêtre de la boucle d'autosave. La fenêtre par défaut est de 30 secondes. Vous n'avez pas à déclencher de sauvegarde manuelle pendant la session — modifiez Profile.Data directement.
Le not_released_handler : gérer les conflits
| Valeur | Comportement |
|---|---|
"ForceLoad" | Demande à l'autre serveur de relâcher. S'il ne répond pas, vole la session. |
"Steal" | Écrase immédiatement le verrou. Plus rapide, à réserver aux locks clairement morts. |
function(place_id, game_job_id) | Callback custom : retourne "Repeat", "Cancel", "ForceLoad" ou "Steal". |
Si le profil est verrouillé par un serveur distant, il sera soit relâché par ce serveur, soit "volé" — le vol étant nécessaire pour les serveurs qui ne répondent pas à temps ou qui ont crashé.
Dead session locks et server hopping
Quand un serveur crashe sans appeler :Release(), le verrou reste actif. Dans de rares cas, si le serveur crashe, le profil restera verrouillé jusqu'à ce qu'il soit ForceLoadé par une nouvelle session.
ProfileService gère ça via ForceLoadMaxSteps (8 tentatives par défaut) et AssumeDeadSessionLock : si un profil n'a pas été mis à jour depuis 30 minutes, le verrou est considéré comme mort.
Pour le server hopping : dans au moins 5 % des cas où un joueur change de serveur rapidement, le profil peut mettre jusqu'à 7 secondes à se charger — ce qui s'améliore avec Profile:ListenToHopReady().
Il est recommandé de relâcher les profils juste avant les téléportations pour accélérer le relâchement du verrou.
-- Avant une téléportation
local profile = Profiles[player]
if profile then profile:Release() end
TeleportService:TeleportAsync(placeId, {player})
Erreurs fréquentes
- Oublier
:Release()dansPlayerRemoving→ dead lock garanti. - Charger deux fois la même clé sans release → charger un profil déjà verrouillé sur le même serveur produit une erreur.
- Ignorer le
nilde:LoadProfileAsync()→ toujours kicker le joueur si le retour est nil. - Modifier
Profile.Dataaprès:Release()→ le profil n'est plus actif, les changements sont perdus. VérifiezProfile:IsActive()dans les tâches asynchrones. - Stocker des Instances ou fonctions dans
Profile.Data→ cela peut entraîner une perte silencieuse de données ou un échec complet de la sauvegarde.
ProfileService vs ProfileStore
ProfileStore est le successeur de ProfileService — il utilise un mécanisme similaire pour le session locking, mais amélioré pour résoudre les conflits entre serveurs plus rapidement.
Différence clé : ProfileStore se base sur les autosaves pour résoudre les conflits en un seul appel UpdateAsync(), et grâce à MessagingService, il peut autosaver moins fréquemment tout en réagissant aux serveurs externes qui tentent de prendre le verrou.
Projet existant avec ProfileService : pas d'urgence à migrer. Nouveau projet : partez directement sur ProfileStore.
FAQ
Q : Puis-je utiliser ProfileService pour des données non-joueur ? Oui — l'abstraction est découplée de l'instance Player : vous pouvez créer des profils pour des maisons de guilde, des instances de jeu sauvegardables, etc. Mais le session locking n'est pas idéal pour des écritures rapides depuis plusieurs serveurs simultanément.
Q : Comment tester sans toucher au vrai DataStore ? Utilisez ProfileStore.Mock — un DataStore simulé en mémoire, parfait en Studio sans activer les services Roblox.
Q : :Reconcile() est-il obligatoire ? Non, mais très conseillé. Sans lui, les nouvelles clés ajoutées au template ne se propagent pas aux profils existants.
Si vous cherchez des scripts data déjà structurés pour votre jeu, la section Scripts Roblox sur GM Market propose des wrappers ProfileService/ProfileStore prêts à l'emploi.
Sources :