I’m unsure of this situation, but I’ve noticed from my game my datastores have been reverting towards older data and newer data, is there something going on with my code?
function DataStoreModule:GetAsync(DataStoreName : {[string] : "String"}, PathName : {[string] : "String"})
if not(DataStoreName or type(DataStoreName) == "string") then return { Success = false }; end;
if not(PathName or type(PathName) == "string") then return { Success = false }; end;
local Success, ResponseMessage = pcall(function()
return game:GetService("DataStoreService"):GetDataStore(DataStoreName):GetAsync(PathName);
end);
if(Success and ResponseMessage ~= nil) then
local RetrievedResponse = { Success = true, Response = {
CacheLog = { TickInterval = os.time(), Path = PathName },
CacheSynced = ResponseMessage
}}; return RetrievedResponse;
else
if not(Success) then warn(ResponseMessage); return { Success = false }; end;
end
end
function DataStoreModule:UpdateAsync(DataStoreName : {[string] : "String"}, PathName : {[string] : "String"}, DataTablePassed : {[string] : "Table"})
if not(DataStoreName or type(DataStoreName) == "string") then return { Success = false }; end;
if not(PathName or type(PathName) == "string") then return { Success = false }; end;
local Success, ResponseMessage = pcall(function()
return DataStoreService:GetDataStore(DataStoreName):UpdateAsync(PathName, function(DataStoreCache : {[string] : "Any"})
return DataTablePassed;
end);
end);
if(Success) then return { Success = true, Response = ResponseMessage }; end;
return { Success = false, Response = ResponseMessage };
end
I’ve been using :UpdateAsync() for saving the data after a person has joined the game, but I’m unsure of what’s going on, it’d be appreciated to get some knowledge on how this works, and why it’s happening.
However we use GetAsync once the player has joined just as normal.
It seems to be like something to do with entirely updateasync, so I’m really not sure (and yes there’s more to this code, it’s just a bunch of values being set into the table and transferring it)
But it does seem to save for a few minutes until after a few minutes not play-testing in studio it’ll reset to older data then continue to do a loop for retrieving old and new data.
The issue with using both GetAsync and UpdateAsync is that GetAsync isn’t ordered, and there’s no way to figure out which one will finish first - GetAsync or UpdateAsync.
Also, UpdateAsync will always take more time to complete than GetAsync, so what will likely happen is that a GetAsync call will finish before UpdateAsync finishes writing new data into datastores.
In other words, you’ll end up getting old data, and whatever new data you tried saving just goes poof.
You’ll start to see this issue manifest itself when you handle cross-server joining, or when a player rejoins a server really quickly.
Solution: Stop using GetAsync. Just use entirely UpdateAsync.
There is no caching issue with UpdateAsync whatsoever, it’s just that people don’t know how it works.
See, when you call UpdateAsync consecutively, Roblox takes the effort to order them, and subsequent UpdateAsync calls will be blocked until the current one is complete.
The cherry on top here, is that subsequent UpdateAsync calls will always return the most recent data, so there’s no race conditions to worry about.
Really the only tradeoff I can think of is speed, but otherwise you have here the most powerful interface to work with datastores.
There’s a reason why ProfileService uses entirely UpdateAsync, you know.
I’ll definitely try this, and see how it goes. This seems like a very plausible cause.
This is as well something I haven’t ever really tried as I’ve always been using Firebase for saving but I’m hoping to migrate towards Roblox’s until then. It only became an issue with saving soon as I transferred over from Firebase to now Roblox’s
I’ve attempted this, but it seems to continue to reset back to an older version of the data that was saved, even if I use UpdateAsync
I’m not exactly sure whats happening but as far as I know the datastore just continues to replace the new data with older versions. And even then it will switch between another old version and continue to do this in a loop like (e.g. 2 billion to 4 billion will keep replacing eachother while the newer data was meant to be 1 billion.)
Alrighty, I tried it with just UpdateAsync, seems to be returning same results, I’m unsure of why but I assume it has to be something to potentially be with the way Roblox is taking it?
function CharacterConstructor:ConnectAsync(Player : {[string] : "Player"})
if not(Player or Player:IsA("Player")) then return { Success = false }; end;
local LeaderStatsFolder = self:CacheAsyncLeaderstats(Player);
if(LeaderStatsFolder) then
local ClientConfigurations = DataStoreConstructor:SetConfigs(Player);
local ClientServiceCache = self:ConnectServerAsync(Player);
local Success, ResponseMessage = pcall(function()
return game:GetService("DataStoreService"):GetDataStore("PublicDataStore"):UpdateAsync(Player.UserId, function(DataCache)
return DataCache
end);
end);
if(ClientConfigurations.Success and Success and ResponseMessage ~= nil) then
ClientConfigurations = ClientConfigurations.Result;
if(LeaderStatsFolder:FindFirstChild("Experience")) then
local ExperienceAmount = ResponseMessage["UserCurrentExperience"];
if(LeaderStatsFolder:FindFirstChild("Experience")) then
local ExperienceValue = LeaderStatsFolder.Experience;
ExperienceValue.Value = ExperienceAmount;
end
end
if(ClientConfigurations:FindFirstChild("GroupGender") and ClientConfigurations:FindFirstChild("GroupPronouns")) then
local GroupGender, GroupPronouns = ClientConfigurations["GroupGender"], ClientConfigurations["GroupPronouns"];
GroupGender.Value = ResponseMessage["UserCoreUtilities"]["UserGender"];
GroupPronouns.Value = ResponseMessage["UserCoreUtilities"]["UserPronouns"];
end
if(ResponseMessage.UserCoreUtilities.UserUtilities.UserArrestUtility ~= nil) then
local ArrestUtilityConfigs = ResponseMessage.UserCoreUtilities.UserUtilities.UserArrestUtility;
if(ArrestUtilityConfigs.UserArrested ~= false) then
ArrestConstructor:ArrestPlayer(Player, {
["ArrestReason"] = ArrestUtilityConfigs.UserArrestedReason;
["ArrestModerator"] = ArrestUtilityConfigs.UserArrestedFrom;
["ArrestTimeLength"] = ArrestUtilityConfigs.UserArrestedLength;
});
end
end
else
ClientConfigurations = ClientConfigurations.Result;
if(LeaderStatsFolder:FindFirstChild("Experience")) then
local ExperienceValue = LeaderStatsFolder.Experience;
local GroupRankExperienceData = GroupConstructor:GetRankExperienceConfigs(GroupConstructor:GetRankFromPlayer(Player));
if(GroupRankExperienceData.Success) then
ExperienceValue.Value = GroupRankExperienceData.Response.ExperienceStartingAmount;
end
end
end
end
end
(This is the function the module uses to initiate when a player joins while below is when the player leaves; or used when bindtoclose.)
function CharacterConstructor:SetAsync(Player : {[string] : "Player"})
if not(Player or Player:IsA("Player")) then return { Success = false }; end;
local ClientCacheAsync = {
UserCurrentExperience = 0;
UserCurrentDiamoinds = 0;
UserCoreUtilities = {
UserGender = "Unknown",
UserPronouns = "Unknown/Unknown",
UserUtilities = {
UserArrestUtility = { UserArrested = false, UserArrestedLength = 0, UserArrestedReason = "", UserArrestedFrom = "" };
UserBanUtility = { Enabled = false, UserBanLength = 0, UserBanModerator = "", UserBanProvidedReason = "" };
NecessityBarsUtility = { Hunger = 100, Toilet = 100 };
};
};
};
local ClientLeaderStatFolder = Player:FindFirstChild("leaderstats");
if(ClientLeaderStatFolder and Player:FindFirstChild("ClientConfigs")) then
if(ClientLeaderStatFolder:FindFirstChild("Experience")) then ClientCacheAsync.UserCurrentExperience = ClientLeaderStatFolder.Experience.Value; end;
local HungerValue, ToiletValue = self.Plugins.Settings:GetConfigValue(Player, "Hunger"), self.Plugins.Settings:GetConfigValue(Player, "Toilet");
if(HungerValue.Success and ToiletValue.Success) then
ClientCacheAsync.UserCoreUtilities.UserUtilities.NecessityBarsUtility.Hunger = HungerValue.Response;
ClientCacheAsync.UserCoreUtilities.UserUtilities.NecessityBarsUtility.Toilet = ToiletValue.Response;
end
local GenderValue, PronounsValue = self.Plugins.Settings:GetConfigValue(Player, "GroupGender"), self.Plugins.Settings:GetConfigValue(Player, "GroupPronouns");
if(GenderValue.Success and PronounsValue.Success) then
ClientCacheAsync.UserCoreUtilities.UserGender = GenderValue.Response;
ClientCacheAsync.UserCoreUtilities.UserPronouns = PronounsValue.Response;
end
end
if(ArrestConstructor:IsPlayerArrested(Player)) then
local ClientArrestConfigs = ArrestConstructor:GetArrestData(Player);
if(ClientArrestConfigs.Success) then
ClientCacheAsync.UserCoreUtilities.UserUtilities.UserArrestUtility.UserArrested = true;
ClientCacheAsync.UserCoreUtilities.UserUtilities.UserArrestUtility.UserArrestedFrom = ClientArrestConfigs.Response.ArrestModerator;
ClientCacheAsync.UserCoreUtilities.UserUtilities.UserArrestUtility.UserArrestedReason = ClientArrestConfigs.Response.ArrestReason;
ClientCacheAsync.UserCoreUtilities.UserUtilities.UserArrestUtility.UserArrestedLength = ClientArrestConfigs.Response.ArrestTimeLength;
end
end;
game:GetService("DataStoreService"):GetDataStore("PublicDataStore"):UpdateAsync(Player.UserId, function(DataCache)
return ClientCacheAsync
end);
pcall(function() self.CurrentPlayers[Player.UserId] = nil; end);
end