/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { Registry } from '@nocobase/utils'; import { Mutex, MutexInterface, E_CANCELED } from 'async-mutex'; export type Releaser = () => void | Promise; export interface ILock { acquire(ttl: number): Releaser | Promise; runExclusive(fn: () => Promise, ttl: number): Promise; } export interface ILockAdapter { connect(): Promise; close(): Promise; acquire(key: string, ttl: number): Releaser | Promise; runExclusive(key: string, fn: () => Promise, ttl: number): Promise; tryAcquire(key: string, timeout?: number): Promise; } export class LockAbortError extends Error { constructor(message, options) { super(message, options); } } export class LockAcquireError extends Error { constructor(message, options?) { super(message, options); } } class LocalLockAdapter implements ILockAdapter { static locks = new Map(); async connect() {} async close() {} private getLock(key: string): MutexInterface { let lock = (this.constructor).locks.get(key); if (!lock) { lock = new Mutex(); (this.constructor).locks.set(key, lock); } return lock; } async acquire(key: string, ttl: number) { const lock = this.getLock(key); const release = (await lock.acquire()) as Releaser; const timer = setTimeout(() => { if (lock.isLocked()) { release(); } }, ttl); return () => { release(); clearTimeout(timer); }; } async runExclusive(key: string, fn: () => Promise, ttl: number): Promise { const lock = this.getLock(key); let timer; try { timer = setTimeout(() => { if (lock.isLocked()) { lock.release(); } }, ttl); return lock.runExclusive(fn); } catch (e) { if (e === E_CANCELED) { throw new LockAbortError('Lock aborted', { cause: E_CANCELED }); } else { throw e; } } finally { clearTimeout(timer); } } async tryAcquire(key: string) { const lock = this.getLock(key); if (lock.isLocked()) { throw new LockAcquireError('lock is locked'); } return { acquire: async (ttl) => { return this.acquire(key, ttl); }, runExclusive: async (fn: () => Promise, ttl) => { return this.runExclusive(key, fn, ttl); }, }; } } export interface LockAdapterConfig { Adapter: new (...args: any[]) => C; options?: Record; } export interface LockManagerOptions { defaultAdapter?: string; } export class LockManager { private registry = new Registry(); private adapters = new Map(); constructor(private options: LockManagerOptions = {}) { this.registry.register('local', { Adapter: LocalLockAdapter, }); } registerAdapter(name: string, adapterConfig: LockAdapterConfig) { this.registry.register(name, adapterConfig); } private async getAdapter(): Promise { const type = this.options.defaultAdapter || 'local'; let client = this.adapters.get(type); if (!client) { const adapter = this.registry.get(type); if (!adapter) { throw new Error(`Lock adapter "${type}" not registered`); } const { Adapter, options } = adapter; client = new Adapter(options); await client.connect(); this.adapters.set(type, client); } return client; } public async close() { for (const client of this.adapters.values()) { await client.close(); } } public async acquire(key: string, ttl = 500): Promise { const client = await this.getAdapter(); return client.acquire(key, ttl); } public async runExclusive(key: string, fn: () => Promise, ttl = 500): Promise { const client = await this.getAdapter(); return client.runExclusive(key, fn, ttl); } public async tryAcquire(key: string) { const client = await this.getAdapter(); return client.tryAcquire(key); } } export default LockManager;