How to Create a Locking Module Provider
In this document, you’ll learn how to create a Locking Module Provider and the methods you must implement in its main service.
Implementation Example#
As you implement your Locking Module Provider, it can be useful to refer to an existing provider and how it's implemeted.
If you need to refer to an existing implementation as an example, check the Redis Locking Module Provider in the Medusa repository.
1. Create Module Provider Directory#
Start by creating a new directory for your module provider.
If you're creating the module provider in a Medusa application, create it under the src/modules
directory. For example, src/modules/my-locking
.
If you're creating the module provider in a plugin, create it under the src/providers
directory. For example, src/providers/my-locking
.
src/modules/my-locking
directory as an example.2. Create the Locking Module Provider Service#
Create the file src/modules/my-locking/service.ts
that holds the module provider's main service. It must implement the ILockingProvider
interface imported from @medusajs/framework/types
:
constructor#
The constructor allows you to access resources from the module's container using the first parameter, and the module's options using the second parameter.
If you're creating a client or establishing a connection with a third-party service, do it in the constructor.
Example
1import { ILockingProvider } from "@medusajs/framework/types"2import { Logger } from "@medusajs/framework/types"3 4type InjectedDependencies = {5 logger: Logger6}7 8type Options = {9 url: string10}11 12class MyLockingProviderService implements ILockingProvider {13 static identifier = "my-lock"14 protected logger_: Logger15 protected options_: Options16 // assuming you're initializing a client17 protected client18 19 constructor (20 { logger }: InjectedDependencies,21 options: Options22 ) {23 this.logger_ = logger24 this.options_ = options25 26 // assuming you're initializing a client27 this.client = new Client(options)28 }29 30 // ...31}32 33export default MyLockingProviderService
Identifier#
Every locking module provider must have an identifier
static property. The provider's ID
will be stored as lp_{identifier}
.
For example:
execute#
This method executes a given asynchronous job with a lock on the given keys. The Locking Module uses this method
when you call its execute
method and your provider is the default provider, or you pass your provider's identifier to its execute
method.
In the method, you should first try to acquire the lock on the given keys before the specified timeout passes. Then, once the lock is acquired, you execute the job. Otherwise, if the timeout passes before the lock is acquired, you cancel the job.
Example
An example of how to implement the execute
method:
1// other imports...2import { Context } from "@medusajs/framework/types"3import { setTimeout } from "node:timers/promises"4 5class MyLockingProviderService implements ILockingProvider {6 // ...7async execute<T>(8 keys: string | string[], 9 job: () => Promise<T>, 10 args?: { timeout?: number }, 11 sharedContext?: Context12 ): Promise<T> {13 // TODO you can add actions using the third-party client you initialized in the constructor14 const timeout = Math.max(args?.timeout ?? 5, 1)15 const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout16 const cancellationToken = { cancelled: false }17 const promises: Promise<any>[] = []18 19 if (timeoutSeconds > 0) {20 promises.push(this.getTimeout(timeoutSeconds, cancellationToken))21 }22 23 promises.push(24 this.acquire_(25 keys,26 {27 expire: args?.timeout ? timeoutSeconds : 0,28 },29 cancellationToken30 )31 )32 33 await Promise.race(promises)34 35 try {36 return await job()37 } finally {38 await this.release(keys)39 }40 }41 42 private async getTimeout(43 seconds: number,44 cancellationToken: { cancelled: boolean }45 ): Promise<void> {46 return new Promise(async (_, reject) => {47 await setTimeout(seconds * 1000)48 cancellationToken.cancelled = true49 reject(new Error("Timed-out acquiring lock."))50 })51 }52}
In this example, you first determine the timeout for acquiring the lock. You also create a cancellationToken
object that you'll use to determine if the lock aquisition has timed out.
You then create an array of the following promises:
- A timeout promise that, if the lock acquisition takes longer than the timeout, sets the
cancelled
property of thecancellationToken
object totrue
. - A promise that acquires the lock. You use a private
acquire_
method which you can find its implementation in theaquire
method's example. If the first promise resolves and cancels the lock acquisition, the lock will not be acquired.
Finally, if the lock is acquired, you execute the job and release the lock after the job is done using the release
method.
Type Parameters
T
objectOptionalParameters
keys
string | string[]job
() => Promise<T>args
objectOptionalAdditional arguments for the job execution.
args
objectOptionalReturns
Promise
Promise<T>acquire#
This method acquires a lock on the given keys. The Locking Module uses this method when you call its acquire
method and your provider is the default provider,
or you pass your provider's identifier to its acquire
method.
In this method, you should only aquire the lock if the timeout hasn't passed. As explained in the execute method's example,
you can use a cancellationToken
object to determine if the lock acquisition has timed out.
If the lock aquisition isn't canceled, you should aquire the lock, setting its expiry and owner. You should account for the following scenarios:
- The lock doesn't have an owner and you don't pass an owner, in which case the lock can be extended or released by anyone.
- The lock doesn't have an owner or has the same owner that you pass, in which case you can extend the lock's expiration time and set the owner.
- The lock has an owner, but you pass a different owner, in which case the method should throw an error.
Example
An example of how to implement the acquire
method:
1type ResolvablePromise = {2 promise: Promise<any>3 resolve: () => void4}5 6class MyLockingProviderService implements ILockingProvider {7 // ...8 async acquire(9 keys: string | string[],10 args?: {11 ownerId?: string | null12 expire?: number13 awaitQueue?: boolean14 }15 ): Promise<void> {16 return this.acquire_(keys, args)17 }18 19 async acquire_(20 keys: string | string[],21 args?: {22 ownerId?: string | null23 expire?: number24 awaitQueue?: boolean25 },26 cancellationToken?: { cancelled: boolean }27 ): Promise<void> {28 keys = Array.isArray(keys) ? keys : [keys]29 const { ownerId, expire } = args ?? {}30 31 for (const key of keys) {32 if (cancellationToken?.cancelled) {33 throw new Error("Timed-out acquiring lock.")34 }35 36 // assuming your client has this method and it validates the owner and expiration37 const result = await this.client.acquireLock(key, ownerId, expire)38 39 if (result !== 1) {40 throw new Error(`Failed to acquire lock for key "${key}"`)41 }42 }43 }44}
In this example, you add a private acquire_
method that you use to acquire the lock. This method accepts an additional cancellationToken
argument that you can use to determine if the lock acquisition has timed out.
You can then use this method in other methods, such as the execute
method.
In the acquire_
method, you loop through the keys and try to acquire the lock on each key if the lock acquisition hasn't timed out. If the lock acquisition fails, you throw an error.
This method assumes that the client you're integrating has a method called acquireLock
that validates the owner and expiration time, and returns 1
if the lock is successfully acquired.
Parameters
keys
string | string[]args
objectOptionalAdditional arguments for acquiring the lock.
args
objectOptionalReturns
Promise
Promise<void>release#
This method releases a lock on the given keys. The Locking Module uses this method when you call its release
method and your provider is the default provider,
or you pass your provider's identifier to its release
method.
In this method, you should release the lock on the given keys. If the lock has an owner, you should only release the lock if the owner is the same as the one passed.
Example
An example of how to implement the release
method:
1// other imports...2import { promiseAll } from "@medusajs/framework/utils"3 4class MyLockingProviderService implements ILockingProvider {5 // ...6 async release(7 keys: string | string[], 8 args?: { ownerId?: string | null }, 9 sharedContext?: Context10 ): Promise<boolean> {11 const ownerId = args?.ownerId ?? "*"12 keys = Array.isArray(keys) ? keys : [keys]13 14 const releasePromises = keys.map(async (key) => {15 // assuming your client has this method and it validates the owner16 const result = await this.client.releaseLock(key, ownerId)17 return result === 118 })19 20 const results = await promiseAll(releasePromises)21 22 return results.every((released) => released)23 }24}
In this example, you loop through the keys and try to release the lock on each key using the client you're integrating. This implementation assumes that the client validates
ownership of the lock and returns a result of 1
if the lock is successfully released.
Parameters
keys
string | string[]args
objectOptionalAdditional arguments for releasing the lock.
args
objectOptionalReturns
Promise
Promise<boolean>Whether the lock was successfully released. If the lock has a different owner than the one passed, the method returns false
.
Promise
Promise<boolean>false
.releaseAll#
This method releases all locks. The Locking Module uses this method when you call its releaseAll
method and your provider is the default provider,
or you pass your provider's identifier to its releaseAll
method.
In this method, you should release all locks if no owner is passed. If an owner is passed, you should only release the locks that the owner has acquired.
Example
An example of how to implement the releaseAll
method:
In this example, you release all locks either of all owners or the owner passed as an argument. This implementation assumes that the client you're integrating has a method called releaseAllLock
that releases all locks
for all owners or a specific owner.
Parameters
args
objectOptionalAdditional arguments for releasing the locks.
args
objectOptionalReturns
Promise
Promise<void>releaseAll
method and your provider is the default provider,
or you pass your provider's identifier to its releaseAll
method.
In this method, you should release all locks if no owner is passed. If an owner is passed, you should only release the locks that the owner has acquired.3. Create Module Definition File#
Create the file src/modules/my-locking/index.ts
with the following content:
This exports the module provider's definition, indicating that the MyLockingProviderService
is the module provider's service.
4. Use Module Provider#
To use your Locking Module Provider, add it to the providers
array of the Locking Module in medusa-config.ts
:
1module.exports = defineConfig({2 // ...3 modules: [4 {5 resolve: "@medusajs/medusa/payment",6 options: {7 providers: [8 {9 // if module provider is in a plugin, use `plugin-name/providers/my-locking`10 resolve: "./src/modules/my-locking",11 id: "my-lock",12 // set this if you want this provider to be used by default13 // and you have other Locking Module Providers registered.14 is_default: true,15 options: {16 url: "http://example.com",17 // provider options...18 }19 },20 ]21 }22 }23 ]24})
5. Test it Out#
When you start the Medusa application, if your Locking Module Provider is the only registered provider without enabling is_default
, you'll see the following message:
This indicates that your Locking Module Provider is being used as the default provider.
The Locking Module will now use your provider to handle all locking operations.