Datastores randomly failing to save data, and I don't know why

To preface this, no errors are shown, and this seemingly occurs randomly.

I have an inventory system for a game and I use a couple of separate datastores to detect the player’s inventory/tools and armor. Each datastore seemingly has the same issue, despite the fact I use the same saving logic that I use in other cases.

The datastores save every few minutes, otherwise they save when a player leaves.

For some reason, every now and then, there will be partial data wipes. Either the player’s armor data will be wiped, or their tools will be wiped, or sometimes items in their inventory will be wiped. I do not know the cause of this, and it has been persistent for the last several months.

I know the data gets wiped because in order to overwrite the data, I erase the datastore entry for the player and then try to input new data. Somewhere between that erasure and inputting the new data, something gets hung up and keeps the data empty, and I’m not sure why. Maybe the server is closing before the data gets saved, or something? I don’t know. I again emphasize no errors.

I’m going to drop my code below and hopefully somebody can point out an obvious issue. This is only one of my datastores (this one is for the Armor), but the other one works under the exact same logic.

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local myDatastore = DataStoreService:GetDataStore("ArmorSystem4")

local function save(Player)

	local ArmorData = {}
	
	for _, clothes in ipairs(game.ReplicatedStorage.PlayerInventories:FindFirstChild(Player.Name).Clothes:GetChildren()) do
		
		warn(clothes.Name)
		
		if clothes:IsA("Accessory") then
			table.insert(ArmorData, {
				clothes.Name,
				clothes.Wearing.Value
			})
		elseif clothes:IsA("Model") then
			table.insert(ArmorData, {
				clothes.Name,
				clothes.Wearing.Value
			})
		end
		
	end
	
	myDatastore:RemoveAsync(Player.Name)
	
	local success, err
	repeat
		success, err = pcall(function()
			print(ArmorData)
			myDatastore:SetAsync(Player.Name, ArmorData)
		end)

		task.wait()
	until success

	if not success then
		warn("SERVER: Failed to save data: " .. tostring(err))
	end

end


local function load(Player)
	Player.CharacterAdded:Wait()
	wait(0.1)
	
	if not Player.Character:FindFirstChild("Shirt") then
		local shirt = Instance.new("Shirt")
		shirt.Name = "Shirt"
		shirt.Parent = Player.Character
	end
	if not Player.Character:FindFirstChild("Pants") then
		local shirt = Instance.new("Pants")
		shirt.Name = "Pants"
		shirt.Parent = Player.Character
	end

	local success, err
	local ArmorData

	repeat
		success, err = pcall(function()
			ArmorData = myDatastore:GetAsync(Player.Name)
		end)
	until success

	if not ArmorData then return end

	if success then
		for i, item in ipairs(ArmorData) do
			
			warn(item[1])

			if game.ReplicatedStorage.PartsAndModels.Armor:FindFirstChild(item[1]) then
				local clothes = game.ReplicatedStorage.PartsAndModels.Armor:FindFirstChild(item[1]):Clone()
				clothes.Parent = game.ReplicatedStorage.PlayerInventories:WaitForChild(Player.Name):WaitForChild("Clothes")
				if item[2] == true then
					clothes.Wearing.Value = true
					if clothes:IsA("Model") then
						if clothes:FindFirstChild("Shirt") then
							Player.Character:FindFirstChild("Shirt").ShirtTemplate = clothes.Shirt.ShirtTemplate							
						end
						if clothes:FindFirstChild("Pants") then
							Player.Character:FindFirstChild("Pants").PantsTemplate = clothes.Pants.PantsTemplate
						end
					elseif clothes:IsA("Accessory") then
						
						if clothes.AccessoryType == Enum.AccessoryType.Hat then
							local helmet = clothes:Clone()
							helmet.Parent = Player.Character
							helmet.Handle.CanCollide = false
							
							for _, hair in ipairs(Player.Character:GetChildren()) do
								if hair:IsA("Accessory") then
									if hair.AccessoryType == Enum.AccessoryType.Hair then
										hair.Handle.Transparency = 1
									end
								end
							end
							
							if helmet:FindFirstChild("RequireNeck") then
								if helmet.RequireNeck.Value == true then
									local neckpiece = game.ReplicatedStorage.PartsAndModels.Armor["Neck Chainmail"]:Clone()
									neckpiece.Parent = Player.Character
								end
							end
														
						elseif clothes.AccessoryType == Enum.AccessoryType.Back then
							if game.ReplicatedStorage.PartsAndModels.BackpackWeights:FindFirstChild(clothes.Name) then
								game.ReplicatedStorage.PlayerInventories:FindFirstChild(Player.Name).MaxWeight.Value = game.ReplicatedStorage.PlayerInventories:FindFirstChild(Player.Name).MaxWeight.Value + game.ReplicatedStorage.PartsAndModels.BackpackWeights:FindFirstChild(clothes.Name).Value
							end
							clothes.Wearing.Value = true
							clothes:Clone().Parent = Player.Character
						end
					end
					if game.ReplicatedStorage.PartsAndModels.ArmorHealth:FindFirstChild(clothes.Name) then
						Player.Character:FindFirstChild("Humanoid").MaxHealth = Player.Character:FindFirstChild("Humanoid").MaxHealth + game.ReplicatedStorage.PartsAndModels.ArmorHealth:FindFirstChild(clothes.Name).Value
						Player.Character:FindFirstChild("Humanoid").Health = Player.Character:FindFirstChild("Humanoid").Health + game.ReplicatedStorage.PartsAndModels.ArmorHealth:FindFirstChild(clothes.Name).Value
					end
				end
			end

		end 
	else
		warn("SERVER: Failed to load inventory data: " .. tostring(err))
	end

end






