Save your player data with ProfileService! (DataStore Module)

Hey @loleris , I was using your ProfileService module today when I came across an error in the ProfileService module. The error is with a function that is trying to execute in a loop many many time. This only happens when testing with more than 2 people in the server. Any idea what might be wrong?

DeepCopy functions usually error when there exists a circular reference inside the table being deep copied. In ProfileService deep copy is ran for the default data template. ProfileService also deep copies Profile.Data in mock mode.

Hey, I had a question. I have other scripts doing things to the player’s data before they leave, so is it fine if I delay the release of their profile a second after they leave?

players.PlayerRemoving:Connect(function(player)
	delay(1, function()
		local profile = profiles[player]
		
		if profile then
			if playerData:FindFirstChild(player.Name) then
				if not playerData[player.Name].Safe.Value then
					profile.Data.Time *= 0.85
				end
			end
		
			profile:Release()
		end
	end)
end)

Sorry for this super late response but how does profile service save data then if we don’t need to enable studio access to API services. I thought enabling that let you use datastores in-studio and in-game and we need to check ourselves with run service to not save in studio

1 Like

ProfileService saves in studio mode when API services are enabled. If this is undesirable then you can set your ProfileStore into .Mock mode when in studio (Can be checked through RunService).

Your method would fail to save. ProfileService expects you to always keep Profile.Data at the most recent final state and provides no way of managing data on the moment of being released. You should lean towards having Profile.Data as a single source of truth

So, can I wait after multiplying their data by 0.85, or do I just need to find another way of setting it earlier?

players.PlayerRemoving:Connect(function(player)
	local profile = profiles[player]
	local folder = playerData:FindFirstChild(player.Name)
		
	if profile then
		if folder then
			if not folder.Safe.Value then
				folder.Data.Time *= 0.85
			end
		end
		
		delay(1, function()
			profile:Release()
		end)
	end
end)
1 Like

Script timeout: exhausted allowed execution time ServerScriptService.Modules.ProfileService, line 1681 ServerScriptService.Modules.ProfileService

Just received this Server Error (happened once in 110.000 Visits)

Try updating your module to the latest version and not tinkering with the SETTINGS table if you happen to have done that.

1 Like

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.