Will look into sharding/partitioning the monthly sorted map!
& Great solution! Should have thought of using a dynamic expiry haha. Great work and thanks!
Will look into sharding/partitioning the monthly sorted map!
& Great solution! Should have thought of using a dynamic expiry haha. Great work and thanks!
This is cool. I have use for it.
Some stuff you should add:
Multi-key sorting: Useful when sorting based on a single key isn’t sufficient.
Custom Sorting Callbacks: Ability for us (developers) to define a custom sorting method that could allow for more complex sorting logic outside of the realm of simple alphanumeric or numeric ordering.
Pagination support: Out-of-the-box pagination support that could help with large sets of data, especially if only a chunk / subset of data is in-use at any given time.
Range Queries: Retrieve a range of elements based on keys / sort keys. i.e fetching a range of scores from a leaderboard.
Bulk Operations: Often times, I will have a queue of things that I want to do apply to my dataset. I don’t want to apply them one by one. Especially for things that are basically the same. I would appreciate bulk-operation support features such as bulk-inserting, bulk-updating, bulk-deleting that would help me to interact with multiple elements in a single operation.
please could someone explain this to me more simply - I do not entirely understand?
Is it like having set data in a key on the game which you can load onto the player?
Thanks for the feedback. We already support a few of them
Multi-key sorting: The items in your Sorted Map will be first sorted by the sort key (if it exists) followed by the key. I’m assuming this is what you mean by multi-key sorting?
Range Queries: The GetRangeAsync
endpoint supports exactly this behavior. You can specify a bound as a Lua table with one or more of sort key and key being provided.
Hi,
I’ve done lots of testing and created a library (GitHub - arxkdev/Leaderboard: Leaderboard is an intuitive, open-source module designed to effortlessly establish and manage robust non-persistent & persistent leaderboards for your Roblox experiences.) meant for streamlining the creation of timed global leaderboards, but the limits for MemoryStoreService are just not feasible to be working with Global Leaderboards, most especially timed ones.
Sharding only works to a very limited extent where you wont have to worry about hitting the size limits, but you will have to worry about the request units you are using per minute across ALL the shards. Something I didn’t realize was that the request limit is subject to the whole experience, not individual maps. This makes working with sharding/partitioning near impossible with this type of use case. Let alone not having any way to request the budget so we cannot adjust our systems accordingly.
Request unit for all (experience) API calls: 1000 + 100 * [number of concurrent users]
Say you have 3 board types for a single stat: "Money"
Board 1: Daily - 1 Shard
Board 2: Weekly - 3 Shards
Board 3: Monthly - 5 Shards
For a total of 9 shards
1 CCU: this is 1,100 request units/minute
for all API calls.
200 CCUs: this is 21,000 request units/minute
for all API calls.
Average server size 8-20, say ours is 10.
20 Servers (200 CCUs / 10 server size), each server:
Keep in mind our threshold is 21,000 request units per minute.
You’re probably wondering, what If I wanted to add another leaderboard for say "Wins"
or what if I want to add another board type, like Hourly?
Answer: You can’t. Do all those calculations and x2 it, 37,200 request units per minute for a threshold of 21,000 request units per minute. In fact, I would be doubtful you could create just ONE board without experiencing random internal errors and random miscalculated rate limiting (??)
This was mentioned in the documentation:
Random errors I get 7/10 times using MSS (not sure if they’re related to the limits being hit or not):
Thank you for your work on this!
SortedMap.Update
are being investigated. Could you share a bit more over DM about your use of UpdateAsync
? Is all the code in the library that you linked? Yeah, so perhaps the main takeaway here is using MemoryStoreService
with Global Leaderboards (more than 1-2) isn’t really the best idea?
Correct me if I’m wrong but even with a single monolithic map being turned into shards would still have a better request limit than using individual shards. Because having say 10 shards would still have to make GetRangeAsync
requests to all of it, and since it works in units, it would be a whopping 1,000 request units for all 10 shards, compared to one monolithic map being a single one time 100 request units but sacrificing that size limits issue.
Great to hear. Guilded profile or I can send you a message on the DevForum here, just let me know. All the code is in the library I linked. UpdateAsync
is used with the UserId being the key, a transformer function which decompresses (logarithmic shortener) the previous value if there is one, returns nil if the old value is higher than the new value, then simply returns the compressed value and the new sort key. Do you think returning nil in the transformer could be causing internal parsing errors?
Some information about the Key modules inside of the library:
Leaderboard (Constructs a single leaderboard given a batch of different leaderboard types) - https://github.com/arxkdev/Leaderboard/blob/main/src/Leaderboard/init.luau
Board (Constructs a single board given a BoardType (Hourly, Daily, Weekly, Monthly, Yearly, All Time) Which constructs MemoryShards given the type of board) - https://github.com/arxkdev/Leaderboard/blob/main/src/Leaderboard/Board/init.luau
MemoryShard (Constructs a MemoryShard where it creates a given amount of shards. It uses hashing of the UserId and prefixing, then uses the modulo operator to formulate an Index to find the appropriate Map) - https://github.com/arxkdev/Leaderboard/blob/main/src/Leaderboard/Board/MemoryShard.luau
Hello! I am a bit confused on this functionality. Maybe I am misunderstanding something, but for me I have found that the addition of a sortKey
hinders the ability of some features in the GetRangeAsync
.
Take this code for example:
local MemoryStoreService = game:GetService("MemoryStoreService")
local PlayersMap = MemoryStoreService:GetSortedMap("Players")
local USER_IDS = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
local COLORS = {"red", "green", "blue"}
local EXPIRATION = 10
for _, userId in USER_IDS do
local randomColor = COLORS[math.random(1, #COLORS)]
local key = randomColor.. "_".. userId
local value = {} -- any data
PlayersMap:SetAsync(key, value, EXPIRATION, userId) -- remove the `userId` parameter and the table will populate
end
----------
local color = "red"
local players = PlayersMap:GetRangeAsync(Enum.SortDirection.Ascending, 10, {key = color}, {key = color.. "__"})
print(`Players' data tagged with {color}:`, players)
On line 13, marked with a comment, the simple addition/removal of the optional sort key changes the results of the GetRangeAsync
. When the sort key is present, the GetRangeAsync
returns an empty table all the time, even though the lower and upper bounds of the function don’t mention the sort key at all. The docs of this function say that including the sort key in the bounds table is not necessary.
If I am misunderstanding this, would you mind explaining why the sort key is causing this issue?
Thank you!
Hello!
Your exclusiveLowerBound
does not have a sortKey provided. When you provide a key
as your exclusiveLowerBound
, the GetRangeAsync
API filters out all items with a sort key. This is because items with a sort key are sorted before items without a sort key.
Here’s an example
Key | Value | SortKey
A 1 nil
B 2 a
C 3 1
D 4 2
E 5 nil
When sorted in ascending order, this is the order in which they are returned
C 3 1 -- items with numeric sort keys sort before items with string sort keys
D 4 2 -- 1 < 2, hence C sorts before D
B 2 a -- items with string sort keys sort before items with no sort keys
A 1 nil -- items with no sort keys are sorted last by key
E 5 nil -- A < E, hence A sorts before E
When you make a request such as this
PlayersMap:GetRangeAsync(Enum.SortDirection.Ascending, 10, {key = A}, {key = Z.. "__"})
, it will ignore the first 3 of the above 5 items, since they sort before item with key A.
In your case, it appears that your key is still what you want to sort on, since it is a composite key made up of the color and userId. You’d need to change your schema to use this new parameter.
One example would be if you change key to just userId, and sortKey to randomColor.. "_".. userId
, then when you call GetRangeAsync
, your exclusiveLowerBound
can have nil
key and color
as the sort key.
Hope this helps!
Thank you for the response! I updated my test code with your example (reproduced below) but still found that the table wouldn’t populate.
local MemoryStoreService = game:GetService("MemoryStoreService")
local PlayersMap = MemoryStoreService:GetSortedMap("Players")
local USER_IDS = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
local COLORS = {"red", "green", "blue"}
local EXPIRATION = 10
for _, userId in USER_IDS do
local randomColor = COLORS[math.random(1, #COLORS)]
local sortKey = randomColor.. "_".. userId
local value = {} -- any data
print(userId, sortKey)
PlayersMap:SetAsync(userId, value, EXPIRATION, sortKey)
end
----------
local color = "red"
local players = PlayersMap:GetRangeAsync(Enum.SortDirection.Ascending, 10, {sortKey = color}, {sortKey = color.. "_~"}) -- tilde has a high utf8 decimal
print(`Players' data tagged with {color}:`, players)
Based on your order example, I believe this is what should be happening in the above code:
Key | Value | SortKey
1 {} red_1
2 {} blue_2
3 {} red_3
4 {} green_4
5 {} red_5
And thus should sort like this when exclusiveLowerBound = {sortKey = "red"}
and exclusiveUpperBound = {sortKey = "red_~"}
:
Key | Value | SortKey
1 {} red_1
3 {} red_3
5 {} red_5
-- these shouldn't be returned since its exclusive🔽
2 {} blue_2
4 {} green_4
Awesome update, I will definitely use it to optimize my matchmaking even more.
So when the sort key is present in the data, do the exclusiveLowerBound
and exclusiveUpperBound
even take into account the key
?
Take this sample for example:
local MemoryStoreService = game:GetService("MemoryStoreService")
local PlayersMap = MemoryStoreService:GetSortedMap("Players")
local USER_IDS = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
local EXPIRATION = 10
local function padNumber(n: number, c: number, decimal: boolean)
return string.format(`%0.{c}{if decimal then "f" else "i"}`, n)
end
for i, userId in USER_IDS do
local sortKey = padNumber(math.random(), 3, true)
local value = {} -- any data
PlayersMap:SetAsync(padNumber(userId, 2), value, EXPIRATION, sortKey)
end
-- Get all user IDs between 3 and 7 (exclusive)
local lowerBound = {key = "03", sortKey = padNumber(0, 3, true)}
local upperBound = {key = "07", sortKey = padNumber(1, 3, true)}
local result = PlayersMap:GetRangeAsync(Enum.SortDirection.Ascending, 10, lowerBound, upperBound)
print(result) -- prints everything, ignores key entirely?
-- Get all percentages between 0.5 and 1
local lowerBound2 = {key = "00", sortKey = padNumber(0.5, 3, true)}
local upperBound2 = {key = "11", sortKey = padNumber(1, 3, true)}
local result2 = PlayersMap:GetRangeAsync(Enum.SortDirection.Ascending, 10, lowerBound2, upperBound2)
print(result2) -- works as intended, but try setting the key to "11" and it doesn't work as expected
I would expect the data to be returned be 04, 05, 06
user IDs, but instead it returns every item, as if the key
is useless in the search and it only takes into account the sortKey
. I understand I could probably make this example work by swapping the key with the sort key, but wouldn’t the expected behavior be for it to work both ways? What if I wanted to search range by key and sort key separately, like in the example above where I want to get a range of user IDs and a range of percentages? In both cases, presumably because I have a sort key set, the key does nothing in the GetRangeAsync() function.
Both of your code snippets are working as designed
To clarify, the bounds are exclusive. In your first snippet (the one with the colors), when your lower bound has a sortKey = “red”, it will ignore all items with red as the sortKey.
The 5 items will sort like so
Key | SortKey
2 blue_2
4 green_4
1 red_1
3 red_3
5 red_5
Entries are sorted first by sortKey (alphabetically, since these are string sort keys) and then by key.
Now say you care about getting all entries with sortKey “red”, you can get them via a call like this
local players = PlayersMap:GetRangeAsync(Enum.SortDirection.Ascending, 10, { sortKey = 'green' }, { sortKey = 'red' .. '_~' }) -- tilde has a high utf8 decimal
If the lower bound is set to red
, all entries with red
as the sort key will be excluded from the results since sort key is exclusive.
In your second example, it appears as if the keys are being ignored, since the sortKey bounds encompass all items. The way the GetRangeAsync
call works is -
Would there be any latency or drawbacks for repeatedly calling GetSortedMap
instead of storing a fetched map in a table?
No there won’t be any drawbacks of getting a sorted map in your script. Could you elaborate what you mean by repeatedly calling this? In most scenarios, you’d need to call GetSortedMap
to get the sorted map by name, and then use the API methods you require for your use case in your script(s).
Apologies, I thought that fetching MemoryStores was rate-limited and thus had to be cached with a single call instance as such;
local map = game:GetService("MemoryStoreService");
local cache = {}
local function loadMap(name: string)
local cached = cache[name]
if not cached then
cache[name] = map:GetSortedMap(name)
end
return cache[name]
end
Is there an ETA on when MemoryStoreHashMap will be released?
Hello! Yes the program is in private beta right now, with plans of releasing it this quarter. We plan to open the beta to the public very shortly. Stay tuned!
This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.