:UpdateAsync() only sometimes works

I am making a saving system for my game using UpdateAsync. The problem is that when the player leaves the game, PlayerRemoving fires, but UpdateAsync doesn’t.

Here is my code:

local function saveData(player, playerUserId)
	if sessionData[playerUserId] then
                print("PlayerRemoving fired")
		playerData:UpdateAsync(playerUserId, function(oldValue)
			local previousData = oldValue.DataId or {DataId = 0}
			if sessionData[playerUserId].DataId == previousData.DataId then
				sessionData[playerUserId].DataId += 1
				print("PlayerData saved")
				return sessionData[playerUserId]
			else
				warn("PlayerData update error")
				return nil
			end
		end)
	end
end

playerService.PlayerRemoving:Connect(function(player)
	saveData(player,player.UserId)
end)

I tested this script in studio, and when I end the game, it only sometimes prints in the output that the data is saved, but it always prints that PlayerRemoving is fired.

UpdateAsync may not complete before the game shuts down. Use a BindableEvent to signal when the data has been saved and wait for the signal before shutting down the game.
UpdateAsync() not working? [SOLVED] - Scripting Support - Developer Forum | Roblox

2 Likes

It still doesn’t fire. I used a BindableEvent like you said, but UpdateAsync doesn’t work.

Here is the script:

local function saveData(player, playerUserId)
	game.ReplicatedStorage.Events.Saving.SaveSystem:Fire(playerUserId)
end

game.ReplicatedStorage.Events.Saving.SaveSystem.Event:Connect(function(playerUserId)
	print("message received")
	local success, errorMessage = pcall(function()
		playerData:UpdateAsync(playerUserId, function(oldValue)
			local previousData
			if oldValue["DataId"] == nil then
				previousData = {DataId = 0}
			else
				previousData = oldValue
			end
			if sessionData[playerUserId].DataId == previousData.DataId then
				sessionData[playerUserId].DataId += 1
				print("PlayerData saved")
				return sessionData[playerUserId]
			else
				warn("PlayerData update error")
				return nil
			end
		end)
	end)
	if success then
		print("Reset DataStores")
	elseif errorMessage then
		warn("Outside"..errorMessage)
	end
end)

playerService.PlayerRemoving:Connect(function(player)
	saveData(player,player.UserId)
end)

The output only prints “message received”, and nothing else.

Huh? You lose me here…

Instead, You don’t have to use BindableEvents and just use game:BindToClose()
You have 30 seconds to run any code to save everyones data. Here is an example on how you might use it:

local function SaveDataFunction(var1, var2)
--This is where you would run the code to save your data...
end
game:BindToClose(function()
	--if not RunService:IsStudio() then
		for i,v in game.Players:GetPlayers() do
              coroutine.wrap(SaveDataFunction)(var1, var2) --You will need to change this line to the real function and variables, but keeping the same format and using the Coroutine. 
          end
    --end
end)

Edit: Fixed the for i, v loop error.

2 Likes

This would work but game:BindToClose() only runs during a shutdown or when the last player in a server is leaving. What about when a player is leaving a server that still has other players in it?

1 Like

You use both PlayerRemoving and BindToClose together (Sometimes Player removing isn’t called when BindToClose is called which equals dataloss). You can add a debounce to prevent both saving at the same time and advance the set up I made above. I provided a basic example on how to use BindToClose but you would still keep PlayerRemoving, and maybe advance it a little bit more to how your using it in this case :slight_smile:

Thanks, but how would I make sure that PlayerRemoving always fires to prevent data loss?

You can’t… that’s why you use BindToClose… it’s kinda the whole purpose of BindToClose and why we use it in datastores, as if PlayerRemoving isn’t fired, that’s usually because the server was forced to be shutdown… (as the player didn’t leave normally, they were forced to leave the server (Developers shutting down the server, Roblox server needs to shutdown from roblox, whatever it may be, the server is closing, and therefore PlayerRemoving isn’t fired))so we get all the players within the server if BindToClose is fired)

2 Likes

Okay, but if PlayerRemoving fails to run and the player is not forced to leave the server and left normally (meaning :BindToClose() doesn’t function), then the player would lose their data. How would I go about preventing this?

Well that shouldn’t happen.

What you kinda asked to me in a rephrased way is:
How would a Player leave the server without them leaving?

PlayerRemoving will fire if the player is leaving… (they open the menu and leave the game, or forced kicked normally by using :Kick() function)

:BindToClose() will/can prevent PlayerRemoving from firing if its unexpectedly shutting down. This is because :BindToClose Is fired first while… → Server Shut downs so all scripts with a RBXConnection will no longer detect the event → All players leave so PlayerRemoving is fired, but scripts can’t detect it. → Dataloss.

But this isn’t an issue with normal leaving at all. I guess it would fire yes, but your scripts won’t read the event because they are shutting down

As stated in the BindToClose() docs:

