So, I’m writing this “fail safe” so when I shut down the game, it saves all data in that server. How do I go about this? My current code:
Module:
aloe.FailSafeSave = function()
-- this is only called when the game is shutting down
for key, value in next, game.Players:GetPlayers() do
aloe.Save(value);
end
end
Does aloe.Save(value) yield? If it doesn’t (i.e. saving starts on a new thread), then it starts those threads, and immediately continues on to the end of the BindToClose function once each of the threads yields (i.e. during a data store call), and I think once the end of that BindToClose function is reached, the game shuts down.
To remedy this, you need to yield your main thread until each of your data save threads finish yielding.
It just spams console while in game (as it saves quite often), saying that if it gets to many request it will start to drop them. But doesn’t get dropped ever. I’m not sure what yield means entirely, sorry for my bad understanding on this. If I add a wait() for 5-10 seconds to prevent the bind to close function from actually ending fully, would this make it possible to do this?
Yielding just means the thread stops executing and lets another part of the code resume executing.
Such a case would be task.wait(), this pauses the current thread and lets another thread take over, until the specified time has passed and then gets resumed again on another compute cycle.
Another example is any asynchronous function, such as UpdateAsync() on a data store. Normally, this pauses/waits/yields the current thread until the function finishes executing (it takes time because it’s communicating with external data servers).
Can you post your aloe.Save() function so we can make sure we’re diagnosing this correctly?
This may help sometimes, but it won’t guarantee the other threads have finished, they could take more than 5-10 seconds. It’s better to implement some sort of system to only resume execution once all your data save threads resume.
I’m seeing now that this all happens on a single thread. That means when it starts the SetAsync function for the first player in the loop, it must finish that save before it will start saving the next player’s data.
It’s possible if you have multiple players that the system takes a while to save 1 or 2 players’ data, and never gets around to saving the rest of the players’ data (BindToClose times out after 30 seconds or something)
To remedy this, each save call should be started on a new thread using task.spawn or similar so that the data saving for each player can happen in parallel.
local dataStoreService = game:GetService("DataStoreService")
local dataKey = dataStoreService:GetOrderedDataStore("Coins")
game.Players.PlayerRemoving:Connect(function(plr)
local sus, err = pcall(function()
dataKey:SetAsync(plr.CharacterAppearanceId,
(player.leaderstats.Coins.Value))
end) if not sus then warn(err) end wait()
end)
game:BindToClose(function()
if not game:GetService("RunService"):IsStudio() then
local players = game:GetService("Players"):GetPlayers()
for _,v in pairs(players) do
local userId = v.UserId
local sus, err = pcall(function()
dataKey:SetAsync(userId, v.leaderboard.Coins.Value)
end)
if not sus then
warn(err)
end
end print("DataSave")
end print("EndOfLine")
end)
Remember BindToClose only works when it’s the last person on the server or a out of the blue crash and everyone gets logged off (in that case the last person would get logged off too).
Also you only have a few seconds to get your task done.
This will start the saving on different threads and allow them to run in parallel which is good, but it will also get to the end of :BindToClose() before they all finish executing. You need to wait for each of those threads to finish before resuming the main thread.
One way to do it is to use coroutines, something like this
aloe.FailSafeSave = function()
-- this is only called when the game is shutting down
local mainThread = coroutine.running() -- This just gets a reference to the current thread so we can yield it and resume it later
local numThreadsRunning = 0
for _, player in ipairs(game.Players:GetPlayers()) do
local startSavingThread = coroutine.wrap(function()
aloe.Save(player) -- Async function, yields until saving finishes (could implement a retry here if you pcall it and check for failure)
numThreadsRunning -= 1 -- This thread finished, decrement the counter
if numThreadsRunning == 0 then
-- Resume the main thread if this was the last thread to run
coroutine.resume(mainThread)
end
end)
numThreadsRunning += 1
-- No need to defer, coroutine.wrap() already yields when it starts,
-- so numThreadsRunning will have a chance to count all the way up before this actually runs
startSavingThread(player) -- Start the saving thread
end
if numThreadsRunning > 0 then
-- Gets resumed once all saveThreads have finished
coroutine.yield()
end
end
aloe.Save = function(player)
dataSet:SetAsync(player.UserId, sessionData[player])
RapSet:SetAsync(player.Name .. ";" .. player.UserId, aloe.RAPCalculate(player))
end
I haven’t tested this, but hopefully it helps you get an idea of what to do. (It probably mostly works, perhaps maybe)
The idea here is this:
We start a new thread for each asynchronous Save function so that they can all run in parallel.
We yield (pause) the main thread until all those subthreads finish.
To determine when to resume the main thread, the subthread needs to know if it’s the last subthread to finish.
In order to know if it’s the last subthread alive, we keep a counter of the number of subthreads that are alive.
First, we count the number of threads that will start (1 for each player’s data we are going to save).
Second, we start all the subthreads up. (This happens second because coroutine.wrap yields for one cycle when it starts, so even though it’s called within the loop, the yielding allows that loop to continue and finish counting the number of threads to start)
Third, the threads finish 1 by 1, each of them decrementing the “alive thread counter” by 1 when it finishes.
Fourth, if, after decrementing, the subthread sees it decremented to 0, it knows it’s the last one alive. At this point, it resumes the main thread.
The main thread is now resumed and the :BindToClose() function is allowed to finish, which exits the game.
Coroutines can be a bit tricky to follow if you’re not familiar with them. If you’re interested, you can read some documentation about coroutines and the different methods.
If the server crashes, :BindToClose() won’t get called, sadly. This can be a data loss scenario.
Another data loss scenario is if the save functions take too long and :BindToClose() times out and shuts down anyway before you resume the thread.
I’ve never hit an out of the blue crash but I did read something like that in the description.
I guess I may not be totally sure what you’re doing. The code I posted can be put right in server scripts and works just fine for me. You would have to change a few things I’m sure but this is a stand alone quit save.
The code you posted suffers from the same problem as OP’s, where the save requests are processed synchronously. This means Player2’s data won’t start saving until Player1’s data finishes saving, and so on. If you have multiple players, not everyone will get a chance to have their data saved.
This is why you need to process them in parallel in separate threads. My previous posts explain this in more detail.
Another note:
In case you don’t have it already, you should also include something like this line from 2112Jay either in your :BindToClose() function or your Save function to prevent your studio sessions from hanging annoyingly when you stop the simulation and also to prevent studio session data from getting saved to production.