DataStore Locked on My Place - Unable to Open Player Data

Hello, I’m having a serious issue with DataStores.
When a player joins, the data fails to load because the DataStore key is locked.

I’ve implemented retry logic with ProfileService, but the key never unlocks even after waiting. At first I thought it might be taking a bit for Roblox to close my data so I went to bed and the next day it still says my data is locked. I know for a fact I’m opening my data only in one place so that cannot be a problem.

Has anyone else experienced this issue recently? Is this something that requires Roblox staff to clear the lock?

Here’s the main script I’m using to handle DataStores.
(This manages player data loading, saving, retries, etc.)

local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DefaultCharacterData = require(script.DefaultData)
local datamod = require(script.MainModule)
game.ServerScriptService:SetAttribute('dataloaded', true)
local module = {}
local HTTPService = game:GetService("HttpService")
local DataStoreService = game:GetService("DataStoreService")

local function DeepCopy(tbl)
	local copy = {}
	for k, v in pairs(tbl) do
		if type(v) == "table" then
			copy[k] = DeepCopy(v)
		else
			copy[k] = v
		end
	end
	return copy
end

local n = 's_0010t_v1'
local temp = {
	cs = 1,
	['1data'] = DeepCopy(DefaultCharacterData),
	['2data'] = DeepCopy(DefaultCharacterData),
	['3data'] = DeepCopy(DefaultCharacterData),
	['1dc'] = false,
	['2dc'] = false,
	['3dc'] = false,
}

-- Configuration
local MAX_RETRY_ATTEMPTS = 5
local RETRY_DELAY = 2

function module._g(p)
	local d = datamod.find(n, p.UserId)
	if not d then
		-- If no datastore found, create one
		local dataObject, playerData = module._c(p)
		return dataObject, playerData
	end

	-- Check if data is valid
	if not d.Value then
		warn("Data value is nil for player:", p.Name, p.UserId)
		-- Try to recreate data
		local dataObject, playerData = module._c(p)
		return dataObject, playerData
	end

	-- Return full datastore object and its value
	return d, d.Value
end

function module._s(p)
	local d = datamod.find(n, p.UserId)
	if d then
		print("Player leaving, saving data for:", p.Name)
		local status, result = d:Save()
		print("Save result:", status, result)
		d:Close()
	else
		warn("No datastore found to save for player:", p.Name)
	end
end

function module.c_s(p, s)
	local d = datamod.find(n, p.UserId)
	if not d or not s or not d.Value then 
		warn("Invalid parameters for slot change:", p.Name, "slot:", s)
		return 
	end

	-- Change current slot
	d.Value.cs = s
	local slotKey = d.Value.cs .. "data"

	-- Return the slot data if it exists
	if d.Value[slotKey] then
		return d.Value[slotKey]
	else
		warn("Slot data not found for player:", p.Name, "slot:", slotKey)
		return nil
	end
end

function module._pr(p)
	local d = datamod.find(n, p.UserId)
	if d then
		local success, errorMsg = d:Save()
		if success ~= "Success" and success ~= "Saved" then
			warn("Failed to save data for player:", p.Name, "Error:", success, errorMsg)
		end
	else
		warn("No datastore found to save for player:", p.Name)
	end
end

function module._r(p)
	local d = datamod.find(n, p.UserId)
	if not d or not d.Value then 
		warn("Cannot reset data for player:", p.Name, "- No valid datastore")
		return 
	end

	local slotKey = d.Value.cs .. "data"
	d.Value[slotKey] = DeepCopy(DefaultCharacterData)

	local success, errorMsg = d:Save()
	if success ~= "Success" and success ~= "Saved" then
		warn("Failed to save reset data for player:", p.Name, "Error:", success, errorMsg)
	else
		print("Successfully reset slot", d.Value.cs, "for player:", p.Name)
	end
end

