feat(providers): locking redis (#9544)

This commit is contained in:
Carlos R. L. Rodrigues
2024-10-15 12:40:24 -03:00
committed by GitHub
parent e77a2ff032
commit 4a03bdbb86
49 changed files with 1764 additions and 483 deletions
@@ -76,14 +76,28 @@ class RedisCacheService implements ICacheService {
* @param key
*/
async invalidate(key: string): Promise<void> {
const keys = await this.redis.keys(this.getCacheKey(key))
const pipeline = this.redis.pipeline()
const pattern = this.getCacheKey(key)
let cursor = "0"
do {
const result = await this.redis.scan(
cursor,
"MATCH",
pattern,
"COUNT",
100
)
cursor = result[0]
const keys = result[1]
keys.forEach(function (key) {
pipeline.del(key)
})
if (keys.length > 0) {
const deletePipeline = this.redis.pipeline()
for (const key of keys) {
deletePipeline.del(key)
}
await pipeline.exec()
await deletePipeline.exec()
}
} while (cursor !== "0")
}
/**
@@ -1,5 +1,5 @@
import { ILockingModule } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { Modules, promiseAll } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { setTimeout } from "node:timers/promises"
@@ -63,7 +63,7 @@ moduleIntegrationTestRunner<ILockingModule>({
expect(userReleased).toBe(false)
await expect(anotherUserLock).rejects.toThrowError(
`"key_name" is already locked.`
`Failed to acquire lock for key "key_name"`
)
const releasing = await service.release("key_name", {
@@ -82,16 +82,20 @@ moduleIntegrationTestRunner<ILockingModule>({
ownerId: "user_id_000",
}
expect(service.acquire(keyToLock, user_1)).resolves.toBeUndefined()
await expect(
service.acquire(keyToLock, user_1)
).resolves.toBeUndefined()
expect(service.acquire(keyToLock, user_1)).resolves.toBeUndefined()
await expect(
service.acquire(keyToLock, user_1)
).resolves.toBeUndefined()
expect(service.acquire(keyToLock, user_2)).rejects.toThrowError(
`"${keyToLock}" is already locked.`
await expect(service.acquire(keyToLock, user_2)).rejects.toThrowError(
`Failed to acquire lock for key "${keyToLock}"`
)
expect(service.acquire(keyToLock, user_2)).rejects.toThrowError(
`"${keyToLock}" is already locked.`
await expect(service.acquire(keyToLock, user_2)).rejects.toThrowError(
`Failed to acquire lock for key "${keyToLock}"`
)
await service.acquire(keyToLock, user_1)
@@ -104,6 +108,40 @@ moduleIntegrationTestRunner<ILockingModule>({
const release = await service.release(keyToLock, user_1)
expect(release).toBe(true)
})
it("should fail to acquire the same key when no owner is provided", async () => {
const keyToLock = "mySpecialKey"
const user_2 = {
ownerId: "user_id_000",
}
await expect(service.acquire(keyToLock)).resolves.toBeUndefined()
await expect(service.acquire(keyToLock)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock, user_2)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock, user_2)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
const releaseNotLocked = await service.release(keyToLock, {
ownerId: "user_id_000",
})
expect(releaseNotLocked).toBe(false)
const release = await service.release(keyToLock)
expect(release).toBe(true)
})
})
it("should release lock in case of failure", async () => {
@@ -118,5 +156,48 @@ moduleIntegrationTestRunner<ILockingModule>({
expect(fn_1).toBeCalledTimes(1)
expect(fn_2).toBeCalledTimes(1)
})
it("should release lock in case of timeout failure", async () => {
const fn_1 = jest.fn(async () => {
await setTimeout(1010)
return "fn_1"
})
const fn_2 = jest.fn(async () => {
return "fn_2"
})
const fn_3 = jest.fn(async () => {
return "fn_3"
})
const ops = [
service
.execute("lock_key", fn_1, {
timeout: 1,
})
.catch((e) => e),
service
.execute("lock_key", fn_2, {
timeout: 1,
})
.catch((e) => e),
service
.execute("lock_key", fn_3, {
timeout: 2,
})
.catch((e) => e),
]
const res = await promiseAll(ops)
expect(res).toEqual(["fn_1", Error("Timed-out acquiring lock."), "fn_3"])
expect(fn_1).toHaveBeenCalledTimes(1)
expect(fn_2).toHaveBeenCalledTimes(0)
expect(fn_3).toHaveBeenCalledTimes(1)
})
},
})
+6
View File
@@ -8,6 +8,12 @@
"url": "https://github.com/medusajs/medusa",
"directory": "packages/locking"
},
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"publishConfig": {
"access": "public"
},
+2 -2
View File
@@ -1,6 +1,6 @@
import { Module, Modules } from "@medusajs/framework/utils"
import { LockingModuleService } from "@services"
import loadProviders from "./loaders/providers"
import { default as loadProviders } from "./loaders/providers"
import LockingModuleService from "./services/locking-module"
export default Module(Modules.LOCKING, {
service: LockingModuleService,
@@ -11,19 +11,13 @@ import {
LockingIdentifiersRegistrationName,
LockingProviderRegistrationPrefix,
} from "@types"
import { Lifetime, asFunction, asValue } from "awilix"
import { Lifetime, aliasTo, asFunction, asValue } from "awilix"
import { InMemoryLockingProvider } from "../providers/in-memory"
const registrationFn = async (klass, container, pluginOptions) => {
const registrationFn = async (klass, container) => {
const key = LockingProviderService.getRegistrationIdentifier(klass)
container.register({
[LockingProviderRegistrationPrefix + key]: asFunction(
(cradle) => new klass(cradle, pluginOptions.options),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}
),
[LockingProviderRegistrationPrefix + key]: aliasTo("__providers__" + key),
})
container.registerAdd(LockingIdentifiersRegistrationName, asValue(key))
@@ -38,24 +38,27 @@ export class InMemoryLockingProvider implements ILockingProvider {
timeout?: number
}
): Promise<T> {
keys = Array.isArray(keys) ? keys : [keys]
const timeoutSeconds = args?.timeout ?? 5
const timeout = Math.max(args?.timeout ?? 5, 1)
const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout
const cancellationToken = { cancelled: false }
const promises: Promise<any>[] = []
if (timeoutSeconds > 0) {
promises.push(this.getTimeout(timeoutSeconds))
promises.push(this.getTimeout(timeoutSeconds, cancellationToken))
}
promises.push(
this.acquire(keys, {
awaitQueue: true,
})
this.acquire_(
keys,
{
expire: timeoutSeconds,
awaitQueue: true,
},
cancellationToken
)
)
await Promise.race(promises).catch(async (err) => {
await this.release(keys)
})
await Promise.race(promises)
try {
return await job()
@@ -71,6 +74,18 @@ export class InMemoryLockingProvider implements ILockingProvider {
expire?: number
awaitQueue?: boolean
}
): Promise<void> {
return this.acquire_(keys, args)
}
async acquire_(
keys: string | string[],
args?: {
ownerId?: string | null
expire?: number
awaitQueue?: boolean
},
cancellationToken?: { cancelled: boolean }
): Promise<void> {
keys = Array.isArray(keys) ? keys : [keys]
const { ownerId, expire } = args ?? {}
@@ -100,7 +115,7 @@ export class InMemoryLockingProvider implements ILockingProvider {
continue
}
if (lock.ownerId === ownerId) {
if (lock.ownerId !== null && lock.ownerId === ownerId) {
if (expire) {
lock.expiration = now + expire * 1000
this.locks.set(key, lock)
@@ -111,10 +126,14 @@ export class InMemoryLockingProvider implements ILockingProvider {
if (lock.currentPromise && args?.awaitQueue) {
await lock.currentPromise.promise
if (cancellationToken?.cancelled) {
return
}
return this.acquire(keys, args)
}
throw new Error(`"${key}" is already locked.`)
throw new Error(`Failed to acquire lock for key "${key}"`)
}
}
@@ -166,9 +185,13 @@ export class InMemoryLockingProvider implements ILockingProvider {
}
}
private async getTimeout(seconds: number): Promise<void> {
private async getTimeout(
seconds: number,
cancellationToken: { cancelled: boolean }
): Promise<void> {
return new Promise((_, reject) => {
setTimeout(() => {
cancellationToken.cancelled = true
reject(new Error("Timed-out acquiring lock."))
}, seconds * 1000)
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { EmailPassAuthService } from "./services/emailpass"
const services = [EmailPassAuthService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.AUTH, {
services,
}
export default providerExport
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { GithubAuthService } from "./services/github"
const services = [GithubAuthService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.AUTH, {
services,
}
export default providerExport
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { GoogleAuthService } from "./services/google"
const services = [GoogleAuthService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.AUTH, {
services,
}
export default providerExport
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { LocalFileService } from "./services/local-file"
const services = [LocalFileService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.FILE, {
services,
}
export default providerExport
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { S3FileService } from "./services/s3-file"
const services = [S3FileService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.FILE, {
services,
}
export default providerExport
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { ManualFulfillmentService } from "./services/manual-fulfillment"
const services = [ManualFulfillmentService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.FULFILLMENT, {
services,
}
export default providerExport
})
@@ -0,0 +1,4 @@
dist
node_modules
.DS_store
yarn.lock
@@ -0,0 +1,220 @@
import { ILockingModule } from "@medusajs/framework/types"
import { Modules, promiseAll } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { setTimeout } from "node:timers/promises"
jest.setTimeout(5000)
const providerId = "locking-redis"
moduleIntegrationTestRunner<ILockingModule>({
moduleName: Modules.LOCKING,
moduleOptions: {
providers: [
{
id: providerId,
resolve: require.resolve("../../src"),
is_default: true,
options: {
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
},
},
],
},
testSuite: ({ service }) => {
describe("Locking Module Service", () => {
let stock = 5
function replenishStock() {
stock = 5
}
function hasStock() {
return stock > 0
}
async function reduceStock() {
await setTimeout(10)
stock--
}
async function buy() {
if (hasStock()) {
await reduceStock()
return true
}
return false
}
beforeEach(async () => {
await service.releaseAll()
})
it("should execute functions respecting the key locked", async () => {
// 10 parallel calls to buy should oversell the stock
const prom: any[] = []
for (let i = 0; i < 10; i++) {
prom.push(buy())
}
await Promise.all(prom)
expect(stock).toBe(-5)
replenishStock()
// 10 parallel calls to buy with lock should not oversell the stock
const promWLock: any[] = []
for (let i = 0; i < 10; i++) {
promWLock.push(service.execute("item_1", buy))
}
await Promise.all(promWLock)
expect(stock).toBe(0)
})
it("should acquire lock and release it", async () => {
await service.acquire("key_name", {
ownerId: "user_id_123",
})
const userReleased = await service.release("key_name", {
ownerId: "user_id_456",
})
const anotherUserLock = service.acquire("key_name", {
ownerId: "user_id_456",
})
expect(userReleased).toBe(false)
await expect(anotherUserLock).rejects.toThrow(
`Failed to acquire lock for key "key_name"`
)
const releasing = await service.release("key_name", {
ownerId: "user_id_123",
})
expect(releasing).toBe(true)
})
it("should acquire lock and release it during parallel calls", async () => {
const keyToLock = "mySpecialKey"
const user_1 = {
ownerId: "user_id_456",
}
const user_2 = {
ownerId: "user_id_000",
}
await expect(
service.acquire(keyToLock, user_1)
).resolves.toBeUndefined()
await expect(
service.acquire(keyToLock, user_1)
).resolves.toBeUndefined()
await expect(service.acquire(keyToLock, user_2)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock, user_2)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await service.acquire(keyToLock, user_1)
const releaseNotLocked = await service.release(keyToLock, {
ownerId: "user_id_000",
})
expect(releaseNotLocked).toBe(false)
const release = await service.release(keyToLock, user_1)
expect(release).toBe(true)
})
it("should fail to acquire the same key when no owner is provided", async () => {
const keyToLock = "mySpecialKey"
const user_2 = {
ownerId: "user_id_000",
}
await expect(service.acquire(keyToLock)).resolves.toBeUndefined()
await expect(service.acquire(keyToLock)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock, user_2)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
await expect(service.acquire(keyToLock, user_2)).rejects.toThrow(
`Failed to acquire lock for key "${keyToLock}"`
)
const releaseNotLocked = await service.release(keyToLock, {
ownerId: "user_id_000",
})
expect(releaseNotLocked).toBe(false)
const release = await service.release(keyToLock)
expect(release).toBe(true)
})
})
it("should release lock in case of failure", async () => {
const fn_1 = jest.fn(async () => {
throw new Error("Error")
})
const fn_2 = jest.fn(async () => {})
await service.execute("lock_key", fn_1).catch(() => {})
await service.execute("lock_key", fn_2).catch(() => {})
expect(fn_1).toHaveBeenCalledTimes(1)
expect(fn_2).toHaveBeenCalledTimes(1)
})
it("should release lock in case of timeout failure", async () => {
const fn_1 = jest.fn(async () => {
await setTimeout(1010)
return "fn_1"
})
const fn_2 = jest.fn(async () => {
return "fn_2"
})
const fn_3 = jest.fn(async () => {
return "fn_3"
})
const ops = [
service
.execute("lock_key", fn_1, {
timeout: 1,
})
.catch((e) => e),
service
.execute("lock_key", fn_2, {
timeout: 1,
})
.catch((e) => e),
service
.execute("lock_key", fn_3, {
timeout: 5,
})
.catch((e) => e),
]
const res = await promiseAll(ops)
expect(res).toEqual(["fn_1", Error("Timed-out acquiring lock."), "fn_3"])
expect(fn_1).toHaveBeenCalledTimes(1)
expect(fn_2).toHaveBeenCalledTimes(0)
expect(fn_3).toHaveBeenCalledTimes(1)
})
},
})
@@ -0,0 +1,10 @@
const defineJestConfig = require("../../../../define_jest_config")
module.exports = defineJestConfig({
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@types": "<rootDir>/src/types",
"^@utils": "<rootDir>/src/utils",
},
})
@@ -0,0 +1,48 @@
{
"name": "@medusajs/locking-redis",
"version": "0.0.1",
"description": "Redis Lock for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/locking-redis"
},
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"engines": {
"node": ">=20"
},
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/framework": "^0.0.1",
"@swc/core": "^1.7.28",
"@swc/jest": "^0.2.36",
"jest": "^29.7.0",
"rimraf": "^5.0.1",
"typescript": "^5.6.2"
},
"peerDependencies": {
"@medusajs/framework": "^0.0.1"
},
"dependencies": {
"ioredis": "^5.4.1"
},
"scripts": {
"watch": "tsc --build --watch",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
"test": "jest --passWithNoTests src",
"test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.spec.ts"
},
"keywords": [
"medusa-providers",
"medusa-providers-locking"
]
}
@@ -0,0 +1,11 @@
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import Loader from "./loaders"
import { RedisLockingProvider } from "./services/redis-lock"
const services = [RedisLockingProvider]
const loaders = [Loader]
export default ModuleProvider(Modules.LOCKING, {
services,
loaders,
})
@@ -0,0 +1,41 @@
import { Modules } from "@medusajs/framework/utils"
import { ProviderLoaderOptions } from "@medusajs/types"
import { RedisCacheModuleOptions } from "@types"
import { asValue } from "awilix"
import Redis from "ioredis"
export default async ({
container,
logger,
options,
moduleOptions,
}: ProviderLoaderOptions): Promise<void> => {
const { redisUrl, redisOptions, namespace } =
options as RedisCacheModuleOptions
if (!redisUrl) {
throw Error(
`No "redisUrl" provided in "${Modules.LOCKING}" module, "locking-redis" provider options. It is required for the "locking-redis" Module provider.`
)
}
const connection = new Redis(redisUrl, {
// Lazy connect to properly handle connection errors
lazyConnect: true,
...(redisOptions ?? {}),
})
try {
await connection.connect()
logger?.info(`Connection to Redis in "locking-redis" provider established`)
} catch (err) {
logger?.error(
`An error occurred while connecting to Redis in provider "locking-redis": ${err}`
)
}
container.register({
redisClient: asValue(connection),
prefix: asValue(namespace ?? "medusa_lock:"),
})
}
@@ -0,0 +1,280 @@
import { promiseAll } from "@medusajs/framework/utils"
import { ILockingProvider } from "@medusajs/types"
import { RedisCacheModuleOptions } from "@types"
import { Redis } from "ioredis"
import { setTimeout } from "node:timers/promises"
export class RedisLockingProvider implements ILockingProvider {
static identifier = "locking-redis"
protected redisClient: Redis & {
acquireLock: (
key: string,
ownerId: string,
ttl: number,
awaitQueue?: boolean
) => Promise<number>
releaseLock: (key: string, ownerId: string) => Promise<number>
}
protected keyNamePrefix: string
protected waitLockingTimeout: number = 5
protected defaultRetryInterval: number = 5
protected maximumRetryInterval: number = 200
constructor({ redisClient, prefix }, options: RedisCacheModuleOptions) {
this.redisClient = redisClient
this.keyNamePrefix = prefix ?? "medusa_lock:"
if (!isNaN(+options?.waitLockingTimeout!)) {
this.waitLockingTimeout = +options.waitLockingTimeout!
}
if (!isNaN(+options?.defaultRetryInterval!)) {
this.defaultRetryInterval = +options.defaultRetryInterval!
}
if (!isNaN(+options?.maximumRetryInterval!)) {
this.maximumRetryInterval = +options.maximumRetryInterval!
}
// Define the custom command for acquiring locks
this.redisClient.defineCommand("acquireLock", {
numberOfKeys: 1,
lua: `
local key = KEYS[1]
local ownerId = ARGV[1]
local ttl = tonumber(ARGV[2])
local awaitQueue = ARGV[3] == 'true'
local setArgs = {key, ownerId, 'NX'}
if ttl > 0 then
table.insert(setArgs, 'EX')
table.insert(setArgs, ttl)
end
local setResult = redis.call('SET', unpack(setArgs))
if setResult then
return 1
elseif not awaitQueue then
-- Key already exists; retrieve the current ownerId
local currentOwnerId = redis.call('GET', key)
if currentOwnerId == '*' then
return 0
elseif currentOwnerId == ownerId then
setArgs = {key, ownerId, 'XX'}
if ttl > 0 then
table.insert(setArgs, 'EX')
table.insert(setArgs, ttl)
end
redis.call('SET', unpack(setArgs))
return 1
else
return 0
end
else
return 0
end
`,
})
// Define the custom command for releasing locks
this.redisClient.defineCommand("releaseLock", {
numberOfKeys: 1,
lua: `
local key = KEYS[1]
local ownerId = ARGV[1]
if redis.call('GET', key) == ownerId then
return redis.call('DEL', key)
else
return 0
end
`,
})
}
private getKeyName(key: string): string {
return `${this.keyNamePrefix}${key}`
}
async execute<T>(
keys: string | string[],
job: () => Promise<T>,
args?: {
timeout?: number
}
): Promise<T> {
const timeout = Math.max(args?.timeout ?? this.waitLockingTimeout, 1)
const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout
const cancellationToken = { cancelled: false }
const promises: Promise<any>[] = []
if (timeoutSeconds > 0) {
promises.push(this.getTimeout(timeoutSeconds, cancellationToken))
}
promises.push(
this.acquire_(
keys,
{
awaitQueue: true,
expire: args?.timeout ? timeoutSeconds : 0,
},
cancellationToken
)
)
await Promise.race(promises)
try {
return await job()
} finally {
await this.release(keys)
}
}
async acquire(
keys: string | string[],
args?: {
ownerId?: string
expire?: number
awaitQueue?: boolean
}
): Promise<void> {
return this.acquire_(keys, args)
}
async acquire_(
keys: string | string[],
args?: {
ownerId?: string
expire?: number
awaitQueue?: boolean
},
cancellationToken?: { cancelled: boolean }
): Promise<void> {
keys = Array.isArray(keys) ? keys : [keys]
const timeout = Math.max(args?.expire ?? this.waitLockingTimeout, 1)
const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout
let retryTimes = 0
const ownerId = args?.ownerId ?? "*"
const awaitQueue = args?.awaitQueue ?? false
const acquirePromises = keys.map(async (key) => {
const errMessage = `Failed to acquire lock for key "${key}"`
const keyName = this.getKeyName(key)
const acquireLock = async () => {
while (true) {
if (cancellationToken?.cancelled) {
throw new Error(errMessage)
}
const result = await this.redisClient.acquireLock(
keyName,
ownerId,
args?.expire ? timeoutSeconds : 0,
awaitQueue
)
if (result === 1) {
break
} else {
if (awaitQueue) {
// Wait for a short period before retrying
await setTimeout(
Math.min(
this.defaultRetryInterval +
(retryTimes / 10) * this.defaultRetryInterval,
this.maximumRetryInterval
)
)
retryTimes++
} else {
throw new Error(errMessage)
}
}
}
}
await acquireLock()
})
await promiseAll(acquirePromises)
}
async release(
keys: string | string[],
args?: {
ownerId?: string | null
}
): Promise<boolean> {
const ownerId = args?.ownerId ?? "*"
keys = Array.isArray(keys) ? keys : [keys]
const releasePromises = keys.map(async (key) => {
const keyName = this.getKeyName(key)
const result = await this.redisClient.releaseLock(keyName, ownerId)
return result === 1
})
const results = await promiseAll(releasePromises)
return results.every((released) => released)
}
async releaseAll(args?: { ownerId?: string | null }): Promise<void> {
const ownerId = args?.ownerId ?? "*"
const pattern = `${this.keyNamePrefix}*`
let cursor = "0"
do {
const result = await this.redisClient.scan(
cursor,
"MATCH",
pattern,
"COUNT",
100
)
cursor = result[0]
const keys = result[1]
if (keys.length > 0) {
const pipeline = this.redisClient.pipeline()
keys.forEach((key) => {
pipeline.get(key)
})
const currentOwners = await pipeline.exec()
const deletePipeline = this.redisClient.pipeline()
keys.forEach((key, idx) => {
const currentOwner = currentOwners?.[idx]?.[1]
if (currentOwner === ownerId) {
deletePipeline.del(key)
}
})
await deletePipeline.exec()
}
} while (cursor !== "0")
}
private async getTimeout(
seconds: number,
cancellationToken: { cancelled: boolean }
): Promise<void> {
return new Promise(async (_, reject) => {
await setTimeout(seconds * 1000)
cancellationToken.cancelled = true
reject(new Error("Timed-out acquiring lock."))
})
}
}
@@ -0,0 +1,45 @@
import { RedisOptions } from "ioredis"
/**
* Module config type
*/
export type RedisCacheModuleOptions = {
/**
* Time to keep data in cache (in seconds)
*/
ttl?: number
/**
* Redis connection string
*/
redisUrl?: string
/**
* Redis client options
*/
redisOptions?: RedisOptions
/**
* Prefix for event keys
* @default `medusa_lock:`
*/
namespace?: string
/**
* Time to wait for lock (in seconds)
* @default 5
*/
waitLockingTimeout?: number
/**
* Default retry interval (in milliseconds)
* @default 5
*/
defaultRetryInterval?: number
/**
* Maximum retry interval (in milliseconds)
* @default 200
*/
maximumRetryInterval?: number
}
@@ -0,0 +1,12 @@
{
"extends": "../../../../_tsconfig.base.json",
"compilerOptions": {
"paths": {
"@models": ["./src/models"],
"@services": ["./src/services"],
"@repositories": ["./src/repositories"],
"@types": ["./src/types"],
"@utils": ["./src/utils"]
}
}
}
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { LocalNotificationService } from "./services/local"
const services = [LocalNotificationService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.NOTIFICATION, {
services,
}
export default providerExport
})
@@ -1,10 +1,8 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { SendgridNotificationService } from "./services/sendgrid"
const services = [SendgridNotificationService]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.NOTIFICATION, {
services,
}
export default providerExport
})
@@ -1,4 +1,4 @@
import { ModuleProviderExports } from "@medusajs/framework/types"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import {
StripeBancontactService,
StripeBlikService,
@@ -17,8 +17,6 @@ const services = [
StripePrzelewy24Service,
]
const providerExport: ModuleProviderExports = {
export default ModuleProvider(Modules.PAYMENT, {
services,
}
export default providerExport
})