At what stage does UpdateAsync error?

hello!
It’s a pretty simple question, this is my script

PET_EXIST:UpdateAsync(Lib.Variables.EXIST.KEY_NAME, function(OLD_TABLE)
			--// Do stuff
			return NEW_TABLE
end)

Say i just got KeyThrottled error, did it do the stuff but never saved it to the datastore or did it not do anything to begin with and just errored?

UpdateAsync can theoretically fail at any point in its process, although the error you’d specified would probably be thrown before it did anything. In any case, even if the call succeeds, your transform function could be called more than once in the case that two servers are trying to write to the same key in quick succession, and data that is accessible to outside code could modify the values you are trying to save before the call finishes, leaving the data in a state of limbo.

To avoid running into issues with incomplete UpdateAsync calls (or even completed calls that take time to complete), you should encapsulate your transform functions so they do not directly interface with any external systems (such as by making copies of data at the time you want to save it and then saving specifically the copies) and then resolve the matter once you get your results from UpdateAsync.

2 Likes

A little bit more detail on what caused the error… if any.

local success, errorMessage = pcall(function()
    PET_EXIST:UpdateAsync(Lib.Variables.EXIST.KEY_NAME, function(OLD_TABLE)
        return NEW_TABLE
    end)
end)

if not success then
    print(errorMessage)
end
1 Like

I appreciate your reply, that was quite a detailed explanation. If you do not mind heres the function I run periodically and on BindToClose which tends to error usually only when I restart servers and mass servers (example 280 server) start up at once, it causes the data to be inaccurate at times.

local function LINK(THROUGH_BOUND)
	if THROUGH_BOUND == false and BOUND == true then return end
	local MAX_RETRIES = 5
	local TRY_DELAY = 2

	for TRY = 1, MAX_RETRIES do
		-- Check DataStore request budget before attempting
		local budget = DS:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.UpdateAsync)

		-- If budget is low, wait or skip this iteration
		if budget < 10 then
			print(budget)
			warn("Low DataStore budget. Waiting before retry.")
			wait(TRY_DELAY * (2 ^ (TRY - 1)))
			continue
		end

		local SUCCESS, MESSAGE = pcall(function()
			PET_EXIST:UpdateAsync(Lib.Variables.EXIST.KEY_NAME, function(OLD_TABLE)
				local NEW_TABLE = OLD_TABLE or Lib.EXIST.HOLDER
				for CLASS_NAME, CLASS_EXIST in pairs(Lib.EXIST.HOLDER) do
					if not NEW_TABLE[CLASS_NAME] then
						NEW_TABLE[CLASS_NAME] = {}
					end
					for OBJ_NAME, OBJ_TABLE in pairs(CLASS_EXIST) do
						for TIER, AMOUNT in pairs(OBJ_TABLE) do
							local AMOUNT_TO_SAVE = Lib.EXIST.HOLDER[CLASS_NAME][OBJ_NAME][TIER]
							if not NEW_TABLE[CLASS_NAME][OBJ_NAME] then
								NEW_TABLE[CLASS_NAME][OBJ_NAME] = Lib.EXIST.GET_TYPES(CLASS_NAME)
							end
							if not NEW_TABLE[CLASS_NAME][OBJ_NAME][TIER] then
								NEW_TABLE[CLASS_NAME][OBJ_NAME][TIER] = 0
							end
							
							NEW_TABLE[CLASS_NAME][OBJ_NAME][TIER] += AMOUNT_TO_SAVE
							Lib.EXIST.HOLDER[CLASS_NAME][OBJ_NAME][TIER] -= AMOUNT_TO_SAVE
							if NEW_TABLE[CLASS_NAME][OBJ_NAME][TIER] < 0 then
								NEW_TABLE[CLASS_NAME][OBJ_NAME][TIER] = 0
							end
						end
					end
				end
				Lib.EXIST.GLOBAL_EXIST = NEW_TABLE
				if not THROUGH_BOUND and BOUND == false then 
					Lib.Network.FireAll("EXIST_CLIENT", NEW_TABLE)
				end
				return NEW_TABLE
			end)
		end)

		if SUCCESS then return end
		wait(TRY_DELAY * (2 ^ (TRY - 1)))
	end 
end

do forgive me for making you read all that but if u can point out any issue let me know, thanks a lot.

1 Like

What is Lib.EXIST.HOLDER and Lib.EXIST.GLOBAL_EXIST? Both of these values are mutated in the transform function, which means that the function running but failing to commit to DataStores for whatever reason (network issues, problems on ROBLOX’s ends, etc.) will put the game in an unstable state.

If these values are live data, they need to get moved out of the transform function. If you need to read from Lib.EXIST.HOLDER, copy it when the function starts running and either don’t modify that copy or use a fresh one when the function gets called again.

For Lib.EXIST.GLOBAL_EXIST, UpdateAsync returns the final value saved, so you store NEW_TABLE in an upvalue and only set it to Lib.EXIST.GLOBAL_EXIST or replicate it to the client when the save is completed successfully.

2 Likes

for Lib.EXIST.HOLDER, its basically a counter to keep track of Items/Companions opened/deleted through the server.
Which if you can see I also substract/add from to keep it neutral

as for Lib.EXIST.GLOBAL_EXIST its basically used for server to client replication

do u think that could still cause a problem and moreover why is it that with the Budget checks and exponential backoff I do, I still reach datastore limitations?

Also thank you so much for giving this topic this much of your time, i really appreciate that

1 Like

You want to treat DataStore calls as atomic and as one-way as possible, even when using UpdateAsync, which gives you the option to make your operation two-way to an extent. Because the calls can fail, trying to reconcile changes mid-operation leaves things out of sync when they do, so you want to structure things in a way where you simply send data in and read the results back.

You may want to look into implementing session-locking for your save structure so you can avoid needing to reconcile the local and remote data sources at all. With session-locking, you can generally just overwrite data since the server performing the operation is the source of truth for the save file so long as it owns the session. You can still allow “remote changes” that can get applied to a save file from a source that doesn’t own the session if absolutely necessary and have an session-owning server complete the change when performing its next save, but for most use cases, it isn’t.

As for the specific KeyThrottled error, this is due to blowing key-specific budgets and not the overall budget for a particular request type. Gone are the days of 6-second cooldowns between writes to the same key, but there are still data throughput limits, so it’s possible you’re hitting those, especially since multiple servers may be trying to interact with the same keys at the same time in the case of server reboots.

1 Like

Again thank you for your help, it truly means the world, I will definitely look into the key points you tried to get across, also, do you mind if I add you on discord?

Sorry, I don’t tend to give out external socials, but you can send me a message on the DevForum if you need anything else.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.