Add concurrency limiting functionality to execution engine

- Introduced ConcurrencyLimiter class to manage concurrent operations and prevent resource exhaustion.
- Implemented run, runAll, and runAllOrThrow methods for executing functions with concurrency control.
- Enhanced ExecutionEngine to utilize concurrency limiting when executing parallel groups and nodes, improving resource management and execution efficiency.
- Integrated mapWithConcurrency and mapWithConcurrencySettled functions for mapping operations with concurrency control.
This commit is contained in:
Nikhil-Doye
2025-10-27 15:00:32 -04:00
parent 571a5a04e5
commit 05cf50b3a3
2 changed files with 164 additions and 10 deletions
+116
View File
@@ -0,0 +1,116 @@
/**
* Concurrency Limiter
* Controls the number of concurrent operations to prevent resource exhaustion
*/
export class ConcurrencyLimiter {
private maxConcurrency: number;
private running: number = 0;
private queue: Array<() => void> = [];
constructor(maxConcurrency: number = 5) {
this.maxConcurrency = Math.max(1, maxConcurrency);
}
/**
* Execute a function with concurrency limiting
*/
async run<T>(fn: () => Promise<T>): Promise<T> {
// Wait until we have capacity
while (this.running >= this.maxConcurrency) {
await new Promise((resolve) => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
// Process next item in queue if any
const nextResolve = this.queue.shift();
if (nextResolve) {
nextResolve();
}
}
}
/**
* Execute multiple functions with concurrency limiting
* Returns results in same order as input functions
*/
async runAll<T>(
fns: Array<() => Promise<T>>
): Promise<PromiseSettledResult<T>[]> {
const promises = fns.map((fn) => this.run(fn));
return Promise.allSettled(promises);
}
/**
* Execute multiple functions with concurrency limiting
* Throws on first error
*/
async runAllOrThrow<T>(fns: Array<() => Promise<T>>): Promise<T[]> {
const promises = fns.map((fn) => this.run(fn));
return Promise.all(promises);
}
/**
* Get current concurrency status
*/
getStatus(): {
running: number;
queued: number;
maxConcurrency: number;
} {
return {
running: this.running,
queued: this.queue.length,
maxConcurrency: this.maxConcurrency,
};
}
/**
* Reset the limiter
*/
reset(): void {
this.running = 0;
this.queue = [];
}
}
/**
* Create a limiter for a specific concurrency level
*/
export function createLimiter(maxConcurrency: number = 5): ConcurrencyLimiter {
return new ConcurrencyLimiter(maxConcurrency);
}
/**
* Map function with concurrency control
* Similar to Promise.all but with concurrency limits
*/
export async function mapWithConcurrency<T, R>(
items: T[],
fn: (item: T) => Promise<R>,
maxConcurrency: number = 5
): Promise<R[]> {
const limiter = new ConcurrencyLimiter(maxConcurrency);
const promises = items.map((item) => limiter.run(() => fn(item)));
return Promise.all(promises);
}
/**
* Map function with concurrency control (settled version)
* Similar to Promise.allSettled but with concurrency limits
*/
export async function mapWithConcurrencySettled<T, R>(
items: T[],
fn: (item: T) => Promise<R>,
maxConcurrency: number = 5
): Promise<PromiseSettledResult<R>[]> {
const limiter = new ConcurrencyLimiter(maxConcurrency);
const promises = items.map((item) => limiter.run(() => fn(item)));
return Promise.allSettled(promises);
}
+48 -10
View File
@@ -10,6 +10,10 @@ import {
ExecutionErrorEvent,
} from "./executionEventBus";
import { workflowValidationEngine } from "./workflowValidationEngine";
import {
mapWithConcurrencySettled,
ConcurrencyLimiter,
} from "./concurrencyLimiter";
export interface ExecutionContext {
nodeId: string;
@@ -540,12 +544,21 @@ export class ExecutionEngine {
"parallel"
);
// Execute groups in parallel
const groupPromises = parallelGroups.map((group) =>
this.executeParallelGroup(group, plan, onNodeUpdate, executionId)
// Get the max concurrency from the plan or use default
const maxConcurrency =
(plan as any).maxConcurrency || this.maxConcurrentExecutions;
// Execute groups in parallel with concurrency limiting
const groupFunctions = parallelGroups.map(
(group) => () =>
this.executeParallelGroup(group, plan, onNodeUpdate, executionId)
);
await Promise.allSettled(groupPromises);
await mapWithConcurrencySettled(
groupFunctions,
(fn) => fn(),
maxConcurrency
);
}
// Execute a parallel group
@@ -560,17 +573,42 @@ export class ExecutionEngine {
) => void,
executionId: string
): Promise<void> {
const nodePromises = group.nodes.map((nodeId) => {
const context = plan.nodes.find((n) => n.nodeId === nodeId);
if (!context) return Promise.resolve();
const maxConcurrency =
group.maxConcurrency ||
(plan as any).maxConcurrency ||
this.maxConcurrentExecutions;
return this.executeNode(context, plan, onNodeUpdate, executionId);
const nodeExecutionFunctions = group.nodes.map((nodeId) => {
return async () => {
const context = plan.nodes.find((n) => n.nodeId === nodeId);
if (!context) return Promise.resolve();
return this.executeNode(context, plan, onNodeUpdate, executionId);
};
});
if (group.waitForAll) {
await Promise.allSettled(nodePromises);
// Execute all nodes with concurrency limiting
await mapWithConcurrencySettled(
nodeExecutionFunctions,
(fn) => fn(),
maxConcurrency
);
} else {
await Promise.race(nodePromises);
// Execute with concurrency limiting and return first result
const promises = nodeExecutionFunctions.map((fn) =>
(async () => {
try {
return await fn();
} catch (error) {
return error;
}
})()
);
// Use Promise.race with limited concurrency
const limiter = new ConcurrencyLimiter(maxConcurrency);
const limitedPromises = promises.map((p) => limiter.run(() => p));
await Promise.race(limitedPromises);
}
}