Memory Store Service Tutorial

Memory Store Service Tutorial

Introduction

Today, I'll be showing you how to use Memory store service. This service allows for easy cross server communication. With low latency, almost like datastores. It makes stuff like queues easier. This data is not persistent with a hardcoded expiration date. Data will also stay, even when all servers of a game shut down. Data will only be removed after the expiration time is met. For this tutorial, i'll show you how to use Queues, and , let's get right into it!

Queues

Explanation

First, let's go over about queues. Queues allow you to send data across servers in a queue like system. Each item has a priority. The higher the priority, the higher up the queue the data will be. Data here can be read using the `ReadAsync()` function.

Scripting

Setting up

For this example, we'll be scripting a cross-server notification anyone clicks a button.

First, we need to make our button. To do that, create a new part

Next, hover over the part in explorer(view → explorer) and click the + and insert a click detector

image

Once you make the button, insert a server script inside the button. Woo hoo!

coding

First, let’s set up our Queue. To do this, we can use the :GetQueue() function. This function will create/get the specified queue name.

local memoryStoreService = game:GetService("MemoryStoreService")

local clicksQueue = memoryStoreService:GetQueue("Clicks") -- "clicks" is the queue name

After we set up the queue, lets set up our variables.

local memoryStoreService = game:GetService("MemoryStoreService")

local clicksQueue = memoryStoreService:GetQueue("Clicks") -- "clicks" is the queue name

local btn = script.Parent -- button

local detector = btn:WaitForChild("ClickDetector") -- click detector

local function logClick(plr:Player) -- function which will log clicks
	
end

detector.MouseClick:Connect(logClick) -- connect our function.

Now that we’ve set up the variables, we’re ready to script the actual stuff!

In order to add data to a queue, you use 1 function, that is :AddAsync() function. This takes 3 arguments. value, the queue item’s value, expiration date, which is how long the data will be there in the queue(this is in seconds.), and finally the priority, which is the priority of the item in the queue.

So let’s add some data to our queue. To do this, we can use the following code:

KEEP IN MIND, ANYTHING YOU DO IN MEMORYSTORESERVICE SHOULD BE BACKED WITH A PCALL, AS THE REQUEST COULD FAIL!

local memoryStoreService = game:GetService("MemoryStoreService")

local clicksQueue = memoryStoreService:GetQueue("Clicks") -- "clicks" is the queue name

local btn = script.Parent -- button

local detector = btn:WaitForChild("ClickDetector") -- click detector

local function logClick(plr:Player) -- function which will log clicks
	local success, err  = pcall(function()
		local value = string.format("%s clicked the BUTTTON", plr.Name) -- add item to queue
		clicksQueue:AddAsync(value, 0.5, 0)
	end)
end

detector.MouseClick:Connect(logClick) -- connect our function.

Code explanation:

Code explanation

the value is the value that is added to the queue. This can be a table, string anything. As long as it’s a UTF character. For info on how to save non UTF characters you can check out this post made by the coolest forummer, @geometricalc2123.

The 2nd arg(0.5) is how long the data will be in the queue. For this example, this data will be in the queue for half a second. The max for this value is 30 days(aka: 2,592,000 seconds.)

Finally, the 3rd arg(0) is the priority. For this example, I set it to 0. You can make it any number you want.

Next, we’ll need a way to read this data. We can do this via the :ReadAsync() function. This will return a table with our queue data.

As of writing this, there is no event for detected when data is added to queues. The only way right now is via a while loop. Here is the code bellow. If you dont understand something, read the code explanation OR the comment on that line.

local memoryStoreService = game:GetService("MemoryStoreService")
local runService = game:GetService("RunService")

local clicksQueue = memoryStoreService:GetQueue("Clicks") -- "clicks" is the queue name

local btn = script.Parent -- button

local detector = btn:WaitForChild("ClickDetector") -- click detector

local function readQueueData()
	local success, queueItems = pcall(function() -- wrap in pcall to prevent app from crashing
		return clicksQueue:ReadAsync(20, false, 0) -- read 20 queue items.
	end)
	
	if success and typeof(queueItems) == "table" then-- check if request succeeded and if the data is a table
		for _, data in ipairs(queueItems) do
			print(data)
		end
	elseif typeof(queueItems) == "string" then -- check if the queue items is a string. If it is, it'll indicate that it is an error.
		warn("failed to fetch queue data for reason of: %s", queueItems)		
	end
