UNPKG

@medusajs/locking-postgres

Version:

Postgres Advisory Locks for Medusa

125 lines 4.93 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgresAdvisoryLockProvider = void 0; const utils_1 = require("@medusajs/framework/utils"); const _models_1 = require("../models"); class PostgresAdvisoryLockProvider extends (0, utils_1.MedusaService)({ Locking: _models_1.Locking }) { constructor(container) { // @ts-ignore super(...arguments); this.manager = container.manager; } getManager() { return this.manager; } async execute(keys, job, args) { const timeout = Math.max(args?.timeout ?? 5, 1); const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout; return await this.getManager().transactional(async (manager) => { const ops = []; if (timeoutSeconds > 0) { ops.push(this.getTimeout(timeoutSeconds)); } const fnName = "pg_advisory_xact_lock"; const allKeys = Array.isArray(keys) ? keys : [keys]; const numKeys = allKeys.map(this.hashStringToInt); const lockPromises = numKeys.map((numKey) => manager.execute(`SELECT ${fnName}(?)`, [numKey])); const lock = Promise.all(lockPromises); ops.push(lock); try { await Promise.race(ops); const ret = await job(); await manager.commit(); return ret; } catch (e) { await manager.rollback(); throw e; } }); } async loadLock(key) { const [row] = await this.getManager().execute(`SELECT owner_id, expiration, NOW() AS now FROM locking WHERE id = ?`, [key]); return row; } async acquire(keys, args) { keys = Array.isArray(keys) ? keys : [keys]; const { ownerId, expire } = args ?? {}; for (const key of keys) { const row = await this.loadLock(key); if (!row) { const expireSql = expire ? `NOW() + INTERVAL '${+expire} SECONDS'` : "NULL"; try { await this.getManager().execute(`INSERT INTO locking (id, owner_id, expiration) VALUES (?, ?, ${expireSql})`, [key, ownerId ?? null]); } catch (err) { if (err.toString().includes("locking_pkey")) { const owner = await this.loadLock(key); if (ownerId != owner.owner_id) { throw new Error(`"${key}" is already locked.`); } } else { throw err; } } continue; } const errMessage = `Failed to acquire lock for key "${key}"`; if (row.owner_id === null || row.owner_id !== ownerId) { throw new Error(errMessage); } if (!row.expiration && row.owner_id == ownerId) { continue; } const canRefresh = row.owner_id == ownerId && (expire || row.expiration <= row.now); if (!canRefresh || !expire) { continue; } await this.getManager().execute(`UPDATE locking SET owner_id = ?, expiration = NOW() + INTERVAL '${+expire} SECONDS' WHERE id = ?`, [ownerId ?? null, key]); } } async release(keys, args) { const { ownerId } = args ?? {}; keys = Array.isArray(keys) ? keys : [keys]; let success = true; for (const key of keys) { const row = await this.loadLock(key); if (!row || row.owner_id != ownerId) { success = false; continue; } await this.getManager().execute(`DELETE FROM locking WHERE id = ?`, [key]); success = success && (!row.expiration || row.expiration > row.now); } return success; } async releaseAll(args) { const { ownerId } = args ?? {}; if (!(0, utils_1.isDefined)(ownerId)) { await this.getManager().execute(`TRUNCATE TABLE locking`); } else { await this.getManager().execute(`DELETE FROM locking WHERE owner_id = ?`, [ownerId]); } } hashStringToInt(str) { let hash = 5381; for (let i = str.length; i--;) { hash = (hash * 33) ^ str.charCodeAt(i); } return hash >>> 0; } async getTimeout(seconds) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error("Timed-out acquiring lock.")); }, seconds * 1000); }); } } exports.PostgresAdvisoryLockProvider = PostgresAdvisoryLockProvider; PostgresAdvisoryLockProvider.identifier = "locking-postgres"; //# sourceMappingURL=advisory-lock.js.map