Can't trace bug in datasystem, BindToClose() infinitely yielding?

Hey all.

For the past 8 hours or so, I’ve been banging my head against a wall trying to trace a bug in a datasystem that I’m working on, and I can’t seem to be able to trace it for the life of me.

Useful info

My data system saves data to datastores when a player leaves the game. If that save fails, it will retry n more times until it either succeeds or MAX_RETRIES is hit, in which case it will stop attempting to save the player’s data. The data is stored in a folder/ValueObject format to allow for easy replication from server → client and to allow for easy debugging in studio.

I have a BindToClose function that basically waits until there are 0 data folders in the data cache folder. Since a cached datafolder is destroyed when the player leaves and after the save operation completes, 0 folders = all player data has been saved, thus the server can shut down safely.

I’ve gone over my code over and over, testing it in literally any way I can think of and my code seems to check out each time…yet when I end a test session, I get the error Not running script because past shutdown deadline .
When the data is saved successfully the first try, the logic works perfectly fine and BindToClose works as expected. However, if the data save fails and the retry operations run, BindToClose seems to just…hang.

The code is in ServerScriptService.Services.DataService and the data cache folder is located in ReplicatedStorage._PlayerData.
You can toggle the simulated datastore errors by opening ServerScriptService.Utils.MockDatastoreService.MockDatastoreService.MockDatastoreConstants and setting SIMULATE_ERROR_RATE to 1 or 0 .

My older data-system’s code is almost identical and does the same thing, yet it works perfectly fine so I can’t tell if there’s some really obscure bug here in my code or if it’s BindToClose being funky, as there’s a known bug regarding it.

The same issue occurs when using the regular datastore API too, so it’s not the datastore lib I’m using by process of elimination.

Repro place file : DataSystem_Bug.rbxl (186.0 KB)

Could a kind soul point out if this is an issue with my code, or if it’s related to the aforementioned bug with BindToClose? I can’t seem to be able to find any problems with my code, regardless of how many different tests I throw at it.

1 Like

Hey Reshiram, I don’t know on the specifics, but changing the scope of a single variable did the trick!

In your SavePlayerData function, I defined the Data variable inside the loop. And as I tested it, as expected, I got three more simulated errors. :slight_smile:

Zoomed out just so you can see

Here’s the edited code:

local function SavePlayerData(Player)
		local DataFolder = PlayerData:FindFirstChild(tostring(Player.UserId))
		for RetryCount = 0,DATASTORE_RETRY_LIMIT do
			local Data = Table.ConvertFolderToTable(DataFolder) 
            -- EDITED RIGHT HERE ^
			local SaveSuccess,_ = self:SaveData(Player,DATASTORE_PRECISE_NAME,Data)
			if SaveSuccess then
				self:Log(
					("[Data Service] Data saved successfully for player '%s'!"):format(Player.Name)
				)
				
				break
			else --! An error occured while loading the player's data, retry
				if (RetryCount == DATASTORE_RETRY_LIMIT) or not DATASTORE_RETRY_ENABLED then --Retries failed
					self:Log(
						("[Data Service] Max retries reached while trying to save data for player '%s'. Data was not saved.")
						:format(Player.Name),
						"Warning"
					)
					break
				else
					self:Log(
						("[Data Service] Data failed to save for player '%s', retrying %s more times.")
						:format(Player.Name,tostring(DATASTORE_RETRY_LIMIT - RetryCount)),
						"Warning"
					)
					--wait(DATASTORE_RETRY_INTERVAL)
				end
			end
		end
		DataFolder:Destroy()
		print("DESTROYED :D")
	end
	Players.PlayerRemoving:connect(SavePlayerData)

Anyways, try it out and see if it works for yourself!

2 Likes

Thank you for pointing this out! :slight_smile:

I figured out why this is happening. My brain basically derped and forgot that if you pass a table to a function, and that function mutates the table, the table is also mutated outside of the scope of the function due to memory pointers.

local Data = {Coins = 120, Level = 4}

local function PrintTable(Tab)
	for Key,Value in pairs(Data) do
		print(Key,Value)
	end
end

local function SaveData(DataToSave)
	Data.Coins = 0
end

print("BEFORE SaveData() :")
PrintTable(Data)

SaveData(Data)
print("AFTER SaveData() :")
PrintTable(Data)

Output:

  BEFORE SaveData() :
  Level 4
  Coins 120
  AFTER SaveData() :
  Level 4
  Coins 0

Essentially this was basic human error on my part.