Save your player data with ProfileService! (DataStore Module)

I’ve checked. “The two files are identical” I’ve done 0 changes, happens to be a 1 in 100,000 error.

How would you go about viewing the data, like a datastore editor?

Another Error after another 50.000 Visits:

ServerScriptService.Modules.ProfileService:1447: [ProfileService]: Invalid ActiveSession value in Profile.MetaData - Fatal corruption ServerScriptService.Modules.ProfileService, line 1447 - function LoadProfileAsync
ServerScriptService.Modules.PlayerCache, line 88 - function PlayerAdded
ServerScriptService.Modules.PlayerCache

1 Like

[03/29/2021] Edge case improvements for errors and :BindToClose(). Time to update!
Both fixes were made thanks to @sayer80 reports!

Muted "Invalid ActiveSession value in Profile.MetaData" error:

Github commit 743c1fe

Screenshot_2

The error was expected to be thrown in a rare scenario where a profile was in the process of being loaded while the server started shutting down - the causes of this were if statements in lines 1283 and 1330 that prevented the StandardProfileUpdateAsyncDataStore() function from returning a profile table with normally expected members. This shouldn’t have caused any damage to player data, though it might have affected other game processes running in the same coroutine as the error would’ve blocked them.

Fixed potential ActiveProfileLoadJobs counter leak:

Github commit c6dd170

This problem shouldn’t have been a big deal since if it ever occured it would only make the Roblox server linger for the maximum time until shutdown is forced for all lua scripts that are still yielding with :BindToClose() functions - this should not affect any parallel :BindToClose() tasks.

All places of exit (return, error, etc.) in ProfileStore:LoadProfileAsync() must decrement the ActiveProfileLoadJobs variable - there were two occurrences where that was missing. If ActiveProfileLoadJobs never reaches zero it will yield indefinetly at the last while loop in Madwork.ConnectToOnClose.


Overall, after updating to this version you’ll see less rare unexpected behavior in other parts of your game (ProfileService killing coroutines with errors) and fewer garbage errors in your analytics.

4 Likes

The only main issue I have with ProfileService is the fact that if you teleport a lot of players from a different server to another after releasing, it usually will Session Lock and make the data load in about a minute instead of around 2-3 seconds (probably due to the overwhelming about of players saving). What I did to fix this was to edit the module and create a custom listener for the ProfileStore called “:ListenToReleaseAndSave” which would make the script wait for it to both Release and Save, and then teleport the player, and when I did this, data would load in 2-3 seconds (as opposed to about a minute)

I suggest to implement a function that allows you to listen when something gets saved as well as if it got released or not because as of right now you’re not able to tell if something saved recently or not. (I actually originally thought that :ListenToRelease would fire once it finishes releasing and saving but it only fires once it gets released)

EDIT: I’m a bit late on editing this, but this has been solved!

2 Likes

[03/30/2021] Great performance improvements for multi-place games utilizing teleports. Time to update again, lol. Big thanks for @Rawblocky’s report - it lead me to making interesting discoveries about ProfileService.

tldr: ProfileService loads slightly faster and does less developer console spam. Go update.

Github commit 1f83c0c

NEW METHOD - Profile:ListenToHopReady():

If you’ve been having problems with ProfileService after universe teleports, this method should make a huge difference:

Added a custom write queue:

ProfileService was relying too heavily on the UpdateAsync queue and, to my ignorance, it was not the safest thing to do either - apparently queued UpdateAsync calls are not guaranteed to be executed in the
order they were sent in. This shouldn’t have been a huge deal in most ProfileService using games because games would rarely grow a huge queue of UpdateAsync calls.

After you update, ProfileService will now use its own UpdateAsync queue system which will increase stability and will make ProfileService throw a lot less queue warnings! LETS GOOO!

Universe teleport testing place:

I’ve been doing some intense testing to make these improvements - you can check how ProfileService performs during teleports with this place file:

UniverseProfileTest.rbxl (49.9 KB)

You’ll need to publish this project to the primary place of a game (universe) and copy the UniverseProfileTest script to a second place within the same game. You’ll also need to change
these values to your place id’s:
image

Optionally, you can setup external analytics in LogHttp - it was useful for figuring out when the server experienced a BindToClose timeout:
image

12 Likes

Oh… Thanks for letting me know, I believed this was a datastore beta problem. I’m passing that on to a engineer to let them know it’s a overall DataStore problem.

1 Like

I put my data manager script in the server script service. I noticed it doesn’t work if it’s a module script. Why is that?

-- ProfileTemplate table is what empty profiles will default to.
-- Updating the template will not include missing template values
--   in existing player profiles!
local ProfileTemplate = {
    Cash = 0,
    Items = {},
    LogInTimes = 0,
}

----- Loaded Modules -----

local ProfileService = require(game.ServerScriptService.ProfileService)

----- Private Variables -----

local Players = game:GetService("Players")

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

local Profiles = {} -- [player] = profile

----- Private Functions -----

local function GiveCash(profile, amount)
    -- If "Cash" was not defined in the ProfileTemplate at game launch,
    --   you will have to perform the following:
    if profile.Data.Cash == nil then
        profile.Data.Cash = 0
    end
    -- Increment the "Cash" value:
    profile.Data.Cash = profile.Data.Cash + amount
