mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-24 14:44:07 +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,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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue