How to make smth shared across all the servers, and dynamically updating?

Hello guys.

I want to make something like daily deals in shop, with available items reroll every 3-6 hours. While I know how I can do this on single server, IDK how to apply that on all possible game servers. When I tried to do this, I have resulted in complete mess. Can someone guide me towards right direction about how this can be created?

2 Likes

You might be able to use a memory store for something like this.

1 Like

But how I can make it so all servers in game don’t turn saved in it value in complete mess?

1 Like

Have one server do it and control it with something like a Boolean. Have other servers read the Boolean and update if it is false.

1 Like

I have tried to update it via one server too. But I had run into following problems:

  1. Server which handled everything died (no players on it anymore)
  2. After tries to fix problem above I have managed to make it so each server tried to assign himself as host, which resulted in errors and mess.
1 Like

Instead of using memory stores as suggested by @12345koip, I would recommend using MessagingService. I think it’s way easier, though I could be wrong as I have never touched memory stores before.

1 Like

Messaging service allow only 20 connections. But it will be almost guaranteed that game will have much more than 20 servers, so this’s not a good way.

1 Like

Where did you get that information from?

20 players and 20 servers are two different things.

Read 2 paragraphs below.

I think you have misunderstood the paragraph. It means it can only display up to “20 servers” if you want to create a list that shows all currently live servers in your game. That is not part of the limitation. I believe the 20 servers limit is based on the user’s preference.

The only problem with MessagingService is that it would not account for new servers created between refreshes; they would have none of the info sent on the message broadcast.

A stretch could be a web-based system and then use of HttpService.

1 Like

For brevity, I’m going to assume each server will have the same rerolls (meaning servers will share the same reroll item pool).

In that case:

  • Delegate to a single server the job of rerolling the items. Save the JobId of that server to a MemoryStore.

  • That server will be in charge of an item pool; it’ll reroll items and save it into another entry within a MemoryStore.

  • Ideally, you want to store up to 3 entries (3 separate item rerolls).* If there’s less than 3 entries, the master server will keep rerolling until there’s 3.

  • If the server would shut down, it’ll use game:BindToClose() to unassign that JobId within that MemoryStore.

  • Anytime the 3 hours time has elapsed, every server will check the MemoryStore to see if the JobId still exists, on top of getting the results of the reroll pool.

  • If it doesn’t, every server in existence will be in a “race” to become the new master server, where they’ll each try to overwrite each other’s JobId within the MemoryStore.
    HOWEVER, they won’t insert new entries. Their job in this case is to just race each other to become the master server.#

  • Repeat step 5 to 6.

  • When a server is first created, it checks whether a JobId exists within a MemoryStore while pulling results from the pool.
    If it does, do nothing.
    Else, see step 1.

*
The reason why you want to store multiple entries is to account for cases where the master server shuts down, and every server is now trying to race to become the new master server.

This effectively becomes a cache of rerolls, and will act as a reliable backup in case a new reroll isn’t possible.

#
If we don’t do this, you’ll have a very bad race condition where every new up-and-coming master server will now try to reroll, causing a huge desync.


I understand this is a lot to digest, but this is the amount of effort you’ll need to put in to make this system as fault-tolerant as possible.

And as others have pointed out, MessagingService has its shortcomings especially with newly created servers, which is why I made the effort not to use them here.

Hope I pointed you in the right direction.

1 Like

I’m feeling a little nice today. Here’s a bit of code I wrote a while ago when I realized the need for a master server.

local security = {}

