feat(AsyncExecutionStack): Improve non-exclusive task management with concurrency limit controls and enhanced monitoring in AsyncExecutionStack.
This commit is contained in:
parent
5d9624bd56
commit
84babb3cd4
@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-04-25 - 6.2.0 - feat(AsyncExecutionStack)
|
||||
Improve non-exclusive task management with concurrency limit controls and enhanced monitoring in AsyncExecutionStack.
|
||||
|
||||
- Added methods to set and get non-exclusive concurrency limits and statistics (setNonExclusiveMaxConcurrency, getActiveNonExclusiveCount, getPendingNonExclusiveCount, and getNonExclusiveMaxConcurrency).
|
||||
- Integrated proper waiting and release mechanisms for non-exclusive slots.
|
||||
- Extended test coverage to validate concurrency limits and ensure correct behavior.
|
||||
|
||||
## 2024-10-13 - 6.1.0 - feat(BackpressuredArray)
|
||||
Add method to check if items are present in BackpressuredArray
|
||||
|
||||
|
12
package.json
12
package.json
@ -27,7 +27,7 @@
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.0.8",
|
||||
"@types/node": "^20.12.7"
|
||||
"@types/node": "^22.13.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
@ -66,5 +66,13 @@
|
||||
"Asynchronous programming",
|
||||
"Event handling",
|
||||
"Data aggregation"
|
||||
]
|
||||
],
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"mongodb-memory-server",
|
||||
"puppeteer"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
11509
pnpm-lock.yaml
generated
11509
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -26,4 +26,74 @@ tap.test('should run in parallel', async (toolsArg) => {
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Test default non-exclusive unlimited concurrency (no cap means all run together)
|
||||
tap.test('default non-exclusive unlimited concurrency', async (tools) => {
|
||||
const stack = new lik.AsyncExecutionStack();
|
||||
// default maxConcurrency should be unlimited (Infinity)
|
||||
expect(Number.isFinite(stack.getNonExclusiveMaxConcurrency())).toBe(false);
|
||||
const activeCounts: number[] = [];
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
tasks.push(
|
||||
stack.getNonExclusiveExecutionSlot(async () => {
|
||||
activeCounts.push(stack.getActiveNonExclusiveCount());
|
||||
await tools.delayFor(20);
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
const maxActive = Math.max(...activeCounts);
|
||||
expect(maxActive).toBe(4);
|
||||
});
|
||||
// Test respecting a non-exclusive concurrency limit
|
||||
tap.test('non-exclusive respects maxConcurrency', async (tools) => {
|
||||
const stack = new lik.AsyncExecutionStack();
|
||||
stack.setNonExclusiveMaxConcurrency(2);
|
||||
const activeCounts: number[] = [];
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
tasks.push(
|
||||
stack.getNonExclusiveExecutionSlot(async () => {
|
||||
activeCounts.push(stack.getActiveNonExclusiveCount());
|
||||
await tools.delayFor(50);
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
// never more than 2 at once
|
||||
const maxActive = Math.max(...activeCounts);
|
||||
expect(maxActive).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
// Test concurrency stats (active vs pending) for non-exclusive tasks
|
||||
tap.test('non-exclusive concurrency stats reflect active and pending', async (tools) => {
|
||||
const stack = new lik.AsyncExecutionStack();
|
||||
stack.setNonExclusiveMaxConcurrency(2);
|
||||
// initially, no tasks
|
||||
expect(stack.getActiveNonExclusiveCount()).toBe(0);
|
||||
expect(stack.getPendingNonExclusiveCount()).toBe(0);
|
||||
|
||||
// enqueue three tasks
|
||||
const p1 = stack.getNonExclusiveExecutionSlot(async () => {
|
||||
await tools.delayFor(30);
|
||||
});
|
||||
const p2 = stack.getNonExclusiveExecutionSlot(async () => {
|
||||
await tools.delayFor(30);
|
||||
});
|
||||
const p3 = stack.getNonExclusiveExecutionSlot(async () => {
|
||||
await tools.delayFor(30);
|
||||
});
|
||||
|
||||
// give time for scheduling
|
||||
await tools.delayFor(10);
|
||||
// two should be running, one pending
|
||||
expect(stack.getActiveNonExclusiveCount()).toBe(2);
|
||||
expect(stack.getPendingNonExclusiveCount()).toBe(1);
|
||||
|
||||
await Promise.all([p1, p2, p3]);
|
||||
// after completion, counts reset
|
||||
expect(stack.getActiveNonExclusiveCount()).toBe(0);
|
||||
expect(stack.getPendingNonExclusiveCount()).toBe(0);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/lik',
|
||||
version: '6.1.0',
|
||||
version: '6.2.0',
|
||||
description: 'Provides a collection of lightweight helpers and utilities for Node.js projects.'
|
||||
}
|
||||
|
@ -10,6 +10,12 @@ interface IExecutionSlot<T> {
|
||||
export class AsyncExecutionStack {
|
||||
private executionSlots: IExecutionSlot<any>[] = [];
|
||||
private isProcessing = false;
|
||||
/** Maximum concurrent non-exclusive tasks (Infinity = unlimited) */
|
||||
private nonExclusiveMaxConcurrency: number = Infinity;
|
||||
/** Currently running non-exclusive task count */
|
||||
private nonExclusiveCurrentCount: number = 0;
|
||||
/** Queue of resolvers waiting for a non-exclusive slot */
|
||||
private nonExclusivePendingQueue: Array<() => void> = [];
|
||||
|
||||
public async getExclusiveExecutionSlot<T = any>(
|
||||
funcArg: () => Promise<T>,
|
||||
@ -42,6 +48,28 @@ export class AsyncExecutionStack {
|
||||
this.processExecutionSlots();
|
||||
return executionDeferred.promise;
|
||||
}
|
||||
/**
|
||||
* Set the maximum number of concurrent non-exclusive tasks.
|
||||
* @param concurrency minimum 1 (Infinity means unlimited)
|
||||
*/
|
||||
public setNonExclusiveMaxConcurrency(concurrency: number): void {
|
||||
if (!Number.isFinite(concurrency) || concurrency < 1) {
|
||||
throw new Error('nonExclusiveMaxConcurrency must be a finite number >= 1');
|
||||
}
|
||||
this.nonExclusiveMaxConcurrency = concurrency;
|
||||
}
|
||||
/** Get the configured max concurrency for non-exclusive tasks */
|
||||
public getNonExclusiveMaxConcurrency(): number {
|
||||
return this.nonExclusiveMaxConcurrency;
|
||||
}
|
||||
/** Number of non-exclusive tasks currently running */
|
||||
public getActiveNonExclusiveCount(): number {
|
||||
return this.nonExclusiveCurrentCount;
|
||||
}
|
||||
/** Number of non-exclusive tasks waiting for a free slot */
|
||||
public getPendingNonExclusiveCount(): number {
|
||||
return this.nonExclusivePendingQueue.length;
|
||||
}
|
||||
|
||||
private async processExecutionSlots() {
|
||||
if (this.isProcessing) {
|
||||
@ -87,13 +115,14 @@ export class AsyncExecutionStack {
|
||||
|
||||
private async executeNonExclusiveSlots(slots: IExecutionSlot<any>[]) {
|
||||
const promises = slots.map(async (slot) => {
|
||||
// wait for an available non-exclusive slot
|
||||
await this.waitForNonExclusiveSlot();
|
||||
try {
|
||||
// execute with optional timeout
|
||||
if (slot.timeout) {
|
||||
const result = await Promise.race([
|
||||
slot.funcToExecute(),
|
||||
plugins.smartdelay.delayFor(slot.timeout).then(() => {
|
||||
throw new Error('Timeout reached');
|
||||
}),
|
||||
plugins.smartdelay.delayFor(slot.timeout).then(() => { throw new Error('Timeout reached'); }),
|
||||
]);
|
||||
slot.executionDeferred.resolve(result);
|
||||
} else {
|
||||
@ -102,9 +131,33 @@ export class AsyncExecutionStack {
|
||||
}
|
||||
} catch (error) {
|
||||
slot.executionDeferred.reject(error);
|
||||
} finally {
|
||||
this.releaseNonExclusiveSlot();
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
/**
|
||||
* Wait until a non-exclusive slot is available (respects max concurrency).
|
||||
*/
|
||||
private waitForNonExclusiveSlot(): Promise<void> {
|
||||
if (this.nonExclusiveCurrentCount < this.nonExclusiveMaxConcurrency) {
|
||||
this.nonExclusiveCurrentCount++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.nonExclusivePendingQueue.push(() => {
|
||||
this.nonExclusiveCurrentCount++;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
/** Release a non-exclusive slot and wake the next waiter, if any. */
|
||||
private releaseNonExclusiveSlot(): void {
|
||||
this.nonExclusiveCurrentCount--;
|
||||
const next = this.nonExclusivePendingQueue.shift();
|
||||
if (next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user