end

local function logClick(plr:Player) -- function which will log clicks
	local success, err  = pcall(function()
		local value = string.format("%s clicked the BUTTTON", plr.Name) -- the value of the queue item
		clicksQueue:AddAsync(value, 0.5, 0)
	end)
	if not success then
		warn("something went wrong while adding item to queue. For reason of: %s", err)
	end
end

detector.MouseClick:Connect(logClick) -- connect our function.

while true do
	readQueueData()
	task.wait(0.1)
end

Code explanation:

Code explanation

First, we wrap the ReadAsync request in a pcall. Which prevents our code from crashing if the request fails. In that same pcall, we’ll return the first 20 items in the queue. Keep in mind, the max for reading a queue in one request is 100.

The first arg(20) is the amount of item’s we’d like to get.

The 2nd arg(false) is an AllOrNothing arg. This controls how the queue returns items. If the arg is true and the queue has less items than the read amount(first arg), nothing will return. It has to have more or the same amount as the read amount.

With false, it’ll return data even if the queue has less items than the read amount.

Next, we’ll check if the code succeeded and if the queueItems is a table.

Next, we’ll loop through the queueItems and print the data.

The elseif is to check if the queue items is a string. This indicates that an error occurred. The reason why we check this is because the queue may return nil, so the elseif would fire resulting in the warn falsely firing.

Now we can test our code in studio and see if it works!

Once you click the button you should see '[USERNAME] clicked the BUTTON!" In the output(view → output)
image

testing

now that we’ve checked that our code works in studio. Let’s see if the code is truly “cross server.” To test this, we can go into multiple accounts and play our game to test. KEEP IN MIND, YOUR STUDIO TESTING WILL NOT APPEAR IN REAL SERVERS! SO TEST IN THE SITE AND NOT STUDIO!!

Once you click the button on your alt. Go back to your main and press F9(or type ‘/console’ in the chat) and click on “Server.”

Once you do, you should see ur alts clicks on ur mains server

If you see something like this, you did it correctly! If not, then repeat the last few steps.
image

Congratz! You just created a cross-server button logger using Memory Store Service!

Here’s a cake to celebrate!

Sorted Maps

Explanation

I'll start off with explaining sorted maps.

Sorted maps allows for easy cross-server communication in a datastore like structure. With keys and values. Sorted maps have all the functions of a datastore. Which is good for data that’s meant to stay for a small amount of time. If you know how to use datastores, you should have little to no learning curve! Without further ado, let’s go right into it!

coding

For this example, I’ll be showing you how to make a cross-server Player join logger. This wont require any setting up as we can do this with 1 script.

First, let’s define our variables.

local players = game:GetService("Players")
local memoryStoreService = game:GetService("MemoryStoreService")

local joinsMap = memoryStoreService:GetSortedMap("Joins") -- creates/gets a sorted map.

local joinMsg = "%s has joined the game!" -- the message that we'll display to the output

local function logJoin(plr:Player)
	
end

players.PlayerAdded:Connect(logJoin) -- connet function

Now that we’ve done that, we’ll need to add data to our Map. To do this, we can use the SetAsync() function. This function takes in the following: key which is how we will access the data. Value which is the value associated with the key. The last arg is the expiration. This is how long the data will exist in seconds. After the expiration is met, the data will be erased from the Map. Now let’s start scripting this!

local players = game:GetService("Players")
local memoryStoreService = game:GetService("MemoryStoreService")

local joinsMap = memoryStoreService:GetSortedMap("Joins") -- creates/gets a sorted map.

local joinMsg = "%s has joined the game!" -- the message that we'll display to the output

local function logJoin(plr:Player)
	joinsMap:SetAsync(plr.UserId, string.format(joinMsg, plr.Name), 0.5) -- add data to map
	
	print(joinsMap:GetAsync(plr.UserId)) -- get data
end

players.PlayerAdded:Connect(logJoin) -- connet function

Code Explanation:

Code Explanation

First, we need to add data to Map. To do this, we can use the :SetAsync() function.

The first argument is the key. Which in this case is the player’s userid.

The second argument is the value. Which will be the join message("[PLAYERNAME] has joined the game!"). You can set the value to numbers, strings, and tables.

Finally, the 3rd argument is the expiration. Since this data doesn’t need to be persisted for long, I set the expiration as half a second.