-- FIXED: Proper lock handling with retry logic
function module._c(p)
	local d = datamod.new(n, p.UserId)
	d.SaveOnClose = true

	-- Retry logic for locked data
	local success, errorMsg = nil, nil
	local attempts = 0

	repeat
		attempts = attempts + 1
		success, errorMsg = d:Open(temp)

		if success == "Success" then
			break
		elseif success == "Locked" then
			warn("Data locked for", p.Name, "on attempt", attempts, "- Retrying in", RETRY_DELAY, "seconds")
			if attempts < MAX_RETRY_ATTEMPTS then
				task.wait(RETRY_DELAY)
			end
		else
			warn("Unexpected error opening data for", p.Name, ":", success, errorMsg)
			if attempts < MAX_RETRY_ATTEMPTS then
				task.wait(RETRY_DELAY)
			end
		end
	until attempts >= MAX_RETRY_ATTEMPTS or success == "Success"

	-- If all retries failed, try alternative approaches
	if success ~= "Success" then
		warn("All retry attempts failed for", p.Name, "- Trying alternative approaches")

		-- Try destroying and recreating the datastore
		d:Destroy()
		task.wait(1)

		d = datamod.new(n, p.UserId)
		d.SaveOnClose = true
		success, errorMsg = d:Open(temp)

		if success == "Success" then
			warn("Successfully opened data for", p.Name, "after destroying previous instance")
		else
			-- Try read-only mode as fallback
			warn("Attempting read-only fallback for", p.Name)
			success, errorMsg = d:Read(temp)

			if success == "Success" then
				warn("Using read-only data for", p.Name, "- Some features may be limited")
			else
				-- Last resort - temporary data
				warn("All methods failed for", p.Name, "- Using temporary data")
				d.Value = DeepCopy(temp)
				success = "Success"
			end
		end
	end

	-- Final check
	if success ~= "Success" then
		warn("All data loading methods failed for player:", p.Name, p.UserId, "- Final status:", success)
		p:Kick("Unable to load your data after multiple attempts. Please try again in a few minutes. If this persists, contact support. Error: " .. tostring(success))
		return nil, nil
	end

	-- Ensure data structure integrity
	if not d.Value or type(d.Value) ~= "table" then
		warn("Invalid data structure for player:", p.Name, p.UserId, "- Recreating")
		d.Value = DeepCopy(temp)
	end

	-- Ensure current slot exists
	if not d.Value.cs then
		warn("Missing current slot for player:", p.Name, "- Setting to 1")
		d.Value.cs = 1
	end

	-- Ensure all slot data exists
	for i = 1, 3 do
		local slotKey = i .. "data"
		if not d.Value[slotKey] then
			warn("Creating missing slot data for player:", p.Name, "slot:", i)
			d.Value[slotKey] = DeepCopy(DefaultCharacterData)
		end
	end

	return d, d.Value
end

-- Helper function to safely modify data
function module.safeModify(p, modifyFunc)
	local d = datamod.find(n, p.UserId)
	if not d or not d.Value then 
		warn("Cannot safely modify data for player:", p.Name, "- No data found")
		return false
	end

	local success, result = pcall(modifyFunc, d)
	if success then
		local saveSuccess = d:Save()
		if saveSuccess ~= "Success" and saveSuccess ~= "Saved" then
			warn("Failed to save after modification for player:", p.Name, "Error:", saveSuccess)
			return false
		end
		return result
	else
		warn("Error during data modification for player:", p.Name, "Error:", result)
		return false
	end
end

return module

And here’s the relevant part of the datastore module that handles locking.
(This is where I suspect the problem might be, since the lock never clears.)

-- Variables
local Proxy = require(script.Proxy)
local Signal = require(script.Signal)
local SynchronousTaskManager = require(script.SynchronousTaskManager)
local dataStoreService, memoryStoreService, httpService = game:GetService("DataStoreService"), game:GetService("MemoryStoreService"), game:GetService("HttpService")
local Constructor, DataStore = {}, {}
local OpenTask, ReadTask, LockTask, SaveTask, CloseTask, DestroyTask, Lock, Unlock, Load, Save, StartSaveTimer, StopSaveTimer, SaveTimerEnded, StartLockTimer, StopLockTimer, LockTimerEnded, ProcessQueue, SignalConnected, Clone, Reconcile, Compress, Decompress, Encode, Decode, BindToClose
local dataStores, bindToClose, active = {}, {}, true
local characters = {[0] = "0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","!","$","%","&","'",",",".","/",":",";","=","?","@","[","]","^","_","`","{","}","~"}
local bytes = {} for i = (0), #characters do bytes[string.byte(characters[i])] = i end
local base = #characters + 1




-- Types
export type Constructor = {
	new: (name: string, scope: string, key: string?) -> DataStore,
	hidden: (name: string, scope: string, key: string?) -> DataStore,
	find: (name: string, scope: string, key: string?) -> DataStore?,
	Response: {Success: string, Saved: string, Locked: string, State: string, Error: string},
}

export type DataStore = {
	[any]: any,
	Value: any,
	Metadata: {[string]: any},
	UserIds: {any},
	SaveInterval: number,
	SaveDelay: number,
	LockInterval: number,
	LockAttempts: number,
	SaveOnClose: boolean,
	Id: string,
	UniqueId: string,
	Key: string,
	State: boolean?,
	Hidden: boolean,
	AttemptsRemaining: number,
	CreatedTime: number,
	UpdatedTime: number,
	Version: string,
	CompressedValue: string,
	StateChanged: Signal.Signal,
	Saving: Signal.Signal,
	Saved: Signal.Signal,
	AttemptsChanged: Signal.Signal,
	ProcessQueue: Signal.Signal,
	Open: (self: DataStore, template: any?) -> (string, any),
	Read: (self: DataStore, template: any?) -> (string, any),
	Save: (self: DataStore) -> (string, any),
	Close: (self: DataStore) -> (string, any),
	Destroy: (self: DataStore) -> (string, any),
	Queue: (self: DataStore, value: any, expiration: number?, priority: number?) -> (string, any),
	Remove: (self: DataStore, id: string) -> (string, any),
	Clone: (self: DataStore) -> any,
	Reconcile: (self: DataStore, template: any) -> (),
	Usage: (self: DataStore) -> (number, number),
}




