OH NO.
Your script that saves data doesn’t seem to work. If that’s you, you’re in the right place. But why is this?
What’s going on?
First off, some data loading and saving scripts look kind of like this
-- in this very specific example it's saving cash
local Players, DataStoreService = game:GetService("Players"), game:GetService("DataStoreService")
local cash_data_store = DataStoreService:GetDataStore("CashDataStore")
local function load(player: Player)
-- ...
end
local function save(player: Player)
-- ...
end
Players.PlayerAdded:Connect(function(player: Player)
load(player)
end)
Players.PlayerRemoving:Connect(function(player: Player)
save(player)
end)
It’s pretty simple process. They join, you load their data, they leave, save that data. You might be thinking that this alone is fine. Well that is where you’re wrong.
This can fail in certain circumstances. When a server is in the process of shutting down, its priority is shutting down and stops doing anything else.
The 3 ways that come to mind are these (do give me more if there are):
- The last player leaves the server
- A developer manually shut it down via the game page
- A network error is encountered and the server therefore is forced to shut down
In these cases, the PlayerRemoving
event listener is never called or doesn’t finish.
What is the solution?
It’s a pretty simple solution. But there’s a catch. I’ll get to that soon.
In addition to saving upon a player’s removal, also save when the server shuts down. This can be done via the DataModel:BindToClose
method, which takes a function as an argument, and is called when the server shuts down. Multiple functions can be bound, and they’ll all be called in a separate thread when the server shuts down.
game:BindToClose(function()
for _, player in ipairs(Players:GetPlayers()) do
save(player)
end
end)
And there it is. In addition to data saving when the player leaves, it also saves the remaining players’ data when the server shuts down.
Remember that I said there’s a catch? Here’s the catch:
Roblox only allows 30 seconds for all the bound functions to execute. NOT 30 FOR EACH ONE. But 30 seconds for all of them execute. When those 30 seconds are over, the server shuts down regardless of if they’re done.
This probably wouldn’t be an issue if data store requests didn’t take indeterminate time to complete. Yes, since data store requests are web requests, they can take a long time depending on the responsiveness speed of the Roblox servers and other factors. This can get even more troublesome if your game can have many players. In my experience a web request took about 1/2 to 1 second to complete. Imagine having 30-player servers. And Roblox wants 700+ player servers apparently. Unfortunately I don’t believe we can accelerate the speed that Roblox picks up our requests.
The issue with this code is it saves data asynchronously rather than synchronously.
What’s going on in that code is, it makes a web request, then waits for its completion. It then makes another one, resulting in a waste of time. There’s that possibility it can go over that 30 second cap, which means the remaining players’ data wouldn’t be saved.
This time should be used to save other players’ data as well. This probably won’t make sense right now but I’ll explain soon.
If you’re unfamiliar with multithreading, only a single thread (“coroutine”) can execute at a given time. They aren’t truly running in parallel, but it seems like they do by switching flawlessly between each other when you yield. Coroutines can be “resumed” by in another coroutine. A coroutine can also “yield” (pause) to pause through its code and give control back to whatever resumed it.
And that’s all you really need. When you use a function that is labelled with Async
(e.g. GlobalDataStore:SetAsync
), it takes indeterminate time to finish, and when you call them, they yield the current thread until the request is completed, and selects a thread that Roblox manages to resume
So what happens when we apply this knowledge into our code?
game:BindToClose(function()
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(save)(player)
end
end)
And just like that, the data should properly be saved.
I’ve also seen some scripts where they wait()
(or yield in some form) in the function, presumably because the writer thinks it buys them more time. That doesn’t buy you any more time.
Or even
game:BindToClose(function()
wait(n)
end)
Which I guess would work, as it stalls the close, but I don’t recommend just yielding. More meaningful work should be done.
I hope you liked this tutorial and found it helpful. This is my first time making a tutorial on here and surely there’s room for improvement. Any feedback is appreciated.