@aws-cdk-testing/cli-integ
Version:
Integration tests for the AWS CDK CLI
141 lines (124 loc) • 3.73 kB
text/typescript
import { ILock, XpMutex, XpMutexPool } from './xpmutex';
/**
* A class that holds a pool of resources and gives them out and returns them on-demand
*
* The resources will be given out front to back, when they are returned
* the most recently returned version will be given out again (for best
* cache coherency).
*
* If there are multiple consumers waiting for a resource, consumers are serviced
* in FIFO order for most fairness.
*/
export class ResourcePool<A extends string=string> {
public static withResources<A extends string>(name: string, resources: A[]) {
const pool = XpMutexPool.fromName(name);
return new ResourcePool(pool, resources);
}
private readonly resources: ReadonlyArray<A>;
private readonly mutexes: Record<string, XpMutex> = {};
private readonly locks: Record<string, ILock | undefined> = {};
private constructor(private readonly pool: XpMutexPool, resources: A[]) {
if (resources.length === 0) {
throw new Error('Must have at least one resource in the pool');
}
// Shuffle to reduce contention
resources = [...resources];
fisherYatesShuffle(resources);
this.resources = resources;
for (const res of resources) {
this.mutexes[res] = this.pool.mutex(res);
}
}
/**
* Take one value from the resource pool
*
* If no such value is currently available, wait until it is.
*/
public async take(): Promise<ILease<A>> {
while (true) {
// Start a wait on the unlock now -- if the unlock signal comes after
// we try to acquire but before we start the wait, we might miss it.
//
// (The timeout is in case the unlock signal doesn't come for whatever reason).
const wait = this.pool.awaitUnlock(10_000);
// Try all mutexes, we might need to reacquire an expired lock
for (const res of this.resources) {
const lease = await this.tryObtainLease(res);
if (lease) {
// Ignore the wait (count as handled)
wait.then(() => {}, () => {});
return lease;
}
}
// None available, wait until one gets unlocked then try again
await wait;
}
}
/**
* Execute a block using a single resource from the pool
*/
public async using<B>(block: (x: A) => B | Promise<B>): Promise<B> {
const lease = await this.take();
try {
return await block(lease.value);
} finally {
await lease.dispose();
}
}
private async tryObtainLease(value: A) {
const lock = await this.mutexes[value].tryAcquire();
if (!lock) {
return undefined;
}
this.locks[value] = lock;
return this.makeLease(value);
}
private makeLease(value: A): ILease<A> {
let disposed = false;
return {
value,
dispose: async () => {
if (disposed) {
throw new Error('Calling dispose() on an already-disposed lease.');
}
disposed = true;
return this.returnValue(value);
},
};
}
/**
* When a value is returned:
*
* - If someone's waiting for it, give it to them
* - Otherwise put it back into the pool
*/
private async returnValue(value: string) {
const lock = this.locks[value];
delete this.locks[value];
await lock?.release();
}
}
/**
* A single value taken from the pool
*/
export interface ILease<A> {
/**
* The value obtained by the lease
*/
readonly value: A;
/**
* Return the leased value to the pool
*/
dispose(): Promise<void>;
}
/**
* Shuffle an array in-place
*/
function fisherYatesShuffle<A>(xs: A[]) {
for (let i = xs.length - 1; i >= 1; i--) {
const j = Math.floor(Math.random() * i);
const h = xs[j];
xs[j] = xs[i];
xs[i] = h;
}
}