Save your player data with ProfileService! (DataStore Module)

I am interested in this plugin after I had a duplication occur on one of my projects that used regular datastores. The method was:
You have two players on the same server, player A and player B
Both players have 5 gold coins.
Player A gives player B 5 gold coins.
Player A now has 0 gold coins, Player B now has 10 gold coins
Player B leaves the server, saving his account’s 10 gold coins on logout
The server crashes before the autosave tick
Player A, due to failure to autosave, now has those 5 gold coins back. Player B, since the server saved his logout, has 10 coins.
Now there is a total of 15 coins in circulation because of a crash-dupe

Does this type of duplication occur natively with this plugin? I haven’t gotten around to fully testing it out yet.

I don’t believe it’s possible to make a fix for this, as apparently BindToClose doesn’t fire on server crash. The best way would be to improve your code so that the server won’t crash unless it is actually Roblox’s fault.

You said you wait at least 80 seconds before stealing a session. Is there a reason why you wait such a long time?

1 Like

Your server could be lagging / DataStore API might have problems saving / Server might’ve crashed. Until a Profile is properly released on the remote server, ProfileService does not know which of those scenarios are true and won’t rush to steal the Profile when a remote session lock is still present.

You can alter this behaviour with the new ”Steal” argument if you have a way to find out whether the remote server that owns the profile is alive or not. If Roblox servers don’t crash constantly and profiles always save successfuly on release, then the 80 second wait will occur rarely.

1 Like

[08/23/2020] ProfileStore:WipeProfileAsync(), ProfileStore.Mock and HeartbeatWait


It is now possible to use ProfileService in mock DataStore mode while having Roblox API services enabled in studio - see ProfileStore.Mock.

local ProfileTemplate = {}
local GameProfileStore = ProfileService.GetProfileStore(
  "PlayerData",
  ProfileTemplate
)

local LiveProfile = GameProfileStore:LoadProfileAsync(
  "profile_key",
  "ForceLoad"
)
local MockProfile = GameProfileStore.Mock:LoadProfileAsync(
  "profile_key",
  "ForceLoad"
)
print(LiveProfile ~= MockProfile) --> true

-- When done using mock profile on live servers: (Prevent memory leak)
MockProfile:Release()
GameProfileStore.Mock:WipeProfile("profile_key")
-- You don't really have to wipe mock profiles in studio testing

ProfileService now has ProfileStore:WipeProfileAsync() functionality for easier erasure of user data.


All wait() occurences have been replaced with a Heartbeat-based waiting function.

11 Likes

@oniich_n There you go :smiley:

This is the code part in the documentation that is relevant to you:

local RunService = game:GetService("RunService")
local GameProfileStore = ProfileService.GetProfileStore("PlayerData", ProfileTemplate)
if RunService:IsStudio() == true then
  GameProfileStore = GameProfileStore.Mock
end
4 Likes

Awesome! Thank you so much :smiley:

How difficult would it be to convert my game’s current datastore setup into this without losing existing player data?

I already know this module autosaves, but how often?
Does it autosave on data change, no matter what? Or does it have a set interval that data changes get saved?
I’m going to add audio settings to my game so players don’t have to redo their preexisting settings, but I don’t want exploiters or just people repeatedly changing their settings to spam the remote to change their settings, and start to throttle my datastore.

It autosaves every 30 seconds, but every request is spread throughout the minute (it doesn’t just request data for every single player at the same time).

I’ve seen a lot of people ask about the whole concept of session locking and why UpdateAsync needs to be used to both set AND load a save. I feel like I could’ve done a better job explaining it in my video now that I understand it a bit better, but that ship has sailed. Maybe in the future. For those of you skeptical about the session locking guarantee that ProfileService offers, I’ll try to explain what happens atomically. Even the most experienced of programmers might learn something.

2 servers send an UpdateAsync request at the same time with the following code. This code loads a player’s saved data:

-- Initially data.SessionJobId will be nil (because no server has the player's data loaded)
print("Sending update async request")
local loadedData = PlayerStore:UpdateAsync(key, function(data)
    if data == nil then
        data = {}
    end
    print("Current SessionJobId: " .. tostring(data.SessionJobId))
    -- Check if the save doesn't have a session lock OR if it's locked by this server. --
    if data.SessionJobId == nil or data.SessionJobId == game.JobId then
        data.SessionJobId = game.JobId
        print("No session lock active, setting SessionJobId to " .. game.JobId)
        -- Handle any fetching or writing right here --
        
        return data
    else
        print("Session already locked")
        return nil -- Returning nil in UpdateAsync only cancels the query. IT DOES NOT SET THE DATA TO NIL.
    end
end))
print("Update async complete")
if loadedData ~= nil then
    print("Loaded data!")