Next, we need to read this data. To do so, we can use the :GetAsync() function. Which takes 1 argument, the key. Which in this case is the player’s userid.

Now that we have our code, we can test.

Once you click play, you should see “[USERNAME HERE] has joined the game!”
image

Now there’s an issue with this code, it’ll specifically get the data from that 1 key when a new player joins. Which means it’s not really cross server. To fix this, we can read data using the GetRangeAsync() function. Which takes 3 args Sort Direction, which is an enum value. If the value is Descending it’ll get data from oldest to newest. If it’s Ascending it’ll get data from newest to oldest. The next argument is the amount of data you’d like to get. This will be influenced with the first arg. The least you can fetch is 1 and the most you can fetch is 200.

We will need to wrap this inside of a while loop. Because as of writing this, there is no event to detect when new data is added.

Now that we understand the args, we can write our code now! If you dont understand something read the comment on that line, OR ask below in the comments.

local players = game:GetService("Players")
local memoryStoreService = game:GetService("MemoryStoreService")

local joinsMap = memoryStoreService:GetSortedMap("Joins") -- creates/gets a sorted map.

local joinMsg = "%s has joined the game!" -- the message that we'll display to the output

local function fetchAllData()
	local success, data = pcall(function()
		return joinsMap:GetRangeAsync(Enum.SortDirection.Ascending, 200) -- return the data
	end)
	if success and typeof(data) == "table" then -- check to see if code succeeded and if data is a table
		for _, join in ipairs(data) do
			local key = join.key
			local value = join.value -- the data's value
			joinsMap:RemoveAsync(key) -- remove the data from the map
		end
	elseif typeof(data) == "string" then -- check to see if data is a string. If it is it means that an error happened
		warn(string.format('failed to fetch JoinMaps data for reason of: %s', data)) -- log error		
	end
end

local function logJoin(plr:Player)
	local success, err = pcall(function()
		joinsMap:SetAsync(plr.UserId, string.format(joinMsg, plr.Name), 0.5) -- add data to map
	end)
	if not success then warn(string.format("failed to set %.f's data for reason of: %s", plr.UserId, err)) end -- log error
end

players.PlayerAdded:Connect(logJoin) -- connet function


while true do
	fetchAllData()
	
	task.wait(0.1) -- IMPORTANT if you dont add this your code wont work!
end

Next, let’s test if this code is truly “cross-server.”

To do this, log into an alt acc and play ur game there.

Once you do go to the console and u should see ur alts name there in the console.

Outro

Thank you so much for reading! I hope this helped you :slight_smile: ! If you have any questions, feedback, or want to improve anything I said, feel free to say it below! PEACE!

49 Likes

Hello there, wonder if there’s a more efficient way of detecting new queue data than using while true do loops.

sadly, nope. As of this post, there are no built in events to detect when a new item is added to a queue. The best way is while loops. You could use RunService, but you could easily be ratelimited if you do that.

1 Like

Anyways thanks for this post, learnt something new!

2 Likes

Yoooo, I couldn’t learn memorystoreservice until you came with clear explanation. Tysm

3 Likes

np. Glad I coud help! It makes my day when someone tells me what I made help them :slight_smile:

1 Like

not sure if people are interested in this, but should I add a tutorial on how to use Sorted Maps?

  • Yes
  • No

0 voters

Alright, seems that people want a Sorted Maps tutorial. I’ll start on that now :slight_smile:

alr just finished the sorted maps part.

If I missed something make sure to point it out(didn’t have that much time to write it.)

3 Likes

This is very useful for making servers.
Thanks for this tutorial!
I really needed a better understanding of how it works. :smile:

1 Like

Hey @TheH0meLands, question about the above tutorials:

If I am understanding this correctly it seems each server would have code for “removing” new items from the Queue or Map (you use MemoryStoreQueue:ReadAsync which makes them invisible to other ReadAsync calls for the period of the invisibility timeout and MemoryStoreSortedMap:RemoveAsync which removes the data from the map)

If the Queue or Map is shared across all servers, then if one server removes new data how would other servers get this data as well if it is removed? Say for example a player joins, one server reads this and removes it, wouldn’t all other servers not find this join message after it has been removed? How can the data be shared across all servers if each server can remove items?

1 Like

oh its a roblox admin? Whyd u delete ur posts lol?

if I understand your question correctly, if you want to update the queue, you have to re-read it.

