Reproduction Steps
In the above example with the following code, a key with the initial value of nil
has 3 UpdateAsync functions called on it within the same frame, and though the final value should be 1
, because the transform function should cancel the update if the current value is not nil
, it is overwritten by all 3 calls.
local THREAD_COUNT = 3
local KEY = tostring(os.time())
local store = game:GetService('DataStoreService'):GetDataStore('test')
local pendingThreads = 0
local outputLines = {'total output:'}
local function getLogTag(i)
return ('%s [key: %s] thread #%s: '):format(tick(), KEY, i)
end
local function log(i, message)
table.insert(outputLines, getLogTag(i) .. tostring(message))
end
print('waiting for responses...')
for i = 1, THREAD_COUNT do
pendingThreads += 1
task.spawn(function ()
local updatedValue = store:UpdateAsync(KEY, function (currentValue)
if currentValue then
log(i, 'transform (current value: ' .. tostring(currentValue) .. ') -> canceling update')
return nil
end
local requestedValue = i
log(i, 'transform (current value: ' .. tostring(currentValue) .. ') -> requested value: ' .. tostring(requestedValue))
return requestedValue
end)
log(i, 'updated to -> ' .. tostring(updatedValue))
local postUpdateValue = store:GetAsync(KEY)
log(i, 'get -> ' .. tostring(postUpdateValue))
pendingThreads -= 1
print(getLogTag(i), ('output so far (%s/%s):'):format(THREAD_COUNT - pendingThreads, THREAD_COUNT), outputLines)
end)
end
while pendingThreads > 0 do
task.wait()
end
print(table.concat(outputLines, '\n'))
local finalValue = store:GetAsync(KEY)
print('final get ->', finalValue)
Expected Behavior
When a transform function passed to UpdateAsync is called with an outdated current key value (e.g. when another UpdateAsync on the same key is running at the same time, perhaps on a different gameserver), it should detect the stale current key value at save time internally, and it should call the transform function again with the latest value & re-attempt the save.
Actual Behavior
If the initial value of a key is nil
at the time that an UpdateAsync transform function is called, and that value is changed by another UpdateAsync on the same key, it will not detect that the key value passed to the transform function was outdated (and will skip the expected process of calling the transform function again with the latest value & reattempting the save), and it will instead save that incorrectly-derived value from the transform function that was called with a stale current value.
This did not happen in datastore v1, but in v2 it began to happen (both during the experimental phase & now that it is the default).
Issue Area: Engine
Issue Type: Other
Impact: High
Frequency: Constantly