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.
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
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.
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.
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
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.
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?