end

local function DoSomethingWithALoadedProfile(player, profile)
    profile.Data.LogInTimes = profile.Data.LogInTimes + 1
    print(player.Name .. " has logged in " .. tostring(profile.Data.LogInTimes)
        .. " time" .. ((profile.Data.LogInTimes > 1) and "s" or ""))
    GiveCash(profile, 100)
    print(player.Name .. " owns " .. tostring(profile.Data.Cash) .. " now!")
end

local function PlayerAdded(player)
    local profile = GameProfileStore:LoadProfileAsync(
        "Player_" .. player.UserId,
        "ForceLoad"
    )
    if profile ~= nil then
        profile:Reconcile() -- Fill in missing variables from ProfileTemplate (optional)
        profile:ListenToRelease(function()
            Profiles[player] = nil
            -- The profile could've been loaded on another Roblox server:
            player:Kick()
        end)
        if player:IsDescendantOf(Players) == true then
            Profiles[player] = profile
            -- A profile has been successfully loaded:
            DoSomethingWithALoadedProfile(player, profile)
        else
            -- Player left before the profile loaded:
            profile:Release()
        end
    else
        -- The profile couldn't be loaded possibly due to other
        --   Roblox servers trying to load this profile at the same time:
        player:Kick() 
    end
end

----- Initialize -----

-- In case Players have joined the server earlier than this script ran:
for _, player in ipairs(Players:GetPlayers()) do
    coroutine.wrap(PlayerAdded)(player)
end

----- Connections -----

Players.PlayerAdded:Connect(PlayerAdded)

Players.PlayerRemoving:Connect(function(player)
    local profile = Profiles[player]
    if profile ~= nil then
        profile:Release()
    end
end)
1 Like

modules only run when they have been required for the first time, but your script doesn’t appear to return anything anyways (modules must return a table with all of its publicly accessible functions and publicly accessible values), so it might as well be a server script

How should I set up profile service for client replication with ReplicaService?

Your post was deleted. Please help.

I’m pretty sure there’s an example in the ReplicaService page.

I know that but how would I go about making a profile and replicate that to the client. The example just shows how to set up a class token. Could you help by showing me an example?

Can anyone show me an example of how to use GlobalUpdates? The API docs confuse me :sweat_smile:

Shout out to okeanskiy and his amazing video that is explaining how it works.
Watch it here: Global Updates: ProfileService Tutorial Part 2 (Roblox Studio) - YouTube

1 Like

umm Im kinda of a noob so i do not know much about how to deal with my problem, so i have the data saving fine for the most part but for some reason one of the data key thingies gets deleted or something and turns into nil for some reason. but its just one of the data thingies(im basically saying that if there was levels coins and exp to save, exp for some reason just gets nil SOMETIMES for some reason.) that turns into nil. The other ones like level and coins never did turn into nil. i do not know why it happenned, i set the script up with the video of okeanskiy. I hope you can help me.

local ServerScriptService = game:GetService("ServerScriptService")
local RS = game:GetService("ReplicatedStorage")

local ProfileService = require(RS.ProfileService)

local UiColorTable = {{1,1,134},{10,124,134},{12,255,247},{26,255,129},{23,135,6},{167,255,2}
,{255,255,3},{255,158,1},{255,74,14},{255,0,0},{255,6,72},{255,6,160},{96,0,103}}

local profileStore = ProfileService.GetProfileStore(
	"Player",{
		UiColorTheme = UiColorTable[math.random(1,13)];
		Sectoriat = 0;
		Level = 1;
		Exp = 0;
	}
)

local Profiles = {}

local function onPlayerAdded(player)
	local profile = profileStore:LoadProfileAsync(
		"Player_" .. player.UserId,
		"ForceLoad"
	)
	if profile then
		profile:ListenToRelease(function()
			Profiles[player] = nil
			player:Kick()
		end)
		
		if player:IsDescendantOf(PlayerService)then
			Profiles[player] = profile
		else
			profile:Release()
		end
	else
		player:Kick()
	end
end

local function onPlayerRemoved(player)
	local profile = Profiles[player]
	
	if profile then
		profile:Release()
	end
end

PlayerService.PlayerAdded:Connect(onPlayerAdded)
PlayerService.PlayerRemoving:Connect(onPlayerRemoved)

local DataManager = {}

function DataManager:Get(player)
	local profile = Profiles[player]
	
	if profile then
		return profile.Data
	end
end

function DataManager:GetProfileStore()
	return profileStore
end

return DataManager```

Note: Sectoriat is basically coins.

What about it isn’t working? Also use ``` on the top and below code.

Well as i stated, for some reason exp just keeps on turning into nil while the others have no problem, btw i have never changed any of the values to nil in my scripts so im kinda scared like if this happened to me a few times already, what would happen when i release my game. People would lose their exp and worst part is since it turns into nil i can’t do anything with exp since it causes a lot of errors like some parts of the scripts would just stop. Thats why i thought i would ask it here.

Did you add the Exp key into the profile template after originally testing it? If so, you’ll need to call :Reconcile() upon finding a profile to ensure that they have the most updated template.

Well then, i will try to write reconcile function , if i can’t get it to work hopefully you guys can help me.