Partial Dataloss issue?

My game has been getting several reports of data loss all of a sudden and it seems to be partial. I’m very confused on how this would happen, I’m guessing an issue with not loading/saving data fast enough or smth.

My loadData function looks like this:

--load player data
local function loadData(player)
	print(player)
	local success = nil
	local playerData = nil
	local attempt = 1

	repeat
		success, playerData = pcall(function()
			return dataBase:GetAsync(player.UserId)
		end)

		attempt += 1
		if not success then
			warn(playerData)
			task.wait()
		end
	until success or attempt == 3

	if success then

		if not playerData then--give default data if they're new
			playerData = {
				["Gems"] = 0,
				["SelectedTowers"] = {"Cameraguy"},
				["OwnedTowers"] = {"Cameraguy"},
				["MaxTowers"] = 5,
				["RedeemedCodes"] = {},
				["Wins"] = 0,
				["Snowflakes"] = 0,
			}
			beamEvent:FireClient(player)
		end

		data[player.UserId] = playerData
		isLoaded[player] = true

Saving data looks like this:

--save player data
local function saveData(player)
	if data[player.UserId] and player.UserId ~= 0 and isLoaded[player] then
		local success, errorInfo = pcall(function()
			dataBase:UpdateAsync(player.UserId, function(oldData)
				if oldData then
					-- Update only the necessary fields to avoid overwriting
					oldData.Gems = data[player.UserId].Gems
					oldData.SelectedTowers = data[player.UserId].SelectedTowers
					oldData.OwnedTowers = data[player.UserId].OwnedTowers
					oldData.MaxTowers = data[player.UserId].MaxTowers
					oldData.RedeemedCodes = data[player.UserId].RedeemedCodes
					oldData.Wins = data[player.UserId].Wins
					oldData.Snowflakes = data[player.UserId].Snowflakes
					return oldData
				else
					-- If no existing data, use the entire data table
					return data[player.UserId]
				end
			end)
		end)

		if success then
			print("Data saved successfully for player:", player.UserId)
		else
			warn("Unable to save data for player:", player.UserId, errorInfo)
		end
	else
		warn("No session data for player:", player.UserId)
	end
	isLoaded[player] = nil
end

Players.PlayerRemoving:Connect(saveData)

