Data store is not saving because of the server fast shutdown

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.

2 Likes

is the task.wait will work on the actual clients ? , i think it only works in studio

1 Like

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

1 Like

No, saving data in separate threads will increase the risk of data loss. You don’t need to save on BindToClose, just wait.

2 Likes

but i already have a function that wait for request budget why this increase the risk of data loss

1 Like

i updated my script based on this tutorial that use multiple threads

1 Like

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.

2 Likes

how i can implement a queue system to my script ?

2 Likes

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
2 Likes

this should be at the start of the SaveData function ? , also how i can make the server wait until the data get saved ?

2 Likes

on BindToClose, just use this code:

game:BindToClose(function()
    task.wait(if game:GetService("RunService"):IsStudio() then 5 else 30)
end)
2 Likes

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 ?

2 Likes

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.

2 Likes

thank you so much for your help i really appreciate it

2 Likes

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.

2 Likes

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?

2 Likes

i think if roblox going down the data store service will not work either

1 Like

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.

2 Likes

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
1 Like