--[[ security.assignAsMaster()
	[ YIELDING ]
	[ MUTATIVE ]

	PARAMETERS: 
	- nil.

	RETURNS:
	- <boolean?> isMaster: Whether the current server is master or not. Returns nil if the operation fails.
	- <string?> jobId: The ID of the master server. Returns nil if the operation fails.

	FOOTNOTES:
	- This function is protected, and yields until MemoryStore returns a result.
	- It also stores the server's JobId within the "masterserverid" key in the "securitycfg" hashmap, using MemoryStoreService, for up to 45 days.
	- It also binds a function that unassigns the JobId within the hashmap when the server closes.
	- Both returns will be nil if the MemoryStore operation fails. 

	Attempts to assign the current server as the master server, if none already exists. If the operation succeeds, returns both whether the current server is master or not, and the master server's ID. ]] 
function security.assignAsMaster(): (boolean?, string?)
	local id = game.JobId
	
	local memorystore = _MEMORYSTORESERVICE:GetHashMap("securitycfg")
	local ok, e = pcall(function()
		return memorystore:UpdateAsync("masterserverid", function(old)
			if old == nil or old == "" then return id
			else return old
			end
		end, 3888000)
	end)
	
	if ok == true then
		local isMaster = id == e
		if isMaster == true then
			game:BindToClose(function()
				memorystore:RemoveAsync("masterserverid")
			end)
		end

		return isMaster, e
		
	else
		warn(`An exception has occurred while running security.assignAsMaster. \n{e}`)
		return
		
	end
end

return security
1 Like

I won’t give you any code, but here’s the basic rundown of how this is achieved with a reliable failsafe.

  • Use a MemoryStore to hold the data.

  • Have one server, your “parent” server, take initial control over relaying/updating/accepting data, and retain that server’s JobId in the MemoryStore.

  • Have other servers, your “children” servers, send a request (basically a keepalive request but checking if it’s alive) using MessagingService to the parent server every X seconds/minutes, and make sure you store the tick time it was sent as well as the JobId of the server that sent it, and use that to time your requests properly.

  • Note: You can isolate the parent server (so that it is the only server that attempts to respond) by only subscribing the parent server to your MessagingService topic, and only publishing to that topic with children servers.

  • Ensure the parent server has a means to respond to the request, and a reasonable timeout set in place so the request does not sit forever.

  • IF the parent server is dead or the latency has forced your timeout to break the wait, you should have the child server that sent the request take up the role of parent server, as this greatly simplifies the logic necessary. Just ensure you update your MemoryStore parameters.

Also, to wrap this up, you might introduce issues if you do not somehow ensure that there are different server groups per region simply due to extra latency that is awkward to compensate for in Roblox.

1 Like

Thanks, after some time I have finished shop system using your tips, but sadly IDK how I’ll test in in cross-server cuz IDK how to run multiple servers with my single account only.
At least, it works good with server-mastering, server-asker and server becoming master when master is died.

If the reroll works on your current server, it’ll most likely also work on all other servers - assuming you’re using UpdateAsync() on your MemoryStore to update the reroll pool - and that you’ve made sure every server reads that pool.

The intent of my solution wasn’t to just botch something and forget about it.
It also addresses a lot of potential issues with the system, so that when something does go wrong, 90% of the time it’s Roblox’s fault, and all you have to do is sit back and hang tight.

And on that note, please don’t use GetAsync and SetAsync. I won’t explain why or this reply will be an essay, but just know it’s entirely why I confidently posted that snippet of code.

I’m also not going to comment on this any further because of potential TOS, but there are tools that let you launch multiple Roblox clients. I used them myself to test out stuff.

Do this at your own risk though.

1 Like

After reading that, I have started to worry a bit, cuz I changed code partially to work normally, because I had needs to not only update items but also get data from Memory store, and I have used some GetAsync to get item rolls data, aswell as SetAsync to set item rolls data from master server.

code for all that changes:

local RS = game:GetService("ReplicatedStorage")
local SS = game:GetService("ServerStorage")
local Remotes = RS:WaitForChild("Remotes")
local Bindables = SS:WaitForChild("Bindables")
local MSS = game:GetService("MemoryStoreService")
local MemoryStore = MSS:GetHashMap("ItemShopRoll")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")

local Libraries = {
	ItemPrice = require(RS.Libraries:WaitForChild("ItemPriceLibrary")),
	ItemRoll = require(script:WaitForChild("ItemRollChance")),
}

local RefreshTime = 30 --10800
local LatencyTime = 5 --180

local Cleanup = false

local ModuleMeta = {}
ModuleMeta.__index = ModuleMeta

function ModuleMeta.Initialize()
	local self = setmetatable({}, ModuleMeta)
	self.Data = {}
	self.Viewers = {}
	
	if Cleanup then
		local Retries = 10
		local Success, Error
		repeat 
			Success, Error = pcall(function()
				return MemoryStore:SetAsync("MasterServer", "", 86400--[[3888000]])
			end)
		until Success or Retries <= 0
		if not Success then
			warn(Error)
		end
		Retries = 10
		repeat 
			Success, Error = pcall(function()
				return MemoryStore:SetAsync("Rolls", "", 86400--[[3888000]])
			end)
		until Success or Retries <= 0
		if not Success then
			warn(Error)
		end
	end
	self:AssignMaster()
	return self
end

function ModuleMeta:RefreshStock(Stock)
	print(Stock)
	self.Data = {}
	
	for i = 1, #Stock, 1 do
		local Item = Stock[i]
		local Price, Currency = Libraries.ItemPrice.GetItemPrice(Item, "Buy")
		if Price then
			table.insert(self.Data, {Name = Item, Price = Price, Currency = Currency})
		end
	end
	for i = 1, #self.Viewers, 1 do
		Remotes.ShopEvent:FireClient(self.Viewers[i], "Refresh", self.Data)
	end
end

function ModuleMeta:GenerateRolls()
	local RollData
	local Success, Error
	local Retries = 10
	repeat 
		Success, Error = pcall(function()
			RollData = MemoryStore:GetAsync("Rolls")
		end)

		if Success then
			local SelectedData
			local Data = string.len(RollData) > 0 and HttpService:JSONDecode(RollData) or {}
			for RollId = #Data, 1, -1 do
				print(RollId)
				local CurrentRoll = Data[RollId]
				if CurrentRoll.Time <= os.time() - RefreshTime then
					for i = 1, #Data, 1 do
						table.remove(Data, 1)
					end
					warn("Generating all 3 rolls cuz they are all expired.")
					for i = 1, 3, 1 do
						local NewRoll = {}
						while #NewRoll < 9 do
							local SelectedItem = Libraries.ItemRoll.GetRandomItem()
							if not table.find(NewRoll, SelectedItem) then
								table.insert(NewRoll, SelectedItem)
							end
						end
						local RollTime = math.floor(os.time() / RefreshTime + i-1) * RefreshTime
						table.insert(Data, {Time = RollTime, Items = NewRoll})
					end
					SelectedData = Data[1]
					MemoryStore:SetAsync("Rolls", HttpService:JSONEncode(Data), 86400)
					break
				elseif CurrentRoll.Time <= os.time() then
					SelectedData = CurrentRoll
					for i = 1, RollId-1, 1 do
						table.remove(Data, 1)
					end
					warn("Generating " .. tostring(RollId-1) .. " rolls")
					for i = 1, RollId-1, 1 do
						local NewRoll = {}
						while #NewRoll < 9 do
							local SelectedItem = Libraries.ItemRoll.GetRandomItem()
							if not table.find(NewRoll, SelectedItem) then
								table.insert(NewRoll, SelectedItem)
							end
						end
						local RollTime = math.floor(os.time() / RefreshTime + #Data) * RefreshTime
						table.insert(Data, {Time = RollTime, Items = NewRoll})
					end
					MemoryStore:SetAsync("Rolls", HttpService:JSONEncode(Data), 86400)
					break
				end
			end
			if not SelectedData then
				warn("Generating all 3 rolls cuz data not exists")
				for i = 1, 3, 1 do
					local NewRoll = {}
					while #NewRoll < 9 do
						local SelectedItem = Libraries.ItemRoll.GetRandomItem()
						if not table.find(NewRoll, SelectedItem) then
							table.insert(NewRoll, SelectedItem)
						end
					end
					local RollTime = math.floor(os.time() / RefreshTime + i-1) * RefreshTime
					table.insert(Data, {Time = RollTime, Items = NewRoll})
				end
				SelectedData = Data[1]
				MemoryStore:SetAsync("Rolls", HttpService:JSONEncode(Data), 86400)
			end
			--self:RefreshStock(SelectedData.Items)
			local TimeToUpdate = self:CheckRoll()
			coroutine.wrap(function()
				task.wait(TimeToUpdate)
				if self.Closing then return end
				self:GenerateRolls()
			end)()
		end
	until Success or Retries <= 0
	warn(Error)
end

function ModuleMeta:AssignMaster()
	local Id = RunService:IsStudio() and "StudioTestServer" or game.JobId
	warn(Id)
	local Success, Error
	local Retries = 10
	local WasMaster = false
	repeat 
		Success, Error = pcall(function()
			return MemoryStore:UpdateAsync("MasterServer", function(OldData)
				print("Master server: " .. tostring(OldData))
				if OldData == nil or OldData == "" then
					return Id
				else
					WasMaster = (Id == OldData)
					return OldData
				end
			end, 86400--[[3888000]])
		end)

		if Success then
			local IsMaster = Id == Error
			print(WasMaster, IsMaster)
			if IsMaster and WasMaster then
				if not self.MasterDeath then
					self.MasterDeath = game:BindToClose(function()
						MemoryStore:RemoveAsync("MasterServer")
					end)
				end
				local TimeToUpdate = self:CheckRoll()
				coroutine.wrap(function()
					task.wait(TimeToUpdate)
					if self.Closing then return end
					self:GenerateRolls()
				end)()
			elseif IsMaster then
				if not self.MasterDeath then
					self.MasterDeath = game:BindToClose(function()
						MemoryStore:RemoveAsync("MasterServer")
					end)
				end
				local TimeToUpdate = self:CheckRoll()
				coroutine.wrap(function()
					task.wait(TimeToUpdate)
					if self.Closing then return end
					self:AssignMaster()
				end)()
			else
				local TimeToUpdate = self:CheckRoll()
				coroutine.wrap(function()
					task.wait(TimeToUpdate)
					if self.Closing then return end
					self:AssignMaster()
				end)()
			end
		end
	until Success or Retries <= 0
	if not Success then
		warn(Error)
	end
	return Id == Error, Error
end

function ModuleMeta:CheckRoll()
	local RollData
	local UpdateTime
	local Success, Error
	local Retries = 10
	repeat 
		Success, Error = pcall(function()
			RollData = MemoryStore:GetAsync("Rolls")
		end)

		if Success then
			local SelectedData
			local Data = string.len(RollData) > 0 and HttpService:JSONDecode(RollData) or {}
			print(Data)
			for RollId = #Data, 1, -1 do
				local CurrentRoll = Data[RollId]
				if CurrentRoll.Time <= os.time() then
					SelectedData = CurrentRoll
					UpdateTime = RefreshTime - (os.time() - CurrentRoll.Time)
					UpdateTime = UpdateTime > LatencyTime and UpdateTime or UpdateTime + RefreshTime
					break
				end
			end
			if not SelectedData then
				warn("FATAL ERROR: NO MATCHING ROLLS FOUND. USING OLD ONES")
				SelectedData = {}
				SelectedData.Items = self.Items or {}
			end
			self:RefreshStock(SelectedData.Items)
			print(UpdateTime)
			print(SelectedData.Time, os.time())
			return UpdateTime
		else
			warn(Error)
		end
		Retries -= 1
	until Success or Retries <= 0
	return
end

return ModuleMeta

(Sorry if this code is mess)

Hang on tight then, this will get technical.

The issue with GetAsync and SetAsync is that they’re both wolf in sheep’s clothing - innocent at first, until you stick around.


Take this code for example:

-- server a
-- run this first (?)
memorystorehashmap:SetAsync("key")

-- server b
-- then this
print(memorystorehashmap:GetAsync("key"))

See that question mark?

SetAsync will always take just a little bit longer to finish up than GetAsync - which means that you may end up getting an old value even though it looked like you called SetAsync first.

You will never know which one will finish first - not even if you read the code.

And if you saved that data that you’ve just gotten, you’ve just lost whatever data you tried saving earlier before.
This is formally known as a race condition.

It also doesn’t help that both of those calls are HTTP, so ping and latency both exacerbate this issue.
It’s even worse when you consider that Get and Set are NOT ordered unlike Update.

If I used Get and Set for .assignAsMaster(), what will realistically happen is that every server will really overwrite each other with their own server IDs, and it’ll be a long while before you get a proper master server, if it can even happen at all.


Update circumvents all that. Since they’re:

  • ordered (yes, even across servers);
    it means the first server that tried assigning itself will be the master server, and every subsequent server that tries will be denied.

  • blocking;
    it means that subsequent Updates will halt themselves until the first Update is complete, so you’ll always get the most recent data.

Some code to get you started:

-- get data
local data
memorystore:UpdateAsync("key", function(old)
    data = old
    return
end)
print(data) -- whatever saved data here

-- set data
local data = {} -- whatever data to save here
memorystore:UpdateAsync("key", function(_)
    return data
end)

Now, you can still use Get and Set if you want to do quick and dirty work, but if you’re expecting to change data across servers (and have them read it especially), you really shouldn’t use Get and Set.


Also, this is also a prevalent issue with datastores, since they use more or less the same architecture, which is why I’ve completely given up on Get and Set.

Whatever I’ve shared here can also apply to datastores, so keep these details in mind if that’s what you’re working on next.

1 Like

Then, if UpdateAsync blocks all calls to certain key, this means that it can’t be used to get Rolls data, cuz when servers will try to get it, they will have denied access because master server updates rolls?
(I understood master server problem you described, thanks for that)
Situation about item rolls:
I think this situation is covered by 3 separate rolls datas stored - servers look for a roll with closest time which is less than current time, and if server deletes first roll, this means that it’s expired and all other ones will be shifted by one, and new third roll is crated, and all servers will base their data off either 2-nd or 1-st roll (for them) based off time they made call, but this all will result in getting same value.
So, in conclusion, I can say that using Get/Set for rolls is safe, but not for things like master server selecting.

Then, if UpdateAsync blocks all calls to certain key, this means that it can’t be used to get Rolls data, cuz when servers will try to get it, they will have denied access because master server updates rolls?

Just to clarify, an existing Update call only blocks future Update calls until that current one is complete. It doesn’t deny them.

This is one of those cases where I can safely recommend Get; but you need to make sure you generated your rolls ahead of time.
In other words, don’t generate rolls just before your servers are expected to fetch them.

servers look for a roll with closest time which is less than current time, and if server deletes first roll, this means that it’s expired and all other ones will be shifted by one, and new third roll is crated, and all servers will base their data off either 2-nd or 1-st roll (for them) based off time they made call, but this all will result in getting same value.

Sounds about right. You nailed it.

So, in conclusion, I can say that using Get/Set for rolls is safe, but not for things like master server selecting.

Correct. You’re on the right track.

1 Like