“The game will wait a maximum of 30 seconds for all bound functions to complete running before shutting down.”

One question, is corountine.wrap required for the function or can I just call it normally?

Yes Kinda. You have 30 seconds to run any code in your game after BindToClose. Because :SetAsync() and :UpdateAsync() Yield, we call the corountine.wrap so everyone’s data is getting saved at the same time, instead of waiting for one’s data to save… Then call next person in loop and wait for their data to save etc. it’s an important step in minimizing data loss, and while you could call it normally, it won’t break it per say, but if the server has a lot of people, some peoples data might not be saved. And for adding 2 extra words, it’s kinda worth to do it anyways.

I tried the solution you gave me in studio, and everything runs fine except for the fact that when I tested the game multiple times in studio, :UpdateAsync() only sometimes fires to save data.

Here is the new script:

local function setupPlayerData(player)
	local playerUserId = player.UserId
	
	local data
	local success, response
	local count = 0
	repeat 
		if count >= 1 then
			warn("Retrying, count:", count, "\nError:", response)
			wait(0.5)
		else
			warn("Trying, count:", count, "\nError:", response)
		end
		success, response = pcall(function()
			data = playerData:GetAsync(playerUserId)
		end)
		count = count + 1
	until success
	if success then
		warn("SUCCESS!!")
		if data then
			sessionData[playerUserId] = data
		else
			sessionData[playerUserId] = dataTemplate
			sessionData[playerUserId].Nickname = player.DisplayName
		end
	else
		warn("NO SUCCESS :(")
	end
end

local function saveData(player, playerUserId)
	print("message receive")
	print(sessionData[player.UserId])
	local success, errorMessage = pcall(function()
		playerData:UpdateAsync(playerUserId, function(oldValue)
			print(oldValue)
			local previousData = oldValue or {DataId = 0}
			
			if sessionData[playerUserId].DataId == previousData.DataId then
				sessionData[playerUserId].DataId += 1
				print("PlayerData saved")
				return sessionData[playerUserId]
			else
				warn("PlayerData update error")
				return nil
			end
		end)
	end)
	if success then
		print("Reset DataStores")
	elseif errorMessage then
		warn(errorMessage)
	end
end

playerService.PlayerRemoving:Connect(function(player)
	if player.Miscellaneous.AlreadySavedData.Value == false then
		player.Miscellaneous.AlreadySavedData.Value = true
		saveData(player,player.UserId)
	end
end)

game:BindToClose(function()
	for i, player in pairs(game.Players:GetPlayers()) do
		if player.Miscellaneous.AlreadySavedData.Value == false then
			player.Miscellaneous.AlreadySavedData.Value = true
			coroutine.wrap(saveData)(player,player.UserId)
		end
	end
end)

This seems to work. saving a table here.

local sus, err = pcall(function() 
 dataKey:SetAsync(plr.UserId, tab)
end) if not sus then warn(err) end

Thanks, but I’m trying to move away from using SetAsync(), because it sometimes can cause data loss.

Think it’s more the pcall function makes it an interrupt (separate task that will complete on it’s own).

It still doesn’t work even though I removed the pcall function. Here is the saveData function:

local function saveData(player, playerUserId)
	playerData:UpdateAsync(playerUserId, function(oldValue)
		print(sessionData[player.UserId], oldValue)
		local previousData = oldValue or {DataId = 0}
		
		if sessionData[playerUserId].DataId == previousData.DataId then
			sessionData[playerUserId].DataId += 1
			print("PlayerData saved")
			return sessionData[playerUserId]
		else
			warn("PlayerData error")
			return nil
		end
	end)
end

:UpdateAsync() Is not meant for preventing Data loss, more so, the cause of :UpdateAsync() and why it Exists is to Compare Old data, before rewriting new data.

If anything UpdateAsync() is worse then SetAsync() when comparing for Dataloss, in the fact that UpdateAsync() in simple terms, Has to read the data like :GetAsync, and Save the data like :SetAsync(), and counts against both the read and write limit. The Benefit of Update Async is when dealing with 2 different servers changing the Same data within that key at the same time, UpdateAsync reads the current key value before updating it.

Now you are Comparing Data so your usage is correct. It won’t really help or prevent dataloss if that is the benefit your looking for. (To be clear, Both :GetAsync and :SetAsync are both capable and great for what they do. But none provide an ‘extra protection’ aganist dataloss, and its way better if you can provide pcalls, and make sure you handling data correctly etc. )

So how would I go about ensuring that UpdateAsync always functions when the player leaves? Or maybe it’s because I’m testing it in studio and not in-game?

I don’t see how you’re losing data with SetAsync. Unless you’re running this while your editing in the studio … that can mess up with anyway of doing it. AS you quit testing at odd times.

This is why I also use …

local rs = game:GetService("RunService")
if not rs:IsStudio() then
 -- your code ...
end

This way I can test the rest of my program without killing my saved data, by exiting my test to soon.