Sorry I’m not sure what you mean… let me try to explain with an example of how I’m thinking about this:

Screen Shot 2022-09-24 at 10.32.35 PM

Suppose there is a priority queue that exists between 3 servers (red, green and blue). It has the values A, B and C in it with A being at the top of the queue. Red server then calls ReadAsync.

The red server reads in value A and based on the docs it says “MemoryStoreQueue:ReadAsync makes them invisible to other ReadAsync calls”. So now A is shaded gray to mark it as invisible. Now green server calls ReadAsync on the queue.

The green server reads in value B (since A is invisible) and then makes B invisible to future ReadAsync calls. I must be misunderstanding something because I don’t see how the Memory Store works across multiple servers if they don’t all read / get updated with the same data.

For context I’m trying to use MemoryStore to store a list of Servers and the Players in them across all Servers.

honestly I dont really understand your question but if you want to keep stuff up to sync just do a while true do loop and re-read it every second or 2.

Guess I’ll try one more time, my question boils down to this:

The queue or map is shared between servers, but each server can individually remove data from them (i.e. if one server removes some data, another server will not read it as it has been removed). How can every server ensure they read all data if each server is removing data asyncronously?

It sounds like even if one server removes data from the queue or map, other servers can still read it, but this is confusing to me and isn’t explained in the documentation.

A MemoryStoreQueue should be used to store a list of tasks, so when one server reads a task other servers won’t try to also do the task.

However when creating your MemoryStoreQueue you can set an invisibility timeout, so you could set it to 0 to not have any invisibility.


For a server list with players a MemoryStoreSortedMap is more fitting.

i haven’t tried using this information in my own code but i think this is gonna solve a problem i’ve been having fore a while. thank u for this tutorial

1 Like

Actually, there is one way you could detect if a new item has been added to a queue. Messaging service could publish a message to all the servers, and then have the other servers listen for that message.

For example (Pseudocode):

On item added to queue do
    PublishMessage("ItemAddedToQueue")
end

OnItemAddedToQueueMessageRecieved do
    ProcessNewItemInQueue()
end

Would I recommend this solution? Only if you absolutely need it. Messaging service does have limitations, so you may be better off using a while loop.

More info on messaging service: MessagingService | Documentation - Roblox Creator Hub

Your tutorial for SortedMap:GetRangeAsync() is missing one bit of information. GetRangeAsync accepts up to 3 parameters, with the third one being the key you want to start the search from. This means you can actually get ALL the data (over 200) by calling GetRangeAsync and specifying the third parameter.

Below is code from my Memory Store - Sorted Map Service free models:

-- Gets range or all keys and data from sorted map data
local function GetSortedMapRange(sortedMapName: string, count: number?, exclusiveLowerBoundKey: string?)
	local sortedMap: MemoryStoreSortedMap = GetSortedMap(sortedMapName)

	-- if count is nil, then get all sorted map data
	local getAll = false
	if count == nil then
		count = 200
		getAll = true
	end


	-- Get data
	local function GetRangeAsync(sortedMap, count, exclusiveLowerBoundKey)
		return sortedMap:GetRangeAsync(Enum.SortDirection.Ascending, count, exclusiveLowerBoundKey)
	end

	local success, data = RepeatCallFunction(GetRangeAsync, sortedMap, count, exclusiveLowerBoundKey)

	-- If fail, return fail and error message
	if not success then return false, data end

	-- If data is less than 200 or reached count, then means reached end of list
	if #data < 200 or (count - #data == 0 and getAll == false) then
		return true, data
	end

	-- Reduce count OR set to nil if getting all data
	count -= #data
	if getAll then
		count = nil
	end

	-- Call itself with exclusive lower bound key (so continues sorted map list)
	local recursiveSuccess, recursiveData = GetSortedMapRange(sortedMapName, nil, data[#data].Key)
	-- If fail, return fail and error message
	if not recursiveSuccess then return false, recursiveData end

	task.desynchronize()
	-- Loop through recursive data and add to existing data
	for _, recursiveData in pairs(recursiveData) do
		table.insert(data, recursiveData)
	end
	task.synchronize()

	return true, data
end

The above uses a recursive loop to call itself and get the next 200 data, and continues until it gets all the data.

If anyone else here is interested in using a memory store script with repeat call attempts and temporary cache, then feel free to check my script here: Memory Store - Sorted Map Service (by AstrophsicaDev)