How would I Prevent Data Loss when Player Leaves using DataStoreService?

How would I prevent Data Loss using regular DataStoreService?

How would I do this when the Player Leaves to be more Specific,
Although this rarely happens with the current code i have, How would I make it less likely for it to happen?

pfr.Save = function(p: Player)
	local Saved, Data = pcall(function()
		return pfr.Store:SetAsync("p_"..p.UserId, pfr.Session[p.Name])
	end)
	
	if Saved then
		print("Saved Data for", p.Name)
	else
		-- pretty big else
	end
	
	pfr.Session[p.Name] = nil
end

Is there a way to prevent this, or can I only make it less likely to happen?

Use DataModel:BindToClose(function) to prevent data loss. Some data stores have a backup datastore as well. In your case, you should loop the save function until it works. If the player clicked a save button, you should display a dialog box with the option to retry saving and state the error message where it failed.

The BindToClose function will also do this for any remaining players in the server, since the server closes before all players leave it, or at least this specific function fires in this regard. In the callback, loop through all the players and find which ones that are in the game. There should only be one player, unless the server was shut down. In this function, call the pfr.Save function you have implemented. Make sure to add another loop to verify the data has been successfully saved, since through my experiements throughout the testing process this has not always been the case where it saves.

Datastore 2 is a big help to catch lost data.
Here is a Dev forum post about it.

1 Like

While DataStore2 is good, there’s an even better source for data saving called ProfileService.

ProfileService is a single module script that implements many things that we should, but don’t, do when making our data loading and saving systems. This includes session locking (locking data to one server so it can’t be accessed from another server to prevent item duplication and accidental data loss), and other features.

As they state on the documentation for it:

Why not DataStore2?

DataStore2 is mostly a Roblox DataStore wrapper module which automatically saves duplicates of your data. ProfileService is an extension module which gives you powerful tools to manage profile session-locking, cross server gifting and profile data organizing.

ProfileService protects your data only from the relevant Roblox server problems. It’s completely stacked when it comes to protecting your game data from item duplication exploits.

ProfileService is striving to be a DataStore solution that is the most accurate implementation of data storage following the development guidelines and practices provided in the official Roblox API.

1 Like

@MrLonely1221 @SuguruDev
I’m Aware, but I’m talking about with Regular DataStoreService,

Wouldnt that just cause a Queue? and what if the Player leaves extremely fast?

You will only do this if it errors. It will not cause a queue because the function yields, therefore you do not need to add a task.wait or any other yielding statement.

The BindToClose function takes care of this part by handling the request after the player leaves.

I see, that would be an Interesting use of BindToClose, But I do have a Couple of Questions.

  1. How would I get that to work with the Current Code i have?
    The code I have makes the Data inaccessible after the Player Leaves, So i dont see how I would save their Data after they leave.

  2. How would BindToClose Handle all the Players Data like this?
    So if the Player leaves, how exactly will it handle their data? You Stated that it would handle the remaining Players in the Server when it closes, would the Data still Save when they are leaving, and would i be able able to Keep players Data and UserId and Save their Data with DataStoreService even after they are gone? or would this just cause a queue.

  3. How would I loop and Send Requests to Save Data?
    How would I do this without Data going to a Queue and within the Time the Player is still there?

I have a package called “Try” that let’s you try a function and then get the result, or catch the error. If it errors you can just call :Retry() on the Attempt and make it try it again.

You could use this package to Try() your save function and if it fails retry it after a set amount of time like the code does in the example.

Or you can do this without the package.

local DataStore = game:GetService("DataStoreService"):GetDataStore("PlayerData")
local MAX_ATTEMPTS = 3

function TryToSave(player: Player, data)
  local attemps = 0
  local success, returnData

  repeat
    success, returnData = pcall(function()
      return DataStore:UpdateAsync("Player_" .. player.UserId, function(oldData, oldKeyInfo: DataStoreKeyInfo)
        --Make any changes to data, or read oldData
        return data --, {player.UserId}, oldKeyInfo --You can uncomment the start of this to return a table of userIds associated with the data, and the oldKeyInfo (Which you can change) if you are using Roblox's MetaData for datastores
      end)
    end)

    task.wait(attempts * 3) --First attempt won't wait, any subsequent attempts will wait 3 sec/attempt
    attempts += 1
  until success or attempts > MAX_ATTEMPTS

  return success --Return true or false whether we successfully saved the players data