-- Constructor
Constructor.new = function(name, scope, key)
	if key == nil then key, scope = scope, "global" end
	local id = name .. "/" .. scope .. "/" .. key
	if dataStores[id] ~= nil then return dataStores[id] end
	local proxy, dataStore = Proxy.new(DataStore, {
		Metadata = {},
		UserIds = {},
		SaveInterval = 30,
		SaveDelay = 0,
		LockInterval = 60,
		LockAttempts = 5,
		SaveOnClose = true,
		Id = id,
		UniqueId = httpService:GenerateGUID(false),
		Key = key,
		State = false,
		Hidden = false,
		AttemptsRemaining = 0,
		CreatedTime = 0,
		UpdatedTime = 0,
		Version = "",
		CompressedValue = "",
		StateChanged = Signal.new(),
		Saving = Signal.new(),
		Saved = Signal.new(),
		AttemptsChanged = Signal.new(),
		ProcessQueue = Signal.new(),
	})
	dataStore.TaskManager = SynchronousTaskManager.new()
	dataStore.LockTime = -math.huge
	dataStore.SaveTime = -math.huge
	dataStore.ActiveLockInterval = 0
	dataStore.ProcessingQueue = false
	dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
	dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
	dataStore.Queue = memoryStoreService:GetQueue(id)
	dataStore.Options = Instance.new("DataStoreSetOptions")
	dataStore.__public.ProcessQueue.DataStore = proxy
	dataStore.__public.ProcessQueue.Connected = SignalConnected
	dataStores[id] = proxy
	if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
	return proxy
end

Constructor.hidden = function(name, scope, key)
	if key == nil then key, scope = scope, "global" end
	local id = name .. "/" .. scope .. "/" .. key
	local proxy, dataStore = Proxy.new(DataStore, {
		Metadata = {},
		UserIds = {},
		SaveInterval = 30,
		SaveDelay = 0,
		LockInterval = 60,
		LockAttempts = 5,
		SaveOnClose = true,
		Id = id,
		UniqueId = httpService:GenerateGUID(false),
		Key = key,
		State = false,
		Hidden = true,
		AttemptsRemaining = 0,
		CreatedTime = 0,
		UpdatedTime = 0,
		Version = "",
		CompressedValue = "",
		StateChanged = Signal.new(),
		Saving = Signal.new(),
		Saved = Signal.new(),
		AttemptsChanged = Signal.new(),
		ProcessQueue = Signal.new(),
	})
	dataStore.TaskManager = SynchronousTaskManager.new()
	dataStore.LockTime = -math.huge
	dataStore.SaveTime = -math.huge
	dataStore.ActiveLockInterval = 0
	dataStore.ProcessingQueue = false
	dataStore.DataStore = dataStoreService:GetDataStore(name, scope)
	dataStore.MemoryStore = memoryStoreService:GetSortedMap(id)
	dataStore.Queue = memoryStoreService:GetQueue(id)
	dataStore.Options = Instance.new("DataStoreSetOptions")
	dataStore.__public.ProcessQueue.DataStore = proxy
	dataStore.__public.ProcessQueue.Connected = SignalConnected
	if active == true then bindToClose[dataStore.__public.UniqueId] = proxy end
	return proxy
end

Constructor.find = function(name, scope, key)
	if key == nil then key, scope = scope, "global" end
	local id = name .. "/" .. scope .. "/" .. key
	return dataStores[id]
end

Constructor.Response = {Success = "Success", Saved = "Saved", Locked = "Locked", State = "State", Error = "Error"}




-- DataStore
DataStore.__tostring = function(proxy)
	return "DataStore"
end

