Need help with my datastore system Am I missing something?

I am a game developer that usually gets 100+ concurrent players. My main concern is that I have reports of people losing data in my game.
I sometimes played the game myself and I rarely had issues of data stores. Sometimes it’s data not saving and sometimes data is wiped completely.

Here are my codes.

local DSS = game.DataStoreService
local DataStore = DSS:GetDataStore("PlayerData")

function AddPlayerData(Plr)
	local PS = script.Data:Clone()
	PS.Parent = Plr

	local save = DataStore:GetAsync(Plr.UserId.."_data")
	if save then
		for i,v in pairs(PS:GetChildren()) do
			if save[v.Name] then
				v.Value = save[v.Name]
			end
		end
	end
	if PS.DataVersion.Value < 1 then
		PS.DataVersion.Value = 1
	end
	
end
game.Players.PlayerAdded:Connect(AddPlayerData)

function SaveData(Plr)
	local Data = Plr.Data
	local tabl = {}
	for i,v in pairs(Data:GetChildren()) do
		tabl[v.Name] = v.Value
	end
	--DataStore:SetAsync(Plr.UserId.."_data",tabl)
	local OldData = DataStore:GetAsync(Plr.UserId.."_data")
	
	if tabl then
		DataStore:UpdateAsync(Plr.UserId.."_data", function(OldData)
			local previousData = OldData or {["DataVersion"] = 1}
			if previousData["DataVersion"] == nil then
				previousData["DataVersion"] = 1
			end
			if Data.DataVersion.Value == previousData["DataVersion"] then
				Data.DataVersion.Value += 1
				tabl["DataVersion"] += 1
				return tabl
			else
				return nil
			end
		end)
	end
	return Data
end
game:BindToClose(function()
	for _,plr in pairs(game.Players:GetPlayers()) do
		SaveData(plr)
	end
end)

game.Players.PlayerRemoving:Connect(SaveData)

-- Note: The code below isn't added before the data bug reports. I didn't have it because there is "too many" data requests.
while task.wait(300) do
	for i,Player in pairs(game.Players:GetChildren()) do
		if Player:FindFirstChild("Data") and Player.Data:FindFirstChild("XP") then
			SaveData(Player)
		end
	end
end

These codes are in one single script and is pretty much all the code in my game that loads and saves data.
Am I missing something from my scripts? Feel free to inform me.

1 Like

okay, i’ve found the error and its pretty simple…

3 Likes

So what is the error? Also keep in mind that the script is not 100% accurate to the original bits of the original script. I watered it down for simplicity reasons.

3 Likes

Instead of: | local save = DataStore:GetAsync(plr.UserId.."_data") |,

you should use [[in the Load function]] → | local Protection, save = pcall(function() return Datastore:GetAsync(plr.UserId.."_data") end) if not save then return end | in one single line, and | local Protection, save = pcall(function() return Datastore:SetAsync(plr.UserId.."_data", tabl) end) if not save then return end|

THAT way, pcall will loop your datastore:Getasync() or Datastore:SetAsync() without returning an error and does not need anything else AND your datastore script must contain both SetAsync to save and GetAsync to load ONCE

“Took me long enough, im eating rn”

3 Likes

So I just replace the --DataStore:SetAsync(Plr.UserId.."_data",tabl) with the second part of your script?

1 Like

DataStore may error and also you did a pretty unsafe approach.
if you want already made professional module you can use ProfileStore
There is a lot reasons why your method is unsafe:

  • No session (jobid) locking
  • Could error
  • Could override (session locking related)
1 Like

So the data not saving is caused by datastore errors, and pcalls would fix your issue, as @hunder305 mentioned. I will also mention that this loop you added

-- Note: The code below isn't added before the data bug reports. I didn't have it because there is "too many" data requests.
while task.wait(300) do
	for i,Player in pairs(game.Players:GetChildren()) do
		if Player:FindFirstChild("Data") and Player.Data:FindFirstChild("XP") then
			SaveData(Player)
		end
	end
end

Can completely stop if the SaveData function errors (due to datastore errors), so make sure to either protect it will pcalls, threads (task.spawn() or other), or by making it so the SaveData(Player) function can’t error. The too many requests errors you are getting is possibly because you are saving every player’s data at the same time, you should add a wait of a couple of seconds in between each save, and see if that fixes it

However, the data wiping is usually caused by developers using object values, not checking if the data was loaded before saving. In your case, your code seems to be safe from that, because of the DataVersion check you have in the UpdateAsync function. So I really don’t know how data wiping is occurring in your game, and adding pcalls to your script might not be enough to fix that one

-- Uses exponential backoff to aleviate the load on roblox servers, if roblox goes down
local function GetRetryDelay(Retries)
	return 2^Retries
end

function DatastoreModule:GetData(Key)

	local Retries = 0
	
	while true do 

		local Success, Result = pcall(function() 
			return Datastore:GetAsync(Key)
		end)

		if Success then
			return Result
		else 
			warn("[GetData]: "..Result)
			
			Retries += 1

			task.wait(GetRetryDelay(Retries))

			continue
		end
	end
end

This is a simplified version of how I do it in my games. The way pcalls work is it returns two values, the first one is whether or not the function inside the pcall errored, and the second is usually what the function inside the pcall returned (here it would be the player data), but in the case of an error, it’s the error message

Hey, just wanted to give some recommendations to you!

Your data service, while functional is incredibly vulnerable to dataloss. I recommend you look into ProfileStore which is a newer successor to ProfileService, and if you’re set on writing your data service yourself you can look at all the safety measures this module uses and try recreating them yourself :smiley:

yes

ahejzkkefbjdks

did it work? gshjkdfhfuqhfjkdsfuighsjklgfsg

the first part worked but the second part is too risky as my game has 100+ concurrent players

1 Like

don’t worry, it also uses protected call, your datastore still remains the same, however if you don’t use protected call, i’m afraid to tell you that it might get a data break, so imo, my option seems safer as it loops until it gets your save value and not just return nil, and if it does then pcall() will loop to redo - save until it is “tabl”

i myself use this same line to protect my data from any sort of breaks in my own experience