DataStore v2 transform functions called with stale data do not get retried with latest data if initial value is nil

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)
local function log(i, message)
    table.insert(outputLines, getLogTag(i) .. tostring(message))

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

            local requestedValue = i
            log(i, 'transform (current value: ' .. tostring(currentValue) .. ') -> requested value: ' .. tostring(requestedValue))
            return requestedValue
        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)

while pendingThreads > 0 do

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

1 Like