The risks greatly outweigh the benefits of running data requests with multiple threads. Requests are much more likely to be dropped when saving with multiple threads due to overloading the store, but you don’t even need to save on BindToClose like I said above. 30 seconds is also enough time to save data in most cases, cases with more players are pretty much guaranteed to lose requests off the end of the data store queue.
is the task.wait will work on the actual clients ? , i think it only works in studio
so saving data using multiple threads may drop the data ? i thought it is a good thing to save each key in it is own thread to make the saving process faster to make the script has enough time to save all the data
No, saving data in separate threads will increase the risk of data loss. You don’t need to save on BindToClose, just wait.
but i already have a function that wait for request budget why this increase the risk of data loss
The request budget isn’t relevant to BindToClose, really - the request budget tells you how many requests you can make before throttling occurs. The issue here is spamming the store with too many requests at a time, which causes the queue to fill and requests after the filled queue to be dropped and therefore not saved. Running it in one thread at least allows you to yield between requests if you want to, you will need to. But, like I already said, saving on BindToClose is just unneccessary requests - PlayerRemoving
will already fire for all the players leaving the game. You should instead look to implementing a custom queue to buffer requests passed to the store.
how i can implement a queue system to my script ?
You can store the player’s UserId in a request table and have it wait until the request is at the front of the queue. You can call a function like this right at the start of your saving function.
local queue = {}
local function queueRequest(player: Player): ()
table.insert(queue, player.UserId) --add to request queue
--wait until the request is at the front of the queue
repeat
task.wait(1)
until
table.find(queue, player.UserId) == 1
--remove request from queue
table.remove(queue, 1)
--wait extra
task.wait(1)
end
this should be at the start of the SaveData function ? , also how i can make the server wait until the data get saved ?
on BindToClose, just use this code:
game:BindToClose(function()
task.wait(if game:GetService("RunService"):IsStudio() then 5 else 30)
end)
ok , thank you so much for your help , i will really appreciate if you can help me to make the wait dynamic
so it wait until the data get saved especially in studio ?
Yep, the data will be saved in studio. It’ll wait 5 seconds because only your data needs to be saved. It’ll wait the full 30 in Roblox servers.
thank you so much for your help i really appreciate it
Glad I could help. If you’ve got no other questions or issues, make sure to mark this topic as solved so others know and it can close. If you think of any other questions or have any other issues, feel free to ask.
Doesn’t BindToClose
handle all types of server shutdown? If the shutdown is due to a system-wide outage (i.e. Roblox going down) wouldn’t it be prudent to use PlayerRemoving
to capture player data as they are kicked, and not spawn concurrent threads in BindToClose
?
i think if roblox going down the data store service will not work either
Quite possibly, maybe not, it’s a good strategy to assume the worst-case scenario. That is why I think it not a good idea to throttle any currently executing process’ during the systems “Swan Song” by spawning more.
i modified the script based on your help
here is the new script :-
local PlayersService = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataStoreService = game:GetService("DataStoreService")
local DataBase = DataStoreService:GetDataStore("PlayersData")
local Assets = ReplicatedStorage:WaitForChild("Assets")
local PlayersGuns = Assets:WaitForChild("PlayersGuns")
local KickedPlayers = {}
local RequestsQueue = {}
local function WaitForBudget(RequestType , BudgetAmount)
local Budget = DataStoreService:GetRequestBudgetForRequestType(RequestType)
while Budget < BudgetAmount do
Budget = DataStoreService:GetRequestBudgetForRequestType(RequestType)
task.wait(5)
end
end
local function SaveData(Player , SessionLock , BindToClose)
local UserId = Player.UserId
if table.find(KickedPlayers , Player.UserId) then
print("player was kicked")
table.remove(KickedPlayers , table.find(KickedPlayers , Player.UserId))
return
end
table.insert(RequestsQueue , UserId)
if SessionLock then
local IsSucsuss , Data = nil
repeat
IsSucsuss , Data = pcall(function()
if not BindToClose then
WaitForBudget(Enum.DataStoreRequestType.UpdateAsync , 1)
end
DataBase:UpdateAsync(tostring(UserId).."Session" , function(OldVersion)
return {
["SessionLock"] = true,
["LastSessionTime"] = os.time(),
}
end)
end)
if not IsSucsuss then
warn(Data)
end
until IsSucsuss
end
local Screws = 0
local MaxPositions = 6
local PlayerDataFolder = Player:FindFirstChild("Data")
if PlayerDataFolder then
local PlayerScrews = PlayerDataFolder:FindFirstChild("Screws")
local PositionStorage = PlayerDataFolder:FindFirstChild("MaxPotions")
if PlayerScrews and PositionStorage then
Screws = PlayerScrews.Value
MaxPositions = PositionStorage.Value
end
end
local GunParts = {}
if Player:FindFirstChild("OwnedGunParts") and #Player.OwnedGunParts:GetChildren() > 0 then
for i , GunPart in pairs(Player.OwnedGunParts:GetChildren()) do
if not table.find(GunParts , GunPart.Name) then
table.insert(GunParts , GunPart.Name)
end
end
end
local UsedGunParts = {}
if Player:FindFirstChild("UsedGunPieces") then
for i , Value in pairs(Player.UsedGunPieces:GetChildren()) do
if Value.Name == "Front" then
UsedGunParts["UsedFrontPiece"] = Value.Value
elseif Value.Name == "Middle" then
UsedGunParts["UsedMiddlePiece"] = Value.Value
elseif Value.Name == "Back" then
UsedGunParts["UsedBackPiece"] = Value.Value
end
end
end
local Gears = {}
if Player:FindFirstChild("OwnedGears") and #Player.OwnedGears:GetChildren() > 0 then
for i , Gear in pairs(Player.OwnedGears:GetChildren()) do
if not table.find(Gears , Gear.Name) then
table.insert(Gears , Gear.Name)
end
end
end
local UsedGear = nil
if Player:FindFirstChild("UsedGear") then
UsedGear = Player.UsedGear.Value
end
local Potions = {}
if Player:FindFirstChild("OwnedPotions") and #Player.OwnedPotions:GetChildren() > 0 then
for i , Potion in pairs(Player.OwnedPotions:GetChildren()) do
for i = 1 , Potion.Value do
table.insert(Potions , Potion.Name)
end
end
end
local Secsuss , ErrorMessage = nil
repeat
Secsuss , ErrorMessage = pcall(function()
if not BindToClose then
WaitForBudget(Enum.DataStoreRequestType.UpdateAsync , 6)
end
DataBase:UpdateAsync(tostring(UserId) , function(OldVersion)
return {
["Screws"] = Screws,
["MaxPotions"] = MaxPositions,
}
end)
DataBase:UpdateAsync(tostring(UserId).."GunParts" , function(OldVersion)
return GunParts
end)
DataBase:UpdateAsync(tostring(UserId).."UsedGunParts" , function(OldVersion)
return UsedGunParts
end)
DataBase:UpdateAsync(tostring(UserId).."Gears" , function(OldVersion)
return Gears
end)
DataBase:UpdateAsync(tostring(UserId).."UsedGear" , function(OldVersion)
return UsedGear
end)
DataBase:UpdateAsync(tostring(UserId).."Potions" , function(OldVersion)
return Potions
end)
end)
until Secsuss
if SessionLock then
local IsSucsess2 , Data2 = nil
repeat
if not BindToClose then
WaitForBudget(Enum.DataStoreRequestType.UpdateAsync , 1)
end
IsSucsess2 , Data2 = pcall(function()
DataBase:UpdateAsync(tostring(UserId).."Session" , function(OldVersion)
return {
["SessionLock"] = false,
["LastSessionTime"] = os.time(),
}
end)
end)
until IsSucsess2
print("yes")
end
if table.find(RequestsQueue , UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , UserId))
end
end
local function LoadCertainData(Key:string , Player:Player , DefualtData)
local Data = nil
repeat
WaitForBudget(Enum.DataStoreRequestType.UpdateAsync , 1)
local Sucsess = pcall(function()
DataBase:UpdateAsync(Key , function(OldVersion)
if OldVersion then
Data = OldVersion
return Data
else
return DefualtData
end
end)
end)
until (Sucsess and Data) or not PlayersService:FindFirstChild(Player.Name)
return Data
end
local function LoadData(Player:Player)
table.insert(RequestsQueue , Player.UserId)
local Character = Player.CharacterAppearanceLoaded:Wait()
for i , Object in pairs(Player.PlayerGui:GetDescendants()) do
if Object:IsA("LocalScript") then
Object.Enabled = false
end
end
local SessionLock , SessionLockTime , IsSucsess , Error = nil
repeat
WaitForBudget(Enum.DataStoreRequestType.UpdateAsync , 1)
IsSucsess , Error = pcall(function()
DataBase:UpdateAsync(tostring(Player.UserId).."Session" , function(OldVersion)
if OldVersion then
SessionLock = OldVersion.SessionLock or false
SessionLockTime = OldVersion.LastSessionTime or os.time()
if SessionLock == true and (os.time() - SessionLockTime) < 1800 then
table.insert(KickedPlayers , Player.UserId)
Player:Kick("Failed to load your data , please try again later")
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return nil
else
SessionLock = false
SessionLockTime = os.time()
return {
["SessionLock"] = true,
["LastSessionTime"] = SessionLockTime,
}
end
else
SessionLock = false
SessionLockTime = os.time()
return {
["SessionLock"] = true,
["LastSessionTime"] = SessionLockTime,
}
end
end)
end)
until (IsSucsess and SessionLock ~= nil and SessionLockTime) or not PlayersService:FindFirstChild(Player.Name)
if SessionLock or not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
local Folder = Instance.new("Folder")
Folder.Name = "Data"
Folder.Parent = Player
local Screws = Instance.new("IntValue")
Screws.Name = "Screws"
Screws.Parent = Folder
local PotionsStorage = Instance.new("IntValue")
PotionsStorage.Name = "MaxPotions"
PotionsStorage.Parent = Folder
local DefaultPlayerData = {
["Screws"] = 0,
["MaxPotions"] = 6,
}
local PlayerData = LoadCertainData(tostring(Player.UserId) , Player , DefaultPlayerData)
if not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
Screws.Value = PlayerData.Screws
PotionsStorage.Value = PlayerData.MaxPotions
local GunPartsFolder = Instance.new("Folder")
GunPartsFolder.Name = "OwnedGunParts"
GunPartsFolder.Parent = Player
local PlayerGunParts = LoadCertainData(tostring(Player.UserId).."GunParts" , Player , {"Default Chamber" , "Default Loading Port" , "Default Comb"})
if not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
if PlayerGunParts then
for i , GunPart in PlayerGunParts do
local Value = Instance.new("StringValue")
Value.Name = GunPart
Value.Parent = GunPartsFolder
end
end
local PlayerUsedGunPieces = Instance.new("Folder")
PlayerUsedGunPieces.Name = "UsedGunPieces"
PlayerUsedGunPieces.Parent = Player
local UsedPlayerGunParts = LoadCertainData(tostring(Player.UserId).."UsedGunParts" , Player , {
["UsedFrontPiece"] = "Default Chamber",
["UsedMiddlePiece"] = "Default Loading Port",
["UsedBackPiece"] = "Default Comb",
})
if not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
if UsedPlayerGunParts then
local Value1 = Instance.new("StringValue")
Value1.Name = "Front"
Value1.Value = UsedPlayerGunParts.UsedFrontPiece
Value1.Parent = PlayerUsedGunPieces
local Value2 = Instance.new("StringValue")
Value2.Name = "Middle"
Value2.Value = UsedPlayerGunParts.UsedMiddlePiece
Value2.Parent = PlayerUsedGunPieces
local Value3 = Instance.new("StringValue")
Value3.Name = "Back"
Value3.Value = UsedPlayerGunParts.UsedBackPiece
Value3.Parent = PlayerUsedGunPieces
end
local GearsFolder = Instance.new("Folder")
GearsFolder.Name = "OwnedGears"
GearsFolder.Parent = Player
local PlayerGears = LoadCertainData(tostring(Player.UserId).."Gears" , Player , {})
if not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
if PlayerGears then
for i , Gear in PlayerGears do
local Value = Instance.new("StringValue")
Value.Name = Gear
Value.Parent = GearsFolder
end
end
local PlayerUsedGear = LoadCertainData(tostring(Player.UserId).."UsedGear" , Player , "")
if not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
local Value = Instance.new("StringValue")
Value.Name = "UsedGear"
Value.Value = if GearsFolder:FindFirstChild(PlayerUsedGear) then PlayerUsedGear else ""
Value.Parent = Player
local PotionsFolder = Instance.new("Folder")
PotionsFolder.Name = "OwnedPotions"
PotionsFolder.Parent = Player
local PlayerPotions = LoadCertainData(tostring(Player.UserId).."Potions" , Player , {})
if not PlayersService:FindFirstChild(Player.Name) then
if table.find(RequestsQueue , Player.UserId) then
table.remove(RequestsQueue , table.find(RequestsQueue , Player.UserId))
end
return
end
if PlayerPotions then
for i , Potion in PlayerPotions do
if PotionsFolder:FindFirstChild(Potion) then
local ExistedValue = PotionsFolder[Potion]
ExistedValue.Value += 1
else
local Value = Instance.new("IntValue")
Value.Name = Potion
Value.Value = 1
Value.Parent = PotionsFolder
end
end
end
for i , Object in pairs(Player.PlayerGui:GetDescendants()) do
if Object:IsA("LocalScript") then
Object.Enabled = true
end
end
end
game.Players.PlayerAdded:Connect(function(Player)
if not table.find(RequestsQueue , Player.UserId) then
task.spawn(LoadData , Player)
end
end)
game.Players.PlayerRemoving:Connect(function(Player)
if not table.find(RequestsQueue , Player.UserId) then
task.spawn(SaveData , Player , true , false)
end
end)
game:BindToClose(function()
for i, Player in pairs(game.Players:GetChildren()) do
if not table.find(RequestsQueue , Player.UserId) then
task.spawn(SaveData , Player , true , true)
end
end
repeat
task.wait(1)
until #RequestsQueue < 1
end)
while true do
task.wait(60)
for i , Player in ipairs(PlayersService:GetChildren()) do
if not table.find(RequestsQueue , Player.UserId) then
task.spawn(SaveData , Player , false , false)
end
end
end
if there is any issues with the new script , please inform it here because i really need help
@12345koip i am sorry for mentioning you but i really need your thoughts about the new script.
That’s not quite right.
Change that to this:
game:BindToClose(function()
task.wait(if game:GetService("RunService"):IsStudio() then 5 else 30)
end)
And your queueing isn’t quite right.
You’re only adding to the queue here. Add my function from above and call it instead of that table.insert
statement.
(Here’s the function for reference)
local queue = {} --the table of queued data is necessary
local function queueRequest(player: Player): ()
table.insert(queue, player.UserId) --add to request queue
--wait until the request is at the front of the queue
repeat
task.wait(1)
until
table.find(queue, player.UserId) == 1
--remove request from queue
table.remove(queue, 1)
--wait extra
task.wait(1)
end