DataStore.__shared = {
	Open = function(proxy, template)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Open failed: Passed value is not a DataStore", 3) end
		if dataStore.__public.State == nil then return "State", "Destroyed" end
		local synchronousTask = dataStore.TaskManager:FindFirst(OpenTask)
		if synchronousTask ~= nil then return synchronousTask:Wait(template) end
		if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
		if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then
			if dataStore.__public.Value == nil then
				dataStore.__public.Value = Clone(template)
			elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
				Reconcile(dataStore.__public.Value, template)
			end
			return "Success"
		end
		return dataStore.TaskManager:InsertBack(OpenTask, proxy):Wait(template)
	end,
	Read = function(proxy, template)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Read failed: Passed value is not a DataStore", 3) end
		local synchronousTask = dataStore.TaskManager:FindFirst(ReadTask)
		if synchronousTask ~= nil then return synchronousTask:Wait(template) end
		if dataStore.__public.State == true and dataStore.TaskManager:FindLast(CloseTask) == nil then return "State", "Open" end
		return dataStore.TaskManager:InsertBack(ReadTask, proxy):Wait(template)
	end,
	Save = function(proxy)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Save failed: Passed value is not a DataStore", 3) end
		if dataStore.__public.State == false then return "State", "Closed" end
		if dataStore.__public.State == nil then return "State", "Destroyed" end
		local synchronousTask = dataStore.TaskManager:FindFirst(SaveTask)
		if synchronousTask ~= nil then return synchronousTask:Wait() end
		if dataStore.TaskManager:FindLast(CloseTask) ~= nil then return "State", "Closing" end
		if dataStore.TaskManager:FindLast(DestroyTask) ~= nil then return "State", "Destroying" end
		return dataStore.TaskManager:InsertBack(SaveTask, proxy):Wait()
	end,
	Close = function(proxy)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Close failed: Passed value is not a DataStore", 3) end
		if dataStore.__public.State == nil then return "Success" end
		local synchronousTask = dataStore.TaskManager:FindFirst(CloseTask)
		if synchronousTask ~= nil then return synchronousTask:Wait() end
		if dataStore.__public.State == false and dataStore.TaskManager:FindLast(OpenTask) == nil then return "Success" end
		local synchronousTask = dataStore.TaskManager:FindFirst(DestroyTask)
		if synchronousTask ~= nil then return synchronousTask:Wait() end
		StopLockTimer(dataStore)
		StopSaveTimer(dataStore)
		return dataStore.TaskManager:InsertBack(CloseTask, proxy):Wait()
	end,
	Destroy = function(proxy)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Destroy failed: Passed value is not a DataStore", 3) end
		if dataStore.__public.State == nil then return "Success" end
		dataStores[dataStore.__public.Id] = nil
		StopLockTimer(dataStore)
		StopSaveTimer(dataStore)
		return (dataStore.TaskManager:FindFirst(DestroyTask) or dataStore.TaskManager:InsertBack(DestroyTask, proxy)):Wait()
	end,
	Queue = function(proxy, value, expiration, priority)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Queue failed: Passed value is not a DataStore", 3) end
		if expiration ~= nil and type(expiration) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
		if priority ~= nil and type(priority) ~= "number" then error("Attempt to Queue failed: Passed value is not nil or number", 3) end
		local success, errorMessage
		for i = 1, 3 do
			if i > 1 then task.wait(1) end
			success, errorMessage = pcall(dataStore.Queue.AddAsync, dataStore.Queue, value, expiration or 604800, priority)
			if success == true then return "Success" end
		end
		return "Error", errorMessage
	end,
	Remove = function(proxy, id)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Remove failed: Passed value is not a DataStore", 3) end
		if type(id) ~= "string" then error("Attempt to RemoveQueue failed: Passed value is not a string", 3) end
		local success, errorMessage
		for i = 1, 3 do
			if i > 1 then task.wait(1) end
			success, errorMessage = pcall(dataStore.Queue.RemoveAsync, dataStore.Queue, id)
			if success == true then return "Success" end
		end
		return "Error", errorMessage
	end,
	Clone = function(proxy)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Clone failed: Passed value is not a DataStore", 3) end
		return Clone(dataStore.__public.Value)
	end,
	Reconcile = function(proxy, template)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Reconcile failed: Passed value is not a DataStore", 3) end
		if dataStore.__public.Value == nil then
			dataStore.__public.Value = Clone(template)
		elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
			Reconcile(dataStore.__public.Value, template)
		end
	end,
	Usage = function(proxy)
		local dataStore = getmetatable(proxy)
		if type(dataStore) ~= "table" or dataStore.__shared ~= DataStore.__shared then error("Attempt to Usage failed: Passed value is not a DataStore", 3) end
		if dataStore.__public.Value == nil then return 0, 0 end
		if type(dataStore.__public.Metadata.Compress) ~= "table" then
			local characters = #httpService:JSONEncode(dataStore.__public.Value)
			return characters, characters / 4194303
		else
			local level = dataStore.__public.Metadata.Compress.Level or 2
			local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
			local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
			dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
			local characters = #httpService:JSONEncode(dataStore.__public.CompressedValue)
			return characters, characters / 4194303
		end
	end,
}

DataStore.__set = {
	Metadata = function(proxy, dataStore, value)
		if type(value) ~= "table" then error("Attempt to set Metadata failed: Passed value is not a table", 3) end
		dataStore.__public.Metadata = value
	end,
	UserIds = function(proxy, dataStore, value)
		if type(value) ~= "table" then error("Attempt to set UserIds failed: Passed value is not a table", 3) end
		dataStore.__public.UserIds = value
	end,
	SaveInterval = function(proxy, dataStore, value)
		if type(value) ~= "number" then error("Attempt to set SaveInterval failed: Passed value is not a number", 3) end
		if value < 10 and value ~= 0 then error("Attempt to set SaveInterval failed: Passed value is less then 10 and not 0", 3) end
		if value > 1000 then error("Attempt to set SaveInterval failed: Passed value is more then 1000", 3) end
		if value == dataStore.__public.SaveInterval then return end
		dataStore.__public.SaveInterval = value
		if dataStore.__public.State ~= true then return end
		if value == 0 then
			StopSaveTimer(dataStore)
		elseif dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
			StartSaveTimer(proxy)
		end
	end,
	SaveDelay = function(proxy, dataStore, value)
		if type(value) ~= "number" then error("Attempt to set SaveDelay failed: Passed value is not a number", 3) end
		if value < 0 then error("Attempt to set SaveDelay failed: Passed value is less then 0", 3) end
		if value > 10 then error("Attempt to set SaveDelay failed: Passed value is more then 10", 3) end
		dataStore.__public.SaveDelay = value
	end,
	LockInterval = function(proxy, dataStore, value)
		if type(value) ~= "number" then error("Attempt to set LockInterval failed: Passed value is not a number", 3) end
		if value < 10 then error("Attempt to set LockInterval failed: Passed value is less then 10", 3) end
		if value > 1000 then error("Attempt to set LockInterval failed: Passed value is more then 1000", 3) end
		dataStore.__public.LockInterval = value
	end,
	LockAttempts = function(proxy, dataStore, value)
		if type(value) ~= "number" then error("Attempt to set LockAttempts failed: Passed value is not a number", 3) end
		if value < 1 then error("Attempt to set LockAttempts failed: Passed value is less then 1", 3) end
		if value > 100 then error("Attempt to set LockAttempts failed: Passed value is more then 100", 3) end
		dataStore.__public.LockAttempts = value
	end,
	SaveOnClose = function(proxy, dataStore, value)
		if type(value) ~= "boolean" then error("Attempt to set SaveOnClose failed: Passed value is not a boolean", 3) end
		dataStore.__public.SaveOnClose = value
	end,
	Id = false,
	UniqueId = false,
	Key = false,
	State = false,
	Hidden = false,
	AttemptsRemaining = false,
	CreatedTime = false,
	UpdatedTime = false,
	Version = false,
	CompressedValue = false,
	StateChanged = false,
	Saving = false,
	Saved = false,
	AttemptsChanged = false,
	ProcessQueue = false,
}




