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
@@ -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)
})