Master Server // Cross Server MSS

This script allows for one server to take control as the master server, it also allows for cross-server events or ‘tasks’


in the example all non-master servers will send a print task to the master server

ServerScriptService - Script
if game:GetService("RunService"):IsStudio() then warn("[Studio]") repeat wait(60) until false end
local MemStore = game:GetService("MemoryStoreService")
local HttpService = game:GetService("HttpService")
local DSS = game:GetService("DataStoreService")
local ServerIndex = MemStore:GetSortedMap("ServerIndex")
local LocalTaskQueue = MemStore:GetQueue(game.JobId)
local Debugging = true



local function Shutdown(Message)
	game.Players.PlayerAdded:Connect(function(Player)
		Player:Kick(Message or "Server shutting down.")
	end)
	for _, Player in pairs(game.Players:GetPlayers()) do
		Player:Kick(Message or "Server shutting down.")
	end
end


local function DebugPrint(Message)
	if not Debugging then return end
	print(Message)
end


local function ProcessTask(Task)
	DebugPrint("Processing Task: "..Task.Name)
	Task.Name = Task.Name:lower()
	
	
	if Task.Name == "shutdown" then
		if not Task.Data then Shutdown() return end
		if type(Task.Data) ~= "string" then Shutdown() return end
		Shutdown(Task.Data)
	elseif Task.Name == "print" then
		if not Task.Data then return end
		if type(Task.Data) ~= "string" then return end
		print(Task.Data)
	end
end




local Tasks = {}
local IS_MASTER = false
local function AddFunctions(Server)
	if Server.JobId == game.JobId then
		Server["IsLocal"] = true
		Server["Shutdown"] = function()
			Shutdown()
		end
		Server["Send"] = function(Task)
			Task.Sender = "Local"
			table.insert(Tasks, Task)
		end
	else
		Server["IsLocal"] = false
		Server["Send"] = function(TaskObj, Settings)
			if not TaskObj then return false end
			TaskObj.Sender = game.JobId
			if not Settings then Settings = {} end
			if Settings["Tries"] == nil then Settings["Tries"] = 3 end
			if Settings["Interval"] == nil then Settings["Interval"] = 3 end
			if Settings["Expiration"] == nil then Settings["Expiration"] = 60 end
			local s, e
			repeat
				s, e = pcall(function()
					MemStore:GetQueue(Server.JobId):AddAsync(HttpService:JSONEncode(TaskObj), Settings.Expiration)
					if not s then Settings.Tries -= 1 wait(Settings.Interval) end
				end)
			until
				s or Settings.Tries == 0
			return s
		end
		Server.Shutdown = function(Message)
		local Task = {
			["Name"] = "Shutdown",
			["Data"] = Message
		}
			Server.Send(Task)
		end
	end
end