--Player naturally disconnects
--Players.PlayerRemoving:Connect(function(Player)
--	save(Player)
--end)

--Player Joins the Server
Players.PlayerAdded:Connect(function(Player)
	load(Player)
	Player.CharacterRemoving:Connect(function(Character)
		if game.ReplicatedStorage["LoadingData"].Loaded.Value == true then
			save(Player)
		end
	end)
end)

game:BindToClose(function()
	for _, plr in ipairs(Players:GetChildren()) do
		if game.ReplicatedStorage["LoadingData"].Loaded.Value == true then
			save(plr)
		end
	end
end)

--Autosave Function
while wait(120) do
	for _, plr in ipairs(Players:GetChildren()) do
		if game.ReplicatedStorage["LoadingData"].Loaded.Value == true then
			save(plr)
		end
	end
end
2 Likes

BindToClose only runs when the server is about to shutdown, use game.Players.PlayerRemoving

1 Like

Shouldn’t the following counteract that anyways? The BindToClose is a redundancy I added because it seemed as though people who were booted in a server shutdown didn’t have their stuff saved.

Players.PlayerAdded:Connect(function(Player)
	load(Player)
	Player.CharacterRemoving:Connect(function(Character)
		if game.ReplicatedStorage["LoadingData"].Loaded.Value == true then
			save(Player)
		end
	end)
end)

Or would the redundancy be whats causing the issue?

2 Likes

why are you saving the data each time the player dies?

To update the datastore when the player dies and drops all of their armor, for example. In a separate script, when the .Died function fires, it takes all of the player’s inventory items (such as armor) and puts them into a loot bag on the ground. So, when the character is then removed after they die, the datastore is updated to reflect they no longer have those items.

2 Likes

Try adding a task.wait(5) at the bottom of the BindToClose function.

2 Likes

Would this delay the game actually closing for a bit to perhaps allow additional time to process datastore requests, if that is the issue?

3 Likes

Isn’t actually true, BindToClose runs when the game is about to shutdown, source — documentation. So it seems to be clear.

2 Likes

I didn’t understand why’s here is a repeat-unit? It’s useless because you have log right after that, and log will never work because you set loop to make attempts to save data until it will not be saved. So for the first one repeat-until, it’s very uncommon loop (Better use while-do), for the second one it’s useless because of reason provided before. But actually to make not reinvent the wheel again, you should use Datastore2, it’s much more simplier and faster in-use.

2 Likes

Are you referring to:

	local success, err
	local ArmorData

	repeat
		success, err = pcall(function()
			ArmorData = myDatastore:GetAsync(Player.Name)
		end)
	until success

If so, then mind you this is my first real excursion into datastores and this repeat until success loop is used on the documentation.

Also, I had someone tell me that the Datastore2 you linked was obsolete because of updates ROBLOX made to Datastores sometime in 2019 or 2020 or something like that, and it was better to use default systems now. Is that not true?

2 Likes

I think this might be the issue. Just try removing this statement and see if it saves - SetAsync() overwrites the data anyway.

Also, I see that you are using a repeat loop to save data until success. This not only sends an absolute TON of requests if data saving fails, but can run infinitely if data stores are down, or just generally run into a continuous error. Add an attempt limit to this.

2 Likes

Unrelated to Saving Failures (or potentially not)

I noticed a few of caveats with your current system.

Rate Limiting (semi-important with new Datastore limits):
You could encounter Rate-Limiting with all the request queues you are sending, which can potentially worsen with bigger servers.

Saving with Player.Name
You should be saving with Player.UserId, due to many reasons. Security, and ROBLOX name-changing system (ya-know, that costs 1000 robux).

You are also using :RemoveAsync() for no reason? There is no improvement from doing this, you are just adding more requests.

2 Likes

Edit:
Additionally with Rate-Limiting, you are Protected Calling your DataStore requests, which is great! But it doesn’t seem you are printing/warning in console to see if there was dropped Queues or errors.

I was wrong, sorry.

2 Likes

I can’t remember why I decided to clear the data before setting it again, but I think I had issues (potentially related) that caused the old data to fail to erase, for example on player death. But I suppose this could be fixed by firing myDatastore:RemoveAsync(Player.Name) on Humanoid.Died, potentially, if I end up needing it at all, which currently doesn’t make sense why I even have it in the first place thinking about it.

I was just going off of examples used in the documentation, as linked previously. What you said makes sense, but what would be a better way to do it? OR should I just drop the repeat until loop in general? What if there is an issue with rate limiting and it drops the request instead of adding it to a queue (as I assume a repeat until loop is best suited for a situation such as this)?

2 Likes

You are right, and I did previously run into a ratelimit issue with a different datastore system (unrelated to this, but same game). But, what would be a better way to handle this? Also, see my post above this - same question, basically.

You’re right. The only reason I wasn’t using Player.UserId was for testing and easily being able to access players datastores via their usernames. An easy thing to fix nonetheless.

2 Likes

That’s incorrect, remove the pcall (if no data then it will return nil and not throw an error), the repeat also needs to be removed

1 Like

True, saving data with the players username is basically erasing the players data when they change their username and keeping useless pieces of data that will never be used

2 Likes

But does the pcall / loop not serve a purpose? Just genuinely confused why people are suggesting this.

2 Likes

I am the co-owner of a game called Castaway. I personally wrote all of the code in the game. We save A LOT of data, including up to 2000+ placed buildings and decorations per player. I have never lost player data in the few years the game has been available to the public.

If you are still having problems with reliably saving player’s data, I would be happy to share with you exactly how my save system reliably saves every player’s data and would love to answer any questions you have.

4 Likes

add a task.wait(number) to your repeat statement, you may be burning out calling the datastore and hanging

2 Likes