File size: 1,696 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | /**
* A FIFO promise semaphore: at most `limit` wrapped tasks run at once; the
* rest wait in arrival order. Wrap a queryFn's work in `run` to bound how many
* of a query family's requests are in flight simultaneously.
*/
export class ConcurrencyLimiter {
private active = 0;
private readonly queue: Array<() => void> = [];
constructor(private readonly limit: number) {}
async run<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T> {
await this.acquire();
try {
// A query cancelled while queued (e.g. its last observer unmounted)
// frees its slot without ever hitting the network.
if (signal?.aborted) {
throw signal.reason ?? new DOMException("Aborted", "AbortError");
}
return await task();
} finally {
this.release();
}
}
private acquire(): Promise<void> {
if (this.active < this.limit) {
this.active += 1;
return Promise.resolve();
}
return new Promise((resolve) => {
this.queue.push(resolve);
});
}
private release(): void {
const next = this.queue.shift();
if (next) {
// The waiter inherits the released slot directly, so `active` stays put.
next();
} else {
this.active -= 1;
}
}
}
/**
* Shared by every per-automation runs query: the dashboard and home page each
* fan out one GET .../runs request per automation, which can exhaust the
* automation service's DB connection pool (~15 connections) and stall every
* request for the pool's 30s acquisition timeout. Three in flight keeps the
* surfaces responsive without saturating the pool.
*/
export const automationRunRequestsLimiter = new ConcurrencyLimiter(3);
|