Roblox DataStores reverting to old data for a few minutes and switches to new data

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.

2 Likes

Which function is caching? I’ve heard that UpdateAsync has some weird caching issues sometimes

1 Like

So far it’s being used like this one a player leaves:

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 };
		};
	};
};

DataStoreConstructor:UpdateAsync("PublicDataStore", Player.UserId, ClientCacheAsync);

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.

2 Likes

That’s not very ideal.

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. :sweat_smile:

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 :slight_smile:

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

Can you try using UpdateAsync itself, without the caching you’ve implemented?

I suspect it’s something to do with your caching.

Yeah of course, let me go ahead and try that without the module real quick! I’ll inform you once more if anything changes!

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?

Can I see your code? I can’t think of why it’d fail for you.

Sure!

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
1 Like

That is… a lot of code, and now I’m lost.
I did test out UpdateAsync myself though, and everything’s checked out for me so far.

And by the rough looks of it, your UpdateAsync implementation seems to be correct, so I can’t really tell where exactly you went wrong.

Yeah apologies for the large code, it’s all just clustered into this script.

I’ve tried looking into it, but I’m not quite sure of the issue either.