end

Edit: Here is Roblox’s take on SetAsync() vs UpdateAsync() Data Stores | Roblox Creator Documentation

I’m probably just reading it wrong, but you’re aware BindToClose is for when the server closes and not when the player’s client closes, correct?

Use the same function you use to save the players data when they normally leave I guess.

Yes you can save their data when they are not inagame.

According to the documentation it will only go to the queue if you attempt to write every players data over 10 times in a minute or you attempt to write a players data more the once under 6 seconds. Even if it does go to the queue once or twice, I would still take that over data loss.

I’m assuming you’re confused as to why I said this, as Roblox does not shut down the server before the last player leaves. However, the BindToClose function will do this.

So, I think the best way to accomplish this would be to, for one, do what @MrLonely1221 suggested with the Try package, and also to have a boolean variable (or use what I later suggested), which will determine if a data store request is being validated. You would then have to do two things:

  • Write to the data store when requested to do so. This might be a player leaving or an automated save process. If this fails, retry the process for up to 3 tries (or whatever limit you specify).
  • Read from the data store to make sure the player’s data is there and is correctly written (this means doing a rawequal on two tables, if that’s how you’re storing data, or just using the == conditional operator). If the data does not match, you will need to retry writing the data again.
  • Optionally yield for 5 seconds. This might make sure that there’s no data corruption, but through my testing it is not necessary. This should only be implemented if you are doing this in multiple threads, where frame times could be unreliable.

These two (or three) steps would functionally run when the player’s data saves, but it’s more important when the player leaves. The only issue is that the game will close any coroutines, or any script for that matter, when the server closes. That’s when BindToClose comes into play. However, you will need to modify the implementation to do so. The way I can see it working is to have a table of pending data requests that will simply be a dictionary, indexed by the data store key, and the value would be set to the target (most recently updated value of the data store in your case). Of course, you can index these however you want to, but preferably this would be the easiest way to manage data requests with the function. Here, let’s assume your table is named “PendingDataStoreRequests”:

--BindToClose function. Please place accordingly into a server script that has these variables implemented.
for key, value in pairs(PendingDataStoreRequests) do
	local result
	local success
	repeat
		task.wait(1)
		success, result = pcall(pfr.Store.GetAsync, pfr.Store, key)
		print("Waiting for data to save...")
	until success and result and ((typeof(result)=="table" and rawget(result)==rawget(value)) or result == value)
	--The data should be saved now.
end

the way I formatted the code is by copying the tab character from a previous snippet

In this code snippet, there’s a table called “PendingDataStoreRequests,” that is present. Remember earlier in this post when I mentioned the method to save data? Well, this table will be implemented into there, and when something is written to the data store, the key and value is also written to the table. The key is removed from the table when the value is saved successfully. The BindToClose function will stop the code from stopping until the value is written, by detecting if there’s anything left and then verifying that it is correct.

However, I’m not very confident that this will work, since BindToClose might be in a very special thread that does not close, but other threads do close. If this is unfortunately the case, you could simply save the remaining players’ data by looping through the players in the BindToClose function, which might be simpler to begin with. I’m not sure which method is better.

The only issue you may have with this compared to professional open-source models like ProfileService and DataStore2 is that there isn’t any session locking. There is no way to tell how the data written is from the same server yet. I’m not sure if this will be an issue for you if you are simply storing currency, but it might be an issue if you have inventory items or trading implemented in your game. I would recommend adding session locking by writing to MemoryStoreService every hour the player plays the game, but there’s probably a better method out there to prevent session locking, and I’m not confident that what I said would work very well.

Overall, this is quite a simple solution, and it should fit your needs. If you have any further questions, feel free to ask them.

1 Like

I had a similar issue with a similar system, when the players’ data wouldn’t save sometimes, however, after I implemented the system below, the data has been saving all the time.

print(data)
local success, err
local tries = 5

repeat
	success, err = pcall(function()
		allDataStore:SetAsync("User_"..plr.UserId, data)
	end)
	
	tries -= 1
	if not success then
		warn(err)
		task.wait(2)
	end
until success or tries==1
	
if not success then
	warn("Unable to save data of "..plr.Name)
end
1 Like

Any idea’s on how to prevent this from happening would be much appreciated!

1 Like

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