Save your player data with ProfileService! (DataStore Module)

I am interested in this, what exactly is “session locking” and how hard would it be to switch over from a game currently using Datastore2?

4 Likes

If your game does not have trading and won’t have it in the future, then there’s no emergency to switch data store modules. I’d say moving between two datastore modules for a game is a difficult challenge and you should avoid doing things like that if possible.

Session-locking basically prevents Server #2 from loading player data before Server #1 finishes saving changes to the same player data - this is how item duplicates can be made in many games that have trading. As far as you should be concerned - using session-locking is mandatory in preventing item duplication loopholes.

19 Likes

I just uploaded a video analyzing the issues (with powerpoint-tier animations) that ProfileService addresses, as well as an in-depth getting started guide to use with a small sword fighting minigame. The place file can be found in the comment section.

18 Likes

Would Profile:IsActive() be able to be checked from ProfileStore:ViewProfileAsync()? I know you can’t write/save any data, but I was wondering if you would be able to check to see if a Profile was locked through IsActive() and ForceLoad it or send a GlobalUpdate depending on is it returns true or false.

-- Example --
local profile_key = KeyHere
local viewProfile = ProfileStore:ViewProfileAsync(profile_key)

if viewProfile:IsActive() == true then
    -- Send Global Update Stuff Here
else
    local profile = ProfileStore:LoadProfileAsync(
        profile_key,
        "ForceLoad"
    )
end
4 Likes

That’s what the not_released_handler is for:

local get_place_id, get_game_job_id
local profile = GameProfileStore:LoadProfileAsync(
    "Player_" .. player.UserId,
    function(place_id, game_job_id)
        get_place_id, get_game_job_id = place_id, game_job_id
        return "Cancel"
    end
)
if profile ~= nil then
    -- Profile was not session locked, and is now session locked to this server
elseif get_place_id ~= nil then
    -- Profile is (most likely) session locked by a remote server
else
    -- Ultra rare case of nothing working... Just kick the player
end

This way ProfileService will make only one UpdateAsync() call in the scenario the profile is not released instead of doing a UpdateAsync() once to check it and then UpdateAsync() again to load it (Which would also result in a queue warning in the dev console).

HOWEVER, be cautious that there are no guarantees that the server of "game_job_id" is alive - this is the case where the profile is indefinetly locked and you will only be able to load the profile using “ForceLoad”.

(Just make sure the profile is not already loaded in the same server, or this code will error)

9 Likes

I saw that you are using :UpdateAsync() to get the get the player’s data. Is there a reason that you use this over the traditional :GetAsync()?

3 Likes

:UpdateAsync() is essentially a :GetAsync() and :SetAsync() combined (it can literally replace either of those two methods), with some additional functionality which helps data be stable when accessed from several servers at the same time. Session-locking needs to set some data inside a profile when the profile is loaded - that’s the main reason why I need :UpdateAsync().

12 Likes

I would advise against this, as it can interfere with DataStore saving and loading sometimes. When using DataStores, I always try to save only when joining and leaving (as well as game binded close).

2 Likes

What kind of interference exactly are you experiencing when doing periodic DataStore saving calls?

4 Likes

In the past, I’ve actually had the Data Store interfere with itself when attempting to auto-save. Auto-saving increases the risk of exhausting the data store, which can especially cause issues for users trying to load into the game and retrieve data.

The result, from my experience, was unintentional data loss for some users at some point or another, or outdated data. When periodic saving was removed, this issue was fixed entirely.

With that being said, some developers may already be using data stores for global leader-boards, game-wide data, etc.

3 Likes

Well I think the goal in effective data management is not to use as few DataStore calls as possible, but stay well within the limits - ProfileService sits at around 10% to 18% of a game’s DataStore call budget when every player owns their own Profile. Auto saving is essential to keep data as secure as possible and not risk having players revert to their past save snapshot after a server crashes with hours of progress lost instead of just seconds.

As for “DataStore interfering with itself” - you could’ve been just running into DataStore throttling due to improper call timing since setting a single key too frequently will throttle your calls much sooner than you run out of DataStore call budget.

14 Likes

So if I understand correctly, I could do something like this. I want to test to see if the Profile is currently loaded in that server. If it is not (if not testProfile then), I want to run your code; otherwise if it is in that server, just send a Global Update.
Now the question I have is, if the “ultra rare case” happens, would I be able to Kick the player then ForceLoad right after to make the current server grab the profile, and does that “ultra rare case” include the game_job_id not being alive?

function PlayerHandler.Test(id)
	local testProfile = Profiles[id]
	
	if not testProfile then
		
		local place_id, game_job_id
		local profile = PlayerDataStore:LoadProfileAsync(
			playerKey.. id,
			function(placeID, gameID)
				place_id, game_job_id = placeID, gameID
				return "Cancel"
			end
		)
		
		if profile ~= nil then
			-- Set Data because Server Grabbed Profile
		elseif place_id ~= nil then
			-- Send Global Update because Profile Locked
		else
			-- Kick User with "id"
		end
		
	elseif testProfile then
		-- Send Global Update
	end
end
5 Likes

GlobalUpdates are meant specifically for saving some sort of data to a Profile regardless of whether the Profile is in a server or not and it certainly streamlines this kind of a task. You should also consider NOT using GlobalUpdates for profiles loaded on the same server since they’re going to take up to a whole minute to proccess instead of just applying something to your data instantly.

If giving a player some sort of an item is not your goal or you want data to move as fast as possible then you shouldn’t be using GlobalUpdates - you weren’t really clear on what you’re trying to achieve.

My guesses are, you should be using MessagingService - Set up a message topic composed of a server’s JobId (like "ProfileCheck_" .. game.JobId) and just try to communicate with that server and wait for the message back from it with a timeout - this would be a much faster solution than GlobalUpdates because GlobalUpdates were not designed for this. If you don’t get a message back from the server that is supposedly locking the profile (after waiting for like 20 seconds), then assume it is indefinetly locked and “ForceLoad” the profile and let the player know that it can take a bit longer to load their data.

7 Likes

Thanks, I ended up using the MessagingService, I was just going to try and do it with GlobalUpdates because it wasn’t all important. But how long does it usually take for a profile to load? The profile itself takes around 30 to 60 seconds.

2 Likes

I had similar issues when making my video, and I figured out that the issue is probably near the end of the :BindToClose near the bottom of the module. If you replace all of the ands with ors in the while loop that waits for the profiles to save/load, then it should fix the issue most of the time.

Mine looks like this now:

while on_close_save_job_count > 0 or ActiveProfileLoadJobs > 0 or ActiveProfileSaveJobs > 0 do
	RunService.Heartbeat:Wait()
end
4 Likes

Ah epic, thanks. I was confused because it didn’t look like I had done anything different besides naming when it came to loading.

3 Likes

If I wait like fifteen seconds before loading my players’ data, would that not prevent item duplications? It pretty much guarantees that the data will be loaded after it was saved originally. Or is session locking somehow superior, I’m confused…

3 Likes

What you said is similar to this:

A part should be in the workspace, but it might not be… I’ll wait 15 seconds to ensure its in the game. (And after those 15 seconds you don’t actually verify the part is there, you just do workspace.Part)

Whereas session locking could be related to using workspace:WaitForChild() to wait for the part.

This is a very basic comparison, but I think the logic behind it is sound.

7 Likes

If you go back to what loleris said here, that should answer your question.

2 Likes

It is true that you would have way less duplication potential, though not guaranteed. Also ProfileService will let you load data instantly instead of waiting those 10-15 seconds - your players will really care about the load times.

10 Likes