-- Functions
OpenTask = function(runningTask, proxy)
	local dataStore = getmetatable(proxy)
	local response, responseData = Lock(dataStore, 3)
	if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
	local response, responseData = Load(dataStore, 3)
	if response ~= "Success" then Unlock(dataStore, 3) for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
	dataStore.__public.State = true
	if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then
		StartSaveTimer(proxy)
		StartLockTimer(proxy)
	end
	for thread, template in runningTask:Iterate() do
		if dataStore.__public.Value == nil then
			dataStore.__public.Value = Clone(template)
		elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
			Reconcile(dataStore.__public.Value, template)
		end
		task.defer(thread, response)
	end
	if dataStore.ProcessingQueue == false and dataStore.__public.ProcessQueue.Connections > 0 then task.defer(ProcessQueue, proxy) end
	dataStore.__public.StateChanged:Fire(true, proxy)
end

ReadTask = function(runningTask, proxy)
	local dataStore = getmetatable(proxy)
	if dataStore.__public.State == true then for thread in runningTask:Iterate() do task.defer(thread, "State", "Open") end return end
	local response, responseData = Load(dataStore, 3)
	if response ~= "Success" then for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end return end
	for thread, template in runningTask:Iterate() do
		if dataStore.__public.Value == nil then
			dataStore.__public.Value = Clone(template)
		elseif type(dataStore.__public.Value) == "table" and type(template) == "table" then
			Reconcile(dataStore.__public.Value, template)
		end
		task.defer(thread, response)
	end
end

LockTask = function(runningTask, proxy)
	local dataStore = getmetatable(proxy)
	local attemptsRemaining = dataStore.__public.AttemptsRemaining
	local response, responseData = Lock(dataStore, 3)
	if response ~= "Success" then dataStore.__public.AttemptsRemaining -= 1 end
	if dataStore.__public.AttemptsRemaining ~= attemptsRemaining then dataStore.__public.AttemptsChanged:Fire(dataStore.__public.AttemptsRemaining, proxy) end
	if dataStore.__public.AttemptsRemaining > 0 then
		if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartLockTimer(proxy) end
	else
		dataStore.__public.State = false
		StopLockTimer(dataStore)
		StopSaveTimer(dataStore)
		if dataStore.__public.SaveOnClose == true then Save(proxy, 3) end
		Unlock(dataStore, 3)
		dataStore.__public.StateChanged:Fire(false, proxy)
	end
	for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
end
	
SaveTask = function(runningTask, proxy)
	local dataStore = getmetatable(proxy)
	if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "State", "Closed") end return end
	StopSaveTimer(dataStore)
	runningTask:End()
	local response, responseData = Save(proxy, 3)
	if dataStore.TaskManager:FindLast(CloseTask) == nil and dataStore.TaskManager:FindLast(DestroyTask) == nil then StartSaveTimer(proxy) end
	for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
end

CloseTask = function(runningTask, proxy)
	local dataStore = getmetatable(proxy)
	if dataStore.__public.State == false then for thread in runningTask:Iterate() do task.defer(thread, "Success") end return end
	dataStore.__public.State = false
	local response, responseData = nil, nil
	if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
	Unlock(dataStore, 3)
	dataStore.__public.StateChanged:Fire(false, proxy)
	if response == "Saved" then
		for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
	else
		for thread in runningTask:Iterate() do task.defer(thread, "Success") end
	end
end

