forked from medusajs/medusa
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fea(providers): locking postgres (medusajs#9545)
Co-authored-by: Adrien de Peretti <[email protected]>
- Loading branch information
1 parent
3b50c6d
commit e9a06f4
Showing
18 changed files
with
641 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"@medusajs/locking-postgres": patch | ||
"@medusajs/modules-sdk": patch | ||
"@medusajs/types": patch | ||
--- | ||
|
||
Locking Module - locking-postgres |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import PostgresLockingProvider from "@medusajs/locking-postgres" | ||
|
||
export * from "@medusajs/locking-postgres" | ||
|
||
export default PostgresLockingProvider | ||
export const discoveryPath = require.resolve("@medusajs/locking-postgres") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
dist | ||
node_modules | ||
.DS_store | ||
yarn.lock |
213 changes: 213 additions & 0 deletions
213
packages/modules/providers/locking-postgres/integration-tests/__tests__/index.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
import { ILockingModule } from "@medusajs/framework/types" | ||
import { Modules, promiseAll } from "@medusajs/framework/utils" | ||
import { moduleIntegrationTestRunner } from "@medusajs/test-utils" | ||
import { setTimeout } from "node:timers/promises" | ||
|
||
jest.setTimeout(10000) | ||
|
||
const providerId = "locking-postgres" | ||
moduleIntegrationTestRunner<ILockingModule>({ | ||
moduleName: Modules.LOCKING, | ||
moduleOptions: { | ||
providers: [ | ||
{ | ||
id: providerId, | ||
resolve: require.resolve("../../src"), | ||
is_default: true, | ||
}, | ||
], | ||
}, | ||
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 | ||
} | ||
|
||
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: 2, | ||
}) | ||
.catch((e) => e), | ||
] | ||
|
||
const res = await promiseAll(ops) | ||
|
||
expect(res).toEqual(["fn_1", expect.any(Error), "fn_3"]) | ||
|
||
expect(fn_1).toHaveBeenCalledTimes(1) | ||
expect(fn_2).toHaveBeenCalledTimes(0) | ||
expect(fn_3).toHaveBeenCalledTimes(1) | ||
}) | ||
}, | ||
}) |
10 changes: 10 additions & 0 deletions
10
packages/modules/providers/locking-postgres/jest.config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
}, | ||
}) |
7 changes: 7 additions & 0 deletions
7
packages/modules/providers/locking-postgres/mikro-orm.config.dev.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import * as entities from "./src/models" | ||
|
||
import { defineMikroOrmCliConfig } from "@medusajs/framework/utils" | ||
|
||
export default defineMikroOrmCliConfig("lockingPostgres", { | ||
entities: Object.values(entities), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"name": "@medusajs/locking-postgres", | ||
"version": "0.0.1", | ||
"description": "Postgres Advisory Locks for Medusa", | ||
"main": "dist/index.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/medusajs/medusa", | ||
"directory": "packages/locking-postgres" | ||
}, | ||
"files": [ | ||
"dist", | ||
"!dist/**/__tests__", | ||
"!dist/**/__mocks__", | ||
"!dist/**/__fixtures__" | ||
], | ||
"engines": { | ||
"node": ">=20" | ||
}, | ||
"author": "Medusa", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"@medusajs/framework": "^0.0.1", | ||
"@mikro-orm/cli": "5.9.7", | ||
"@mikro-orm/core": "5.9.7", | ||
"@mikro-orm/migrations": "5.9.7", | ||
"@mikro-orm/postgresql": "5.9.7", | ||
"@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" | ||
}, | ||
"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", | ||
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:generate", | ||
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial -n InitialSetupMigration", | ||
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create", | ||
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up", | ||
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" | ||
}, | ||
"keywords": [ | ||
"medusa-providers", | ||
"medusa-providers-locking" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { ModuleProvider, Modules } from "@medusajs/framework/utils" | ||
import { PostgresAdvisoryLockProvider } from "./services/advisory-lock" | ||
|
||
const services = [PostgresAdvisoryLockProvider] | ||
|
||
export default ModuleProvider(Modules.LOCKING, { | ||
services, | ||
}) |
55 changes: 55 additions & 0 deletions
55
.../modules/providers/locking-postgres/src/migrations/.snapshot-medusa-locking-postgres.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"namespaces": [ | ||
"public" | ||
], | ||
"name": "public", | ||
"tables": [ | ||
{ | ||
"columns": { | ||
"id": { | ||
"name": "id", | ||
"type": "text", | ||
"unsigned": false, | ||
"autoincrement": false, | ||
"primary": false, | ||
"nullable": false, | ||
"mappedType": "text" | ||
}, | ||
"owner_id": { | ||
"name": "owner_id", | ||
"type": "text", | ||
"unsigned": false, | ||
"autoincrement": false, | ||
"primary": false, | ||
"nullable": true, | ||
"mappedType": "text" | ||
}, | ||
"expiration": { | ||
"name": "expiration", | ||
"type": "timestamptz", | ||
"unsigned": false, | ||
"autoincrement": false, | ||
"primary": false, | ||
"nullable": true, | ||
"length": 6, | ||
"mappedType": "datetime" | ||
} | ||
}, | ||
"name": "locking", | ||
"schema": "public", | ||
"indexes": [ | ||
{ | ||
"keyName": "locking_pkey", | ||
"columnNames": [ | ||
"id" | ||
], | ||
"composite": false, | ||
"primary": true, | ||
"unique": true | ||
} | ||
], | ||
"checks": [], | ||
"foreignKeys": {} | ||
} | ||
] | ||
} |
Oops, something went wrong.