peter
Version:
Peter Test Framework
89 lines (76 loc) • 2.61 kB
JavaScript
/**
* @file src/utils/concurrency-lock-pool.js
* @author Ryan Rossiter, ryan@kingsds.network
* @date July 2020
*
* This class module is used to manage a pool of concurrency locks.
* It provides an acquire method that will allow [size] locks to be
* acquired before blocking until one of the already-acquired locks
* are released.
*/
module.exports = class ConcurrencyLockPool {
/**
* @constructor
* @param {number} size
*/
constructor(size) {
if (!Number.isFinite(size) || !(size >= 0))
throw new TypeError(`The size of ConcurrencyLockPool specified is not a finite positive number. It's '${size}' (a ${typeof size}).`);
this.idCounter = 0;
this.size = size;
/** @type {Symbol[]} */
this.locks = [];
/** @type {null | Promise} */
this.acquireBlocker = null;
/** @type {null | (() => void)} */
this.releaseAcquireBlocker = null;
/** @type {null | Promise} */
this.emptyBlocker = null;
/** @type {null | (() => void)} */
this.releaseEmptyBlocker = null;
}
/**
* Acquires a lock from the pool.
* Must be awaited on as this could block until a lock is released.
* Returns a function for releasing the acquired lock.
*
* @returns {Promise<() => void>}
*/
async acquire() {
await this.acquireBlocker; // block until a lock is available
const lockSymbol = Symbol(`Concurrency Lock ${this.idCounter++}`);
if (this.locks.push(lockSymbol) === this.size) {
// this lock filled the lock pool, create a new acquireBlocker promise
this.acquireBlocker = new Promise((resolve) => (this.releaseAcquireBlocker = resolve));
}
if (this.emptyBlocker === null) {
this.emptyBlocker = new Promise((resolve) => (this.releaseEmptyBlocker = resolve));
}
return this.release.bind(this, lockSymbol);
}
/**
* @param {Symbol} lockSymbol
*/
release(lockSymbol) {
const lockIndex = this.locks.indexOf(lockSymbol);
if (lockIndex === -1) {
throw new Error(`Tried to release lock that doesn't exist: ${lockSymbol}`);
}
this.locks.splice(lockIndex, 1);
if (this.releaseAcquireBlocker) {
this.releaseAcquireBlocker();
this.releaseAcquireBlocker = null;
}
if (this.releaseEmptyBlocker && this.locks.length === 0) {
this.releaseEmptyBlocker();
this.releaseEmptyBlocker = null;
}
}
/**
* The returned promise will only resolve once the pool has been emptied (all locks released)
* @returns {Promise<void>}
*/
async waitUntilEmpty() {
await this.emptyBlocker;
}
}