local Servers = {}
local RunningTasks = false
local function GetServers()
	local lServers = {}
	local startFrom = nil
	while true do
		local items = ServerIndex:GetRangeAsync(Enum.SortDirection.Ascending, 100, startFrom)
		for _, item in ipairs(items) do
			item.value = HttpService:JSONDecode(item.value)
			AddFunctions(item.value)
			lServers[item.key] = item.value
		end
		if #items < 100 then break end
		startFrom = items[#items].key
	end
	local Longest = nil
	for JobId, Server in pairs(lServers) do
		if Longest == nil then
			Longest = Server
			continue
		end
		if Longest.Runtime < Server.Runtime then
			Longest = Server
		end
	end
	IS_MASTER = Longest.JobId == game.JobId
	Servers = lServers
	return Servers
end


local ConnectionTicks = {}
game.Players.PlayerAdded:Connect(function(Player)
	ConnectionTicks[Player.UserId] = tick()
end)
game.Players.PlayerRemoving:Connect(function(Player)
	ConnectionTicks[Player.UserId] = nil
end)


local function GetPlayers()
	local Plyrs = {}
	for _, Player in pairs(game:GetService("Players"):GetPlayers()) do
		table.insert(Plyrs, {
			["UserId"] = Player.UserId,
			["Name"] = Player.Name,
			["Ping"] = math.round(Player:GetNetworkPing()*1000),
			["Connection"] = math.round(tick() - (ConnectionTicks[Player.UserId] or tick())),
			["Age"] = Player.AccountAge,
		})
	end
	return Plyrs
end


local FPST = 0
local AvgServerFPS = 0
local FPSI = 0
game:GetService("RunService").Heartbeat:Connect(function(step)
	FPST += 1 / step
	FPSI += 1
	if FPSI == 10 then
		AvgServerFPS = FPST/10
		FPSI = 0
		FPST = 0
	end
end)


local function RegisterSelf()
	local ServerObj = {
		["JobId"] = game.JobId,
		["Players"] = GetPlayers(),
		["Runtime"] = math.round(time()),
		["FPS"] = math.round(AvgServerFPS),
		["Master"] = IS_MASTER,
	}
	ServerIndex:SetAsync(game.JobId, HttpService:JSONEncode(ServerObj), 60)
	DebugPrint("Registered self as: "..HttpService:JSONEncode(ServerObj))
end


game:BindToClose(function()
	local s, e
	repeat
		s, e = pcall(function()
			ServerIndex:RemoveAsync(game.JobId)
		end)
		if not s then wait(3) end
	until
		s
end)


local LastTicks = {
	["RegisterSelf"] = tick()-10,
	["GetServers"] = tick()-10,
}
while wait(1) do
	if tick() - LastTicks["RegisterSelf"] >= 10 then
		LastTicks["RegisterSelf"] = tick()
		RegisterSelf()
	end
	if tick() - LastTicks["GetServers"] >= 10 then
		LastTicks["GetServers"] = tick()
		local Master = nil
		for JobId, Server in pairs(GetServers()) do
			DebugPrint("JobId: "..Server.JobId..", Players: "..#Server.Players..", Master: "..(Server.Master and "Y" or "N"))
			if not Master and Server.Master then Master = Server end
		end
		if Master then
			if not Master.IsLocal then
				local Task = {
					["Name"] = "Print",
					["Data"] = "Task from: "..game.JobId
				}
				Master.Send(Task)
				DebugPrint("Sent task to master")
			end
		end
	end
	pcall(function()
		local items, id = LocalTaskQueue:ReadAsync(1, false, 0)
		if #items > 0 then
			local Task = HttpService:JSONDecode(items[1])
			if Task then
				table.insert(Tasks, Task)
			end
			LocalTaskQueue:RemoveAsync(id)
		end
	end)
	for _, Task in pairs(Tasks) do
		ProcessTask(Task)
	end
end
Usage Notes
  • Tasks are processed in the function ‘ProcessTask’, here is where you would add your own custom tasks.

  • The eldest server will be the master server.

  • Update frequency can be changed in the main loop near the end of the script

  • The server ‘obj’ contains .Players, .IsLocal, .Send(), .Shutdown(), .JobId, .FPS, .Master and .Runtime

  • The player ‘obj’ contains .Age, .UserId, .Name, .Ping and .Connection

  • Server.FPS is the server’s average frames per second.

  • Player.Connection is how long the player has been connected to the server (seconds)

Send(TaskObj, SettingsObj):

local TaskObj = {
    ["Name"] = "TaskName",
    ["Data"] = nil
}

local SettingsObj = {
    ["Tries"] = 0,
    ["Interval"] = 0,
    ["Expiration"] = 0,
}
22 Likes

What would the function of this be?
Why would we want to designate a “Master Server”?

1 Like

I have been thinking of how to implement a system like this for matchmaking, and this seems like a decent starting point for that. My question, however: there appears to be no system to deal with the case where a server is unable to respond to a request. This could happen in a situation where a server for whatever reason is running at full capacity (memory leaks, etc.), and this could cause a service disruption in the audience.

If you are simply doing this for fun, I think a fun/good idea would be to implement a timeout and a multi-master system where servers are able to divide themselves among multiple master servers when server count reaches a certain capacity to reduce overall load. Not sure if the second part is actually needed, but it could be useful in the case of intensive algorithms or when dealing with a large amount of data, such as the case of a matchmaking system.

2 Likes

Isn’t ReservedServer and TeleportToPrivateServer() not good for matchmaking? Altrough maybe not good unless its PvE type of game, and this could be useful for pvp matchmaking and letting other players find that server easily

2 Likes

This is actually PERFECT for a webhook rate limit handler I want to create. The issue with having a queue across all servers is that the amount in the queue would take time to update and could create inconsistencies. Thank you so much for creating this!

1 Like

I need a bit of help :sweat_smile:, I didn’t get how can I add custom tasks and execute them when necessary. Could anyone make a detailed description or send a video please?

This could be used as a way to interact with roblox servers via your roblox scripts. For example, updating a datastore not aimed at a player, but aimed at storing information every server will use. It may not be efficient for EVERY server to do this, since you may hit limits very quickly. A ‘master server’ will be the one server that does all this work!

Will this work with ideas like:

Change Map Colors, Global Events (like mining X Amount of Rocks in total Game will give x2 Gold, etc.)
or
ADD NPC/Mob to all of them at the same time
etc.

Hey! Nice resource. I just have one a question.

What’s the difference between using this and MessagingService?

I just want to clear the confusion I have :slight_smile:

1 Like