DestroyTask = function(runningTask, proxy)
	local dataStore = getmetatable(proxy)
	local response, responseData = nil, nil
	if dataStore.__public.State == false then
		dataStore.__public.State = nil
	else
		dataStore.__public.State = nil
		if dataStore.__public.SaveOnClose == true then response, responseData = Save(proxy, 3) end
		Unlock(dataStore, 3)
	end
	dataStore.__public.StateChanged:Fire(nil, proxy)
	dataStore.__public.StateChanged:DisconnectAll()
	dataStore.__public.Saving:DisconnectAll()
	dataStore.__public.Saved:DisconnectAll()
	dataStore.__public.AttemptsChanged:DisconnectAll()
	dataStore.__public.ProcessQueue:DisconnectAll()
	bindToClose[dataStore.__public.UniqueId] = nil
	if response == "Saved" then
		for thread in runningTask:Iterate() do task.defer(thread, response, responseData) end
	else
		for thread in runningTask:Iterate() do task.defer(thread, "Success") end
	end
end

Lock = function(dataStore, attempts)
	local success, value, id, lockTime, lockInterval, lockAttempts = nil, nil, nil, nil, dataStore.__public.LockInterval, dataStore.__public.LockAttempts
	for i = 1, attempts do
		if i > 1 then task.wait(1) end
		lockTime = os.clock()
		success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == nil or id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, lockInterval * lockAttempts + 30)
		if success == true then break end
	end
	if success == false then return "Error", value end
	if value == nil then return "Locked", id end
	dataStore.LockTime = lockTime + lockInterval * lockAttempts
	dataStore.ActiveLockInterval = lockInterval
	dataStore.__public.AttemptsRemaining = lockAttempts
	return "Success"
end

Unlock = function(dataStore, attempts)
	local success, value, id = nil, nil, nil
	for i = 1, attempts do
		if i > 1 then task.wait(1) end
		success, value = pcall(dataStore.MemoryStore.UpdateAsync, dataStore.MemoryStore, "Id", function(value) id = value return if id == dataStore.__public.UniqueId then dataStore.__public.UniqueId else nil end, 0)
		if success == true then break end
	end
	if success == false then return "Error", value end
	if value == nil and id ~= nil then return "Locked", id end
	return "Success"
end

Load = function(dataStore, attempts)
	local success, value, info = nil, nil, nil
	for i = 1, attempts do
		if i > 1 then task.wait(1) end
		success, value, info = pcall(dataStore.DataStore.GetAsync, dataStore.DataStore, dataStore.__public.Key)
		if success == true then break end
	end
	if success == false then return "Error", value end
	if info == nil then
		dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
	else
		dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = info:GetMetadata(), info:GetUserIds(), info.CreatedTime, info.UpdatedTime, info.Version
	end
	if type(dataStore.__public.Metadata.Compress) ~= "table" then
		dataStore.__public.Value = value
	else
		dataStore.__public.CompressedValue = value
		local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
		dataStore.__public.Value = Decompress(dataStore.__public.CompressedValue, decimals)
	end
	return "Success"
end

Save = function(proxy, attempts)
	local dataStore = getmetatable(proxy)
	local deltaTime = os.clock() - dataStore.SaveTime
	if deltaTime < dataStore.__public.SaveDelay then task.wait(dataStore.__public.SaveDelay - deltaTime) end
	dataStore.__public.Saving:Fire(dataStore.__public.Value, proxy)
	local success, value, info = nil, nil, nil
	if dataStore.__public.Value == nil then
		for i = 1, attempts do
			if i > 1 then task.wait(1) end
			success, value, info = pcall(dataStore.DataStore.RemoveAsync, dataStore.DataStore, dataStore.__public.Key)
			if success == true then break end
		end
		if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
		dataStore.__public.Metadata, dataStore.__public.UserIds, dataStore.__public.CreatedTime, dataStore.__public.UpdatedTime, dataStore.__public.Version = {}, {}, 0, 0, ""
	elseif type(dataStore.__public.Metadata.Compress) ~= "table" then
		dataStore.Options:SetMetadata(dataStore.__public.Metadata)
		for i = 1, attempts do
			if i > 1 then task.wait(1) end
			success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.Value, dataStore.__public.UserIds, dataStore.Options)
			if success == true then break end
		end	
		if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
		dataStore.__public.Version = value
	else
		local level = dataStore.__public.Metadata.Compress.Level or 2
		local decimals = 10 ^ (dataStore.__public.Metadata.Compress.Decimals or 3)
		local safety = if dataStore.__public.Metadata.Compress.Safety == nil then true else dataStore.__public.Metadata.Compress.Safety
		dataStore.__public.CompressedValue = Compress(dataStore.__public.Value, level, decimals, safety)
		dataStore.Options:SetMetadata(dataStore.__public.Metadata)
		for i = 1, attempts do
			if i > 1 then task.wait(1) end
			success, value = pcall(dataStore.DataStore.SetAsync, dataStore.DataStore, dataStore.__public.Key, dataStore.__public.CompressedValue, dataStore.__public.UserIds, dataStore.Options)
			if success == true then break end
		end
		if success == false then dataStore.__public.Saved:Fire("Error", value, proxy) return "Error", value end
		dataStore.Version = value
	end
	dataStore.SaveTime = os.clock()
	dataStore.__public.Saved:Fire("Saved", dataStore.__public.Value, proxy)
	return "Saved", dataStore.__public.Value
end

StartSaveTimer = function(proxy)
	local dataStore = getmetatable(proxy)
	if dataStore.SaveThread ~= nil then task.cancel(dataStore.SaveThread) end
	if dataStore.__public.SaveInterval == 0 then return end
	dataStore.SaveThread = task.delay(dataStore.__public.SaveInterval, SaveTimerEnded, proxy)
