mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-24 06:34:09 +00:00
update rules
This commit is contained in:
parent
4c390fdd37
commit
d5522b2fef
8549 changed files with 11783 additions and 1888 deletions
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
title: Cross-server messaging
|
||||
description: Cross-server messaging enables you to communicate with other servers or client instances of your experience.
|
||||
---
|
||||
|
||||
Normally, the code within an experience can only affect the server or clients that it's running on, but there may be situations where you want different servers to communicate with each other, including:
|
||||
|
||||
- **Global Announcements** — Send announcements such as "A user found a special item!" to all the experience's servers.
|
||||
- **Real-Time Server Browser** — Compile a list of all the experience's servers and who is in them (updated every minute) and display the list on a maximum of 20 servers.
|
||||
|
||||
You can support cross-server messaging in your experience using `Class.MessagingService`. You can also use the [Teleportation Playground](https://www.roblox.com/games/3112653247/Teleportation-Playground) sample experience to see how cross‑server messaging works before you implement it. Lastly, see [here](../cloud/guides/usage-messaging.md) to explore cross‑server communication using external tools.
|
||||
|
||||
## Cross-server messaging setup
|
||||
|
||||
To enable cross-server messaging, you must set up a **topic** which is a customized message channel that's accessible from multiple servers. After you create a topic, you can [subscribe users](#subscribe-users-to-receive-messages) to the topic to receive messages and enable [publishing messages](#publish-messages) to the topic.
|
||||
|
||||
### Subscribe users to receive messages
|
||||
|
||||
Use `Class.MessagingService:SubscribeAsync()` to subscribe users to a topic and specify a callback function that detects messages publishing to that topic. For example, the following code sample subscribes all users to a **FriendServerEvent** topic that receives messages when any user is teleported to a different server.
|
||||
|
||||
```lua
|
||||
local MessagingService = game:GetService("MessagingService")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local MESSAGING_TOPIC = "FriendServerEvent"
|
||||
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
-- Subscribe to the topic
|
||||
local subscribeSuccess, subscribeConnection = pcall(function()
|
||||
return MessagingService:SubscribeAsync(MESSAGING_TOPIC, function(message)
|
||||
print(message.Data)
|
||||
end)
|
||||
end)
|
||||
if subscribeSuccess then
|
||||
-- Unsubscribe from topic upon player ancestry change
|
||||
player.AncestryChanged:Connect(function()
|
||||
subscribeConnection:Disconnect()
|
||||
end)
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
### Publish messages
|
||||
|
||||
Use `Class.MessagingService:PublishAsync()` to match a specific topic and publish a message to it. For example, the following code sample uses `Class.MessagingService:PublishAsync()|PublishAsync()` to notify all users when a user joins a new server, including the `Class.Player.Name` representing the user's display name and the `Class.DataModel.JobId|JobId`, a unique identifier for the running experience server instance.
|
||||
|
||||
```lua
|
||||
local MessagingService = game:GetService("MessagingService")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local MESSAGING_TOPIC = "FriendServerEvent"
|
||||
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
-- Publish to topic
|
||||
local publishSuccess, publishResult = pcall(function()
|
||||
local message = player.Name .. " joined server with 'JobId' of " .. game.JobId
|
||||
MessagingService:PublishAsync(MESSAGING_TOPIC, message)
|
||||
end)
|
||||
if not publishSuccess then
|
||||
print(publishResult)
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
---
|
||||
title: Choose between cloud services
|
||||
description: When to use standard data stores, ordered data stores, memory stores, secrets stores, and configs.
|
||||
---
|
||||
|
||||
Roblox offers several options for data storage. Each storage option has pros and cons and is appropriate for different use cases. This table summarizes the available options and provides the closest points of comparison from standard web services.
|
||||
|
||||
| Service | Persistence | Scope | In-experience access | Cloud comparison |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Data stores** | Permanent | Cross-server | Read-write | NoSQL database |
|
||||
| **Memory stores** | 45 day maximum | Cross-server | Read-write | In-memory cache |
|
||||
| **Configs** | Permanent | Cross-server | Read-only | App configuration and feature flags |
|
||||
| **Secrets stores** | Permanent | Cross-server | Read-only | Secrets management |
|
||||
| **Luau in-memory** | Session duration | Single server | Read-write | Server RAM |
|
||||
|
||||
<Alert severity="success">
|
||||
Within Roblox experiences, cloud services are only accessible from server scripts.
|
||||
</Alert>
|
||||
|
||||
## When to use data stores
|
||||
|
||||
`Class.DataStoreService` stores long-term data that needs to last between sessions, such as user progress or inventory items. Data stores are consistent per experience, so every server for every place within an experience can access and change the same data. There are two types of data stores: standard and ordered.
|
||||
|
||||
- **Standard data stores** can store data like numbers, strings, and tables that don't need to be ranked or sorted. They store data as key-value pairs; each entry is stored under a key that is unique within its data store and that you can retrieve, update, or delete. For more information, see [Data stores](./data-stores/index.md).
|
||||
|
||||
- **Ordered data stores** can only store numbers. Each entry is stored under a key that is unique within its data store and that you can retrieve, update, or delete. You can rank and sort this data numerically and retrieve it in ascending or descending order based on stored numerical values. For more information, see [Ordered data stores](./data-stores/index.md#ordered-data-stores).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<td width="15%"></td>
|
||||
<td width="40%">**Standard data stores**</td>
|
||||
<td width="40%">**Ordered data stores**</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>**Data type**</td>
|
||||
<td>Numbers, strings, booleans, and tables.</td>
|
||||
<td>Only numbers.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>**Common use cases**</td>
|
||||
<td>User progress, inventory items, and experience settings.</td>
|
||||
<td>All-time, persistent ranking systems and leaderboards. Unlike leaderboards in memory stores, this leaderboard data is permanent.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>**Past version backup**</td>
|
||||
<td>Automatically manages previous versions of your data for 30 days.</td>
|
||||
<td>Does not manage previous versions of your data.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Alert severity="info">
|
||||
If you want to add granular permission control to your data stores and access them outside of Studio or Roblox servers, use the [Open Cloud APIs for data stores](/cloud/reference/features/storage).
|
||||
</Alert>
|
||||
|
||||
## When to use memory stores
|
||||
|
||||
`Class.MemoryStoreService` is a high throughput and low latency service that stores temporary data that needs to be updated or accessed frequently, such as global leaderboards or matchmaking queues. With memory stores, every server for every place within an experience can access and change the same data quickly and frequently. Data in a memory store expires after a certain period of time, up to 45 days.
|
||||
|
||||
If you need to quickly read (not write to) a small number of values across all servers, [configs](#when-to-use-configs) are the better choice. Memory stores are not as performant as configs when frequently accessing a single key or partition.
|
||||
|
||||
Although memory stores store temporary data, they also support permanent features like a global marketplace. The marketplace is permanent, but the items for sale inside it have an expiration date.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<td width="15%"></td>
|
||||
<td width="80%">**Memory stores**</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>**Data type**</td>
|
||||
<td>Numbers, strings, booleans, and tables that don't need to persist for over 45 days.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">**Common use cases**</td>
|
||||
<td>Skill-based matchmaking, match states for multiplayer experiences.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Daily and monthly leaderboards.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## When to use configs
|
||||
|
||||
[Configs](../production/configs.md) let you update in-game variables in real time without restarting servers. They are ideal for feature flags and any in-game values you hope to tune over time or [experiment](../production/experiments.md) with.
|
||||
|
||||
- Configs are read-only from within an experience—you modify them on Creator Hub or in Roblox Studio—so they don't serve the same use cases as data stores and memory stores.
|
||||
- Changes to configs deploy over the course of five minutes; changes to data and memory stores are more instantaneous.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<td width="15%"></td>
|
||||
<td width="80%">**Configs**</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>**Data type**</td>
|
||||
<td>Numbers, strings, booleans, and JSON objects.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">**Common use cases**</td>
|
||||
<td>Enabling a new dungeon, disabling a limited-time game mode, starting a holiday event.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enemy health, weapon damage, item prices, experience multipliers, welcome messages.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## When to use secrets stores
|
||||
|
||||
Use the [secrets store](secrets.md) for your experience to store any API keys, passwords, and access tokens that you use to authenticate with external services. Storing these values in a data store presents unnecessary security risks.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<td width="15%"></td>
|
||||
<td width="80%">**Secrets stores**</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>**Data type**</td>
|
||||
<td>Strings.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>**Common use cases**</td>
|
||||
<td>API keys, passwords, and access tokens for analytics or music services.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## When to use in-memory storage in Luau
|
||||
|
||||
Use in-memory storage in Luau to store temporary data that needs to be accessed with minimal latency and without the cost of making external service calls. This storage is built into Luau and requires no setup.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<td width="15%"></td>
|
||||
<td width="80%">**In-memory storage in Luau**</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>**Data type**</td>
|
||||
<td>Numbers, strings, booleans, and tables.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">**Common use cases**</td>
|
||||
<td>Data that is only relevant to a single server session and doesn't need to be persisted, such as status effects or temporary points that reset when the user leaves the experience.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Values that change frequently, like counters, timers, state flags, or a health bar.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data that drives game logic, which needs to be accessible instantly, like temporary variables or an enemy's current health.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
title: Best practices for data stores
|
||||
description: Guidelines that help you manage your data and data stores (DataStores) more efficiently.
|
||||
---
|
||||
|
||||
Best practices are guidelines that help you manage your data more efficiently.
|
||||
|
||||
## General best practices
|
||||
|
||||
### Create fewer data stores
|
||||
|
||||
Data stores have similar behavior to tables in databases. When you minimize the number of data stores in an experience and put related data in the same data store, you're able to configure each data store individually and improve the service's efficiency to operate the data.
|
||||
|
||||
### Use a single object for related data
|
||||
|
||||
To use the maximum [4 MB object size limit](./error-codes-and-limits.md#throughput-limits) more efficiently, fetch all relevant data in one call. `Class.GlobalDataStore:SetAsync()|SetAsync()` updates all data so that all data for the same user is always in sync.
|
||||
|
||||
The versioning system versions individual objects instead of the entire data store. This means self-contained objects are consistent when you restore data stores to older versions.
|
||||
|
||||
### Use key prefixes to organize your data
|
||||
|
||||
Filter keys with a specific [prefix](./versioning-listing-and-caching.md#listing-and-prefixes) when calling `Class.DataStore:ListKeysAsync()|ListKeysAsync()`. For example, you can save keys with a prefix like `/User_1234/profiles/warrior` and `/User_1234/profiles/mage` in experiences that support users with multiple character profiles. You can then use a prefix search with `/User_1234/profiles` to get a list of all profiles belonging to that user.
|
||||
|
||||
## Optimization best practices
|
||||
|
||||
Data stores have storage limits that apply per experience. When your experience approaches or exceeds its quota, you should take action to reduce usage and avoid potential additional costs.
|
||||
|
||||
### Monitor storage usage
|
||||
|
||||
Use these tools for data store observability:
|
||||
|
||||
- **Data Stores Dashboard**, which provides you with a visual overview of your storage usage over time. Use this dashboard to monitor how your storage scales and to identify periods of unexpected growth in usage.
|
||||
- **Data Stores Manager**, which gives you a direct view into your experience's stored data. The Manager includes key metrics like your current storage usage and the total keys you have stored across all of your data stores. If your experience exceeds its quota, the Manager displays an alert and your new estimated monthly cost.
|
||||
|
||||
If your experience has more than 100 data stores, the Manager might not display their detailed metrics. In this case, consider using programmatic analysis through Open Cloud or batch operations.
|
||||
- **Notifications**, which notify you when your experience approaches or exceeds storage limits. Notifications include guidance and links to dashboards where you can then review and reduce your usage.
|
||||
|
||||
### Resolve over-quota usage
|
||||
|
||||
If your experience exceeds your storage limits, you can:
|
||||
|
||||
- **Delete unused data stores.** If you're testing or have temporary stores, deleting them regularly helps maintain healthy storage usage. When you delete a data store, it can take Roblox up to 30 days to process your request.
|
||||
- **Delete individual keys.** This is useful when you need to remove temporary items or a certain player's data.
|
||||
- **Use the Batch Processor.** In cases when deleting data manually isn't efficient, you can use the [Batch Processor](https://github.com/Roblox/data-stores-batch-processor-cli) or the Open Cloud Data Stores API to delete data in bulk.
|
||||
|
||||
### Use data store versions instead of new keys
|
||||
|
||||
When saving updates or new versions of player or configuration data, you can use the data stores versioning system instead of creating a new key for each version. Versioning allows you to roll back or inspect previous saves without increasing your total key count or storage footprint.
|
||||
|
||||
### Use memory stores for temporary data
|
||||
|
||||
Use [memory stores](../memory-stores/index.md) instead of data stores for caches or ephemeral data. The data in memory stores automatically expires after 45 days, which reduces your long-term storage growth.
|
||||
|
||||
### Clean up your data after testing
|
||||
|
||||
After you're done testing, delete test data stores through the Data Stores Manager. Avoid creating new scopes or entire new data stores just for test data.
|
||||
|
||||
### Store player data by key
|
||||
|
||||
Instead of storing player data by data store, store it by key. Deleting a key is faster than deleting a data store.
|
||||
|
||||
### Remove data after events
|
||||
|
||||
After seasonal events or temporary features, delete unused data through the Batch Processor or Open Cloud APIs.
|
||||
|
||||
### Review usage regularly
|
||||
|
||||
Make sure to review your usage regularly and detect anomalies early with the Data Stores Dashboard.
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
title: Data Stores Manager
|
||||
description: The Data Stores Manager helps you monitor your data stores (DataStores) on Creator Hub.
|
||||
---
|
||||
|
||||
With the Data Stores Manager, you can browse and monitor your data stores, their key entries, and their storage usage directly on the Creator Hub.
|
||||
|
||||
## Access the Data Stores Manager
|
||||
|
||||
<Alert severity="info">
|
||||
If you're the owner of an experience or the group owner of a group-owned experience, you already have full access to the Data Stores Manager by default.
|
||||
|
||||
To give members of your group access to the Data Stores Manager, you can [grant them one of the following permissions](../../projects/groups.md#roles-and-permissions):
|
||||
|
||||
- **View Data Stores** lets them view the Data Stores Manager page in the Creator Hub.
|
||||
- **Edit Data Stores** lets them delete keys.
|
||||
- **Delete Data Stores** lets them delete data stores.
|
||||
- **Edit all group experiences** gives them full access to all of the functionality of the Data Stores Manager.
|
||||
</Alert>
|
||||
|
||||
To access the Data Stores Manager:
|
||||
|
||||
1. Go to Creations and select an experience.
|
||||
2. Go to **Configure** ⟩ **Data Stores Manager**.
|
||||
|
||||
<img src="./../../assets/data/data-store/Data-Stores-Manager-Page.png" alt="Overview of Data Stores Manager page displaying the Summary section, the Data Stores list, and the Keys list." />
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
The **Summary** section of the Data Stores Manager page includes the **Total Size** and the **Storage Limit** of your data stores. Total Size is calculated by adding the number of bytes consumed by all existing keys in your experience, and Storage Limit is calculated based on your experience's lifetime user count. For more information about storage limits, see [Limits](./error-codes-and-limits.md#limits).
|
||||
|
||||
If your Total Size exceeds your Storage Limit, your **Est. Monthly Costs** will populate in your Summary and show an estimation of the impact of the usage that has gone over your quota. For tips on how to reduce your storage consumption, see [Best practices](./best-practices.md).
|
||||
|
||||
## View data stores and keys
|
||||
|
||||
<Alert severity="warning">
|
||||
If your experience has more than 100 data stores, the data store size and number of keys won't be available in the Data Stores list.
|
||||
</Alert>
|
||||
|
||||
The **Data Stores** list displays the name, size, and number of keys for all data stores in your experience. You can filter this list by entering a prefix in the search bar.
|
||||
|
||||
To further drill down into each specific data store, you can:
|
||||
|
||||
- Select a data store from the data store list to display a list of all keys in that data store.
|
||||
- Enter a prefix in the search bar to filter the list of keys. The prefix must start with the scope name.
|
||||
- Select a key entry to display that key's value, metadata, version history, status, and when it was last updated.
|
||||
- Select both a key entry and a version, and click **Compare versions** to compare the current key entry version with the version you selected.
|
||||
- Select a previous version of a key and click **Revert** to go back to that version. You must have Edit permissions to do this. Reverting to a previous version conditionally updates the list and writes a new version with the value of the version you have previously selected.
|
||||
|
||||
<img src="./../../assets/data/data-store/Data-Stores-Manager-Compare-Versions.png" alt="Comparison of two key entries in a data store." />
|
||||
|
||||
## Delete data stores and keys
|
||||
|
||||
<Alert severity="info">
|
||||
When you schedule a data store for permanent deletion, that data store immediately becomes inaccessible. Any attempts to read or write to its entries will fail.
|
||||
</Alert>
|
||||
|
||||
To schedule a data store or key for permanent deletion:
|
||||
|
||||
1. Go to Creations and select an experience.
|
||||
2. Go to **Configure** ⟩ **Data Stores Manager**.
|
||||
3. Click the **Action** menu next to the item you want to delete.
|
||||
4. Click **Mark for Deletion**.
|
||||
|
||||
There is a cooldown period between when you delete a data store or key and when that item is actually deleted. During this cooldown period, you can undo the deletion and restore the item to its normal state.
|
||||
|
||||
To restore a data store, click the **Action** menu next to the data store you want to restore and then click **Restore**, or call the [`UndeleteDataStore`](/cloud/reference/DataStore#Cloud_UndeleteDataStore) method from the Open Cloud API.
|
||||
|
||||
To restore a key, call the `Class.GlobalDataStore.UpdateAsync|UpdateAsync` method from the Engine API or the [`UpdateDataStoreEntry`](/cloud/reference/DataStoreEntry#Cloud_UpdateDataStoreEntry__Using_Universes_DataStores) method from the Open Cloud API.
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
---
|
||||
title: Data store error codes and limits
|
||||
description: Error codes you might encounter and limits you might hit when using data stores (DataStores) to store data.
|
||||
---
|
||||
|
||||
Requests you make to data stores can fail due to poor connectivity or other issues. To handle errors and return messages with an error code, wrap data store functions in `Global.LuaGlobals.pcall()`.
|
||||
|
||||
## Error code reference
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Error code</th>
|
||||
<th>Error name</th>
|
||||
<th>Error message</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`101`</td>
|
||||
<td>`KeyNameEmpty`</td>
|
||||
<td>Key name can't be empty.</td>
|
||||
<td>Check if the key input into the data store function is an empty string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`102`</td>
|
||||
<td>`KeyNameLimit`</td>
|
||||
<td>Key name exceeds the 50 character limit.</td>
|
||||
<td>Check if the key input into the data store function exceeds a length of 50.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`103`</td>
|
||||
<td>`ValueNotAllowed`</td>
|
||||
<td>Can't allow <b>X</b> in `DataStore`.</td>
|
||||
<td>A bad update function returned a value of type <b>X</b>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`104`</td>
|
||||
<td>`CantStoreValue`</td>
|
||||
<td>Can't store <b>X</b> in `DataStore`.</td>
|
||||
<td>The update function returned a value of type <b>X</b> that didn't serialize.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`105`</td>
|
||||
<td>`ValueTooLarge`</td>
|
||||
<td>Serialized value exceeds <b>X</b> limit.</td>
|
||||
<td>If you're setting a value with `Class.GlobalDataStore:SetAsync()|SetAsync()` or `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()`, the serialized length of the value can't exceed the size <b>X</b>. To check the serialized length of the data, use `Class.HttpService:JSONEncode()|JSONEncode()`.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`106`</td>
|
||||
<td>`MaxValueInvalid`</td>
|
||||
<td>`MaxValue` must be an integer.</td>
|
||||
<td>If you're passing a maximum value to `Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()` for an `Class.OrderedDataStore`, it must be an integer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`106`</td>
|
||||
<td>`MinValueInvalid`</td>
|
||||
<td>`MinValue` must be an integer.</td>
|
||||
<td>If you're passing a minimum value to `Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()` for an `Class.OrderedDataStore`, it must be an integer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`106`</td>
|
||||
<td>`PageSizeGreater`</td>
|
||||
<td>`PageSize` must be within a predefined range.</td>
|
||||
<td>The minimum page size for an `Class.OrderedDataStore` is 1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`106`</td>
|
||||
<td>`PageSizeLesser`</td>
|
||||
<td>`PageSize` must be within a predefined range.</td>
|
||||
<td>The maximum page size for an `Class.OrderedDataStore` is 100.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`107`</td>
|
||||
<td>`MinMaxOrderInvalid`</td>
|
||||
<td>`MaxValue` must be greater than or equal to `MinValue`.</td>
|
||||
<td>The maximum value must be greater than or equal to the minimum value for `Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()`.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`301`</td>
|
||||
<td>`GetAsyncThrottle`</td>
|
||||
<td>`GetAsync` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.GlobalDataStore:GetAsync()|GetAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`302`</td>
|
||||
<td>`SetAsyncThrottle`</td>
|
||||
<td>`SetAsync` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.GlobalDataStore:SetAsync()|SetAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`303`</td>
|
||||
<td>`IncreAsyncThrottle`</td>
|
||||
<td>`IncrementAsync` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.GlobalDataStore:IncrementAsync()|IncrementAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`304`</td>
|
||||
<td>`UpdateAsyncThrottle`</td>
|
||||
<td>`UpdateAsync` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`304`</td>
|
||||
<td>`TransformThrottle`</td>
|
||||
<td>`UpdateAsync` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`305`</td>
|
||||
<td>`GetSortedThrottle`</td>
|
||||
<td>`GetSorted` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`306`</td>
|
||||
<td>`RemoveAsyncThrottle`</td>
|
||||
<td>`RemoveAsync` request dropped. Request was throttled but queue was full.</td>
|
||||
<td>`Class.GlobalDataStore:RemoveAsync()|RemoveAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`401`</td>
|
||||
<td>`DataModelNoAccess`</td>
|
||||
<td>Request failed. `DataModel` is inaccessible while the experience is shutting down.</td>
|
||||
<td>`Class.DataModel` is uninitialized because the experience is shutting down.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`402`</td>
|
||||
<td>`LuaWebSrvsNoAccess`</td>
|
||||
<td>Request failed. `LuaWebService` is inaccessible while the experience is shutting down.</td>
|
||||
<td>`Class.LuaWebService` is uninitialized because the experience is shutting down.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`403`</td>
|
||||
<td>`StudioAccessToApisNotAllowed`</td>
|
||||
<td>Can't write to `DataStore` from Studio because API access is not enabled.</td>
|
||||
<td>API access must be active in order to use Data Stores in Studio.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`404`</td>
|
||||
<td>`InternalError`</td>
|
||||
<td>`OrderedDataStore` doesn't exist.</td>
|
||||
<td>The `Class.OrderedDataStore` associated with this request wasn't found. This might be a sign of data corruption. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`501`</td>
|
||||
<td>`InternalError`</td>
|
||||
<td>Can't parse response because data might be corrupted.</td>
|
||||
<td>The server was unable to parse the response to your request. This might be a sign of data corruption. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`502`</td>
|
||||
<td>`RequestRejected`</td>
|
||||
<td>API Services rejected the request with error <b>X</b>.</td>
|
||||
<td>Error <b>X</b> occurred when processing on Roblox servers. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`503`</td>
|
||||
<td>`InternalError`</td>
|
||||
<td>Data store request was successful but key wasn't found.</td>
|
||||
<td>The key requested wasn't found in the Data Store. This might be a sign of data corruption. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`504`</td>
|
||||
<td>`InternalError`</td>
|
||||
<td>Data Store request was successful but the response wasn't formatted correctly.</td>
|
||||
<td>The server was unable to parse the response to your request. This might be a sign of data corruption. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`505`</td>
|
||||
<td>`InternalError`</td>
|
||||
<td>`OrderedDataStore` request was successful but the response wasn't formatted correctly.</td>
|
||||
<td>The server was unable to parse the response to your `Class.OrderedDataStore` request. This might be a sign of data corruption. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`509`</td>
|
||||
<td>`OperationNotAllowed`</td>
|
||||
<td>Data Store operations are blocked while running on a Personal RCC to prevent possible data corruption.</td>
|
||||
<td>Data Store writes are blocked on private RCC channels.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`511`</td>
|
||||
<td>`AttributeSizeTooLarge`</td>
|
||||
<td>Metadata attribute size exceeds <b>X</b> limit.</td>
|
||||
<td>The serialized metadata size exceeds the limit of <b>X</b>. The value <b>X</b> is dynamic. If the size changes, the value also changes.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`512`</td>
|
||||
<td>`UserIdLimitExceeded`</td>
|
||||
<td>`UserID` size exceeds <b>X</b> limit.</td>
|
||||
<td>The length of the user IDs array provided by the user exceeds the limit of <b>X</b>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`513`</td>
|
||||
<td>`AttributeFormatError`</td>
|
||||
<td>Attribute `userId` format is invalid.</td>
|
||||
<td>The user ID provided isn't a number.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`513`</td>
|
||||
<td>`AttributeFormatError`</td>
|
||||
<td>Attribute metadata format is invalid.</td>
|
||||
<td>The metadata isn't a table.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`GetVersionAsyncThrottle`</td>
|
||||
<td>`GetVersionAsync` request dropped. Request was throttled.</td>
|
||||
<td>`Class.DataStore:GetVersionAsync()|GetVersionAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`GetVersionAtTimeAsyncThrottle`</td>
|
||||
<td>`GetVersionAtTimeAsync` request dropped. Request was throttled.</td>
|
||||
<td>`Class.DataStore:GetVersionAtTimeAsync()|GetVersionAtTimeAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`ListDataStoresAsyncThrottle`</td>
|
||||
<td>`ListDataStoresAsync` request dropped. Request was throttled.</td>
|
||||
<td>`Class.DataStoreService:ListDataStoresAsync()|ListDataStoresAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`ListKeysAsyncThrottle`</td>
|
||||
<td>`ListKeysAsync` request dropped. Request was throttled.</td>
|
||||
<td>`Class.DataStore:ListKeysAsync()|ListKeysAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`ListVersionsAsyncThrottle`</td>
|
||||
<td>`ListVersionsAsync` request dropped. Request was throttled.</td>
|
||||
<td>`Class.DataStore:ListVersionsAsync()|ListVersionsAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`RemoveVersionAsyncThrottle`</td>
|
||||
<td>`RemoveVersionAsync` request dropped. Request was throttled.</td>
|
||||
<td>`Class.DataStore:RemoveVersionAsync()|RemoveVersionAsync()` request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput.</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>`InvalidTimestamp`</td>
|
||||
<td>Timestamp must be positive and not more than ten minutes in the future.</td>
|
||||
<td>The timestamp provided to `Class.DataStore:GetVersionAtTimeAsync()|GetVersionAtTimeAsync()` was not valid.</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Server error codes
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Error name</th>
|
||||
<th>Error message</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`DatastoreThrottled`</td>
|
||||
<td>The request rate exceeds the allowed maximum for the `datastore`.</td>
|
||||
<td>Too many requests were sent to a single Data Store. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`InternalServerError`</td>
|
||||
<td>An internal server error occurred.</td>
|
||||
<td>Occasional error on Roblox servers. Try again, ideally with exponential backoff.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`InvalidPlace`</td>
|
||||
<td>The provided place is invalid.</td>
|
||||
<td>No matching Universe ID for the place. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`InvalidTarget`</td>
|
||||
<td>The provided target is invalid.</td>
|
||||
<td>Ordered Data Store key name exceeds the 50 character limit.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`InvalidUniverse`</td>
|
||||
<td>The provided universe is invalid.</td>
|
||||
<td>No matching Place ID for the universe. Try again later.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`InvalidUserIds`</td>
|
||||
<td>The provided user IDs have an invalid format.</td>
|
||||
<td>Failed to parse user IDs.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`KeyThrottled`</td>
|
||||
<td>The request rate exceeds the allowed maximum for the key.</td>
|
||||
<td>The request rate exceeds the maximum allowed request rate for a single key.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`KeyNotFound`</td>
|
||||
<td>The requested key doesn't exist.</td>
|
||||
<td>The key doesn't exist.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`N/A`</td>
|
||||
<td>No pages to advance to.</td>
|
||||
<td>This error occurs when you call `Class.Pages:AdvanceToNextPageAsync()` on the last page.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Limits
|
||||
|
||||
Data models have **limits**. If an experience exceeds these limits, the service automatically throttles the experience's data store usage and causes future requests to be placed in one of the following queues:
|
||||
|
||||
- Set
|
||||
- Ordered set
|
||||
- Get
|
||||
- Ordered get
|
||||
|
||||
Requests in a queue are handled in the order they are received. The called function continues to yield as long as its request is still queued. If the data store key itself is throttled, the request is placed in a queue but is temporarily skipped.
|
||||
|
||||
Each queue has a limit of 30 requests. When the limit of a queue is reached, requests fail with an error code in the 301-306 range, indicating that the requests have been dropped entirely.
|
||||
|
||||
### Server limits
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Server limits">
|
||||
Each server is allowed a certain number of data store requests based on the request type and number of users. Servers often receive a one-time allotment of additional requests when they first start up. Use `Class.DataStoreService:GetRequestBudgetForRequestType()|GetRequestBudgetForRequestType()` to confirm the number of data store requests that the current place can make.
|
||||
|
||||
For each request type, the limit is shared among all listed functions.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request type</th>
|
||||
<th>Functions</th>
|
||||
<th>Requests per minute</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Get</b></td>
|
||||
<td>`Class.GlobalDataStore:GetAsync()|GetAsync()`</td>
|
||||
<td>60 + numPlayers × 10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Set</b></td>
|
||||
<td>`Class.DataStore:SetAsync()|SetAsync()`<br></br>`Class.DataStore:IncrementAsync()|IncrementAsync()`<br></br>`Class.DataStore:UpdateAsync()|UpdateAsync()`<br></br>`Class.DataStore:RemoveAsync()|RemoveAsync()`</td>
|
||||
<td>60 + numPlayers × 10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Get Sorted</b></td>
|
||||
<td>`Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()`</td>
|
||||
<td>5 + numPlayers × 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Ordered Set</b></td>
|
||||
<td>`Class.OrderedDataStore:SetAsync()|SetAsync()`<br></br>`Class.OrderedDataStore:IncrementAsync()|IncrementAsync()`<br></br>`Class.OrderedDataStore:UpdateAsync()|UpdateAsync()`<br></br>`Class.OrderedDataStore:RemoveAsync()|RemoveAsync()`</td>
|
||||
<td>30 + numPlayers × 5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Get Version</b></td>
|
||||
<td>`Class.DataStore:GetVersionAsync()|GetVersionAsync()`<br></br>`Class.DataStore:GetVersionAtTimeAsync()|GetVersionAtTimeAsync()`</td>
|
||||
<td>5 + numPlayers × 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>List</b></td>
|
||||
<td>`Class.DataStoreService:ListDataStoresAsync()|ListDataStoresAsync()`<br></br>`Class.DataStore:ListKeysAsync()|ListKeysAsync()`<br></br>`Class.DataStore:ListVersionsAsync()|ListVersionsAsync()`</td>
|
||||
<td>5 + numPlayers × 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Remove Version</b></td>
|
||||
<td>`Class.DataStore:RemoveVersionAsync()|RemoveVersionAsync()`</td>
|
||||
<td>5 + numPlayers × 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</TabItem>
|
||||
<TabItem label="Future experience limits">
|
||||
|
||||
<Alert severity="info">
|
||||
Server limits will be replaced by experience-level limits starting in 2026.
|
||||
</Alert>
|
||||
|
||||
Each experience is allowed a certain number of data store requests based on the data store type, request type, and number of concurrent users. For each data store type and request type, the limit is shared among all listed functions.
|
||||
|
||||
Note that `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` will consume from both the read and write request limits.
|
||||
|
||||
<h5>Standard data stores</h5>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request type</th>
|
||||
<th>Functions</th>
|
||||
<th>Requests per minute</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Read</b></td>
|
||||
<td>`Class.DataStore:GetAsync()|GetAsync()`<br></br>`Class.DataStore:GetVersionAsync()|GetVersionAsync()`<br></br>`Class.DataStore:GetVersionAtTimeAsync()|GetVersionAtTimeAsync()`<br></br>`Class.DataStore:UpdateAsync()|UpdateAsync()`</td>
|
||||
<td>250 + concurrentUsers × 40</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Write</b></td>
|
||||
<td>`Class.DataStore:SetAsync()|SetAsync()`<br></br>`Class.DataStore:IncrementAsync()|IncrementAsync()`<br></br>`Class.DataStore:UpdateAsync()|UpdateAsync()`</td>
|
||||
<td>250 + concurrentUsers × 20</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>List</b></td>
|
||||
<td>`Class.DataStoreService:ListDataStoresAsync()|ListDataStoresAsync()`<br></br>`Class.DataStore:ListKeysAsync()|ListKeysAsync()`<br></br>`Class.DataStore:ListVersionsAsync()|ListVersionsAsync()`</td>
|
||||
<td>10 + concurrentUsers × 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Remove</b></td>
|
||||
<td>`Class.DataStore:RemoveAsync()|RemoveAsync()`</td>
|
||||
<td>100 + concurrentUsers × 40</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>RemoveVersion (deprecated)</b></td>
|
||||
<td>`Class.DataStore:RemoveVersionAsync()|RemoveVersionAsync()`</td>
|
||||
<td>5 + concurrentUsers × 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5>Ordered data stores</h5>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request type</th>
|
||||
<th>Functions</th>
|
||||
<th>Requests per minute</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Read</b></td>
|
||||
<td>`Class.OrderedDataStore:GetAsync()|GetAsync()`<br></br>`Class.OrderedDataStore:UpdateAsync()|UpdateAsync()`</td>
|
||||
<td>250 + concurrentUsers × 40</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Write</b></td>
|
||||
<td>`Class.OrderedDataStore:SetAsync()|SetAsync()`<br></br>`Class.OrderedDataStore:IncrementAsync()|IncrementAsync()`<br></br>`Class.OrderedDataStore:UpdateAsync()|UpdateAsync()`</td>
|
||||
<td>250 + concurrentUsers × 20</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>List</b></td>
|
||||
<td>`Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()`</td>
|
||||
<td>100 + concurrentUsers × 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Remove</b></td>
|
||||
<td>`Class.OrderedDataStore:RemoveAsync()|RemoveAsync()`</td>
|
||||
<td>100 + concurrentUsers × 40</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Data limits
|
||||
|
||||
Data stores limit how much data can be used per entry.
|
||||
|
||||
The data store name, key name, and [scope](./versioning-listing-and-caching.md#scopes) must all be under a certain character length. Use `Library.string.len()` to check their length.
|
||||
|
||||
The data (key value) is also stored as a string, regardless of its initial type. You can check the size of the data with the `Class.HttpService:JSONEncode()|JSONEncode()` function, which converts Luau data into a serialized JSON table.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Maximum number of characters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Data store name</td>
|
||||
<td>50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Key name</td>
|
||||
<td>50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scope</td>
|
||||
<td>50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data (key value)</td>
|
||||
<td>4,194,304 per key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Metadata limits
|
||||
|
||||
Limits to the number of characters in user-defined metadata.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Maximum number of characters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Key name</td>
|
||||
<td>50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Value</td>
|
||||
<td>250</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Key-value pairs</td>
|
||||
<td>300</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Alert severity="info">
|
||||
There's no limit to the total number of key-value pairs, but the total size can't exceed 300 characters.
|
||||
</Alert>
|
||||
|
||||
### Throughput limits
|
||||
|
||||
Per-key throughput limits ensure that performance is optimal on Roblox servers. Each limit applies to every single key across all servers in an experience and refreshes over time.
|
||||
|
||||
Roblox examines the usage of quota associated with the key over the last 60 seconds. If the usage, including the current request, is within the throughput limit, the request is approved. If the usage exceeds the limit, the request is denied.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request type</th>
|
||||
<th>Limit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Read</td>
|
||||
<td>25 MB per minute</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Write</td>
|
||||
<td>4 MB per minute</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
In addition to the above throughput limits, Roblox organizes data into partitions based on an internal schema. As a result, when the backend server receives a high volume of requests to the same data store, it can result in further throttling. Regardless of the cause, throttling manifests as either `DatastoreThrottled` or `KeyThrottled` errors, depending on whether the throughput limit was exceeded for a single data store or a key. These error messages apply to both ordered and standard data stores.
|
||||
|
||||
<Alert severity="info">
|
||||
For every request, Roblox rounds throughput up to the next kilobyte. For example, if you write 800 bytes and 1.2 KB in two requests, Roblox counts that as 3 KB total throughput (1 KB and 2 KB, respectively).
|
||||
</Alert>
|
||||
|
||||
### Storage limits
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Storage limits">
|
||||
Currently, there are no enforced storage limits on data stores.
|
||||
</TabItem>
|
||||
<TabItem label="Future storage limits">
|
||||
In the future, to provide a scalable and stable storage experience, data stores will implement an experience-level storage limit on your storage usage.
|
||||
|
||||
This limit will be the sum of a base limit for each of your experiences and a per-user limit based on the number of lifetime users in your experience. A lifetime user is any user who has joined your experience at least once.
|
||||
|
||||
The storage limit will be calculated using the formula `Total latest version storage limit = 100 MB + 1 MB * lifetime user count`.
|
||||
|
||||
Any data that you delete or replace, even if still accessible through version APIs, will not count towards your experience's storage usage.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
358
.cursor/rules/roblox/en-us/cloud-services/data-stores/index.md
Normal file
358
.cursor/rules/roblox/en-us/cloud-services/data-stores/index.md
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
---
|
||||
title: Data stores
|
||||
description: How to implement data stores (DataStores) to store persistent data.
|
||||
comments: The Creator Hub links to some of the anchors on this page, so if you move any of the headers, the team might need to update the links.
|
||||
---
|
||||
|
||||
The `Class.DataStoreService` lets you store data that needs to persist between sessions, like items in a player's inventory or skill points. Data stores are consistent per experience, so any place in an experience can access and change the same data, including places on different servers.
|
||||
|
||||
If you want to add granular permission control to your data stores and access them outside of Studio or Roblox servers, you can use [Open Cloud APIs for data stores](/cloud/reference/DataStore).
|
||||
|
||||
To view and monitor all the data stores in an experience through the Creator Hub, use the [Data Stores Manager](./data-stores-manager.md).
|
||||
|
||||
For temporary data that you need to update or access frequently, use [memory stores](./../memory-stores/index.md).
|
||||
|
||||
## Enable Studio access
|
||||
|
||||
By default, experiences tested in Studio can't access data stores, so you must first enable them. Accessing data stores in Studio can be dangerous for live experiences because Studio accesses the same data stores as the client application. To avoid overwriting production data, do not enable this setting for live experiences. Instead, enable it for a separate test version of the experience.
|
||||
|
||||
To enable Studio access in a [published](../../production/publishing/publish-experiences-and-places.md) experience:
|
||||
|
||||
1. Open Studio's **File** ⟩ **Experience Settings** window.
|
||||
2. Navigate to **Security**.
|
||||
3. Enable the **Enable Studio Access to API Services** toggle.
|
||||
4. Click **Save**.
|
||||
|
||||
## Access data stores
|
||||
|
||||
To access a data store inside an experience:
|
||||
|
||||
1. Add `Class.DataStoreService` to a server-side `Class.Script|Script`.
|
||||
2. Use the `Class.DataStoreService:GetDataStore()|GetDataStore()` function and specify the name of the data store you want to use. If the data store doesn't exist, Studio creates one when you save your experience data for the first time.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
```
|
||||
|
||||
<Alert severity="warning">
|
||||
The server can only access data stores through `Class.Script|Scripts`. Attempting client-side access in a `Class.LocalScript` causes an error.
|
||||
</Alert>
|
||||
|
||||
## Create data
|
||||
|
||||
A data store is essentially a dictionary, similar to a Luau table. A unique **key** indexes each value in the data store, like a user's unique `Class.Player.UserId` or a named string for an experience promo.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>**User data key**</th>
|
||||
<th>**Value**</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`31250608`</td>
|
||||
<td>50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`351675979`</td>
|
||||
<td>20</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`505306092`</td>
|
||||
<td>78000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>**Promo data key**</th>
|
||||
<th>**Value**</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`ActiveSpecialEvent`</td>
|
||||
<td>SummerParty2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`ActivePromoCode`</td>
|
||||
<td>BONUS123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`CanAccessPartyPlace`</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
To create a new entry, call `Class.GlobalDataStore:SetAsync()|SetAsync()` with the key name and a value.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
|
||||
local success, errorMessage = pcall(function()
|
||||
experienceStore:SetAsync("User_1234", 50)
|
||||
end)
|
||||
if not success then
|
||||
print(errorMessage)
|
||||
end
|
||||
```
|
||||
|
||||
<Alert severity="warning">
|
||||
Functions like `Class.GlobalDataStore:SetAsync()|SetAsync()` that access a data store's contents are network calls that might occasionally fail. To catch and handle errors, make sure to wrap these calls in `Global.LuaGlobals.pcall()`.
|
||||
</Alert>
|
||||
|
||||
## Update data
|
||||
|
||||
To change any stored value in a data store, call `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` with the entry's key name and a callback function that defines how you want to update the entry. This callback takes the current value and returns a new value based on the logic you define. If the callback returns `nil`, the write operation is cancelled and the value isn't updated.
|
||||
|
||||
<Alert severity="warning">
|
||||
The callback function you pass into `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` does **not** have permission to yield. It can't contain any yielding functions like `Library.task.wait()`.
|
||||
</Alert>
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local nicknameStore = DataStoreService:GetDataStore("Nicknames")
|
||||
|
||||
local function makeNameUpper(currentName)
|
||||
local nameUpper = string.upper(currentName)
|
||||
return nameUpper
|
||||
end
|
||||
|
||||
local success, updatedName = pcall(function()
|
||||
return nicknameStore:UpdateAsync("User_1234", makeNameUpper)
|
||||
end)
|
||||
if success then
|
||||
print("Uppercase Name:", updatedName)
|
||||
end
|
||||
```
|
||||
|
||||
### Set vs update
|
||||
|
||||
Use set to quickly update a specific key. The `Class.GlobalDataStore:SetAsync()|SetAsync()` function:
|
||||
|
||||
- Can cause data inconsistency if two servers try to set the same key at the same time
|
||||
- Only counts against the write limit
|
||||
|
||||
Use update to handle multi-server attempts. The `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` function:
|
||||
|
||||
- Reads the current key value from the server that last updated it before making any changes
|
||||
- Is slower because it reads before it writes
|
||||
- Counts against both the read and write limits
|
||||
|
||||
## Read data
|
||||
|
||||
To read the value of a data store entry, call `Class.GlobalDataStore:GetAsync()|GetAsync()` with the entry's key name.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
|
||||
local success, currentExperience = pcall(function()
|
||||
return experienceStore:GetAsync("User_1234")
|
||||
end)
|
||||
if success then
|
||||
print(currentExperience)
|
||||
end
|
||||
```
|
||||
|
||||
<Alert severity="warning">
|
||||
The values you retrieve using `Class.GlobalDataStore:GetAsync()|GetAsync()` sometimes can be out of sync with the backend due to the [caching](./versioning-listing-and-caching.md#caching) behavior. For more information, see [Disabling caching](./versioning-listing-and-caching.md#disable-caching).
|
||||
</Alert>
|
||||
|
||||
## Increment data
|
||||
|
||||
To increment an integer in a data store, call `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()` with the entry's key name and a number for how much to change the value. `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()` is a convenience function that lets you avoid calling `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` and manually incrementing the integer.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
|
||||
local success, newExperience = pcall(function()
|
||||
return experienceStore:IncrementAsync("Player_1234", 1)
|
||||
end)
|
||||
if success then
|
||||
print(newExperience)
|
||||
end
|
||||
```
|
||||
|
||||
## Remove data
|
||||
|
||||
To remove an entry and return the value associated with the key, call `Class.GlobalDataStore:RemoveAsync()|RemoveAsync()`.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local nicknameStore = DataStoreService:GetDataStore("Nicknames")
|
||||
|
||||
local success, removedValue = pcall(function()
|
||||
return nicknameStore:RemoveAsync("User_1234")
|
||||
end)
|
||||
if success then
|
||||
print(removedValue)
|
||||
end
|
||||
```
|
||||
|
||||
## Metadata
|
||||
|
||||
<Alert severity="info">
|
||||
Ordered data stores don't support [versioning](versioning-listing-and-caching.md#versioning) and metadata, so `Class.DataStoreKeyInfo|DataStoreKeyInfo` is always `nil` for keys in an `Class.OrderedDataStore|OrderedDataStore`. If you need to support versioning and metadata, use `Class.DataStore|DataStore`.
|
||||
</Alert>
|
||||
|
||||
There are two types of metadata associated with keys:
|
||||
|
||||
- **Service-defined**: Default read-only metadata, like the most recent update time and creation time. Every object has service-defined metadata.
|
||||
- **User-defined**: Custom metadata for tagging and categorization. Defined using the `Class.DataStoreSetOptions` object and the `Class.DataStoreSetOptions:SetMetadata()|SetMetadata()` function.
|
||||
|
||||
To manage metadata, expand the `Class.GlobalDataStore:SetAsync()|SetAsync()`, `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()`, `Class.GlobalDataStore:GetAsync()|GetAsync()`, `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()`, and `Class.GlobalDataStore:RemoveAsync()|RemoveAsync()` functions.
|
||||
|
||||
- `Class.GlobalDataStore:SetAsync()|SetAsync()` accepts the optional third and fourth arguments:
|
||||
|
||||
- A table of `Class.Player.UserId|UserIds`. This can help with content copyright and intellectual property tracking and removal.
|
||||
- A `Class.DataStoreSetOptions` object, where you can define custom metadata using the `Class.DataStoreSetOptions:SetMetadata()|SetMetadata()` function.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
|
||||
local setOptions = Instance.new("DataStoreSetOptions")
|
||||
setOptions:SetMetadata({["ExperienceElement"] = "Fire"})
|
||||
|
||||
local success, errorMessage = pcall(function()
|
||||
experienceStore:SetAsync("User_1234", 50, {1234}, setOptions)
|
||||
end)
|
||||
if not success then
|
||||
print(errorMessage)
|
||||
end
|
||||
```
|
||||
|
||||
- `Class.GlobalDataStore:GetAsync()|GetAsync()`, `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()`, and `Class.GlobalDataStore:RemoveAsync()|RemoveAsync()` return a second value in the `Class.DataStoreKeyInfo` object. This second value contains both service-defined properties and functions to fetch user-defined metadata.
|
||||
|
||||
- The `Class.DataStoreKeyInfo:GetUserIds()|GetUserIds()` function fetches the table of `Class.Player.UserId|UserIds` that you passed to `Class.GlobalDataStore:SetAsync()|SetAsync()`.
|
||||
- The `Class.DataStoreKeyInfo:GetMetadata()|GetMetadata()` function fetches user-defined metadata that you passed to `Class.GlobalDataStore:SetAsync()|SetAsync()` through `Class.DataStoreSetOptions:SetMetadata()|SetMetadata()`.
|
||||
- The `Class.DataStoreKeyInfo.Version|Version` property fetches the version of the key.
|
||||
- The `Class.DataStoreKeyInfo.CreatedTime|CreatedTime` property fetches the time the key was created, formatted as the number of milliseconds since epoch.
|
||||
- The `Class.DataStoreKeyInfo.UpdatedTime|UpdatedTime` property fetches the last time the key was updated, formatted as the number of milliseconds since epoch.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
|
||||
local success, currentExperience, keyInfo = pcall(function()
|
||||
return experienceStore:GetAsync("User_1234")
|
||||
end)
|
||||
if success then
|
||||
print(currentExperience)
|
||||
print(keyInfo.Version)
|
||||
print(keyInfo.CreatedTime)
|
||||
print(keyInfo.UpdatedTime)
|
||||
print(keyInfo:GetUserIds())
|
||||
print(keyInfo:GetMetadata())
|
||||
end
|
||||
```
|
||||
|
||||
- The callback function of `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` takes an additional parameter in the `Class.DataStoreKeyInfo` object that describes the current key state. It returns the modified value, the keys associated with `Class.Player.UserId|UserIds`, and the key's metadata.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local nicknameStore = DataStoreService:GetDataStore("Nicknames")
|
||||
|
||||
local function makeNameUpper(currentName, keyInfo)
|
||||
local nameUpper = string.upper(currentName)
|
||||
local userIDs = keyInfo:GetUserIds()
|
||||
local metadata = keyInfo:GetMetadata()
|
||||
return nameUpper, userIDs, metadata
|
||||
end
|
||||
|
||||
local success, updatedName, keyInfo = pcall(function()
|
||||
return nicknameStore:UpdateAsync("User_1234", makeNameUpper)
|
||||
end)
|
||||
if success then
|
||||
print(updatedName)
|
||||
print(keyInfo.Version)
|
||||
print(keyInfo.CreatedTime)
|
||||
print(keyInfo.UpdatedTime)
|
||||
print(keyInfo:GetUserIds())
|
||||
print(keyInfo:GetMetadata())
|
||||
end
|
||||
```
|
||||
|
||||
<Alert severity="error">
|
||||
When calling `Class.GlobalDataStore:SetAsync()|SetAsync()`, `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()`, and `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()`, you must always update metadata definitions with a value, even when there are no changes to the current value. If you don't, you lose the current value.
|
||||
</Alert>
|
||||
|
||||
For limits when defining metadata, see the [metadata limits](../../cloud-services/data-stores/error-codes-and-limits.md#metadata-limits).
|
||||
|
||||
## Ordered data stores
|
||||
|
||||
By default, data stores don't sort their content. If you need to get data in an ordered way, like in persistent leaderboard stats, call `Class.DataStoreService:GetOrderedDataStore()|GetOrderedDataStore()` instead of `Class.DataStoreService:GetDataStore()|GetDataStore()`.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local characterAgeStore = DataStoreService:GetOrderedDataStore("CharacterAges")
|
||||
```
|
||||
|
||||
Ordered data stores support the same basic functions as default data stores, plus the unique `Class.OrderedDataStore:GetSortedAsync()|GetSortedAsync()` function. This retrieves **multiple sorted keys** based on a specific sorting order, page size, and minimum/maximum values.
|
||||
|
||||
The following example sorts character data into pages with three entries, each in descending order, then loops through the pages and outputs each character's name and age.
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local characterAgeStore = DataStoreService:GetOrderedDataStore("CharacterAges")
|
||||
|
||||
-- Populates ordered data store
|
||||
local characters = {
|
||||
Mars = 19,
|
||||
Janus = 20,
|
||||
Diana = 18,
|
||||
Venus = 25,
|
||||
Neptune = 62
|
||||
}
|
||||
for char, age in characters do
|
||||
local success, errorMessage = pcall(function()
|
||||
characterAgeStore:SetAsync(char, age)
|
||||
end)
|
||||
if not success then
|
||||
print(errorMessage)
|
||||
end
|
||||
end
|
||||
|
||||
-- Sorts data by descending order into pages of three entries each
|
||||
local success, pages = pcall(function()
|
||||
return characterAgeStore:GetSortedAsync(false, 3)
|
||||
end)
|
||||
if success then
|
||||
while true do
|
||||
-- Gets the current (first) page
|
||||
local entries = pages:GetCurrentPage()
|
||||
-- Iterates through all key-value pairs on page
|
||||
for _, entry in entries do
|
||||
print(entry.key .. " : " .. tostring(entry.value))
|
||||
end
|
||||
-- Checks if last page has been reached
|
||||
if pages.IsFinished then
|
||||
break
|
||||
else
|
||||
print("----------")
|
||||
-- Advances to next page
|
||||
pages:AdvanceToNextPageAsync()
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<Alert severity="info">
|
||||
When you iterate through `Class.DataStoreService:GetOrderedDataStore()|GetOrderedDataStore()` using `Class.Pages:AdvanceToNextPageAsync()|AdvanceToNextPageAsync()`, the limit for requests is the same as the maximum page size you set for an ordered data store. `Class.Pages:AdvanceToNextPageAsync()|AdvanceToNextPageAsync()` always has the same limit as the class that originally requires it.
|
||||
</Alert>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: Data store observability
|
||||
description: Explains how to use the observability dashboard for data stores (DataStores).
|
||||
---
|
||||
|
||||
The data stores observability dashboard provides real-time charts on your request counts and on your usage against future data store limits, and allows you to filter the request data by standard or ordered data stores.
|
||||
|
||||

|
||||
|
||||
## Access the dashboard
|
||||
|
||||
The data stores dashboard is available for any experience that uses `Class.DataStoreService`, but you must either be the experience owner or have [analytics group permissions](../../production/analytics/analytics-dashboard.md#grant-group-permission) to access the dashboard.
|
||||
|
||||
1. Navigate to the [Creations](https://create.roblox.com/dashboard/creations) page on the **Creator Hub**.
|
||||
2. Under the **Creator Hub** dropdown, select your account or the group owning the target experience.
|
||||
3. Select the experience.
|
||||
4. In the **Monitoring** dropdown, select **Data Stores**.
|
||||
|
||||
## Available charts
|
||||
|
||||
- **Storage Usage Bytes** on amount of data store storage and limit currently available for the selected universe.
|
||||
- **Request Count by API** on API request count per minute by API method, such as `Class.DataStore:SetAsync()` or `Class.OrderedDataStore:GetSortedAsync()`.
|
||||
- **Request Count by Status** on API request count by [response status](#response-status-codes).
|
||||
- **Request by API x Status** on response statuses returned by all or a specific API method.
|
||||
- **Read Request Type Quota Usage** on number of read API method calls against future read category limits.
|
||||
- **Write Request Type Quota Usage** on number of write API method calls against future write category limits.
|
||||
- **List Request Type Quota Usage** on number of list API method calls against future list category limits.
|
||||
- **Remove Request Type Quota Usage** on number of remove API method calls against future remove category limits.
|
||||
|
||||
Use the selector at the top of the page to filter by standard or ordered data stores. The default view includes standard data store only.
|
||||
|
||||
Use the **Explore** button next to each chart to deep dive into that metric, compare the current period against past periods, or break down that metric by its available categories.
|
||||
|
||||
Each chart contains data for the past 30 days, and you can select to view a custom time range with the selector at the top of the page. If you select a time range earlier than 30 days, the system returns a **Request Failed** error.
|
||||
|
||||
Data from the most recent three minutes might be incomplete, so it's normal to see a drop at the end of the charts.
|
||||
|
||||
Storage usage and limit are displayed in bytes, where 1 kilobyte is 1024 bytes.
|
||||
|
||||
## Response status codes
|
||||
|
||||
The dashboard's **Request Count by Status** and **Requests by API x Status** charts include status codes of API responses that you can use to understand usage and troubleshoot errors. For a table that lists and describes all of these status codes (aside from `200 OK`), see [Error codes](error-codes-and-limits.md#error-code-reference).
|
||||
|
||||
## Service limits
|
||||
|
||||
For information on current limits, usage quotas, and future limits, see [Limits](error-codes-and-limits.md#limits).
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
---
|
||||
title: Implement player data and purchasing systems
|
||||
description: Guide for implementing player data and purchase handling systems.
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Roblox provides a set of APIs to interface with data stores via `Class.DataStoreService`. The most common use case for these APIs is for saving, loading, and replicating _player data_. That is, data associated with the player's progress, purchases, and other session characteristics that persists between individual play sessions.
|
||||
|
||||
Most experiences on Roblox use these APIs to implement some form of a player data system. These implementations differ in their approach, but generally seek to solve the same set of issues.
|
||||
|
||||
## Common problems
|
||||
|
||||
Below are some of the most common problems player data systems attempt to solve:
|
||||
|
||||
- **In Memory Access:** `Class.DataStoreService` requests make web requests that operate asynchronously and are subject to rate limits. This is appropriate for an initial load at the start of the session, but not for high frequency read and write operations during the normal course of gameplay. Most developers' player data systems store this data in-memory on the Roblox server, limiting `Class.DataStoreService` requests to the following scenarios:
|
||||
|
||||
- Initial read at the start of a session
|
||||
- Final write at the end of the session
|
||||
- Periodic writes at an interval to mitigate the scenario where the final write fails
|
||||
- Writes to ensure data is saved while processing a purchase
|
||||
|
||||
- **Efficient Storage:** Storing all of a player's session data in a single table lets you update multiple values atomically and handle the same amount of data in fewer requests. It also removes the risk of inter-value desynchronization and makes rollbacks easier to reason around.
|
||||
|
||||
Some developers also implement custom serialization to compress large data structures (typically to save in-game user-generated content).
|
||||
|
||||
- **Replication:** The client needs regular access to a player's data (for example, to update the UI). A generic approach to replicating player data to the client lets you transmit this information without having to create bespoke replication systems for each component of data. Developers often want the option to be selective about what is and isn't replicated to the client.
|
||||
|
||||
- **Error Handling:** When DataStores cannot be accessed, most solutions will implement a retrying mechanism and a fallback to 'default' data. Special care is needed to ensure fallback data doesn't later overwrite 'real' data, and that this is communicated to the player appropriately.
|
||||
|
||||
- **Retries:** When data stores are inaccessible, most solutions implement a retry mechanism and a fallback to default data. Take special care to ensure that fallback data doesn't later overwrite "real" data, and communicate the situation to the player appropriately.
|
||||
|
||||
- **Session Locking:** If a single player's data is loaded and in-memory on multiple servers, issues can occur in which one server saves out-of-date information. This can lead to data loss and common item duplication loopholes.
|
||||
|
||||
- **Atomic Purchase Handling:** Verify, award, and record purchases atomically to prevent items from being lost or awarded multiple times.
|
||||
|
||||
## Sample code
|
||||
|
||||
<Alert severity="warning">
|
||||
This code is provided for reference purposes only and has not proven itself over a long period of time in a popular experience. It exists so that you can examine its approach to solving various problems and apply them to your own player data systems. Don't use this code in your experience as-is without extensive testing.
|
||||
</Alert>
|
||||
|
||||
Roblox has reference code to assist you with designing and building player data systems. The remainder of this page examines background, implementation details, and general caveats.
|
||||
|
||||
<a href="https://create.roblox.com/store/asset/14376223840/Purchase-Handling)">
|
||||
<Button variant="contained">Get the Code</Button>
|
||||
</a><br />
|
||||
|
||||
After you import the model into Studio, you should see the following folder structure:
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/Sample-Hierarchy.png" alt="Explorer window showing the purchasing system model." width="320" />
|
||||
|
||||
## Architecture
|
||||
|
||||
This high-level diagram illustrates the key systems in the sample and how they interface with code in the rest of the experience.
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/sample-architecture.png" alt="An architecture diagram for the code sample." width="80%" />
|
||||
|
||||
## Retries
|
||||
|
||||
**Class:** [`DataStoreWrapper`](#sample-code)
|
||||
|
||||
### Background
|
||||
|
||||
As `Class.DataStoreService` makes web requests under the hood, its requests are not guaranteed to succeed. When this happens, the `Class.GlobalDataStore|DataStore` methods throw errors, allowing you to handle them.
|
||||
|
||||
A common "gotcha" can occur if you attempt to handle data store failures like this:
|
||||
|
||||
```lua
|
||||
local function retrySetAsync(dataStore, key, value)
|
||||
for _ = 1, MAX_ATTEMPTS do
|
||||
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
|
||||
|
||||
if success then
|
||||
break
|
||||
end
|
||||
|
||||
task.wait(TIME_BETWEEN_ATTEMPTS)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
While this is a perfectly valid retry mechanism for a generic function, it is not suitable for `Class.DataStoreService` requests because it does not guarantee the order in which requests are made. Preserving the order of requests is important for `Class.DataStoreService` requests because they interact with state. Consider the following scenario:
|
||||
|
||||
1. Request A is made to set the value of key `K` to 1.
|
||||
1. The request fails, so a retry is scheduled to run in 2 seconds.
|
||||
1. Before the retry occurs, request B sets the value of `K` to 2, but the retry of request A immediately overwrites this value and sets `K` to 1.
|
||||
|
||||
Even though `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` operates on the latest version of the key's value, `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` requests must still be processed in order to avoid invalid transient states (for example, a purchase subtracts coins before a coin addition gets processed, resulting in negative coins).
|
||||
|
||||
Our player data system uses a new class, `DataStoreWrapper`, which provides yielding retries that are guaranteed to be processed in order per key.
|
||||
|
||||
### Approach
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/retry-diagram.png" alt="An process diagram illustrating the retry system" width="60%" />
|
||||
|
||||
`DataStoreWrapper` provides methods corresponding to the `Class.GlobalDataStore|DataStore` methods: `Class.GlobalDataStore:GetAsync()|DataStore:GetAsync()`, `Class.GlobalDataStore:SetAsync()|DataStore:SetAsync()`, `Class.GlobalDataStore:UpdateAsync()|DataStore:UpdateAsync()` and `Class.GlobalDataStore:RemoveAsync()|DataStore:RemoveAsync()`.
|
||||
|
||||
These methods, when called:
|
||||
|
||||
1. Add the request to a queue. Each key has its own queue, where requests are processed in order and in series. The requesting thread yields until the request has completed.
|
||||
|
||||
This functionality is based on the `ThreadQueue` class, which is a coroutine-based task scheduler and rate limiter. Rather than returning a promise, `ThreadQueue` yields the current thread until the operation is complete and throws an error if it fails. This is more consistent with idiomatic asynchronous Luau patterns.
|
||||
|
||||
1. If a request fails, it retries with a configurable exponential backoff. These retries form part of the callback submitted to the `ThreadQueue`, so they are guaranteed to complete before the next request in the queue for this key begins.
|
||||
|
||||
1. When a request is completed, the request method returns with the `success, result` pattern
|
||||
|
||||
`DataStoreWrapper` also exposes methods to get the queue length for a given key and clear out stale requests. The latter option is particularly useful in scenarios when the server is shutting down and there is no time to process any but the most recent requests.
|
||||
|
||||
### Caveats
|
||||
|
||||
`DataStoreWrapper` follows the principle that, outside of extreme scenarios, every data store request should be allowed to complete (successfully or otherwise), even if a more recent request makes it redundant. When a new request occurs, stale requests aren't removed from the queue, but are instead allowed to complete before the new request is started. The rationale for this is rooted in this module's applicability as a generic data store utility rather than a specific tool for player data, and is as follows:
|
||||
|
||||
1. It's hard to decide on an intuitive set of rules for when a request is safe to remove from the queue. Consider the following queue:
|
||||
|
||||
`Value=0, SetAsync(1), GetAsync(), SetAsync(2)`
|
||||
|
||||
The expected behavior is that `Class.GlobalDataStore:GetAsync()|GetAsync()` would return `1`, but if we remove the `Class.GlobalDataStore:SetAsync()|SetAsync()` request from the queue due to it being made redundant by the most recent one, it would return `0`.
|
||||
|
||||
The logical progression is that when a new write request is added, only prune stale requests as far back as the most recent read request. `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()`, by far the most common operation (and the only one used by this system), can both read and write, so it would be difficult to reconcile within this design this without adding extra complexity.
|
||||
|
||||
`DataStoreWrapper` could require you to specify whether an `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` request was permitted to read and/or write, but it would have no applicability to our player data system, where this cannot be determined ahead of time due to the session locking mechanism (covered in more detail later).
|
||||
|
||||
2. Once removed from the queue, it's hard to decide on an intuitive rule for _how_ this should be handled. When a `DataStoreWrapper` request is made, the current thread is yielded until it is completed. If we removed stale requests from the queue, we would have to decide whether to return `false, "Removed from queue"` or to never return and discard the active thread. Both approaches come with their own drawbacks and offload additional complexity onto the consumer.
|
||||
|
||||
Ultimately, our view is that the simple approach (processing every request) is preferable here and creates a clearer environment to navigate in when approaching complex issues like session locking. The only exception to this is during `Class.DataModel:BindToClose()`, where clearing the queue becomes necessary to save all users' data in time and the value individual function calls return is no longer an ongoing concern. To account for this, we expose a `skipAllQueuesToLastEnqueued` method. For more context, see [Player Data](#player-data).
|
||||
|
||||
## Session locking
|
||||
|
||||
**Class:** [`SessionLockedDataStoreWrapper`](#sample-code)
|
||||
|
||||
### Background
|
||||
|
||||
Player data is stored in memory on the server and is only read from and written to the underlying data stores when necessary. You can read and update in-memory player data instantly without needing web requests and avoid exceeding `Class.DataStoreService` limits.
|
||||
|
||||
For this model to work as intended, it is imperative that no more than one server is able to load a player's data into memory from the `Class.GlobalDataStore|DataStore` at the same time.
|
||||
|
||||
For example, if server A loads a player's data, server B can't load that data until server A releases its lock on it during a final save. Without a locking mechanism, server B could load out-of-date player data from the data store before server A has a chance to save the more recent version that it has in memory. Then if server A saves its newer data after server B loads the out-of-date data, server B would overwrite that newer data during its next save.
|
||||
|
||||
Even though Roblox only allows a client to be connected to one server at a time, you can't assume that data from one session is always saved before the next session starts. Consider the following scenarios that can occur when a player leaves server A:
|
||||
|
||||
1. Server A makes a `Class.GlobalDataStore|DataStore` request to save their data, but the request fails and requires several retries to successfully complete. During the retry period, the player joins server B.
|
||||
2. Server A makes too many `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` calls to the same key and gets throttled. The final save request is placed in a queue. While the request is in the queue, the player joins server B.
|
||||
3. On server A, some code connected to the `Class.Players.PlayerRemoving|PlayerRemoving` event yields before the player's data is saved. Before this operation completes, the player joins server B.
|
||||
4. The performance of server A has degraded to the point that the final save is delayed until after the player joins server B.
|
||||
|
||||
These scenarios should be rare, but they do occur, particularly in situations where a player disconnects from one server and connects to another in rapid succession (for example, while teleporting). Some malicious users might even attempt to abuse this behavior to complete actions without them persisting. This can be particularly impactful in experiences that allow players to trade and is a common source of item duplication exploits.
|
||||
|
||||
Session locking addresses this vulnerability by ensuring that when a player's `Class.GlobalDataStore|DataStore` key is first read by the server, the server atomically writes a lock to the key's metadata inside the same `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` call. If this lock value is present when any other server attempts to read or write the key, the server does not proceed.
|
||||
|
||||
### Approach
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/session-lock-diagram.png" alt="An process diagram illustrating the session locking system" max-width="80%" />
|
||||
|
||||
`SessionLockedDataStoreWrapper` is a meta-wrapper around the `DataStoreWrapper` class. `DataStoreWrapper` provides queueing and retrying functionality, which `SessionLockedDataStoreWrapper` supplements with session locking.
|
||||
|
||||
`SessionLockedDataStoreWrapper` passes every `Class.GlobalDataStore|DataStore` request—regardless of whether it is `Class.GlobalDataStore:GetAsync()|GetAsync`, `Class.GlobalDataStore:SetAsync()|SetAsync` or `Class.GlobalDataStore:UpdateAsync()|UpdateAsync`—through `Class.GlobalDataStore:UpdateAsync()|UpdateAsync`. This is because `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` allows a key to be both read and written to atomically. It is also possible to abandon the write based on the value read by returning `nil` in the transformation callback.
|
||||
|
||||
The transformation function passed into `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` for each request performs the following operations:
|
||||
|
||||
1. Verifies the key is safe to access, abandoning the operation if it is not. "Safe to access" means:
|
||||
|
||||
- The key's metadata object does not include an unrecognized `LockId` value that was last updated less than the lock expiry time ago. This accounts for respecting a lock placed by another server and for ignoring that lock if it expired.
|
||||
|
||||
- If this server has placed its own `LockId` value in the key's metadata previously, then this value is still in the key's metadata. This accounts for the situation where another server has taken over this server's lock (by expiry or by force) and later released it. Alternatively phrased, even if `LockId` is `nil`, another server could still have replaced and removed a lock in the time since you locked the key.
|
||||
|
||||
2. `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` performs the `Class.GlobalDataStore|DataStore` operation the consumer of `SessionLockedDataStoreWrapper` requested. For example, `Class.GlobalDataStore:GetAsync()|GetAsync()` translates to `function(value) return value end`.
|
||||
|
||||
3. Depending on the parameters passed into the request, `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` either locks or unlocks the key:
|
||||
|
||||
1. If the key is to be locked, `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` sets the `LockId` in the key's metadata to a GUID. This GUID is stored in-memory on the server so it can be verified the next time it accesses the key. If the server already has a lock on this key, it makes no changes. It also schedules a task to warn you if you don't access the key again to maintain the lock within the lock's expiration time.
|
||||
|
||||
2. If the key is to be unlocked, `Class.GlobalDataStore:UpdateAsync()|UpdateAsync` removes the `LockId` in the key's metadata.
|
||||
|
||||
A custom retry handler is passed into the underlying `DataStoreWrapper` so that the operation is retried if it was aborted at step 1 due to the session being locked.
|
||||
|
||||
A custom error message is also returned to the consumer, allowing the player data system to report an alternative error in the case of session locking to the client.
|
||||
|
||||
### Caveats
|
||||
|
||||
The session locking regime relies on a server always releasing its lock on a key when it is done with it. This should always happen through an instruction to unlock the key as part of the final write in `Class.Players.PlayerRemoving|PlayerRemoving` or `Class.DataModel:BindToClose()|BindToClose()`.
|
||||
|
||||
However, the unlock can fail in certain situations. For example:
|
||||
|
||||
- The server crashed or `Class.DataStoreService` was inoperable for all attempts to access the key.
|
||||
- Due to an error in logic or similar bug, the instruction to unlock the key was not made.
|
||||
|
||||
To maintain the lock on a key, you must regularly access it for as long as it is loaded in memory. This would normally be done as part of the auto-save loop running in the background in most player data systems, but this system also exposes a `refreshLockAsync` method if you need to do it manually.
|
||||
|
||||
If the lock expiry time has been exceeded without the lock being updated, then any server is free to take over the lock. If a different server takes the lock, attempts by the current server to read or write the key fail unless it establishes a new lock.
|
||||
|
||||
## Developer Product processing
|
||||
|
||||
**Singleton:** [`ReceiptHandler`](#sample-code)
|
||||
|
||||
### Background
|
||||
|
||||
The `ProcessReceipt` callback performs the critical job of determining when to finalize a purchase. `ProcessReceipt` is called in very specific scenarios. For its set of guarantees, see `Class.MarketplaceService.ProcessReceipt`.
|
||||
|
||||
Although the definition of "handling" a purchase can differ between experiences, we use the following criteria
|
||||
|
||||
1. The purchase has not previously been handled.
|
||||
1. The purchase is reflected in the current session.
|
||||
1. The purchase has been saved to a `Class.GlobalDataStore|DataStore`.
|
||||
|
||||
Every purchase, even one-time consumables, should be reflected in the `Class.GlobalDataStore|DataStore` so users' purchase history is included with their session data.
|
||||
|
||||
This requires conducting the following operations before returning `PurchaseGranted`:
|
||||
|
||||
1. Verify the `PurchaseId` has not already been recorded as handled.
|
||||
2. Award the purchase in the player's in-memory player data.
|
||||
3. Record the `PurchaseId` as handled in the player's in-memory player data.
|
||||
4. Write the player's in-memory player data to the `Class.GlobalDataStore|DataStore`.
|
||||
|
||||
Session locking simplifies this flow, as you no longer need to worry about the following scenarios:
|
||||
|
||||
- The in-memory player data in the current server potentially being out-of-date, requiring you to fetch the latest value from the `Class.GlobalDataStore|DataStore` before verifying the `PurchaseId` history
|
||||
- The callback for the same purchase running in another server, requiring you to both read and write the `PurchaseId` history and save the updated player data with the purchase reflected atomically to prevent race conditions
|
||||
|
||||
Session locking guarantees that, if an attempt to write to the player's `Class.GlobalDataStore|DataStore` is successful, no other server has successfully read or written to the player's `Class.GlobalDataStore|DataStore` between the data being loaded and saved in this server. In short, the in-memory player data in this server is the most up-to-date version available. There are some caveats, but they do not impact this behavior.
|
||||
|
||||
### Approach
|
||||
|
||||
The comments in `ReceiptProcessor` outline the approach:
|
||||
|
||||
1. Verify that the player's data is currently loaded on this server and that it loaded without any errors.
|
||||
|
||||
Because this system uses session locking, this check also verifies that the in-memory data is the most up-to-date version.
|
||||
|
||||
If the player's data hasn't loaded yet (which is expected when a player joins a game), wait for the player's data to load. The system also listens for the player leaving the experience before their data loads, as it should not yield indefinitely and block this callback from being invoked again on this server for this purchase if the player rejoins.
|
||||
|
||||
1. Verify the `PurchaseId` is not already recorded as processed in the player data.
|
||||
|
||||
Due to session locking, the array of `PurchaseIds` the system has in memory is the most up-to-date version. If the `PurchaseId` is recorded as processed and reflected in a value that has been loaded to or saved to the `Class.GlobalDataStore|DataStore`, return `PurchaseGranted`. If it is recorded as processed, but _not_ reflected in the `Class.GlobalDataStore|DataStore`, return `NotProcessedYet`.
|
||||
|
||||
1. Update the Player Data locally in this server to "award" the purchase.
|
||||
|
||||
`ReceiptProcessor` takes a generic callback approach and assigns a different callback for each `DeveloperProductId`.
|
||||
|
||||
<Alert severity="warning">
|
||||
As soon as this step completes, the player receives access to the purchase in the current session. This means that if the player's data fails to save in step 5, the player receives the purchase for free for the duration of the session.
|
||||
</Alert>
|
||||
|
||||
1. Update the player data locally in this server to store the `PurchaseId`.
|
||||
|
||||
1. Submit a request to save the in-memory data to the `Class.GlobalDataStore|DataStore`, returning `PurchaseGranted` if the request is successful. If not, return `NotProcessedYet`.
|
||||
|
||||
If this save request is not successful, a later request to save the player's in-memory session data could still succeed. During the next `ProcessReceipt` call, step 2 handles this situation and returns `PurchaseGranted`.
|
||||
|
||||
## Player data
|
||||
|
||||
**Singletons:** [`PlayerData.Server`](#sample-code), [`PlayerData.Client`](#sample-code)
|
||||
|
||||
### Background
|
||||
|
||||
Modules that provide an interface for code to synchronously read and write player session data are common in Roblox experiences. This section covers `PlayerData.Server` and `PlayerData.Client`.
|
||||
|
||||
### Approach
|
||||
|
||||
`PlayerData.Server` and `PlayerData.Client` handle the following:
|
||||
|
||||
1. Loading the player's data into memory, including handling cases in which it fails to load
|
||||
2. Providing an interface for server code to query and change the player data
|
||||
3. Replicating changes in the player's data to the client so that client code can access it
|
||||
4. Replicating loading and/or saving errors to the client so that it can show error dialogs
|
||||
5. Saving the player's data periodically, when the player leaves, and when the server shuts down
|
||||
|
||||
#### Load player data
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/data-load-diagram.png" alt="An process diagram illustrating the loading system" width="60%" />
|
||||
|
||||
1. `SessionLockedDataStoreWrapper` makes a `getAsync` request to the data store.
|
||||
|
||||
If this request fails, the default data is used and the profile is marked as "errored" to ensure it is not written to the data store later.
|
||||
|
||||
An alternative option is to kick the player, but we recommend letting the player play with default data and clear messaging as to what occurred rather than removing them from the experience.
|
||||
|
||||
1. An initial payload is sent to `PlayerDataClient` containing the loaded data and the error status (if any).
|
||||
|
||||
1. Any threads yielded using `waitForDataLoadAsync` for the player are resumed.
|
||||
|
||||
#### Provide an interface for server code
|
||||
|
||||
- `PlayerDataServer` is a singleton that can be required and accessed by any server code running in the same environment.
|
||||
- Player data is organized into a dictionary of keys and values. You can manipulate these values on the server using the `setValue`, `getValue`, `updateValue` and `removeValue` methods. These methods all operate synchronously without yielding.
|
||||
- The `hasLoaded` and `waitForDataLoadAsync` methods are available to ensure the data has loaded before you access it. We recommend doing this once during a loading screen before other systems are started to avoid having to check for load errors before every interaction with data on the client.
|
||||
- A `hasErrored` method can query if the player's initial load failed, causing them to use default data. Check this method before allowing the player to make any purchases, as purchases cannot be saved to data without a successful load.
|
||||
- A `playerDataUpdated` signal fires with the `player`, `key`, and `value` whenever a player's data is changed. Individual systems can subscribe to this.
|
||||
|
||||
#### Replicate changes to the client
|
||||
|
||||
- Any change to player data in `PlayerDataServer` is replicated to `PlayerDataClient`, unless that key was marked as private using setValueAsPrivate
|
||||
- `setValueAsPrivate` is used to denote keys that should not be sent to the client
|
||||
- `PlayerDataClient` includes a method to get the value of a key (get) and a signal that fires when it is updated (updated). A `hasLoaded` method and a `loaded` signal are also included, so the client can wait for data to load & replicate before starting its systems
|
||||
- `PlayerDataClient` is a singleton that can be required and accessed by any client code running in the same environment
|
||||
|
||||
#### Replicate errors to the client
|
||||
|
||||
- Error statuses encountered when saving or loading player data are replicated to `PlayerDataClient`.
|
||||
- Access this information with the `getLoadError` and `getSaveError` methods, along with the `loaded` and `saved` signals.
|
||||
- There are two types of errors: `DataStoreError` (the `Class.DataStoreService` request failed) and `SessionLocked` (see [Session Locking](#session-locking)).
|
||||
- Use these events to disable client purchase prompts and implement warning dialogs. This image shows an example dialog:
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/data-warning.png" alt="A screenshot of an example warning that could be shown when player data fails to load" width="60%" />
|
||||
|
||||
#### Save player data
|
||||
|
||||
<img src="../../assets/data/player-data-purchasing/data-save-diagram.png" alt="A process diagram illustrating the saving system" width="60%" />
|
||||
|
||||
1. When the player leaves the experience, the system takes the following steps:
|
||||
|
||||
1. Check if it is safe to write the player's data to the data store. Scenarios where it would be unsafe include the player's data failing to load or still undergoing loading.
|
||||
1. Make a request through the `SessionLockedDataStoreWrapper` to write the current in-memory data value to the data store and remove the session lock once complete.
|
||||
1. Clears the player's data (and other variables such as metadata and error statuses) from server memory.
|
||||
|
||||
1. On a periodic loop, the server writes each player's data to the data store (provided it is safe to save). This welcome redundancy mitigates loss in case of a server crash and is also necessary to maintain the session lock.
|
||||
|
||||
1. When a request to shutdown the server is received, the following occurs in a `Class.DataModel:BindToClose()|BindToClose` callback:
|
||||
|
||||
1. A request is made to save each player's data in the server, following the process normally gone through when a player leaves the server. These requests are made in parallel, as `Class.DataModel:BindToClose()|BindToClose` callbacks only have 30 seconds to complete.
|
||||
1. To expedite the saves, all other requests in each key's queue are cleared from the underlying `DataStoreWrapper` (see [Retries](#retries)).
|
||||
1. The callback doesn't return until all requests have completed.
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
---
|
||||
title: Data store versioning, listing, and caching
|
||||
description: How to manage data stores (DataStores) through versioning, listing, and caching.
|
||||
---
|
||||
|
||||
Manage your data using versioning, listing, and caching.
|
||||
|
||||
## Versioning
|
||||
|
||||
Versioning happens when you [set](./index.md#create-data), [update](./index.md#update-data), and [increment](./index.md#increment-data) data. The functions `Class.GlobalDataStore:SetAsync()|SetAsync()`, `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()`, and `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()` create versioned backups of your data using the first write to each key in each UTC hour. Successive writes to a key in the same UTC hour permanently overwrite the previous data.
|
||||
|
||||
Versioned backups expire 30 days after a new write overwrites them. The latest version never expires.
|
||||
|
||||
The following functions perform versioning operations:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
`Class.DataStore:ListVersionsAsync()|ListVersionsAsync()`
|
||||
</td>
|
||||
<td>
|
||||
Lists all versions for a key by returning a `Class.DataStoreVersionPages` instance that you can use to enumerate all version numbers. You can filter versions using a time range.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
`Class.DataStore:GetVersionAsync()|GetVersionAsync()`
|
||||
</td>
|
||||
<td>
|
||||
Retrieves a specific version of a key using the key's version number.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
`Class.DataStore:RemoveVersionAsync()|RemoveVersionAsync()`
|
||||
</td>
|
||||
<td>
|
||||
Deletes a specific version of a key.
|
||||
|
||||
This function also creates a tombstone version while retaining the previous version. For example, if you call `RemoveAsync("User_1234")` and then try to call `GetAsync("User_1234")`, you get `nil` back. However, you can still use `Class.DataStore:ListVersionsAsync()|ListVersionsAsync()` and `Class.DataStore:GetVersionAsync()|GetVersionAsync()` to retrieve older versions of the data.
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
You can use versioning to handle user requests. If a user reports that a problem occurred at 2020-10-09T01:42, you can revert data to a previous version using the following example:
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
|
||||
|
||||
local DATA_STORE_KEY = "User_1234"
|
||||
|
||||
local maxDate = DateTime.fromUniversalTime(2020, 10, 09, 01, 42)
|
||||
|
||||
-- Gets the version closest to the given time
|
||||
local listSuccess, pages = pcall(function()
|
||||
return experienceStore:ListVersionsAsync(DATA_STORE_KEY, Enum.SortDirection.Descending, nil, maxDate.UnixTimestampMillis)
|
||||
end)
|
||||
if listSuccess then
|
||||
local items = pages:GetCurrentPage()
|
||||
if #items > 0 then
|
||||
-- Reads the closest version
|
||||
local closestEntry = items[1]
|
||||
local success, value, info = pcall(function()
|
||||
return experienceStore:GetVersionAsync(DATA_STORE_KEY, closestEntry.Version)
|
||||
end)
|
||||
-- Restores current value by overwriting it with the closest version
|
||||
if success then
|
||||
local setOptions = Instance.new("DataStoreSetOptions")
|
||||
setOptions:SetMetadata(info:GetMetadata())
|
||||
experienceStore:SetAsync(DATA_STORE_KEY, value, nil, setOptions)
|
||||
end
|
||||
else
|
||||
-- No entries found
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Snapshots
|
||||
|
||||
The [Snapshot Data Stores Open Cloud API](/cloud/reference/DataStore#Cloud_SnapshotDataStores) lets you take a snapshot of all data stores in an experience once a day. Before you publish any experience update that changes your data storage logic, make sure to take a snapshot. Taking a snapshot guarantees that you have the most recent data available from the previous version of the experience.
|
||||
|
||||
For example, without a snapshot, if you publish an update at 3:30 UTC that causes data corruption, the corrupted data overwrites any data written between 3:00-3:30 UTC. If you take a snapshot at 3:29 UTC, though, the corrupted data doesn't overwrite anything written before 3:29 UTC, and the latest data for all keys written between 3:00-3:29 UTC is preserved.
|
||||
|
||||
## Listing and prefixes
|
||||
|
||||
Data stores let you list by prefix. For example, listing by the first **n** characters of a name, like "d" , "do", or "dog" for any key or data store with a prefix of "dog".
|
||||
|
||||
You can specify a prefix when listing all data stores or keys, and get back only objects that match that prefix. Both `Class.DataStoreService:ListDataStoresAsync()|ListDataStoresAsync()` and `Class.DataStore:ListKeysAsync()|ListKeysAsync()` functions return a `Class.DataStoreListingPages` object that you can use to enumerate the list.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`Class.DataStoreService:ListDataStoresAsync()|ListDataStoresAsync()`</td>
|
||||
<td>Lists all data stores.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.DataStore:ListKeysAsync()|ListKeysAsync()`</td>
|
||||
<td>Lists all keys in a data store.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Scopes
|
||||
|
||||
<Alert severity="warning">
|
||||
For new experiences, use [listing and prefixes](#listing-and-prefixes) to organize keys in your data store instead of the legacy scopes feature. For existing experiences that use scopes, continue using them.
|
||||
</Alert>
|
||||
|
||||
You can organize keys in a data store further by setting a unique string as a scope for the second parameter of `Class.DataStoreService:GetDataStore()|GetDataStore()`. The default scope (if no scope is given) is `global`. The scope is automatically prepended to the beginning of all keys in all operations done on the data store.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Scope</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`houses/User_1234`</td>
|
||||
<td>houses</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`pets/User_1234`</td>
|
||||
<td>pets</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`inventory/User_1234`</td>
|
||||
<td>inventory</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The combination of data store name, scope, and key uniquely identifies a key. All three values are required to identify a key with a scope. For example, you can read a `global`-scoped key named `User_1234` as:
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
local inventoryStore = DataStoreService:GetDataStore("PlayerInventory")
|
||||
local success, currentGold = pcall(function()
|
||||
return inventoryStore:GetAsync("User_1234")
|
||||
end)
|
||||
```
|
||||
|
||||
If the key `User_1234` has a scope of `gold`, though, you can only read it as:
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
local inventoryStore = DataStoreService:GetDataStore("PlayerInventory", "gold")
|
||||
local success, currentGold = pcall(function()
|
||||
return inventoryStore:GetAsync("User_1234")
|
||||
end)
|
||||
```
|
||||
|
||||
### AllScopes property
|
||||
|
||||
`Class.DataStoreOptions` contains an `Class.DataStoreOptions.AllScopes|AllScopes` property that lets you return keys from all [scopes](#scopes) in a list. You can then use the `Class.DataStoreKey.KeyName|KeyName` property of a list item for common data store operations like [reading data](./index.md#reading-data) with `Class.GlobalDataStore:GetAsync()|GetAsync()` and [removing data](./index.md#removing-data) with `Class.GlobalDataStore:RemoveAsync()|RemoveAsync()`.
|
||||
|
||||
When you use the `AllScopes` property, the second parameter of `Class.DataStoreService:GetDataStore()|GetDataStore()` must be an empty string (`""`).
|
||||
|
||||
```lua
|
||||
local DataStoreService = game:GetService("DataStoreService")
|
||||
|
||||
local options = Instance.new("DataStoreOptions")
|
||||
options.AllScopes = true
|
||||
|
||||
local ds = DataStoreService:GetDataStore("DS1", "", options)
|
||||
```
|
||||
|
||||
<Alert severity="info">
|
||||
When you use the `Class.DataStoreOptions.AllScopes|AllScopes` property, `Class.DataStore:ListKeysAsync()|ListKeysAsync()` returns every key with their scope as the prefix argument, such as `global/player_data_1234` or `houses/house3`. Remember that the default scope is `global`.
|
||||
</Alert>
|
||||
|
||||
If you enable the `Class.DataStoreOptions.AllScopes|AllScopes` property and create a new key in the data store, you must always specify a scope for that key in the format of scope/keyname. If you don't, the APIs throw an error. For example, `gold/player_34545` is acceptable with gold as the scope, but `player_34545` leads to an error.
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`global/K1`</td>
|
||||
<td>`house/K1`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`global/L2`</td>
|
||||
<td>`house/L2`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`global/M3`</td>
|
||||
<td>`house/M3`</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Caching
|
||||
|
||||
Use caching to temporarily store data from data stores to improve performance and reduce the number of requests made to the server. For example, an experience can cache a copy of its data so that it can access that data quickly without having to make another call to the data store.
|
||||
|
||||
Caching applies to modifications you make to data store keys using:
|
||||
|
||||
- `Class.GlobalDataStore:GetAsync()|GetAsync()` to [read data](./index.md#reading-data).
|
||||
- `Class.GlobalDataStore:SetAsync()|SetAsync()` to [create data](./index.md#creating-data).
|
||||
- `Class.GlobalDataStore:UpdateAsync()|UpdateAsync()` to [update data](./index.md#updating-data).
|
||||
- `Class.GlobalDataStore:IncrementAsync()|IncrementAsync()` to [increment data](./index.md#incrementing-data).
|
||||
- `Class.GlobalDataStore:RemoveAsync()|RemoveAsync()` to [remove data](./index.md#removing-data).
|
||||
|
||||
`Class.DataStore:GetVersionAsync()|GetVersionAsync()`, `Class.DataStore:ListVersionsAsync()|ListVersionsAsync()`, `Class.DataStore:ListKeysAsync()|ListKeysAsync()`, and `Class.DataStoreService:ListDataStoresAsync()|ListDataStoresAsync()` don't implement caching and always fetch the latest data from the service backend.
|
||||
|
||||
By default, the engine uses `Class.GlobalDataStore:GetAsync()|GetAsync()` to store values you retrieve from the backend in a local cache for four seconds. Also by default, `Class.GlobalDataStore:GetAsync()|GetAsync()` requests for cached keys return the cached value instead of continuing to the backend. Your `Class.GlobalDataStore:GetAsync()|GetAsync()` requests that return a cached value don't count towards your [server limits](../../cloud-services/data-stores/error-codes-and-limits.md#server-limits) and [throughput limits](../../cloud-services/data-stores/error-codes-and-limits.md#throughput-limits).
|
||||
|
||||
All `Class.GlobalDataStore:GetAsync()|GetAsync()` calls that retrieve a value not being cached from the backend update the cache immediately and restart the four second timer.
|
||||
|
||||
<Alert severity="warning">
|
||||
Caching is local to a particular data store instance, so different data stores can have their caches in different states. For example, if you access a key twice through two different data store instances, like getting one data store with a specified scope and another through having the `Class.DataStoreOptions.AllScopes|AllScopes` property enabled, each data store instance has its own cache. If you change the value of that key in one data store instance and not the other, you end up with inconsistent data.
|
||||
</Alert>
|
||||
|
||||
### Disable caching
|
||||
|
||||
To disable caching and opt out of using the cache to retrieve the most up-to-date value from the servers, add the `Class.DataStoreGetOptions` parameter to your `Class.GlobalDataStore:GetAsync()|GetAsync()` call and set the `Class.DataStoreGetOptions.UseCache|UseCache` property to `false` to make your request ignore any keys in the cache.
|
||||
|
||||
Disabling caching is useful if you have multiple servers writing to a key with high frequency and need to get the latest value from servers. However, it can cause you to consume more of your [data stores limits and quotas](../../cloud-services/data-stores/error-codes-and-limits.md#limits), since `Class.GlobalDataStore:GetAsync()|GetAsync()` requests bypassing caching always count towards your throughput and server limits.
|
||||
|
||||
## Serialization
|
||||
|
||||
The `Class.DataStoreService` stores data in JSON format. When you save Luau data in Studio, Roblox uses a process called serialization to convert that data into JSON to save it in data stores. Roblox then converts your data back to Luau and returns it to you in another process called deserialization.
|
||||
|
||||
Serialization and deserialization support the following Luau data types:
|
||||
|
||||
- [Nil](../../luau/nil.md)
|
||||
- [Booleans](../../luau/booleans.md)
|
||||
- [Numbers](../../luau/numbers.md)
|
||||
- You should not store the special numeric values `inf`, `-inf`, and `nan`, because these values don't conform to JSON standards. You can't access keys that contain these values with Open Cloud.
|
||||
- [Strings](../../luau/strings.md)
|
||||
- [Tables](../../luau/tables.md)
|
||||
- Tables must only contain other supported data types
|
||||
- Numeric keys are translated into strings if the length of the table is 0
|
||||
- [Buffers](/reference/engine/libraries/buffer)
|
||||
|
||||
If you try to store a data type that serialization doesn't support, you either:
|
||||
|
||||
- Fail in storing that data type and get an error message.
|
||||
- Succeed in storing that data type as `nil`.
|
||||
|
||||
To debug why your data type is being stored as `nil`, you can use the `Class.HttpService.JSONEncode|JSONEncode` function. When you pass your Luau data type into this function, you receive it back in the format Roblox would have stored it with data stores, which lets you preview and investigate the returned data.
|
||||
|
||||
<Alert severity="info">
|
||||
Serialization doesn't happen when you use the [DataStore Open Cloud API](/cloud/reference/DataStore) because that data is already sent to Roblox in JSON format and doesn't need to be converted.
|
||||
</Alert>
|
||||
255
.cursor/rules/roblox/en-us/cloud-services/extended-services.md
Normal file
255
.cursor/rules/roblox/en-us/cloud-services/extended-services.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
---
|
||||
title: Extended Services
|
||||
description: Extended Services is a platform that lets you manage your free and paid services.
|
||||
---
|
||||
|
||||
Extended Services is a solution that lets you manage service usage and payment beyond Roblox's default limits.
|
||||
|
||||
## Eligibility requirements
|
||||
|
||||
To use Extended Services, your Roblox account must:
|
||||
|
||||
- Be [ID-verified](../production/publishing/account-verification.md#verify-through-government-id) and 18+ years old.
|
||||
- Be located in a [supported country](#supported-countries).
|
||||
- Have a valid email address associated with it.
|
||||
- Not currently be banned or moderated.
|
||||
- Accept and comply with the [terms of service](https://en.help.roblox.com/hc/articles/37967848292500).
|
||||
|
||||
<Alert severity="info">
|
||||
For group accounts, only the group owner needs to meet the eligibility requirements.
|
||||
</Alert>
|
||||
|
||||
## Unlock Extended Services
|
||||
|
||||
If you meet all of the [eligibility requirements](#eligibility-requirements), you can finish setting up Extended Services by entering your account details and payment information. For group-owned experiences, only the group owner can unlock Extended Services and onboard the experience onto it.
|
||||
|
||||
To unlock and use Extended Services:
|
||||
|
||||
1. In the [Creator Dashboard](https://create.roblox.com/dashboard/creations), go to **Creations** and select an experience.
|
||||
2. Go to **Extended Services**.
|
||||
3. If you haven't entered your account details and payment information yet, click **Add missing information** on the top banner.
|
||||
1. Enter your account information. The **Account type**, **Tax ID type**, and **Tax ID** fields are optional.
|
||||
- You can also go to **Finances** ⟩ **Account Information** under the **Extended Services** section to manage your account information. You don't need to update anything else on this page.
|
||||
2. Click **Next**.
|
||||
3. Enter a payment method. You can enter a new payment method or select from the available payment options already associated with your Roblox account.
|
||||
- You can also go to **Finances** ⟩ **Payments** to add new payment methods, or go to **Finances** ⟩ **Billing** to set a new default payment method.
|
||||
4. Click **Save**.
|
||||
|
||||
<h5>Account status</h5>
|
||||
|
||||
Your Extended Services account can be in one of the following statuses:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th width="15%">**Status**</th>
|
||||
<th width="40%">**Description**</th>
|
||||
<th width="80%">**Required action**</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>**Normal**</td>
|
||||
<td>You can use all Extended Services features.</td>
|
||||
<td>No action is required.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>**Overdue**</td>
|
||||
<td>Your account is overdue because your automatic payment method failed. Roblox offers a grace period of **seven days** before suspending your Extended Services account.</td>
|
||||
<td>Change your payment method and retry a manual payment, or wait until Roblox attempts to charge your payment method again after the grace period has ended.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>**Suspended**</td>
|
||||
<td>You can't use any paid Extended Services. You can, however, still use services with default limits.</td>
|
||||
<td>Fix your eligibility requirements, add a valid payment method, or manually pay the overdue balance on your account.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Enable a service
|
||||
|
||||
To enable a service:
|
||||
|
||||
1. Go to **Extended Services**.
|
||||
2. Toggle on the service you want to enable.
|
||||
3. Enter a monthly budget for each applicable resource under the service. The minimum monthly budget you can set is $0.50 USD.
|
||||
|
||||
Your payment method is charged either at the end of the month when a bill is generated, or when your usage reaches $10 USD, whichever comes first.
|
||||
|
||||
You can increase or decrease a service's monthly budget any time. Your service is automatically throttled if you decrease your budget to an amount below your current month-to-date usage, or if you exceed the budget you have set.
|
||||
|
||||
Roblox notifies you by email when your usage reaches 80% of your budget, and then again when it reaches 100%.
|
||||
|
||||
## Manage billing
|
||||
|
||||
To access the Extended Services billing dashboard:
|
||||
|
||||
1. Go to **Finances** ⟩ **Billing**.
|
||||
2. Select the **Cloud services** tab to display the following:
|
||||
- Your pending balance, including month-to-date cost
|
||||
- Your billing history, including all Extended Services billing activity across your experiences
|
||||
|
||||
For a detailed breakdown of your current pending balance costs, click **View Details** next to your payment method. For a detailed invoice of a previous bill, click **View Bill** under **Billing history**.
|
||||
|
||||
## Service pricing
|
||||
|
||||
See the following table for Extended Services pricing.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>**Service**</th>
|
||||
<th>**Default**</th>
|
||||
<th>**Extended**</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}><a href="https://create.roblox.com/docs/cloud-services/memory-stores" style={{color: 'inherit', textDecoration: 'underline'}}><strong>Memory stores</strong></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Request units</td>
|
||||
<td style={{border: 'none'}}>1000 + (CCU x 120) request units per minute</td>
|
||||
<td style={{border: 'none'}}>$0.003 / 1M request units</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{paddingBottom: '24px'}}>Storage</td>
|
||||
<td style={{paddingBottom: '24px'}}>64KB + (CCU x 1.2KB)</td>
|
||||
<td style={{paddingBottom: '24px'}}>$0.10 / GB per Hour¹</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}><a href="https://create.roblox.com/docs/audio/objects#text-to-speech" style={{color: 'inherit', textDecoration: 'underline'}}><strong>Text-to-speech</strong></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{paddingBottom: '24px'}}>Requests</td>
|
||||
<td style={{paddingBottom: '24px'}}>1 + (CCU x 6) requests per minute</td>
|
||||
<td style={{paddingBottom: '24px'}}>$5.00 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}><a href="https://create.roblox.com/docs/cloud-services/data-stores" style={{color: 'inherit', textDecoration: 'underline'}}><strong>Access to standard data stores</strong> (coming soon)</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Get</td>
|
||||
<td style={{border: 'none'}}>250 + (CCU * 40) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.08 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Set</td>
|
||||
<td style={{border: 'none'}}>250 + (CCU * 20) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.80 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>List</td>
|
||||
<td style={{border: 'none'}}>10 + (CCU * 2) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.60 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Remove</td>
|
||||
<td style={{border: 'none'}}>100 + (CCU * 40) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.80 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}><a href="https://create.roblox.com/docs/cloud-services/data-stores" style={{color: 'inherit', textDecoration: 'underline'}}><strong>Access to ordered data stores</strong> (coming soon)</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Get</td>
|
||||
<td style={{border: 'none'}}>250 + (CCU * 40) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.08 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Set</td>
|
||||
<td style={{border: 'none'}}>250 + (CCU * 20) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.25 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>GetSorted</td>
|
||||
<td style={{border: 'none'}}>100 + (CCU * 2) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.32 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}>Remove</td>
|
||||
<td style={{border: 'none'}}>100 + (CCU * 40) requests per minute</td>
|
||||
<td style={{border: 'none'}}>$0.25 / 1M requests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{border: 'none'}}><a href="https://create.roblox.com/docs/cloud-services/data-stores" style={{color: 'inherit', textDecoration: 'underline'}}><strong>Data store storage</strong> (coming soon)</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{paddingBottom: '24px'}}>Storage</td>
|
||||
<td style={{paddingBottom: '24px'}}>100 MB + (1MB * Lifetime Players) GB per Month</td>
|
||||
<td style={{paddingBottom: '24px'}}>$0.12 / GB per Month</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<figcaption>¹ GB per Hour = the storage consumption multiplied by the amount of time. For example, 100GB Hours might equate to "100GB x 1 hour" or "200GB x 0.5 hours".</figcaption>
|
||||
|
||||
## Supported countries
|
||||
|
||||
Extended Services is available in the following countries.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">Country</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Australia</td>
|
||||
<td>Germany</td>
|
||||
<td>Poland</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Austria</td>
|
||||
<td>Hong Kong</td>
|
||||
<td>Portugal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Belgium</td>
|
||||
<td>Hungary</td>
|
||||
<td>Romania</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Brazil</td>
|
||||
<td>Ireland</td>
|
||||
<td>Singapore</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bulgaria</td>
|
||||
<td>Italy</td>
|
||||
<td>Slovakia</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Croatia</td>
|
||||
<td>Latvia</td>
|
||||
<td>Slovenia</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cyprus</td>
|
||||
<td>Lithuania</td>
|
||||
<td>South Africa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Czechia</td>
|
||||
<td>Malta</td>
|
||||
<td>Spain</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Denmark</td>
|
||||
<td>Mexico</td>
|
||||
<td>Sweden</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Estonia</td>
|
||||
<td>Netherlands</td>
|
||||
<td>Switzerland</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Finland</td>
|
||||
<td>New Zealand</td>
|
||||
<td>Thailand</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>France</td>
|
||||
<td>Norway</td>
|
||||
<td>United States</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
247
.cursor/rules/roblox/en-us/cloud-services/http-service.md
Normal file
247
.cursor/rules/roblox/en-us/cloud-services/http-service.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
---
|
||||
title: In-experience HTTP requests
|
||||
description: Use HttpService to send HTTP requests to third-party web services and Open Cloud.
|
||||
---
|
||||
|
||||
You can use `Class.HttpService` to send generic HTTP requests to third-party web services for use cases like analytics, data storage, or error logging. `Class.HttpService` also supports certain Open Cloud endpoints.
|
||||
|
||||
## Enable HTTP requests
|
||||
|
||||
The `Class.HttpService:GetAsync()`, `Class.HttpService:PostAsync()`, and `Class.HttpService:RequestAsync()` methods aren't enabled by default. To send requests, you must **Allow HTTP Requests** under [Experience Settings](../studio/experience-settings.md) for your experience.
|
||||
|
||||
## Use in plugins
|
||||
|
||||
You can use `Class.HttpService` in Studio plugins to check for updates, download content, or other business logic. The first time a plugin attempts to use the service, the user might be prompted to give the plugin permission to communicate with the particular web address. Users can accept, deny, and revoke these permissions at any time through the **Plugin Management** window.
|
||||
|
||||
Plugins can also communicate with other software running on the same computer through the `localhost` and `127.0.0.1` hosts. By running programs compatible with such plugins, you can extend the functionality of your plugin beyond the normal capabilities of Studio, such as interacting with your computer's file system. Beware that such software must be distributed separately from the plugin itself and can pose security risks.
|
||||
|
||||
## Use with Open Cloud
|
||||
|
||||
`Class.HttpService` can currently call a subset of the Open Cloud endpoints. You can call these endpoints the same way that you'd call any other endpoint via `HttpService`. The only difference is that you must include an Open Cloud API key in the request:
|
||||
|
||||
1. [Create an Open Cloud API key](/cloud/auth/api-keys#create-api-keys).
|
||||
2. [Save the API key to your secrets store](/cloud-services/secrets#add-secrets).
|
||||
3. Make the request.
|
||||
|
||||
The following code sample demonstrates how to update a user's group membership from within an experience:
|
||||
|
||||
```lua
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local groupId = "your_group_id"
|
||||
local membershipId = "your_membership_id"
|
||||
local roleId = "your_role_id"
|
||||
|
||||
local function request()
|
||||
local response = HttpService:RequestAsync({
|
||||
Url = `https://apis.roblox.com/cloud/v2/groups/{groupId}/memberships/{membershipId}`,
|
||||
Method = "PATCH",
|
||||
Headers = {
|
||||
["Content-Type"] = "application/json", -- When sending JSON, set this!
|
||||
["x-api-key"] = HttpService:GetSecret("APIKey"), -- Set in Creator Hub
|
||||
},
|
||||
Body = HttpService:JSONEncode({ role = `groups/{groupId}/roles/{roleId}` }),
|
||||
})
|
||||
|
||||
if response.Success then
|
||||
print("The response was successful:", response.StatusCode, response.StatusMessage)
|
||||
else
|
||||
print("The response returned an error:", response.StatusCode, response.StatusMessage)
|
||||
end
|
||||
print("Response body:\n", response.Body)
|
||||
print("Response headers:\n", HttpService:JSONEncode(response.Headers))
|
||||
end
|
||||
|
||||
-- Wrap the function in pcall() for safety
|
||||
local success, errorMessage = pcall(request)
|
||||
if not success then
|
||||
print("The HTTP request failed to send:", errorMessage)
|
||||
end
|
||||
```
|
||||
|
||||
### Supported Open Cloud endpoints
|
||||
|
||||
The following endpoints are supported. Due to current limitations on `Class.HttpService`, the `..` string is not allowed in URL path parameters to Roblox domains. This means, for example, that data stores and entries containing this string are currently inaccessible from `Class.HttpService`.
|
||||
|
||||
#### Assets
|
||||
|
||||
- [GetAsset](/cloud/features/assets#/default/Assets_GetAsset)
|
||||
- [ListAssetVersions](/cloud/features/assets#/default/listAssetVersions)
|
||||
- [GetAssetVersion](/cloud/features/assets#/default/Assets_GetAssetVersion)
|
||||
|
||||
#### Bans and blocks
|
||||
|
||||
- [ListUserRestrictions (Place)](/cloud/features/bans-and-blocks#/UserRestriction/Cloud_ListUserRestrictions__Using_Universes)
|
||||
- [GetUserRestriction (Place)](/cloud/features/bans-and-blocks#/UserRestriction/Cloud_GetUserRestriction__Using_Universes_Places)
|
||||
- [UpdateUserRestriction (Place)](/cloud/features/bans-and-blocks#/UserRestriction/Cloud_UpdateUserRestriction__Using_Universes_Places)
|
||||
- [ListUserRestrictions (Universe)](/cloud/features/bans-and-blocks#/UserRestriction/Cloud_ListUserRestrictions)
|
||||
- [GetUserRestriction (Universe)](/docs/cloud/features/bans-and-blocks#/UserRestriction/Cloud_GetUserRestriction__Using_Universes)
|
||||
- [UpdateUserRestriction (Universe)](/cloud/features/bans-and-blocks#/UserRestriction/Cloud_UpdateUserRestriction__Using_Universes)
|
||||
- [ListUserRestrictionLogs (Universe)](https://create.roblox.com/docs/cloud/features/bans-and-blocks#/UserRestriction/Cloud_ListUserRestrictionLogs)
|
||||
|
||||
#### Creator Store
|
||||
|
||||
- [CreateCreatorStoreProduct](/cloud/features/creator-store#/CreatorStoreProduct/Cloud_CreateCreatorStoreProduct)
|
||||
- [GetCreatorStoreProduct](/cloud/features/creator-store#/CreatorStoreProduct/Cloud_GetCreatorStoreProduct)
|
||||
- [UpdateCreatorStoreProduct](/cloud/features/creator-store#/CreatorStoreProduct/Cloud_UpdateCreatorStoreProduct)
|
||||
- [CreatorStoreAssetsSearch](/cloud/features/creator-store#toolbox-service)
|
||||
|
||||
#### Data and memory stores
|
||||
|
||||
Data stores:
|
||||
|
||||
- [ListDataStores](/cloud/reference/DataStore#Cloud_ListDataStores)
|
||||
- [SnapshotDataStores](/cloud/features/storage#/DataStore/Cloud_SnapshotDataStores)
|
||||
- [ListDataStoreEntries](/cloud/reference/DataStoreEntry#Cloud_ListDataStoreEntries__Using_Universes)
|
||||
- [CreateDataStoreEntry](/cloud/reference/DataStoreEntry#Cloud_CreateDataStoreEntry__Using_Universes)
|
||||
- [GetDataStoreEntry](/cloud/reference/DataStoreEntry#Cloud_GetDataStoreEntry__Using_Universes_DataStores)
|
||||
- [DeleteDataStoreEntry](/cloud/reference/DataStoreEntry#Cloud_DeleteDataStoreEntry__Using_Universes_DataStores)
|
||||
- [UpdateDataStoreEntry](/cloud/reference/DataStoreEntry#Cloud_UpdateDataStoreEntry__Using_Universes_DataStores)
|
||||
- [IncrementDataStoreEntry](/cloud/reference/DataStoreEntry#Cloud_IncrementDataStoreEntry__Using_Universes_DataStores)
|
||||
- [ListDataStoreEntryRevisions](/cloud/features/storage#/DataStoreEntry/Cloud_ListDataStoreEntryRevisions__Using_Universes_DataStores)
|
||||
- [ListDataStoreEntries (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_ListDataStoreEntries__Using_Universes_DataStores)
|
||||
- [CreateDataStoreEntry (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_CreateDataStoreEntry__Using_Universes_DataStores)
|
||||
- [GetDataStoreEntry (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_GetDataStoreEntry__Using_Universes_DataStores_Scopes)
|
||||
- [DeleteDataStoreEntry (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_DeleteDataStoreEntry__Using_Universes_DataStores_Scopes)
|
||||
- [UpdateDataStoreEntry (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_UpdateDataStoreEntry__Using_Universes_DataStores_Scopes)
|
||||
- [IncrementDataStoreEntry (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_IncrementDataStoreEntry__Using_Universes_DataStores_Scopes)
|
||||
- [ListDataStoreEntryRevisions (With Scope)](/cloud/features/storage#/DataStoreEntry/Cloud_ListDataStoreEntryRevisions__Using_Universes_DataStores_Scopes)
|
||||
|
||||
Memory stores:
|
||||
|
||||
- [CreateMemoryStoreQueueItem](/cloud/features/storage#/MemoryStoreQueueItem/Cloud_CreateMemoryStoreQueueItem)
|
||||
- [DiscardMemoryStoreQueueItems](/cloud/features/storage#/MemoryStoreQueueItem/Cloud_DiscardMemoryStoreQueueItems)
|
||||
- [ReadMemoryStoreQueueItems](/cloud/features/storage#/MemoryStoreQueueItem/Cloud_ReadMemoryStoreQueueItems)
|
||||
- [ListMemoryStoreSortedMapItems](/cloud/features/storage#/MemoryStoreSortedMapItem/Cloud_ListMemoryStoreSortedMapItems)
|
||||
- [CreateMemoryStoreSortedMapItem](/cloud/features/storage#/MemoryStoreSortedMapItem/Cloud_CreateMemoryStoreSortedMapItem)
|
||||
- [GetMemoryStoreSortedMapItem](/cloud/features/storage#/MemoryStoreSortedMapItem/Cloud_GetMemoryStoreSortedMapItem)
|
||||
- [DeleteMemoryStoreSortedMapItem](/cloud/features/storage#/MemoryStoreSortedMapItem/Cloud_DeleteMemoryStoreSortedMapItem)
|
||||
- [UpdateMemoryStoreSortedMapItem](/cloud/features/storage#/MemoryStoreSortedMapItem/Cloud_UpdateMemoryStoreSortedMapItem)
|
||||
- [FlushMemoryStore](/cloud/features/storage#/MemoryStore/Cloud_FlushMemoryStore)
|
||||
|
||||
Ordered data stores:
|
||||
|
||||
- [ListOrderedDataStoreEntries](/cloud/features/storage#/OrderedDataStoreEntry/Cloud_ListOrderedDataStoreEntries)
|
||||
- [CreateOrderedDataStoreEntry](/cloud/features/storage#/OrderedDataStoreEntry/Cloud_CreateOrderedDataStoreEntry)
|
||||
- [GetOrderedDataStoreEntry](/cloud/features/storage#/OrderedDataStoreEntry/Cloud_GetOrderedDataStoreEntry)
|
||||
- [DeleteOrderedDataStoreEntry](/cloud/features/storage#/OrderedDataStoreEntry/Cloud_DeleteOrderedDataStoreEntry)
|
||||
- [UpdateOrderedDataStoreEntry](/cloud/features/storage#/OrderedDataStoreEntry/Cloud_UpdateOrderedDataStoreEntry)
|
||||
- [IncrementOrderedDataStoreEntry](/cloud/features/storage#/OrderedDataStoreEntry/Cloud_IncrementOrderedDataStoreEntry)
|
||||
|
||||
#### Groups
|
||||
|
||||
- [GetGroup](/cloud/reference/Group#Cloud_GetGroup)
|
||||
- [ListGroupJoinRequests](/cloud/reference/GroupJoinRequest#Cloud_ListGroupJoinRequests)
|
||||
- [AcceptGroupJoinRequest](/cloud/reference/GroupJoinRequest#Cloud_AcceptGroupJoinRequest)
|
||||
- [DeclineGroupJoinRequest](/cloud/reference/GroupJoinRequest#Cloud_DeclineGroupJoinRequest)
|
||||
- [ListGroupMemberships](/cloud/reference/GroupMembership#Cloud_ListGroupMemberships)
|
||||
- [UpdateGroupMembership](/cloud/reference/GroupMembership#Cloud_UpdateGroupMembership)
|
||||
- [ListGroupRoles](/cloud/reference/GroupRole#Cloud_ListGroupRoles)
|
||||
- [GetGroupRole](/cloud/reference/GroupRole#Cloud_GetGroupRole)
|
||||
- [GetGroupShout](/cloud/reference/GroupShout#Cloud_GetGroupShout)
|
||||
|
||||
#### Inventories
|
||||
|
||||
- [ListInventoryItems](/cloud/reference/InventoryItem#Cloud_ListInventoryItems)
|
||||
|
||||
#### Luau execution
|
||||
|
||||
- [CreateLuauExecutionSessionTaskBinaryInput](/cloud/features/luau-execution#/LuauExecutionSessionTaskBinaryInput/Cloud_CreateLuauExecutionSessionTaskBinaryInput)
|
||||
- [CreateLuauExecutionSessionTask](/cloud/features/luau-execution#/LuauExecutionSessionTask/Cloud_CreateLuauExecutionSessionTask__Using_Universes)
|
||||
- [CreateLuauExecutionSessionTask (with version)](/cloud/features/luau-execution#/LuauExecutionSessionTask/Cloud_CreateLuauExecutionSessionTask__Using_Universes_Places)
|
||||
- [GetLuauExecutionSessionTask](/cloud/features/luau-execution#/LuauExecutionSessionTask/Cloud_GetLuauExecutionSessionTask)
|
||||
- [ListLuauExecutionSessionTaskLogs](/cloud/features/luau-execution#/LuauExecutionSessionTaskLog/Cloud_ListLuauExecutionSessionTaskLogs)
|
||||
|
||||
#### Notifications
|
||||
|
||||
- [CreateUserNotification](/cloud/features/notifications#/UserNotification/Cloud_CreateUserNotification)
|
||||
|
||||
#### Places
|
||||
|
||||
- [GetPlace](/cloud/features/places#/Place/Cloud_GetPlace)
|
||||
- [UpdatePlace](/cloud/features/places#/Place/Cloud_UpdatePlace)
|
||||
- [GetInstance](/cloud/features/places#/Instance/Cloud_GetInstance)
|
||||
- [UpdateInstance](/cloud/features/places#/Instance/Cloud_UpdateInstance)
|
||||
|
||||
#### Universes
|
||||
|
||||
- [UpdateUniverse](/cloud/features/universes#/Universe/Cloud_UpdateUniverse)
|
||||
- [GetUniverse](/cloud/features/universes#/Universe/Cloud_GetUniverse)
|
||||
- [PublishUniverseMessage](/cloud/features/universes#/Universe/Cloud_PublishUniverseMessage)
|
||||
- [RestartUniverseServers](/cloud/features/universes#/Universe/Cloud_RestartUniverseServers)
|
||||
|
||||
#### Users
|
||||
|
||||
- [GetUser](/cloud/features/users#/User/Cloud_GetUser)
|
||||
- [GenerateUserThumbnail](/cloud/features/users#/User/Cloud_GenerateUserThumbnail)
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only the `x-api-key` and `content-type` headers are allowed.
|
||||
- The `x-api-key` header must be a `Datatype.Secret`. See [Secrets stores](./secrets.md).
|
||||
- The `".."` string is not allowed in URL path parameters.
|
||||
- Only the HTTPS protocol is supported.
|
||||
- You cannot use port `1194` or any port below `1024`, except `80` and `443`. If you try to use a blocked port, you receive either a `403 Forbidden` or `ERR_ACCESS_DENIED` error.
|
||||
|
||||
## Rate limits
|
||||
|
||||
For each Roblox experience server, there is a limit of 2500 Open Cloud requests per minute. Exceeding this can cause request-sending methods to stall for around 30 seconds. Your `Global.LuaGlobals.pcall()` may also fail with a message of `Number of Open Cloud requests exceeded limit`.
|
||||
|
||||
- Open Cloud requests **do not** consume the same overall limit of 500 HTTP requests per minute enforced on all other requests.
|
||||
- Each endpoint has its own limit per API key owner (can be a user or a group) that is enforced no matter where the calls come from (`Class.HttpService`, the web, etc.).
|
||||
|
||||
For detailed information about Open Cloud rate limits, authentication-based rate limiting, and best practices, see [Rate Limits](/cloud/reference/rate-limits).
|
||||
|
||||
## Best practices
|
||||
|
||||
To optimize your `Class.HttpService` usage and avoid exceeding the limits, apply the following best practices:
|
||||
|
||||
- **Handle errors gracefully**. Web requests can fail for many reasons. Use `Global.LuaGlobals.pcall()` and have a plan for when requests fail. Furthermore, strictly validate and sanitize all received data from external APIs, ensuring correct data where you can.
|
||||
|
||||
- Use [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) to stay below limits.
|
||||
|
||||
If a request returns a recoverable error, instead of immediately retrying, wait for two seconds, then four, eights, etc. between attempts. This helps limit congestion and improves the chance of a successful request by giving the endpoint time to "cool off."
|
||||
|
||||
- Aggregate and send data in bulk.
|
||||
|
||||
When possible, it's recommended to let your server collect all necessary data in order to send one HTTP request, rather than multiple small requests. For example, if you are sending an HTTP request for every player in your server, check if the API has a bulk/batch endpoint and, if so, collect the information from all players and send it all in one request.
|
||||
|
||||
In some cases you may have to use `Class.HttpService:RequestAsync()` to include data in the body of the request.
|
||||
|
||||
- **Use HTTP/2 endpoints**. HTTP/2 provides significant performance benefits through features such as header compression and request/response multiplexing over a single connection. `Class.HttpService` automatically uses HTTP/2 when available. Note that the HTTP/2 specification requires all header names to be sent in lowercase.
|
||||
|
||||
## Observability
|
||||
|
||||
The **Observability Dashboard** provides insights and analytics for monitoring and troubleshooting your `Class.HttpService` usage. The dashboard features two primary charts: **Request Count** which tracks the volume of `Class.HttpService` requests from your experience, and **Response Time** which measures the latency for endpoints to respond.
|
||||
|
||||
The available dimensions for filtering and breakdown are defined as follows:
|
||||
|
||||
<h3>Request Type</h3>
|
||||
|
||||
- `GET`
|
||||
- `POST`
|
||||
- `PUT`
|
||||
- `PATCH`
|
||||
- `DELETE`
|
||||
- `Other` (for non-specified request types)
|
||||
|
||||
<h3>Status</h3>
|
||||
|
||||
- Success (HTTP 1xx and 2xx status codes)
|
||||
- Redirect (HTTP 3xx status codes)
|
||||
- 400 (Bad Request)
|
||||
- 401 (Unauthorized)
|
||||
- 403 (Forbidden)
|
||||
- 404 (Not Found)
|
||||
- 429 (Too Many Requests)
|
||||
- 500 (Internal Server Error)
|
||||
- 503 (Service Unavailable)
|
||||
- ExternalError (any other non-specified error codes returned from the external service)
|
||||
- InternalError (an issue returned from `Class.HttpService` within Roblox)
|
||||
|
||||
The **Response Time** chart is not correlated with status data. If you select "Status" as a breakdown or filter, this chart will not display data.
|
||||
|
||||
## Additional considerations
|
||||
|
||||
- Requests should provide a secure form of authentication, such as a pre-shared secret key, so that bad actors cannot pose as one of your Roblox servers.
|
||||
- Be aware of the general capacity and rate-limiting policies of the web servers to which requests are being sent.
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
title: Best practices when designing MemoryStore data structures
|
||||
description: Explains how to best design data structures to reduce the chance of experiencing throttling.
|
||||
---
|
||||
|
||||
Depending on the data structure type, MemoryStoreService enforces [limits](../../cloud-services/memory-stores/index.md#data-structure-size-limits) on the memory and number of items in a data structure. All data structures are also constrained by a global per-partition request limit.
|
||||
|
||||
Each Roblox experience has the [Memory Store Observability Dashboard](../../cloud-services/memory-stores/observability.md), which includes a set of charts that you can use to monitor memory store usage.
|
||||
|
||||
## Sorted maps and queues
|
||||
|
||||
Sorted maps and queues both have limits on the maximum number of items and maximum total memory. Additionally, the items in one of these data structures always reside on a single partition. Every request to one of those data structures is a request to the same partition.
|
||||
|
||||
When a sorted map or queues reaches its item or memory limit, the best course of action is to remove unnecessary items manually or by adding an expiration policy for items. Alternatively, if only the memory limit is causing throttling, you can try to reduce the size of your items by stripping unnecessary information out of your keys and values.
|
||||
|
||||
If you need all of your items or are experiencing throttling due to request throughput, the only solution is sharding.
|
||||
|
||||
### Sharding
|
||||
|
||||
Sharding is the process of storing a set of related data across multiple data structures. In other words, it means taking an existing, high-throughput data structure and replacing it with multiple, smaller ones that together contain the same set of data as the original.
|
||||
|
||||
The key challenge to sharding is finding a way to spread the data across multiple data structures in a way that maintains the same functionality as the original.
|
||||
|
||||
For hash maps, although the data structure is already partitioned, sharding is done by spreading the requests among several keys.
|
||||
|
||||
### Sharding a sorted map
|
||||
|
||||
To shard a sorted map, consider splitting your data into alphabetic subsections with character ranges. For example, assume that you only have keys with the first letter from A-Z, and you believe four sorted maps is sufficient for your current use case and future growth:
|
||||
|
||||
- The first map can cover A-G, the second H-N, the third O-T, and the fourth U-Z.
|
||||
- To insert or retrieve an item, use the appropriate map based on the item's starting character.
|
||||
|
||||
<Alert severity="info">
|
||||
Use a helper function to get the correct sorted map from an item key. This way, you don't have to repeat the same block of code for every function call.
|
||||
</Alert>
|
||||
|
||||
```lua title="Sharding a Sorted Map"
|
||||
-- Initialize the MemoryStore Service
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
-- Create your Sorted Map buckets
|
||||
local sm_AtoG = MemoryStoreService:GetSortedMap("AtoG")
|
||||
local sm_HtoM = MemoryStoreService:GetSortedMap("HtoM")
|
||||
local sm_NtoT = MemoryStoreService:GetSortedMap("NtoT")
|
||||
local sm_UtoZ = MemoryStoreService:GetSortedMap("UtoZ")
|
||||
|
||||
-- Helper function to retrieve the correct bucket from the Item Key
|
||||
local function getSortedMapBucket(itemKey)
|
||||
if (itemKey >= "a" and itemKey < "h") then
|
||||
return sm_AtoG
|
||||
elseif (itemKey < "n") then
|
||||
return sm_HtoM
|
||||
elseif (itemKey < "u") then
|
||||
return sm_NtoT
|
||||
else
|
||||
return sm_UtoZ
|
||||
end
|
||||
end
|
||||
|
||||
-- Initialize player names with default value of 0
|
||||
for _, player in game:GetService("Players"):GetPlayers() do
|
||||
local bucket = getSortedMapBucket(player)
|
||||
bucket:SetAsync(player, 0, 600)
|
||||
end
|
||||
|
||||
-- Retrieve a player's value
|
||||
local player = "myPlayer"
|
||||
local bucket = getSortedMapBucket(player)
|
||||
local playerScore = bucket:GetAsync(player)
|
||||
print(playerScore)
|
||||
```
|
||||
|
||||
### Sharding a queue
|
||||
|
||||
Sharding a queue is tricker than sharding a sorted map. Although you want to spread the request throughput across multiple queues, adds, reads, and removes only ever occur at the front or back of the queue.
|
||||
|
||||
One solution is to use a revolving queue, which means creating multiple queues and rotating between them when you add or read an item:
|
||||
|
||||
1. Create several queues and add them to an array.
|
||||
1. Create two local pointers. One represents the queue you want to read and remove items from. The other represents the queue you want to add items to:
|
||||
|
||||
- For read operations, calculate the number of items you need from each queue, as well as where to move the read pointer to.
|
||||
- For remove operations, pass the IDs from the read to each queue.
|
||||
- For add operations, add to the queue at the add pointer and increment the pointer.
|
||||
|
||||
```lua title="Sharding a Queue"
|
||||
-- Initialize the MemoryStore Service
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
-- Create your Queues
|
||||
local q1 = MemoryStoreService:GetQueue("q1")
|
||||
local q2 = MemoryStoreService:GetQueue("q2")
|
||||
local q3 = MemoryStoreService:GetQueue("q3")
|
||||
local q4 = MemoryStoreService:GetQueue("q4")
|
||||
|
||||
-- Put the Queues in an Array
|
||||
local queueArr = { q1, q2, q3, q4 }
|
||||
|
||||
-- Create two pointers representing the indices of the read and add queues
|
||||
local readIndex = 1
|
||||
local addIndex = 1
|
||||
|
||||
-- Create a local function that updates the indices appropriately
|
||||
local function rotateIndex(index, n)
|
||||
return (index + n - 1) % 4 + 1
|
||||
end
|
||||
|
||||
-- Create a local function that reads n items from the queue
|
||||
local function readFromQueue(count, allOrNothing, waitTimeout)
|
||||
local endIndex = count % 4
|
||||
local countPerQueue = count // 4
|
||||
local items = {}
|
||||
local ids = {}
|
||||
|
||||
-- loop through each queue
|
||||
for i = 1, 4, 1 do
|
||||
-- determine if this queue will read an extra item
|
||||
local diff = i - readIndex
|
||||
if diff < 0 then
|
||||
diff += 4
|
||||
end
|
||||
|
||||
local queue = queueArr[i]
|
||||
|
||||
-- read items from each queue
|
||||
-- +1 items if matches extra read criteria
|
||||
if diff < endIndex then
|
||||
items[i], ids[i] = queue:ReadAsync(countPerQueue + 1, allOrNothing,waitTimeout)
|
||||
else
|
||||
items[i], ids[i] = queue:ReadAsync(countPerQueue, allOrNothing,waitTimeout)
|
||||
end
|
||||
end
|
||||
|
||||
readIndex = rotateIndex(readIndex, count)
|
||||
|
||||
return items, ids
|
||||
end
|
||||
|
||||
-- Create a local function that removes n items from the queue
|
||||
local function removeFromQueue(ids)
|
||||
for i = 1, 4, 1 do
|
||||
local queue = queueArr[readIndex]
|
||||
queue:RemoveAsync(ids[i])
|
||||
end
|
||||
end
|
||||
|
||||
-- Create a local function that adds an item to the queue
|
||||
local function addToQueue(itemKey, expiration, priority)
|
||||
local queue = queueArr[readIndex]
|
||||
queue:AddAsync(itemKey, expiration, priority)
|
||||
addIndex = rotateIndex(addIndex, 1)
|
||||
end
|
||||
|
||||
-- Write some code!
|
||||
|
||||
for _, player in game:GetService("Players"):GetPlayers() do
|
||||
addToQueue(player, 600, 0)
|
||||
end
|
||||
|
||||
local players, ids = readFromQueue(20, true, -1)
|
||||
removeFromQueue(ids)
|
||||
```
|
||||
|
||||
## Hash maps
|
||||
|
||||
Hash maps do not have individual memory or item count limits and are automatically sharded, but you can still encounter throttling if you use them poorly.
|
||||
|
||||
For example, consider an experience with a hash map of data, stored as the value of a single key named `metadata`. If this metadata contains a nested object with information such as place ID, player count, and more, every time the metadata is needed, you have no choice but to call `GetAsync("metadata")` and retrieve the entire object. In this case, all requests go to a single key and therefore a single partition.
|
||||
|
||||
Rather than storing all metadata as a single, nested object, the better approach is to store each field as its own key so that the hash map can take advantage of automatic sharding. If you need separation between metadata and the rest of the hash map, add a naming prefix (e.g. `metadata_user_count` rather than just `user_count`).
|
||||
|
||||
Additionally, if one or few keys is being accessed frequently, it's important to shard these calls among a lot of keys. For example, if all experience servers must retrieve a value from one hash map key you may run into partition throttling. To prevent this you can spread those calls among several keys with the same value until partition throttling no longer occurs.
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
---
|
||||
title: Memory store hash map
|
||||
description: Explains how to implement the hash map data structure for memory stores.
|
||||
---
|
||||
|
||||
**Hash maps**, similar to sorted maps, let you store in-memory data as key-value pairs. Unlike sorted maps, they maintain no ordering guarantees. This data structure is useful for cases that require simple data caching and rapid access, such as shared inventories, physical auction houses, and more. Hash maps automatically handle partitioning your data and are very useful if you have more than 1,000 keys. For smaller key spaces, we recommend [sorted maps](../../cloud-services/memory-stores/sorted-map.md).
|
||||
|
||||
## Limits
|
||||
|
||||
Hash maps have a key size limit of 128 characters and a value size limit of 32 KB.
|
||||
|
||||
Otherwise, hash maps use the same [API request](../../cloud-services/memory-stores/index.md#api-requests-limits) and [memory quota](../../cloud-services/memory-stores/index.md#memory-size-quota) limits as the other memory store data structures.
|
||||
|
||||
## Get a hash map
|
||||
|
||||
To get a hash map, call `Class.MemoryStoreService:GetHashMap()` with a name for the hash map. The name is global within the experience, so you can access the same hash map on any script using this name.
|
||||
|
||||
```lua title="Getting a Hash Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("HashMap1")
|
||||
```
|
||||
|
||||
After you get a hash map, call any of the following functions to read or write data in it:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreHashMap:SetAsync()`</td>
|
||||
<td>[Add](#add-or-overwrite-data) a new key or overwrite the value if the key already exists.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreHashMap:GetAsync()`</td>
|
||||
<td>[Read](#get-data) a particular key.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreHashMap:ListItemsAsync()`</td>
|
||||
<td>[List](#get-data) items in a hash map.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreHashMap:UpdateAsync()`</td>
|
||||
<td>[Update](#update-data) the value of a key after retrieving it from a hash map.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreHashMap:RemoveAsync()`</td>
|
||||
<td>[Remove](#remove-data) a key from the hash map.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
For in-depth documentation about each function, see `Class.MemoryStoreHashMap`.
|
||||
|
||||
<Alert severity="warning">
|
||||
All functions accessing data structures in memory stores are asynchronous network calls that might occasionally fail. You should wrap these calls in `Global.LuaGlobals.pcall()` to catch and handle errors, as shown in the code samples.
|
||||
</Alert>
|
||||
|
||||
## Add or overwrite data
|
||||
|
||||
To add a new key or overwrite the value of a key in the hash map, call `Class.MemoryStoreHashMap:SetAsync()` with the key **name**, its **value**, and an **expiration time** in seconds. The memory automatically cleans up once the key expires. The maximum expiration time is 3,888,000 seconds (45 days).
|
||||
|
||||
<Alert severity="warning">
|
||||
Under the EU General Data Protection Regulation (GDPR), if your memory stores have user data subject to the [right to be forgotten](https://gdpr.eu/right-to-be-forgotten/) you must remove the data within 30 days, even if you set your memory store key's expiration up to 45 days.
|
||||
</Alert>
|
||||
|
||||
```lua title="Adding Data to a Hash Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("HashMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return hashMap:SetAsync("User_1234", 1000, 30)
|
||||
end)
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
```
|
||||
|
||||
## Get data
|
||||
|
||||
You can either get a value associated with a specific key or get multiple key-value pairs in the hash map.
|
||||
|
||||
### Get data with one key
|
||||
|
||||
To get a value associated with one key from the hash map, call `Class.MemoryStoreHashMap:GetAsync()` with the key **name**.
|
||||
|
||||
```lua title="Getting a Particular Key from a Hash Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("HashMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return hashMap:SetAsync("User_1234", 1000, 30)
|
||||
end)
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
|
||||
local item
|
||||
local getSuccess, getError = pcall(function()
|
||||
item = hashMap:GetAsync("User_1234")
|
||||
end)
|
||||
if getSuccess then
|
||||
print(item)
|
||||
else
|
||||
warn(getError)
|
||||
end
|
||||
```
|
||||
|
||||
### Get data with multiple key-value pairs
|
||||
|
||||
To get all the key-value pairs from the hash map as a single operation, call `Class.MemoryStoreHashMap:ListItemsAsync()` with the desired page size. This function lists all existing keys in a paginated manner. For example, the following code sample retrieves up to 32 items from the hash map.
|
||||
|
||||
```lua title="Listing items in a Hash Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("HashMap1")
|
||||
|
||||
-- Get list of items, 32 items at a time
|
||||
local success, pages = pcall(function()
|
||||
return hashMap:ListItemsAsync(32)
|
||||
end)
|
||||
if success then
|
||||
while true do
|
||||
-- Get the current page
|
||||
local entries = pages:GetCurrentPage()
|
||||
-- Iterate through all key-value pairs on page
|
||||
for _, entry in ipairs(entries) do
|
||||
print(entry.key .. " : " .. tostring(entry.value))
|
||||
end
|
||||
-- Check if last page has been reached
|
||||
if pages.IsFinished then
|
||||
break
|
||||
else
|
||||
print("----------")
|
||||
-- Advance to next page
|
||||
pages:AdvanceToNextPageAsync()
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Update data
|
||||
|
||||
To retrieve the value of a key from a hash map and update it, call `Class.MemoryStoreHashMap:UpdateAsync()` with the key **name**, a **callback function** to update the key, and an **expiration time** in seconds.
|
||||
|
||||
For most experiences, multiple servers can update the same key concurrently and change the value. As `Class.MemoryStoreHashMap:UpdateAsync()|UpdateAsync()` always modifies the latest value before updating, you should use it to read the latest value as the input for your callback function.
|
||||
|
||||
For example, the following code sample updates the resource count of a resource in a shared inventory. `Class.MemoryStoreHashMap:UpdateAsync()|UpdateAsync()` ensures that all player contributions will make their way into this shared inventory, even if these contributions are made simultaneously. In this function, it also enforces a max resource count of 500.
|
||||
|
||||
```lua title="Updating Resource Count for a Resource in a Shared Inventory"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("ResourceInventory")
|
||||
|
||||
local function contributeResources(itemResource, addedCount)
|
||||
local success, newResourceCount = pcall(function()
|
||||
return hashMap:UpdateAsync(itemResource, function(resource)
|
||||
resource = resource or {count = 0}
|
||||
resource.count = resource.count + addedCount
|
||||
|
||||
-- ensure we don't exceed the maximum resource count
|
||||
if resource.count > 500 then
|
||||
resource.count = 500
|
||||
end
|
||||
return resource
|
||||
end, 1200)
|
||||
end)
|
||||
if success then
|
||||
print(newResourceCount)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The latency for `Class.MemoryStoreHashMap:UpdateAsync()|UpdateAsync()` is similar to `Class.MemoryStoreHashMap:GetAsync()|GetAsync()` and `Class.MemoryStoreHashMap:SetAsync()|SetAsync()` unless there is contention.
|
||||
|
||||
When contention occurs, the system automatically retries the operation until one of these three happens: the operation succeeds, the callback function returns `nil`, or the maximum number of retries is reached. If the system reaches the maximum number of retries, it returns a conflict.
|
||||
|
||||
## Remove data
|
||||
|
||||
You can use `Class.MemoryStoreHashMap:RemoveAsync()` for both removing one key from the hash map and deleting all data in a memory store hash map.
|
||||
|
||||
### Remove a key
|
||||
|
||||
To remove a key from the hash map, call `Class.MemoryStoreHashMap:RemoveAsync()` with a key **name**.
|
||||
|
||||
```lua title="Remove a Key from a Hash Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("HashMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return hashMap:SetAsync("User_1234", 1000, 30)
|
||||
end)
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
|
||||
local removeSuccess, removeError = pcall(function()
|
||||
hashMap:RemoveAsync("User_1234")
|
||||
end)
|
||||
if not removeSuccess then
|
||||
warn(removeError)
|
||||
end
|
||||
```
|
||||
|
||||
### Delete all data
|
||||
|
||||
To delete all data in a hash map, list all your items with `Class.MemoryStoreHashMap:ListItemsAsync()`, then remove them with `Class.MemoryStoreHashMap:RemoveAsync()`.
|
||||
|
||||
```lua title="Delete all data in a Hash Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local hashMap = MemoryStoreService:GetHashMap("HashMap1")
|
||||
|
||||
-- Get list of items, 32 items at a time
|
||||
local success, pages = pcall(function()
|
||||
return hashMap:ListItemsAsync(32)
|
||||
end)
|
||||
if success then
|
||||
while true do
|
||||
-- Get the current page
|
||||
local entries = pages:GetCurrentPage()
|
||||
local removeSuccess = true
|
||||
local removeError = nil
|
||||
-- Iterate through all key-value pairs on page
|
||||
for _, entry in ipairs(entries) do
|
||||
print(entry.key .. " : " .. tostring(entry.value))
|
||||
removeSuccess, removeError = pcall(function()
|
||||
hashMap:RemoveAsync(entry.key)
|
||||
end)
|
||||
|
||||
if not removeSuccess then
|
||||
warn(removeError)
|
||||
end
|
||||
end
|
||||
-- Check if last page has been reached
|
||||
if pages.IsFinished then
|
||||
print("Finished deleting all data.")
|
||||
break
|
||||
else
|
||||
print("----------")
|
||||
-- Advance to next page
|
||||
pages:AdvanceToNextPageAsync()
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
363
.cursor/rules/roblox/en-us/cloud-services/memory-stores/index.md
Normal file
363
.cursor/rules/roblox/en-us/cloud-services/memory-stores/index.md
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
---
|
||||
title: Memory stores
|
||||
description: Explains how to implement memory store data structures to store frequent in-memory data.
|
||||
---
|
||||
|
||||
`Class.MemoryStoreService` is a high throughput and low latency data service that provides fast in-memory data storage accessible from all servers in a live session. **Memory Stores** are suitable for frequent and ephemeral data that change rapidly and don't need to be durable, because they are faster to access and vanish when reaching the maximum lifetime. For data that needs to persist across sessions, use [data stores](../../cloud-services/data-stores/index.md).
|
||||
|
||||
## Data structures
|
||||
|
||||
Instead of directly accessing raw data, memory stores have three primitive data structures shared across servers for quick processing: [sorted map](../../cloud-services/memory-stores/sorted-map.md), [queue](../../cloud-services/memory-stores/queue.md), and [hash map](../../cloud-services/memory-stores/hash-map.md). Each data structure is a good fit for certain use cases:
|
||||
|
||||
- **Skill-based matchmaking** - Save user information, such as skill level, in a shared **queue** among servers, and use lobby servers to run matchmaking periodically.
|
||||
- **Cross-server trading and auctioning** - Enable universal trading between different servers, where users can bid on items with real-time changing prices, with a **sorted map** of key-value pairs.
|
||||
- **Global leaderboards** - Store and update user rankings on a shared leaderboard inside a **sorted map**.
|
||||
- **Shared inventories** - Save inventory items and statistics in a shared **hash map**, where users can utilize inventory items concurrently with one another.
|
||||
- **Cache for Persistent Data** - Sync and copy your persistent data in a data store to a memory store **hash map** that can act as a cache and improve your experience's performance.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Does your data need to be sorted in a specific order?]
|
||||
B[Use a sorted map.]
|
||||
C[Do you need the ability to scan all of your items at once?]
|
||||
D[Use a hash map.]
|
||||
E[Do you expect to have fewer than 1,000 keys?]
|
||||
|
||||
A -- YES --> B
|
||||
A -- NO --> C
|
||||
|
||||
C -- YES --> E
|
||||
C -- NO --> D
|
||||
|
||||
E -- YES --> B
|
||||
E -- NO --> D
|
||||
```
|
||||
|
||||
In general, if you need to access data based on a specific key, use a hash map. If you need that data to be ordered, use a sorted map. If you need to process your data in a specific order, use a queue.
|
||||
|
||||
## Limits and quotas
|
||||
|
||||
To maintain the scalability and system performance, memory stores have data usage quotas for the memory size, API requests, and the data structure size.
|
||||
|
||||
Memory stores have an eviction policy based on expiration time, also known as time to live (TTL). Items are evicted after they expire, and memory quota is freed up for new entries. When you hit the memory limit, all subsequent write requests fail until items expire or you manually delete them.
|
||||
|
||||
### Memory size quota
|
||||
|
||||
The memory quota limits the total amount of memory that an experience can consume. It's not a fixed value; instead, it changes over time depending on the number of users in the experience according to the formula **64KB + 1.2KB \* [number of users]**. The quota applies on the experience level instead of the server level.
|
||||
|
||||
When users join the experience, the additional memory quota is available immediately. When users leave the experience, the quota doesn't reduce immediately. There's a traceback period of eight days before the quota reevaluates to a lower value.
|
||||
|
||||
After your experience hits the memory size quota, any API requests that increase the memory size always fail. Requests that decrease or don't change the memory size still succeed.
|
||||
|
||||
With the [observability](../../cloud-services/memory-stores/observability.md) dashboard, you can view the memory size quota of your experience in real time using the **Memory Usage** chart.
|
||||
|
||||
### API request limits
|
||||
|
||||
A **request unit** quota applies to all `Class.MemoryStoreService` API calls. This quota is **1000 + 120 \* [number of concurrent users]** request units per minute.
|
||||
|
||||
Most API calls only consume one request unit, with a few exceptions:
|
||||
|
||||
- `Class.MemoryStoreSortedMap:GetRangeAsync()`
|
||||
|
||||
Consumes units based on the number of returned items. For example, if this method returns 10 items, the call counts as 10 request units. If it returns an empty response, it counts as one request unit.
|
||||
|
||||
- `Class.MemoryStoreQueue:ReadAsync()`
|
||||
|
||||
Consumes units based on the number of returned items, just like `MemoryStoreSortedMap:GetRangeAsync()`, but consumes an additional unit every two seconds while reading. Specify the maximum read time with the `waitTimeout` parameter.
|
||||
|
||||
- `Class.MemoryStoreHashMap:UpdateAsync()`
|
||||
|
||||
Consumes a minimum of two units.
|
||||
|
||||
- `Class.MemoryStoreHashMap:ListItemsAsync()`
|
||||
|
||||
Consumes **[number of partitions scanned] + [items returned]** units.
|
||||
|
||||
The requests quota is also applied on the experience level instead of the server level. This provides flexibility to allocate the requests among servers as long as the total request rate does not exceed the quota. If you exceed the quota, you receive an error response when the service throttles your requests.
|
||||
|
||||
With the [observability](../../cloud-services/memory-stores/observability.md) feature available, you can view the request unit quota of your experience in real time.
|
||||
|
||||
### Data structure size limits
|
||||
|
||||
For a single sorted map or queue, the following size and item count limits apply:
|
||||
|
||||
- Maximum number of items: 1,000,000
|
||||
- Maximum total size (including keys for sorted map): 100 MB
|
||||
|
||||
### Per-partition limits
|
||||
|
||||
See [per-partition limits](per-partition-limits.md).
|
||||
|
||||
## Best practices
|
||||
|
||||
To keep your memory usage pattern optimal and avoid hitting the [limits](#limits-and-quotas), follow these best practices:
|
||||
|
||||
- **Remove processed items.** Consistently cleaning up read items using `Class.MemoryStoreQueue:RemoveAsync()` method for queues and `Class.MemoryStoreSortedMap:RemoveAsync()` for sorted maps can free up memory and keep the data structure up-to-date.
|
||||
|
||||
- **Set the expiration time to the smallest time frame possible when adding data.** Though the default expiration time is 45 days for both `Class.MemoryStoreQueue:AddAsync()` and `Class.MemoryStoreSortedMap:SetAsync()`, setting the shortest possible time can automatically clean up old data to prevent them from filling up your memory usage quota.
|
||||
|
||||
- Don't store a large amount of data with a long expiration, as it risks exceeding your memory quota and potentially causing issues that can break your entire experience.
|
||||
- Always either explicitly delete unneeded items or set a short item expiration.
|
||||
- Generally, you should use explicit deletion for releasing memory and item expiration as a safety mechanism to prevent unused items from occupying memory for an extended period of time.
|
||||
|
||||
- Only keep necessary values in memory.
|
||||
|
||||
For example, for an auction house experience, you only need to maintain the highest bid. You can use `Class.MemoryStoreSortedMap:UpdateAsync()` on one key to keep the highest bid rather than keeping all bids in your data structure.
|
||||
|
||||
- Use [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) to help stay below API request limits.
|
||||
|
||||
For example, if you receive a `DataUpdateConflict`, you might retry after two seconds, then four, eight, etc. rather than constantly sending requests to `Class.MemoryStoreService` to get the correct response.
|
||||
|
||||
- Split giant data structures into multiple smaller ones by [sharding](<https://en.wikipedia.org/wiki/Shard_(database_architecture)>).
|
||||
|
||||
It's often easier to manage data in smaller structures rather than storing everything in one large data structure. This approach can also help avoid usage and rate limits. For example, if you have a sorted map that uses prefixes for its keys, consider separating each prefix into its own sorted map. For an especially popular experience, you might even separate users into multiple maps based on the last digits of their user IDs.
|
||||
|
||||
- [Shard](../../cloud-services/memory-stores/best-practices.md) frequently accessed keys in hash maps with multiple copies of the key to distribute load.
|
||||
|
||||
- Compress stored values.
|
||||
|
||||
For example, consider using the [LZW](https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch) algorithm to reduce the stored value size.
|
||||
|
||||
- Enroll in Extended Services.
|
||||
|
||||
You can increase your Storage and Request Limit quotas by onboarding onto [Extended Services](https://create.roblox.com/docs/cloud-services/extended-services)
|
||||
|
||||
## Observability
|
||||
|
||||
The [Observability Dashboard](../../cloud-services/memory-stores/observability.md) provides insights and analytics for monitoring and troubleshooting your memory store usage. With real-time updating charts on different aspects of your memory usage and API requests, you can track the memory usage pattern of your experience, view the current allocated quotas, monitor the API status, and identify potential issues for performance optimization.
|
||||
|
||||
The following table lists and describes all status codes of API responses available on the Observability Dashboard's **Request Count by Status** and **Requests by API x Status** charts. For more information on how to resolve these errors, see [Troubleshooting](#troubleshooting). For the specific quota or limit that an error relates to, see [Limits and Quotas](#limits-and-quotas).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status code</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Success</td>
|
||||
<td>Success.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataStructureMemoryOverLimit</td>
|
||||
<td>Exceeds data structure level memory size limit (100MB).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataUpdateConflict</td>
|
||||
<td>Conflict due to concurrent update.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AccessDenied</td>
|
||||
<td>Unauthorized to access experience data. This request doesn't consume request units or use quota.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InternalError</td>
|
||||
<td>Internal error.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InvalidRequest</td>
|
||||
<td>The request doesn't have required information or has malformed information.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataStructureItemsOverLimit</td>
|
||||
<td>Exceeds data structure level item count limit (1M).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NoItemFound</td>
|
||||
<td>No item found in `Class.MemoryStoreQueue:ReadAsync()` or `Class.MemoryStoreSortedMap:UpdateAsync()`. `ReadAsync()` polls every 2 seconds and returns this status code until it finds items in the queue.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataStructureRequestsOverLimit</td>
|
||||
<td>Exceeds data structure level request unit limit (100,000 request units per minute).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PartitionRequestsOverLimit</td>
|
||||
<td>Exceeds partition request unit limit.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TotalRequestsOverLimit</td>
|
||||
<td>Exceeds universe-level request unit limit.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TotalMemoryOverLimit</td>
|
||||
<td>Exceeds universe-level memory quota.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ItemValueSizeTooLarge</td>
|
||||
<td>Value size exceeds limit (32KB).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The following table lists states codes from client side, which are currently not available on the Observability Dashboard.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status code</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>InternalError</td>
|
||||
<td>Internal Error.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UnpublishedPlace</td>
|
||||
<td>You must publish this place to use MemoryStoreService.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InvalidClientAccess</td>
|
||||
<td>MemoryStoreService must be called from server.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InvalidExpirationTime</td>
|
||||
<td>The field 'expiration' time must be between 0 and 3,888,000.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InvalidRequest</td>
|
||||
<td>Unable to convert value to json.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InvalidRequest</td>
|
||||
<td>Unable to convert sortKey to a valid number or string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TransformCallbackFailed</td>
|
||||
<td>Failed to invoke transformation callback function.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RequestThrottled</td>
|
||||
<td>Recent MemoryStores requests hit one or more limits.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UpdateConflict</td>
|
||||
<td>Exceeded max number of retries.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
The following table lists and describes the recommended solution for each response status code:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Error</th>
|
||||
<th>Troubleshooting options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>DataStructureRequestsOverLimit / PartitionRequestsOverLimit</td>
|
||||
<td rowspan="2">
|
||||
<ul>
|
||||
<li>Add a local cache by saving information to another variable and rechecking after a certain time interval, such as 30 seconds.</li>
|
||||
<li>Use the **Request Count by Status** chart to verify that you are receiving more **Success** responses than **NoItemFounds**. Limit the amount of times you hit `Class.MemoryStoreService` with a failed request.</li>
|
||||
<li>Implement a short delay between requests.</li>
|
||||
<li>Follow the [best practices](#best-practices), including:</li>
|
||||
<ul>
|
||||
<li>Sharding your data structures if you receive a significant amount of **DataStructureRequestsOverLimit**/**PartitionRequestsOverLimit** responses.</li>
|
||||
<li>Sharding your hash map keys if you receive a significant amount of **PartitionRequestsOverLimit** responses on hash map calls.</li>
|
||||
<li>Reducing or batching calls to specific data structures or hash map keys if you see **PartitionRequestsOverLimit** responses.</li>
|
||||
<li>Implement an exponential backoff for finding a reasonable rate of requests to send.</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TotalRequestsOverLimit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataStructureItemsOverLimit</td>
|
||||
<td rowspan="3">
|
||||
<ul>
|
||||
<li>Apply [best practices](#best-practices) on reducing the memory size.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataStructureMemoryOverLimit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TotalMemoryOverLimit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataUpdateConflict</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Implement a short delay between requests to avoid multiple requests updating the same key at the same time.</li>
|
||||
<li>For sorted maps, use the callback function on the `Class.MemoryStoreSortedMap:UpdateAsync()` method to abort a request after a certain number of attempts, as the following code sample shows:</li>
|
||||
```lua title="Example of Aborting Request"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
local map = MemoryStoreService:GetSortedMap("AuctionItems")
|
||||
|
||||
function placeBid(itemKey, bidAmount)
|
||||
map:UpdateAsync(itemKey, function(item)
|
||||
item = item or { highestBid = 0 }
|
||||
if item.highestBid < bidAmount then
|
||||
item.highestBid = bidAmount
|
||||
return item
|
||||
end
|
||||
print("item is "..item.highestBid)
|
||||
return nil
|
||||
end, 1000)
|
||||
end
|
||||
|
||||
placeBid("MyItem", 50)
|
||||
placeBid("MyItem", 40)
|
||||
print("done")
|
||||
```
|
||||
<li>Investigate to see if you're calling `Class.MemoryStoreService` efficiently to avoid conflicts. Ideally, you shouldn't over-send requests.</li>
|
||||
<li>Consistently remove items once they are read using the `Class.MemoryStoreQueue:RemoveAsync()` method for queues and `Class.MemoryStoreSortedMap:RemoveAsync()` for sorted maps.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Internal Error</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Check the <a href="https://status.roblox.com/pages/59db90dbcdeb2f04dadcf16d">Roblox status page</a>.</li>
|
||||
<li>File a <a href="https://devforum.roblox.com/t/how-to-post-a-bug-report/24388">bug report</a> describing the issue with your experience's Universe ID.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>InvalidRequest</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Make sure that you include correct and valid parameters in your request. Examples of invalid parameters include:</li>
|
||||
<ul>
|
||||
<li>An empty string</li>
|
||||
<li>A string that exceeds the length limit</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ItemValueSizeTooLarge</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>Shard or split the item value into multiple keys.</li>
|
||||
<ul>
|
||||
<li>To organize grouped keys, sort them alphabetically by adding a `prefix` to the key.</li>
|
||||
</ul>
|
||||
<li>Encoding or compressing stored values.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Test and debug in Studio
|
||||
|
||||
The data in `Class.MemoryStoreService` is isolated between Studio and production, so changing the data in Studio doesn't affect production behavior. This means that your API calls from Studio don't access production data, allowing you to safely test memory stores and new features before going to production.
|
||||
|
||||
Studio testing has the same [limits and quotas](#limits-and-quotas) as production. For quotas calculated based on the number of users, the resulting quota can be very small since you are the only user for Studio testing. When testing from Studio, you might also notice slightly higher latency and elevated error rates compared to usage in production due to some additional checks that are performed to verify access and permissions.
|
||||
|
||||
For information on how to debug a memory store on live experiences or when testing in studio, use [Developer Console](../../studio/developer-console.md).
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
title: Memory store observability
|
||||
description: Explains how to use the observability dashboard for memory stores.
|
||||
---
|
||||
|
||||
The memory stores observability dashboard provides real-time charts on your memory usage and API requests. It also has a built-in alert system that notifies you by email when an issue arises to help you troubleshoot in sync. For further information about specific errors, you can view your [Error Report](../../production/analytics/error-report.md) to find the error logs.
|
||||
|
||||
## Access the dashboard
|
||||
|
||||
The memory stores observability dashboard is available for any experience using `Class.MemoryStoreService`, but you must either be the experience owner or have [analytics group permissions](../../production/analytics/analytics-dashboard.md#grant-group-permission) to access the dashboard.
|
||||
|
||||
To access the dashboard:
|
||||
|
||||
1. Navigate to the [Creations](https://create.roblox.com/dashboard/creations) page on the **Creator Dashboard**.
|
||||
2. Expand the account switcher in the upper-left and select your account or the group owning the target experience.
|
||||
3. Select the experience.
|
||||
4. In the **Monitoring** dropdown, select **Memory Stores**.
|
||||
|
||||
## Available charts
|
||||
|
||||
The dashboard includes two categories of line graphs:
|
||||
|
||||
- **Quota Usage** charts for tracking your usage compared to the [dynamically allocated quotas](../../cloud-services/memory-stores/index.md#limits-and-quotas), which are calculated based on the number of users in your experience.
|
||||
- **Memory Usage** on your memory usage per minute in bytes compared to how much your allocated quota left.
|
||||
- **API Request Unit** on your total request units per minute compared to how much your allocated quota left. This chart can be broken down by each API method.
|
||||
- **API Usage and Performance** charts for monitoring the API usage pattern and performance based on API method and response status.
|
||||
- **Request Count by API** on API request count per minute by API method, such as `Class.MemoryStoreQueue:ReadAsync()` or `Class.MemoryStoreSortedMap:UpdateAsync()`.
|
||||
- **Request Count by Status** on API request count by [response status](#response-status-codes).
|
||||
- **Request by API x Status** on response statuses returned by all or a specific API method.
|
||||
|
||||
The chart contains data for the past 30 days, and you can select to view a custom time range with the selector at the top of the page. If you select a time range earlier than 30 days, the system returns a **Request Failed** error.
|
||||
|
||||
## Response status codes
|
||||
|
||||
The Observability Dashboard's **Request Count by Status** and **Requests by API x Status** charts include status codes of API responses that you can use to understand and troubleshoot errors. For a table that lists and describes all of these status codes, see [Observability](index.md#observability). In addition, for information on how to resolve these errors, or the specific quota or limit that an error relates to, see [Troubleshooting](index.md#troubleshooting) and [Limits and Quotas](index.md#limits-and-quotas), respectively.
|
||||
|
||||
## Notification alerts
|
||||
|
||||
The email alert system automatically detects and sends you alerts of memory store usage issues that can harm the performance of your experience. There are two types of alerts:
|
||||
|
||||
- **Warnings** are alerts with lower priority about issues that can potentially impact your experience. You should resolve them before they become critical issues. The dashboard flags these alerts by highlighting the chart in yellow with a warning message.
|
||||
- **Criticals** are alerts with higher priority on issues directly impacting your experience. You should resolve these issues as soon as possible to prevent degraded performance. The dashboard flags these alerts by highlighting the chart in red with an error message.
|
||||
|
||||
When your experience triggers two types of alerts at the same time, the dashboard displays the alert header message and highlights the chart region for your attention as **Critical**.
|
||||
|
||||
Currently, the system has four built-in alerts for memory usage and API requests. If your experience exceeds an alerting threshold, the system notifies you at most once a day for each alert. When receiving an email alert, you should view the dashboard and take actions to prevent degraded performance.
|
||||
|
||||
### Memory usage alerts
|
||||
|
||||
The system sends the following memory usage alerts:
|
||||
|
||||
- A **Warning** alert when your experience's memory usage has exceeded 70% of the total quota within the past hour.
|
||||
|
||||
<img src="../../assets/data/memory-store/Memory-Usage-Warning.png" width="100%" />
|
||||
- A **Critical** alert when your experience exceeded the memory size quota for the past hour.
|
||||
|
||||
<img src="../../assets/data/memory-store/Memory-Usage-Critical.png" width="100%" />
|
||||
|
||||
These issues usually occur when you overpopulate a memory store data structure with too much data, which can consume the quota quickly even if you only send requests on a few keys. For example, if you set a long expiration time on keys or don't clean up processed data, extra data can fill up your memory quota.
|
||||
|
||||
To prevent and resolve these issues, apply [best practices](../../cloud-services/memory-stores/index.md#best-practices) to keep track of data structures you're actively using and set up a mechanism for cleaning up processed data.
|
||||
|
||||
### API request alerts
|
||||
|
||||
The system sends the following API request alerts:
|
||||
|
||||
- A **Critical** alert when the number of your memory store request failures has exceeded 20% within the past hour.
|
||||
|
||||
<img src="../../assets/data/memory-store/Request-Failure-Critical.png" width="100%" />
|
||||
- A **Critical** alert when more than 10% of your memory store requests are being throttled for the past hour.
|
||||
- This alert is based on the number of **DataStructureRequestsOverLimit** + **TotalRequestsOverLimit**, which are error responses indicating that your experience is throttled because it sends too many requests.
|
||||
- This alert isn't related to the request size, only quantity.
|
||||
|
||||
<img src="../../assets/data/memory-store/Request-Throttled-Critical.png" width="100%" />
|
||||
|
||||
All API requests have corresponding [response status codes](#response-status-codes), so you can view the ratio of **Success** responses to all error responses using the **Request Count by Status** chart for [troubleshooting](index.md#troubleshooting).
|
||||
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
title: Partitions and data distribution
|
||||
description: Explains the concept of a partition and per-partition limits.
|
||||
---
|
||||
|
||||
With the release of the `Class.MemoryStoreHashMap` data structure, Roblox removed all existing limits for individual data structures and replaced them with a single, global "per-partition" throttling limit. The exact limit fluctuates based on internal values and how the automatic partitioning process distributes your data, but generally allows for much higher usage before throttling, particularly for hash maps. This new limit enables flexible usage of memory stores across all data structures.
|
||||
|
||||
## Partitions
|
||||
|
||||
The MemoryStores API stores data on _partitions_, which are just subdivisions of storage. Whenever you write an item to a memory store, that item is stored on exactly one partition. Partitions are fully managed by the MemoryStores API; you do not need to manage them yourself.
|
||||
|
||||
## Partition assignment
|
||||
|
||||
Partition storage is different according to the data structure an item is being stored on. For sorted maps and queues, each data structure is assigned a single partition.
|
||||
|
||||
For example, consider a carnival game with a sorted map called `PlayerScores` and a queue called `PlayerLine` of players waiting to play the game:
|
||||
|
||||
<img src="../../assets/data/memory-store/Per-Partition-Limits-1.png" width="100%" />
|
||||
|
||||
Unlike sorted maps and queues, hash maps are allotted multiple partitions, and data is automatically distributed across these partitions. If you were to add a hash map called `Prizes`, the partitions might look like this:
|
||||
|
||||
<img src="../../assets/data/memory-store/Per-Partition-Limits-3.png" width="100%" />
|
||||
|
||||
Note how the hash map exists on all partitions, and each partition has some subset of items.
|
||||
|
||||
## Limits
|
||||
|
||||
Having a per-partition limit allows for higher throughput to all data structures. It also favors hash maps, because they're distributed across all partitions.
|
||||
|
||||
For example, consider an example per-partition limit of 50,000 requests per minute (RPM):
|
||||
|
||||
- In the very best case, a sorted map and a queue are limited to 50,000 RPM, because each one resides on a single partition.
|
||||
- Requests to hash maps are spread across the item keys, which are themselves spread across partitions, so hash maps can have a much higher effective limit before throttling, many times that of the other data structures assuming that requests are spread across many item keys.
|
||||
|
||||
<img src="../../assets/data/memory-store/Per-Partition-Limits-4.png" width="100%" />
|
||||
|
||||
For this reason, if you don't need sorting or "first in, first out" functionality, hash maps are usually the best choice for a memory store data structure. For more information, see the [best practices](../../cloud-services/memory-stores/best-practices.md).
|
||||
137
.cursor/rules/roblox/en-us/cloud-services/memory-stores/queue.md
Normal file
137
.cursor/rules/roblox/en-us/cloud-services/memory-stores/queue.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
---
|
||||
title: Memory store queue
|
||||
description: Explains how to implement the queue data structure for memory stores.
|
||||
---
|
||||
|
||||
A **queue** is a linear data structure with a collection of items that either follows the first-in-first-out (FIFO) principle or prioritizes elements based on predefined criteria. [Memory stores](../../cloud-services/memory-stores/index.md) support two types of queue, the FIFO [regular queues](../../luau/queues.md#regular-queues) and [priority queues](../../luau/queues.md#priority-queues). Both types share the same set of functions for initializing an empty queue, adding data to the queue, reading data from the queue, and removing data from the queue.
|
||||
|
||||
Memory store queues are useful for order-based processing and storing user information, such as skill levels, to facilitate matchmaking based on your desired criteria. For example, you can add a lobby place as the start place of your experience, use memory store queues as a centralized user information storage system accessible by multiple servers, manage the placement order of users using the queues, and teleport user who have completed the matchmaking to the main place of your experience.
|
||||
|
||||
## Get a queue
|
||||
|
||||
To get a queue, call `Class.MemoryStoreService:GetQueue()` with a **name**, which is global within the experience for any script to access, and an optional **invisibility timeout** in seconds, which prevents duplicated processing of the same queue item. Invisibility timeout is 30 seconds by default if you leave it empty like the following code sample.
|
||||
|
||||
```lua title="Getting an Empty Queue"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local queue = MemoryStoreService:GetQueue("Queue1")
|
||||
```
|
||||
|
||||
When a queue is processing an item in it, the invisibility timeout applies to the item, turning it invisible from being processed by other servers, as multiple servers can update the queue concurrently. Though it's expected to complete both read and removal operations for an item during the invisibility timeout duration, if an error occurs that causes the item to remain in the queue after the timeout elapses, the items become visible for processing again. In doing this, the invisibility timeout guarantees that all items in a queue can still be processed even if unexpected issues occur.
|
||||
|
||||
After you get a queue, call any of the following functions:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreQueue:AddAsync()`</td>
|
||||
<td>[Add](#add-data) a new item to the queue.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreQueue:ReadAsync()`</td>
|
||||
<td>[Read](#read-and-remove-data) one or more items from the queue as a single operation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreQueue:RemoveAsync()`</td>
|
||||
<td>[Remove](#read-and-remove-data) one or more items previously read from the queue.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreQueue:GetSizeAsync()`</td>
|
||||
<td>[Get](#get-size) the number of items in the queue.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Alert severity="warning">
|
||||
All functions accessing data structures in memory stores are asynchronous network calls that might occasionally fail. You should wrap these calls in `Global.LuaGlobals.pcall()` to catch and handle errors, like the code sample in each section shows.
|
||||
</Alert>
|
||||
|
||||
## Add data
|
||||
|
||||
To add a new item to the queue, call `Class.MemoryStoreQueue:AddAsync()` with the item value, an expiration time in seconds, and an optional priority of the item. If you want to keep your queue in the FIFO sequence, you can leave the priority empty or pass `0`.
|
||||
|
||||
```lua title="Adding Data to a Queue"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local queue = MemoryStoreService:GetQueue("Queue1")
|
||||
|
||||
local addSuccess, addError = pcall(function()
|
||||
queue:AddAsync("User_1234", 30, 1)
|
||||
end)
|
||||
if not addSuccess then
|
||||
warn(addError)
|
||||
end
|
||||
```
|
||||
|
||||
## Read and remove data
|
||||
|
||||
To read one or more items from the queue at once, call `Class.MemoryStoreQueue:ReadAsync()`, which returns an `id` representing the read item. When you finish processing items, immediately call `Class.MemoryStoreQueue:RemoveAsync()` to delete them from the queue with its `id`. This ensures that you never process an item more than once. To capture and respond to all items that are continuously being added to a queue, include a [loop](../../luau/control-structures.md) like the following code sample:
|
||||
|
||||
```lua title="Reading and Removing Data from Queue with Loop"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local queue = MemoryStoreService:GetQueue("Queue1")
|
||||
|
||||
local addSuccess, addError = pcall(function()
|
||||
queue:AddAsync("User_1234", 30, 1)
|
||||
end)
|
||||
if not addSuccess then
|
||||
warn(addError)
|
||||
end
|
||||
|
||||
-- Queue processing loop
|
||||
while true do
|
||||
local readSuccess, items, id = pcall(function()
|
||||
return queue:ReadAsync(1, false, 30)
|
||||
end)
|
||||
if not readSuccess then
|
||||
task.wait(1)
|
||||
elseif #items > 0 then
|
||||
print(items, id)
|
||||
local removeSuccess, removeError = pcall(function()
|
||||
queue:RemoveAsync(id)
|
||||
end)
|
||||
if not removeSuccess then
|
||||
warn(removeError)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Get size
|
||||
|
||||
To get the number of items in the queue, call
|
||||
`Class.MemoryStoreQueue:GetSizeAsync()`. This method takes an optional boolean `excludeInvisible`.
|
||||
When it is `true`, invisible items are excluded from the returned count.
|
||||
This boolean defaults to `false`.
|
||||
|
||||
```lua title="Getting the Size of a Queue"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local queue = MemoryStoreService:GetQueue("Queue1")
|
||||
|
||||
local addSuccess, addError = pcall(function()
|
||||
return queue:AddAsync("User_1234", 30, 1)
|
||||
end)
|
||||
|
||||
if not addSuccess then
|
||||
warn(addError)
|
||||
end
|
||||
|
||||
local size
|
||||
local success, sizError = pcall(function()
|
||||
size = queue:GetSizeAsync(true)
|
||||
end)
|
||||
|
||||
if success then
|
||||
print(size)
|
||||
else
|
||||
warn(sizeError)
|
||||
end
|
||||
```
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
---
|
||||
title: Memory store sorted map
|
||||
description: Explains how to implement the sorted map data structure for memory stores.
|
||||
---
|
||||
|
||||
The **sorted map** data structure of [memory stores](../../cloud-services/memory-stores/index.md) allows you to store frequent in-memory data as key-value pairs with an optional sort key and maintain a specific order based on the sort keys and keys. Unlike queues, the order of keys entering a map doesn't determine the order of processing, making sorted maps useful for sorting based data organization for implementing in-experience entities for engagement such as leaderboards and cross-server auctioning.
|
||||
|
||||
## Limits
|
||||
|
||||
In addition to the [data structure size limits](../../cloud-services/memory-stores/index.md#data-structure-size-limits), sorted maps have a key size limit of 128 characters, a value size limit of 32 KB, and a sort key size limit of 128 characters.
|
||||
|
||||
If you need to store data that surpasses this limit for your experience, you can adopt the sharding technique to split and distribute them through **key prefix** into multiple data structures. Sharding memory stores can also help improve the scalability of your system.
|
||||
|
||||
## Get a sorted map
|
||||
|
||||
To get a sorted map, call `Class.MemoryStoreService:GetSortedMap()` with a **name** you want to define for the map. The name is global within the experience, so you can access the same sorted map on any script using the name.
|
||||
|
||||
```lua title="Getting a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
```
|
||||
|
||||
After you get a sorted map, call any of the following functions:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreSortedMap:SetAsync()`</td>
|
||||
<td>[Add](#add-or-overwrite-data) a new key or overwrite the value and/or sort key if the key already exists.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreSortedMap:GetAsync()`</td>
|
||||
<td>[Read](#get-data) a particular key.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreSortedMap:GetRangeAsync()`</td>
|
||||
<td>[Read](#get-data) all existing keys or a specific range of them.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreSortedMap:UpdateAsync()`</td>
|
||||
<td>[Update](#update-data) the value of a key and/or sort key after retrieving it from a sorted map.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreSortedMap:RemoveAsync()`</td>
|
||||
<td>[Remove](#remove-data) a key from the sorted map.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Class.MemoryStoreSortedMap:GetSizeAsync()`</td>
|
||||
<td>[Get](#get-size) the number of items in the sorted map.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Alert severity="warning">
|
||||
All functions accessing data structures in memory stores are asynchronous network calls that might occasionally fail. You should wrap these calls in `Global.LuaGlobals.pcall()` to catch and handle errors, like the code sample in each section does.
|
||||
</Alert>
|
||||
|
||||
## Add or overwrite data
|
||||
|
||||
To add a new key or overwrite the value or sort key of a key in the sorted map, call `Class.MemoryStoreSortedMap:SetAsync()` with the key **name**, its **value**, an **expiration time** in seconds and an **optional sort key**. The memory automatically cleans up once the key expires. The maximum expiration time is 3,888,000 seconds (45 days). The sort key, if provided, must be a valid number (integer or floating point) or a string.
|
||||
|
||||
In the sorting order of your keys, a sort key takes precedence over a key. For example, when sorting in ascending order, numeric sort keys sort first, followed by string sort keys, followed by items with no sort key. All items with numeric sort keys are sorted by sort key, if the sort key for two items is equal, they are sorted by key. Similarly, all items with string sort keys are sorted by sort key, if the sort key for two items is equal, they are sorted by key. All items with no sort key are sorted just by the key.
|
||||
|
||||
Example of some data sorted in ascending order -
|
||||
|
||||
```text
|
||||
{key: "player1", value: someValue1, sortKey: -1}
|
||||
{key: "player2", value: someValue2, sortKey: 0}
|
||||
{key: "player4", value: someValue3, sortKey: 1}
|
||||
{key: "player5", value: someValue4, sortKey: 1}
|
||||
{key: "player3", value: someValue5, sortKey: 3.14}
|
||||
{key: "player6", value: someValue6, sortKey: "someString"}
|
||||
{key: "player0", value: someValue7}
|
||||
{key: "player7", value: someValue8}
|
||||
```
|
||||
|
||||
Note how `player0` sorts after all keys with a sort key. `player6` sorts after all keys with a numeric sort key. `player4` and `player5` have the same sort key, so they are sorted in ascending order by key.
|
||||
|
||||
<Alert severity="warning">
|
||||
Under the EU General Data Protection Regulation (GDPR), if your memory stores have user data subject to [Right to be Forgotten](https://gdpr.eu/right-to-be-forgotten/) you **must** remove the data in 30 days, even if you set your memory store key's expiration up to 45 days.
|
||||
</Alert>
|
||||
|
||||
```lua title="Adding Data to a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return sortedMap:SetAsync("User_1234", 1000, 30, 3.14152)
|
||||
end)
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
```
|
||||
|
||||
## Get data
|
||||
|
||||
You can either get a data value and sort key associated with a specific key or get multiple values and sort keys for keys within a range.
|
||||
|
||||
### Get data with one key
|
||||
|
||||
To get a value and sort key associated with one key from the sorted map, call `Class.MemoryStoreSortedMap:GetAsync()` with the key **name**.
|
||||
|
||||
```lua title="Getting a Particular Key from a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return sortedMap:SetAsync("User_1234", 1000, 30, 3.14152)
|
||||
end)
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
|
||||
local item
|
||||
local getSuccess, getError = pcall(function()
|
||||
item = sortedMap:GetAsync("User_1234")
|
||||
end)
|
||||
if getSuccess then
|
||||
print(item)
|
||||
else
|
||||
warn(getError)
|
||||
end
|
||||
```
|
||||
|
||||
### Get data with multiple keys
|
||||
|
||||
To get data for multiple keys from the sorted map as a single operation, call `Class.MemoryStoreSortedMap:GetRangeAsync()`. This function lists all existing keys by default, but you can set the upper and lower bounds for the key range. For example, the following code sample retrieves up to 20 items starting from the beginning of the sorted map, with keys greater than or equal to `10`, sort keys greater than or equal to `100` and keys less than or equal to `50`, sort keys less than or equal to `500`.
|
||||
|
||||
```lua title="Getting a Range of Keys from a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
|
||||
local lowerBound = {}
|
||||
lowerBound["key"] = "10"
|
||||
lowerBound["sortKey"] = 100
|
||||
local upperBound = {}
|
||||
upperBound["key"] = "50"
|
||||
upperBound["sortKey"] = 500
|
||||
|
||||
-- Get up to 20 items starting from the beginning
|
||||
local getSuccess, items = pcall(function()
|
||||
return sortedMap:GetRangeAsync(
|
||||
Enum.SortDirection.Ascending, 20, lowerBound, upperBound)
|
||||
end)
|
||||
if getSuccess then
|
||||
for _, item in items do
|
||||
print(item.key)
|
||||
print(item.sortKey)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Update data
|
||||
|
||||
To retrieve the value and sort key of a key from a sorted map and update it, call `Class.MemoryStoreSortedMap:UpdateAsync()` with the key **name**, a **callback function** to update the value and sort key for this key, and an **expiration time** in seconds. The maximum expiration time is 3,888,000 seconds (45 days).
|
||||
|
||||
For most experiences, multiple servers can update the same key concurrently and change the value. As `Class.MemoryStoreSortedMap:UpdateAsync()|UpdateAsync()` always modifies the latest value before updating, you should use it to read the latest value as the input for your callback function.
|
||||
|
||||
For example, the following code sample updates the score in a leaderboard for a player. The score is calculated as kills / deaths. `Class.MemoryStoreSortedMap:UpdateAsync()|UpdateAsync()` ensures that the kills and deaths are updated for the most recent values even if multiple experience servers update the same item simultaneously. A player's kills and deaths are monotonically increasing values and can hence only increase in value in a session.
|
||||
|
||||
```lua title="Updating the leaderboard score for a player in a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("Leaderboard")
|
||||
|
||||
local function updateLeaderboard(itemKey, killsToAdd, deathsToAdd)
|
||||
local success, newStats, newScore = pcall(function()
|
||||
return sortedMap:UpdateAsync(itemKey, function(playerStats, playerScore)
|
||||
playerStats = playerStats or { kills = 0, deaths = 0 }
|
||||
playerStats.kills += killsToAdd
|
||||
playerStats.deaths += deathsToAdd
|
||||
if playerStats then
|
||||
-- `playerScore` is the sortKey being used to sort items in the map
|
||||
playerScore = playerStats.kills / math.max(playerStats.deaths, 1)
|
||||
return playerStats, playerScore
|
||||
end
|
||||
return nil
|
||||
end, 30)
|
||||
end)
|
||||
if success then
|
||||
print(newStats)
|
||||
print(newScore)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The latency for `Class.MemoryStoreSortedMap:UpdateAsync()|UpdateAsync()` is similar to `Class.MemoryStoreSortedMap:GetAsync()|GetAsync()` and `Class.MemoryStoreSortedMap:SetAsync()|SetAsync()` unless there is contention.
|
||||
|
||||
When contention occurs, the system automatically retries the operation until one of these three happens: the operation succeeds, the callback function returns `nil`, or the maximum number of retries is reached. If the system reaches the maximum number of retries, it returns a conflict.
|
||||
|
||||
## Remove data
|
||||
|
||||
You can use `Class.MemoryStoreSortedMap:RemoveAsync()` for both removing one key from the sorted map and deleting all data in a memory store sorted map.
|
||||
|
||||
### Remove a key
|
||||
|
||||
To remove a key from the sorted map, call `Class.MemoryStoreSortedMap:RemoveAsync()` with a key **name**.
|
||||
|
||||
```lua title="Remove a Key from a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return sortedMap:SetAsync("User_1234", 1000, 30, "someStringSortKey")
|
||||
end)
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
|
||||
local removeSuccess, removeError = pcall(function()
|
||||
sortedMap:RemoveAsync("User_1234")
|
||||
end)
|
||||
if not removeSuccess then
|
||||
warn(removeError)
|
||||
end
|
||||
```
|
||||
|
||||
### Delete all data
|
||||
|
||||
To delete memory in sorted maps, list all your keys with `Class.MemoryStoreSortedMap:GetRangeAsync()`, then remove them with `Class.MemoryStoreSortedMap:RemoveAsync()`.
|
||||
|
||||
```lua title="Delete Memory in a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
|
||||
-- Initial lower bound of nil starts flush from first item
|
||||
local exclusiveLowerBound = nil
|
||||
|
||||
while true do
|
||||
-- Get up to a hundred items starting from current lower bound
|
||||
local getRangeSuccess, items = pcall(function()
|
||||
return sortedMap:GetRangeAsync(Enum.SortDirection.Ascending, 100, exclusiveLowerBound)
|
||||
end)
|
||||
if getRangeSuccess then
|
||||
local removeSuccess = true
|
||||
local removeError = nil
|
||||
|
||||
for _, item in items do
|
||||
removeSuccess, removeError = pcall(function()
|
||||
sortedMap:RemoveAsync(item.key)
|
||||
end)
|
||||
end
|
||||
|
||||
-- If there was an error removing items, try again with the same exclusive lower bound
|
||||
if not removeSuccess then
|
||||
warn(removeError)
|
||||
-- If range is less than a hundred items, end of map is reached
|
||||
elseif #items < 100 then
|
||||
break
|
||||
else
|
||||
-- The last retrieved key is the exclusive lower bound for the next iteration
|
||||
exclusiveLowerBound = {}
|
||||
exclusiveLowerBound["key"] = items[#items].key
|
||||
exclusiveLowerBound["sortKey"] = items[#items].sortKey
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Get size
|
||||
|
||||
To get the number of items in the sorted map, call
|
||||
`Class.MemoryStoreSortedMap:GetSizeAsync()`.
|
||||
|
||||
```lua title="Getting the Size of a Sorted Map"
|
||||
local MemoryStoreService = game:GetService("MemoryStoreService")
|
||||
|
||||
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
|
||||
|
||||
local setSuccess, _ = pcall(function()
|
||||
return sortedMap:SetAsync("User_1234", 1000, 30, 3.14152)
|
||||
end)
|
||||
|
||||
if setSuccess then
|
||||
print("Set succeeded.")
|
||||
end
|
||||
|
||||
local size
|
||||
local success, sizError = pcall(function()
|
||||
size = sortedMap:GetSizeAsync()
|
||||
end)
|
||||
|
||||
if success then
|
||||
print(size)
|
||||
else
|
||||
warn(sizeError)
|
||||
end
|
||||
```
|
||||
70
.cursor/rules/roblox/en-us/cloud-services/secrets.md
Normal file
70
.cursor/rules/roblox/en-us/cloud-services/secrets.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
title: Secrets stores
|
||||
description: Securely manage API keys, passwords, and assess tokens for third-party services with your experience's secrets store.
|
||||
---
|
||||
|
||||
Roblox offers a **secrets** store for each experience. Secrets are sensitive information like API keys, passwords, and access tokens that you use to authenticate with external services. For example, if you want to connect to a third-party analytics or music service, you likely need to use an API key to authenticate with it.
|
||||
|
||||
You could copy and paste the API key into a script or add it to a data store, but those approaches carry unnecessary security risks. The better solution is to use the secrets store and access the key using a small set of secure methods.
|
||||
|
||||
## Add secrets
|
||||
|
||||
To view, create, or edit secrets, you must be the experience owner or group owner. You can have up to 500 secrets per experience.
|
||||
|
||||
1. Navigate to the [Creator Dashboard](https://create.roblox.com/dashboard/creations).
|
||||
2. Select your experience, and then select **Secrets** ⟩ **Create Secret**.
|
||||
3. Enter a name, the secret, and the applicable domain.
|
||||
|
||||
- The name acts as a unique identifier for the secret, so we recommend something descriptive.
|
||||
- Secrets can be up to 1,024 characters in length. API keys and access tokens should come from the service provider, but if the secret is a password, you probably created it yourself.
|
||||
- You can use a limited wildcard syntax for the domain, such as `*` for any domain (not recommended) or `*.example.com` for any subdomain at `example.com`. Specific domains are even better, such as `my.example.com`.
|
||||
|
||||
<Alert severity="success">
|
||||
You can also use Open Cloud to manage your secrets. See the [reference documentation](../cloud/api/secrets-store.md) and [guide](../cloud/guides/secrets-store.md).
|
||||
</Alert>
|
||||
|
||||
## Local secrets
|
||||
|
||||
For security reasons, the secrets store for each experience is only available to live servers or [collaborative testing](../studio/testing-modes.md#collaborative-testing) environments. If you try to access a secret during [local playtesting](../studio/testing-modes.md#playtesting), you receive a `Can't find secret with given key` error. You receive an identical error if you try to access secrets from a client script.
|
||||
|
||||
To specify secrets for local testing, go to Studio and then go to **File** ⟩ **Experience Settings** ⟩ **Security**. You can create new secrets and edit or delete existing secrets in the **Secrets** section.
|
||||
|
||||
<img src="../assets/data/secrets-store/LocalSecrets.png" width="750px" alt="View, create, edit secrets with the Secrets UI."/>
|
||||
|
||||
<img src="../assets/data/secrets-store/LocalSecrets_Edit.png" width="750px" alt="Add a new secret with name secretName1, value 123456, and restricted to domain *.example.com."/>
|
||||
|
||||
## Use secrets
|
||||
|
||||
Before using secrets within your experience, you must enable **Allow HTTP Requests** in the **Security** section of Studio's **File** ⟩ **Experience Settings** window. Then call `Class.HttpService:GetSecret()` within a server script:
|
||||
|
||||
```lua
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local testSecret = HttpService:GetSecret("test_secret")
|
||||
```
|
||||
|
||||
Part of the appeal of using secret stores is that you can't accidentally print a secret. Instead of the secret itself, the following code outputs the name you provided when creating the secret:
|
||||
|
||||
```lua
|
||||
print(testSecret) --> Secret(test_secret)
|
||||
```
|
||||
|
||||
You can't manipulate the string directly. Instead, the `Datatype.Secret` data type lets you add a prefix and suffix to the secret to help form a URL or insert content like `Bearer`:
|
||||
|
||||
```lua
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local testSecret = HttpService:GetSecret("test_secret")
|
||||
|
||||
local prefix = "https://my.example.com/endpoint?apiKey="
|
||||
local suffix = "&user=username"
|
||||
local url = testSecret:AddPrefix(prefix)
|
||||
url = url:AddSuffix(suffix)
|
||||
print(url) --> https://my.example.com/endpoint?apiKey=Secret(test_secret)&user=username
|
||||
```
|
||||
|
||||
After you have a URL with the secret inserted, you can make standard HTTP requests using methods like `Class.HttpService:RequestAsync()`. Of course, you can ignore these methods and insert the secret directly into a header, too.
|
||||
|
||||
<Alert severity="info">
|
||||
You can't include secrets in the HTTP request body, only the URL and headers.
|
||||
</Alert>
|
||||
Loading…
Add table
Add a link
Reference in a new issue