else
    print("Could not load data due to active session lock")
end

It’s not possible to guarantee which server will be processed first as it’s up to Roblox’s internal network speed along with other small factors, but whichever server made the request that reaches Roblox’s datastore servers first we will label Server A. The other one is Server B.
Server A will have a JobId of efcc7b83-df93-4b44-abfd-b957d4182bba.
Server B will have a JobId of 6fd555f0-cedd-47bd-916c-73677dc8c314.

Server A’s server output will look like this:

--- SERVER A OUTPUT ---
Sending update async request
Current SessionJobId: nil
No session lock active, setting SessionJobId to efcc7b83-df93-4b44-abfd-b957d4182bba
Update async complete
Loaded data!

Server B’s server output will look like this, assuming that the 2 UpdateAsync requests were sent at the exact same time from different servers concurrently trying to modify the same key:

--- SERVER B OUTPUT ---
Sending update async request
Current SessionJobId: nil
No session lock active, setting SessionJobId to 6fd555f0-cedd-47bd-916c-73677dc8c314
Current SessionJobId: efcc7b83-df93-4b44-abfd-b957d4182bba
Session already locked
Update async complete
Could not load data due to active session lock

If you look at server B’s output, you can see that it appears the callback function ran twice (each time with different data in data), despite us only using 1 UpdateAsync request. The reason why Server B’s updateasync callback ran twice is because that’s exactly how UpdateAsync works and why you should never use SetAsync to save player data.


It’s a very rare occurance, but Roblox’s databases will internally figure out which server should be processed first and if there’s any other requests made to that same key at the same time, they will all be rejected and the game servers that made them will have to re-process their callback functions. This makes it virtually impossible for 2 servers to have the same profile loaded at once.

Of course when releasing a profile, network errors happen, server crashes happen. This is why ProfileService might take up to 90 seconds to “steal” a profile (which is a lot better than an infinite amount of time!)

29 Likes

Only problem I had with this module is that if I want to teleport people to different worlds in my game, it well have super long load times, longer then the 10 seconds. But everything else is working great. Maybe am doing something wrong tbh, any help is appreciated.

Whenever your server is about to teleport the player (or group of players), make sure to release their profile.

2 Likes

I’m saving a table with this Module, but I have an issue. New value added to the table won’t save to the DataStore and will be nil. For ex, I have a table: {val1 = 0}. If I run the game and later add for example val2 = false in the table: {val1 = 0, val2 = false} then it won’t save val2 to the DataStore. I don’t know if this is intentional or not, but I have to change the DS key everytime I want to add a new value to the table.

Does it only happen if it saves the value as false or does it happen with any new key with any new value added? If it only happens with false, it might be something in ProfileService that’s not explicitly checking for nil.

It also happens to number value.

Oh nevermind, sorry to bother. Seems like it worked now. :grin:

Just so there is no confusion. I don’t have to release a profile before attempting to teleport a player. I can use PlayerRemoving to release a profile right? And in most cases, profiles released with PlayerRemoving will be released before the next server tries to load the the profile?

1 Like

Technically yes, however just to be safe I would Release the profile before Teleporting so it’s Released faster.

There is no point in keeping the Data anyways if you can guarantee that the player will leave the current server for sure. (Teleport functions can fail)

Hard to say, Datastore could be unreliable at times, or your code might fail, so it’s better to be safe than sorry.

1 Like

I’m assuming that in 98.04% of cases, teleporting will succeed and profiles will be released on .PlayerRemoved before the next server gets to loads them.

If I don’t release the profile before teleporting a player then I wont have to reload their profile in the unlikely chance that the teleport fails.

And on the off chance that datastores are working slowly for whatever reason then the worst outcome is that I have to wait a little bit longer on the second server for the profile to be released on the first server. ProfileService will handle the retrying.

I didn’t think of this before but, if you follow the basic usage of kicking the player from the server when their profile gets released then you cannot call :release() before trying to teleport the player.