game:BindToClose(function() --if game shuts down
	if not RunService:IsStudio() then
		print("Shutting down")
		for _, player in ipairs(Players:GetPlayers()) do
			task.spawn(function()
				saveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end
end)

Basically, sometimes when players join, a lot of their inventory (OwnedTowers) is wiped (not all of it tho) This makes it rlly confusing for me since it rules out that it’s just not doing the saving or loading since it is but not fully for some reason

If anyone has any idea on how this could be caused please respond since I want this fixed asap, any help is appreciated!

1 Like

Ok apparently it also sometimes doesn’t remove SelectedTowers but does remove the OwnedTowers, idek whats up

Your example looks fine. The only thing that springs to mind would be the issue regarding saving mixed table data. IIrc, data stores share the same serialization limitations as remote events/functions. When you try to save a mixed table in the data store, some keys will be silently removed and will be gone when you try to reload the data.

In your example, OwnedTowers and SelectedTowers look like they’re used as an array. Are they consistently treated as an array throughout the rest of your code?

Please investigate further and figure out the exact problem first. Send us a before and after snapshot of your data (data on load vs data saved and reloaded next game). Based on what you described it’s hard to know if your data is getting ROLLED BACK, PARTIALLY SAVED, or SOMETHING ELSE. Each issue is solved in different ways and has different causes.

ROLLED BACK:

  • you join game
  • you have 1x stick, 2x rocks
  • you play for a bit
  • you leave the game with 1x pickaxe, 10x sticks, 20x rocks
  • you rejoin the game later
  • you find out you have 1x stick, 2x rocks again

PARTIAL SAVE:

  • you join game
  • you have 1x stick, 2x rocks
  • you play for a bit
  • you leave the game with 1x pickaxe, 10x sticks, 20x rocks
  • you rejoin the game later
  • you find out you have 1x pickaxe, 20x rocks, but you’re missing 10x sticks

This only applies to keys being indexed and only for the remote system. It’s fine if you have mixed values, but you can’t have mixed keys. It’s generally good practice to not mix numbers with string keys and people apply it to other systems (ie datastore), but I don’t think roblox’s documentation mentions it for datastore.

1 Like

Roblox doesn’t mention it in the data stores documentation but they do share the same behavior. With mixed tables, keys will be dropped when you read them back from the data store (or rather, they’re dropped as soon as you save it to the data store).

local DataStoreService = game:GetService("DataStoreService")
local MixedData = {
    "Value1",
    100,
    Key1 = 10,
    Key2 = "Test"
}

DataStoreService:GetGlobalDataStore():SetAsync("MixedData", MixedData)
local ReadMixedData = DataStoreService:GetGlobalDataStore():GetAsync("MixedData")
print(ReadMixedData) -- { [1] = "Value1", [2] = 100 }

It seem to happen because of the fast leave > join.
If your datas aren’t saved or didn’t finished to save when leaving and you fastly join a new server next, then it will load the old datas (the one before your last session) instead of the last datas.

You are checking if the current datas are loaded before saving, but you are not checking if the most recent datas was saved before loading.

1 Like

Honestly the biggest issue I see is that you are only saving twice:

  1. On PlayerRemoving
  2. On BindToClose

If you don’t want to use other popular DataStore modules out there, consider doing an autosave.

Edit: Not to mention you don’t have a fallback method if the saving fails.

Ok so I’m just going off what people have been saying as I wasn’t able to replicate the issue myself so I don’t have images either but I can rule out it’s not data getting rolled back because players are losing towers up to many sessions ago, so the data is getting partially wiped, only “other” thing I think could be the issue is this one error I got like

"error: "DataStoreService: InvalidUniverse: The provided universe is not valid. API: GetAsync, Data Store: database "

I save on both of them because PlayerRemoving, when the player leaves obviously, and BindToClose, when the game shuts down. Though what you said about a fallback method sounds good, what would it look like?

You can avoid saving twice by checking if a PlayerRemoving was already runned or not about the selected player, then the BindToClose function will only save unsaved player data.

When server shutdown or get closed, sometimes only BindToClose is running, sometimes both BindToClose and playerRemoving are running at same time, but playerRemoving is always runned at first, so it allow you to skip the save of all already saved players datas.

local PlayerRemoved = {}

PlayerService.PlayerRemoving:Connect(function(Player)
	table.insert(PlayerRemoved, Player.UserId)
	--save
	table.remove(PlayerRemoved, Player.UserId)
end)

game:BindToClose(function()
    for _, Player in pairs(PlayerService:GetPlayers()) do
        if not table.find(PlayerRemoved, Player.UserId) then
            --save
        end
    end
end)

What would be the issue of saving twice though?

If the server is full of players when it shutdown, then all save might fail due to too many request at same time.

DataStore request was added to queue. If request queue fills, further requests will be dropped. Try sending fewer requests.

Considering you only have 30 seconds to save all players datas when the server is closing, it is better to avoid getting this kind of error.

1 Like

Implemented it, any other steps you think I should take to prevent data loss?

1 Like

If your playerbase isn’t complaining about losing gems/wins/snowflakes and only about OwnedTowers:

  • your data is being partially lost/corrupted
  • investigate the code for setting and removing OwnedTowers. It’s likely your inventory script isn’t working properly not the datastore.

If your playerbase IS complaining about losing gems/wins/snowflakes:

  • your data is getting rolled back

It could be that your :GetAsync is failing/erroring. In your code here you give up after 3 super fast tries. You need to rework it to be a bit smarter.

  • instead of trying it only 3 times and then giving up and letting the players play your game, add extra safety
  • instead of trying every task.wait(), try task.wait(1), keep trying indefinitely until the player leaves the game
  • if it’s not working, either kick the player, warn them, or pause gameplay (load screen) until it’s fixed

Same thing with your SAVE function, it’s trying once then giving up. Use the points above to make it better too.**

Fix this too

You almost have the code pattern properly implemented. This is why your code fails:

  • server shutdowns
  • BindToClose is called, server waits for this function to finish
  • there is no while/for loop to stall the BindToClose function
  • BindToClose ends up spawning a bunch of saveData functions and immediately terminates
  • server ends all running threads and shutdowns mid save
  • data loss

Fixed:

game:BindToClose(function() --if game shuts down
	if not RunService:IsStudio() then
		print("Shutting down")
		for _, player in ipairs(Players:GetPlayers()) do
			task.spawn(function()
				saveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end

	while not next(isLoaded) do -- we keep waiting until all player datas are saved and closed by the spawned functions
		task.wait(1)
	end
end)

In code implementation, there is a check to see if the data has been saved or not yet. Because of this “debounce” check multiple calls to save is safe.

Wow, thanks for the really thorough response, I did some close examination and I think it’s an issue with the trading system, it has the most to do with data and OwnedTowers (it has several callings of loadData if the player isn’t loaded, sets OwnedTowers equal to a new array which could cause problems, etc)

luckily I still have the contacts of the guy (he made it like 6 months ago but since I’m only getting many reports now I think it’s an issue with both the system and roblox DataStore but with the Data part improved, I think this is this is the last thing to solve) who made it and I’m trying to get back in touch with him since he’s probably the most fitted guy to tamper with the code, but yeah, thanks for your help!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.