import AsyncLock from 'async-lock'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { SingleThreaded } from '../../init/cluster/SingleThreaded'; import { getLoggerFor } from '../../logging/LogUtil'; import { InternalServerError } from '../errors/InternalServerError'; import type { ResourceLocker } from './ResourceLocker'; /** * A resource locker making use of the `async-lock` library. * Note that all locks are kept in memory until they are unlocked which could potentially result * in a memory leak if locks are never unlocked, so make sure this is covered with expiring locks for example, * and/or proper `finally` handles. */ export class MemoryResourceLocker implements ResourceLocker, SingleThreaded { protected readonly logger = getLoggerFor(this); private readonly locker: AsyncLock; private readonly unlockCallbacks: Record void>; public constructor() { this.locker = new AsyncLock(); this.unlockCallbacks = {}; } public async acquire(identifier: ResourceIdentifier): Promise { const { path } = identifier; this.logger.debug(`Acquiring lock for ${path}`); return new Promise((resolve): void => { this.locker.acquire(path, (done): void => { this.unlockCallbacks[path] = done; this.logger.debug(`Acquired lock for ${path}. ${this.getLockCount()} locks active.`); resolve(); }, (): void => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.unlockCallbacks[path]; this.logger.debug(`Released lock for ${path}. ${this.getLockCount()} active locks remaining.`); }); }); } public async release(identifier: ResourceIdentifier): Promise { const { path } = identifier; if (!this.unlockCallbacks[path]) { throw new InternalServerError(`Trying to unlock resource that is not locked: ${path}`); } this.unlockCallbacks[path](); } /** * Counts the number of active locks. */ private getLockCount(): number { return Object.keys(this.unlockCallbacks).length; } }