docs: add documentation for Locking Module (#11824)

* add locking docs

* fix main navbar

* added implementation example links

* generate refs

* update architecture

* fix vale error
This commit is contained in:
Shahed Nasser
2025-03-13 12:20:24 +02:00
committed by GitHub
parent 5cf0bf4d93
commit 28b0d08591
94 changed files with 15932 additions and 10857 deletions
@@ -0,0 +1,160 @@
import { Table, CardList } from "docs-ui"
export const metadata = {
title: `Locking Module`,
}
# {metadata.title}
In this document, you'll learn about the Locking Module and its providers.
## What is the Locking Module?
The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes that are trying to access the same resource at the same time, and ensures data consistency.
Medusa uses the Locking Module to control concurrency, avoid race conditions, and protect parts of code that should not be executed by more than one process at a time. This is especially essential in distributed or multi-threaded environments.
For example, Medusa uses the Locking Module in inventory management to ensure that only one transaction can update the stock levels at a time. By using the Locking Module in this scenario, Medusa prevents overselling an inventory item and keeps its quantity amounts accurate, even during high traffic periods or when receiving concurrent requests.
---
## How to Use the Locking Module?
You can use the Locking Module as part of the [workflows](!docs!/learn/fundamentals/workflows) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
In a step of your workflow, you can resolve the Locking Module's service and use its methods to execute an asynchronous job, acquire a lock, or release locks.
For example:
```ts
import { Modules } from "@medusajs/framework/utils"
import {
createStep,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async ({}, { container }) => {
const lockingModuleService = container.resolve(
Modules.LOCKING
)
const productModuleService = container.resolve(
Modules.PRODUCT
)
await lockingModuleService.execute("prod_123", async () => {
await productModuleService.deleteProduct("prod_123")
})
}
)
export const workflow = createWorkflow(
"workflow-1",
() => {
step1()
}
)
```
In the example above, you create a workflow that has a step. In the step, you resolve the services of the Locking and Product modules from the [Medusa container](!docs!/learn/fundamentals/medusa-container).
Then, you use the `execute` method of the Locking Module to acquire a lock for the product with the ID `prod_123` and execute an asynchronous function, which deletes the product.
---
## When to Use the Locking Module?
You should use the Locking Module when you need to ensure that only one process can access a shared resource at a time. As mentioned in the inventory example previously, you don't want customers to order quantities of inventory that are not available, or to update the stock levels of an item concurrently.
In those scenarios, you can use the Locking Module to acquire a lock for a resource and execute a critical section of code that should not be accessed by multiple processes simultaneously.
---
## What is a Locking Module Provider?
A Locking Module Provider implements the underlying logic of the Locking Module. It manages the locking mechanisms and ensures that only one process can access a shared resource at a time.
Medusa provides [multiple Locking Module Providers](#list-of-locking-module-providers) that are suitable for development and production. You can also create a [custom Locking Module Provider](/references/locking-module-provider) to implement custom locking mechanisms or integrate with third-party services.
### Default Locking Module Provider
By default, Medusa uses the In-Memory Locking Module Provider. This provider uses a plain JavaScript map to store the locks. While this is useful for development, it is not recommended for production environments as it is only intended for use in a single-instance environment.
To add more providers, you can register them in the `medusa-config.ts` file. For example:
```ts
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/locking",
options: {
providers: [
// add providers here...
]
}
}
]
})
```
When you register other providers in `medusa-config.ts`, Medusa will set the default provider based on the following scenarios:
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Scenario</Table.HeaderCell>
<Table.HeaderCell>Default Provider</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
One provider is registered.
</Table.Cell>
<Table.Cell>
The registered provider.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
Multiple providers are registered and none of them has an `is_default` flag.
</Table.Cell>
<Table.Cell>
In-Memory Locking Module Provider.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
Multiple providers and one of them has an `is_default` flag.
</Table.Cell>
<Table.Cell>
The provider with the `is_default` flag.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
---
## List of Locking Module Providers
Medusa provides the following Locking Module Providers. You can use one of them, or [Create a Locking Module Provider](./references/locking-module-provider).
<CardList
items={[
{
title: "Redis",
href: "/architectural-modules/locking/redis",
badge: {
variant: "green",
children: "Recommended"
}
},
{
title: "PostgreSQL",
href: "/architectural-modules/locking/postgres",
}
]}
/>
@@ -0,0 +1,105 @@
export const metadata = {
title: `PostgreSQL Locking Module Provider`,
}
# {metadata.title}
The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database.
The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks.
<Note>
While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](../redis/page.mdx) if possible.
</Note>
---
## Register the PostgreSQL Locking Module Provider
To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:
```ts title="medusa-config.ts"
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/locking",
options: {
providers: [
{
resolve: "@medusajs/medusa/locking-postgres",
id: "postgres-lock",
// set this if you want this provider to be used by default
// and you have other Locking Module Providers registered.
is_default: true,
},
]
}
}
]
})
```
### Run Migrations
The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider:
```bash
npx medusa db:migrate
```
This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database.
---
## Use Provider with Locking Module
The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:
export const defaultHighlights = [
["11", "is_default"]
]
```ts title="medusa-config.ts" highlights={defaultHighlights}
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/locking",
options: {
providers: [
{
resolve: "@medusajs/medusa/locking-postgres",
id: "postgres-lock",
is_default: true,
},
]
}
}
]
})
```
If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods.
For example, when using the `acquire` method in a [workflow step](!docs!/learn/fundamentals/workflows):
```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async ({}, { container }) => {
const lockingModuleService = container.resolve(
Modules.LOCKING
)
await lockingModuleService.acquire("prod_123", {
provider: "lp_locking-postgres",
})
}
)
```
@@ -0,0 +1,289 @@
import { Table, Prerequisites } from "docs-ui"
export const metadata = {
title: `Redis Locking Module Provider`,
}
# {metadata.title}
The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments.
This provider is recommended for production environments where Medusa is running in a multi-instance setup.
---
## Register the Redis Locking Module Provider
<Prerequisites
items={[
{
text: "A redis server set up locally or a database in your deployed application.",
link: "https://redis.io/download",
}
]}
/>
To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:
```ts title="medusa-config.ts"
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/locking",
options: {
providers: [
{
resolve: "@medusajs/medusa/locking-redis",
id: "redis-lock",
// set this if you want this provider to be used by default
// and you have other Locking Module Providers registered.
is_default: true,
options: {
redisUrl: process.env.LOCKING_REDIS_URL,
}
},
]
}
}
]
})
```
### Environment Variables
Make sure to add the following environment variable:
```bash
LOCKING_REDIS_URL=<YOUR_LOCKING_REDIS_URL>
```
Where `<YOUR_LOCKING_REDIS_URL>` is the URL of your Redis server, either locally or in the deployed environment.
<Note title="Tip">
The default Redis URL in a local environment is `redis://localhost:6379`.
</Note>
### Redis Locking Module Provider Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`redisUrl`
</Table.Cell>
<Table.Cell>
A string indicating the Redis connection URL.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`redisOptions`
</Table.Cell>
<Table.Cell>
An object of Redis options. Refer to the [Redis API Reference](https://redis.github.io/ioredis/index.html#RedisOptions) for details on accepted properties.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`namespace`
</Table.Cell>
<Table.Cell>
A string used to prefix all locked keys with `{namespace}`.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`medusa_lock:`. So, all locked keys are prefixed with `medusa_lock:`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`waitLockingTimeout`
</Table.Cell>
<Table.Cell>
A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`5`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`defaultRetryInterval`
</Table.Cell>
<Table.Cell>
A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`5`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`maximumRetryInterval`
</Table.Cell>
<Table.Cell>
A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`200`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
---
## Test out the Module
To test out the Redis Locking Module Provider, start the Medusa application:
```bash npm2yarn
npm run dev
```
You'll see the following message logged in the terminal:
```bash
info: Connection to Redis in "locking-redis" provider established
```
This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server.
If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations.
---
## Use Provider with Locking Module
The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:
export const defaultHighlights = [
["11", "is_default"]
]
```ts title="medusa-config.ts" highlights={defaultHighlights}
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/locking",
options: {
providers: [
{
resolve: "@medusajs/medusa/locking-redis",
id: "redis-lock",
is_default: true,
options: {
// ...
}
},
]
}
}
]
})
```
If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods.
For example, when using the `acquire` method in a [workflow step](!docs!/learn/fundamentals/workflows):
```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
const step1 = createStep(
"step-1",
async ({}, { container }) => {
const lockingModuleService = container.resolve(
Modules.LOCKING
)
await lockingModuleService.acquire("prod_123", {
provider: "lp_locking-redis",
})
}
)
```