end

StopSaveTimer = function(dataStore)
	if dataStore.SaveThread == nil then return end
	task.cancel(dataStore.SaveThread)
	dataStore.SaveThread = nil
end

SaveTimerEnded = function(proxy)
	local dataStore = getmetatable(proxy)
	dataStore.SaveThread = nil
	if dataStore.TaskManager:FindLast(SaveTask) ~= nil then return end
	dataStore.TaskManager:InsertBack(SaveTask, proxy)
end

StartLockTimer = function(proxy)
	local dataStore = getmetatable(proxy)
	if dataStore.LockThread ~= nil then task.cancel(dataStore.LockThread) end
	local startTime = dataStore.LockTime - dataStore.__public.AttemptsRemaining * dataStore.ActiveLockInterval
	dataStore.LockThread = task.delay(startTime - os.clock() + dataStore.ActiveLockInterval, LockTimerEnded, proxy)
end

StopLockTimer = function(dataStore)
	if dataStore.LockThread == nil then return end
	task.cancel(dataStore.LockThread)
	dataStore.LockThread = nil
end

LockTimerEnded = function(proxy)
	local dataStore = getmetatable(proxy)
	dataStore.LockThread = nil
	if dataStore.TaskManager:FindFirst(LockTask) ~= nil then return end
	dataStore.TaskManager:InsertBack(LockTask, proxy)
end

ProcessQueue = function(proxy)
	local dataStore = getmetatable(proxy)
	if dataStore.__public.State ~= true then return end
	if dataStore.__public.ProcessQueue.Connections == 0 then return end
	if dataStore.ProcessingQueue == true then return end
	dataStore.ProcessingQueue = true
	while true do
		local success, values, id = pcall(dataStore.Queue.ReadAsync, dataStore.Queue, 100, false, 30)
		if dataStore.__public.State ~= true then break end
		if dataStore.__public.ProcessQueue.Connections == 0 then break end
		if success == true and id ~= nil then dataStore.__public.ProcessQueue:Fire(id, values, proxy) end
	end
	dataStore.ProcessingQueue = false
end

SignalConnected = function(connected, signal)
	if connected == false then return end
	ProcessQueue(signal.DataStore)
end

Clone = function(original)
	if type(original) ~= "table" then return original end
	local clone = {}
	for index, value in original do clone[index] = Clone(value) end
	return clone
end

Reconcile = function(target, template)	
	for index, value in template do
		if type(index) == "number" then continue end
		if target[index] == nil then
			target[index] = Clone(value)
		elseif type(target[index]) == "table" and type(value) == "table" then
			Reconcile(target[index], value)
		end
	end
end

Compress = function(value, level, decimals, safety)
	local data = {}
	if type(value) == "boolean" then
		table.insert(data, if value == false then "-" else "+")
	elseif type(value) == "number" then
		if value % 1 == 0 then
			table.insert(data, if value < 0 then "<" .. Encode(-value) else ">" .. Encode(value))
		else
			table.insert(data, if value < 0 then "(" .. Encode(math.round(-value * decimals)) else ")" .. Encode(math.round(value * decimals)))
		end
	elseif type(value) == "string" then
		if safety == true then value = value:gsub("", " ") end
		table.insert(data, "#" .. value .. "")
	elseif type(value) == "table" then
		if #value > 0 and level == 2 then
			table.insert(data, "|")
			for i = 1, #value do table.insert(data, Compress(value[i], level, decimals, safety)) end
			table.insert(data, "")
		else
			table.insert(data, "*")
			for key, tableValue in value do table.insert(data, Compress(key, level, decimals, safety)) table.insert(data, Compress(tableValue, level, decimals, safety)) end
			table.insert(data, "")
		end
	end
	return table.concat(data)
end

Decompress = function(value, decimals, index)	
	local i1, i2, dataType, data = value:find("([-+<>()#|*])", index or 1)
	if dataType == "-" then
		return false, i2
	elseif dataType == "+" then
		return true, i2
	elseif dataType == "<" then
		i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
		return -Decode(data), i2
	elseif dataType == ">" then
		i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
		return Decode(data), i2
	elseif dataType == "(" then
		i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
		return -Decode(data) / decimals, i2
	elseif dataType == ")" then
		i1, i2, data = value:find("([^-+<>()#|*]*)", i2 + 1)
		return Decode(data) / decimals, i2
	elseif dataType == "#" then
		i1, i2, data = value:find("(.-)", i2 + 1)
		return data, i2
	elseif dataType == "|" then
		local array = {}
		while true do
			data, i2 = Decompress(value, decimals, i2 + 1)
			if data == nil then break end
			table.insert(array, data)
		end
		return array, i2
	elseif dataType == "*" then
		local dictionary, key = {}, nil
		while true do
			key, i2 = Decompress(value, decimals, i2 + 1)
			if key == nil then break end
			data, i2 = Decompress(value, decimals, i2 + 1)
			dictionary[key] = data
		end
		return dictionary, i2
	end
	return nil, i2
end

Encode = function(value)
	if value == 0 then return "0" end
	local data = {}
	while value > 0 do
		table.insert(data, characters[value % base])
		value = math.floor(value / base)
	end
	return table.concat(data)
end

Decode = function(value)
	local number, power, data = 0, 1, {string.byte(value, 1, #value)}	
	for i, code in data do
		number += bytes[code] * power
		power *= base
	end
	return number
end

BindToClose = function()
	active = false
	for uniqueId, proxy in bindToClose do
		local dataStore = getmetatable(proxy)
		if dataStore.__public.State == nil then continue end
		dataStores[dataStore.__public.Id] = nil
		StopLockTimer(dataStore)
		StopSaveTimer(dataStore)
		if dataStore.TaskManager:FindFirst(DestroyTask) == nil then dataStore.TaskManager:InsertBack(DestroyTask, proxy) end
	end
	while next(bindToClose) ~= nil do task.wait() end
end




-- Events
game:BindToClose(BindToClose)




return table.freeze(Constructor) :: Constructor

And here’s the error that I get:

  02:00:54.439  Data locked for Skelatron173 on attempt 1 - Retrying in 2 seconds  -  Server - DataHandler2:135
  02:00:56.541  Data locked for Skelatron173 on attempt 2 - Retrying in 2 seconds  -  Server - DataHandler2:135
  02:00:58.788  Data locked for Skelatron173 on attempt 3 - Retrying in 2 seconds  -  Server - DataHandler2:135
  02:01:00.924  Data locked for Skelatron173 on attempt 4 - Retrying in 2 seconds  -  Server - DataHandler2:135
  02:01:03.024  Data locked for Skelatron173 on attempt 5 - Retrying in 2 seconds  -  Server - DataHandler2:135
  02:01:03.024  All retry attempts failed for Skelatron173 - Trying alternative approaches  -  Server - DataHandler2:149
  02:01:04.123  Attempting read-only fallback for Skelatron173  -  Server - DataHandler2:163
  02:01:04.207  Using read-only data for Skelatron173 - Some features may be limited  -  Server - DataHandler2:167

I’d appreciate if anyone can confirm:

  • Is this a Roblox-side issue where locks sometimes get stuck indefinitely?
  • Or do I need to change something in how I’m opening/closing DataStores?
  • Do I need Roblox staff intervention to clear the lock?

Thanks in advance for the help.

I’m just gonna be honest, delete this post if you name your functions like that. No offense, but you are not taking development seriously if your DataStore API methods named in a private manner and just two characters long

1 Like

Wow, you are going to be an epic programmer. However, this seems a bit overcomplicated to me. Not sure if this helps. Good luck.

local ms = game:GetService("MemoryStoreService")
local locks = ms:GetSortedMap("PlayerLocks")
local serverId = game.JobId

local function lockPlayer(uid)
	local ok = pcall(function()
		local now = os.time()
		local existing = locks:GetAsync(uid)
		if not existing or existing.expire < now then
			locks:SetAsync(uid,{server=serverId,expire=now+60},60)
			return true
		end
	end)
	return ok
end

local function unlockPlayer(uid)
	pcall(function()
		local data = locks:GetAsync(uid)
		if data and data.server == serverId then
			locks:RemoveAsync(uid)
		end
	end)
end

game.Players.PlayerAdded:Connect(function(p)
	if not lockPlayer(p.UserId) then
		p:Kick("Data is locked")
		return
	end
	p.AncestryChanged:Connect(function(_,parent)
		if not parent then unlockPlayer(p.UserId) end
	end)
end)

game:BindToClose(function()
	for _,p in ipairs(game.Players:GetPlayers()) do
		unlockPlayer(p.UserId)
	end
end)

Roblox’s datastore don’t have a locking feature (it’s something you implement yourself), so not, it is unrelated to roblox. The lock probably comes from ProfileService, but I am not familiar with ProfileService, and your datastore script seems quite complicated

I would also suggest ignoring the first, rude, reply on this thread.

holy overcomplicated…

1 Like

Your issue comes from

	--[...]

	repeat
		attempts = attempts + 1
		success, errorMsg = d:Open(temp)

		if success == "Success" then
			break
		elseif success == "Locked" then
			warn("Data locked for", p.Name, "on attempt", attempts, "- Retrying in", RETRY_DELAY, "seconds")
			if attempts < MAX_RETRY_ATTEMPTS then
				task.wait(RETRY_DELAY)
			end
		else
			warn("Unexpected error opening data for", p.Name, ":", success, errorMsg)
			if attempts < MAX_RETRY_ATTEMPTS then
				task.wait(RETRY_DELAY)
			end
		end
	until attempts >= MAX_RETRY_ATTEMPTS or success == "Success"

	--[...]

on line 135 in your module._c() function. The open fails because the datastore is locked. Why is it locked? Idk, that is handled by what you call datamod in your script. How does datamod handle locking? Idk, it’s in other scripts that you didn’t provide, and I am not sure how that module works

I am guessing you didn’t write the datamod module. I looked at the ProfileService documentation, and didn’t ding much resemblance. What is that module? To fix this issue, you should look at the documentation of whatever you are using, instead of looking through the code of said module. There is just too much code