crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
690 lines (689 loc) • 25.6 kB
JavaScript
/**
* FlowScheduler
*
* Resource-aware scheduling system for optimizing multi-flow execution,
* implementing advanced performance optimization strategies to maximize
* throughput while respecting system constraints.
*/
import { EventEmitter } from 'events';
import { FlowExecutionTracker } from './FlowExecutionTracker.js';
export class FlowScheduler extends EventEmitter {
readyQueue = [];
pendingFlows = new Map();
runningFlows = new Map();
completedFlows = new Map();
failedFlows = new Map();
flowMap = new Map();
resourceUsage;
timer;
tracker;
metrics;
logger = console;
// Performance optimization: pre-computed dependency resolution maps
flowsBlockedBy = new Map();
flowsBlocking = new Map();
// Performance history for predictive scheduling
flowExecutionHistory = new Map(); // flowType -> execution times
lastScheduleTime = 0;
// Priority queue indexing for O(log n) operations
priorityChanged = false;
options;
constructor(options = {}, tracker) {
super();
// Default options optimized for performance
const defaultOptions = {
maxConcurrency: 10,
optimizeFor: 'balanced',
resourceLimits: {
availableCpu: navigator?.hardwareConcurrency ?? 4,
availableMemory: 1024,
maxConcurrentIo: 10,
maxConcurrentNetwork: 10
},
enablePredictiveScheduling: true,
adaptiveWindowSize: 5,
defaultPriority: 0,
enableWorkStealing: true,
maxQueueTime: 60000,
priorityBoostAmount: 10,
scheduleInterval: 1000,
enableBackpressure: true,
tracker: tracker || new FlowExecutionTracker()
};
this.options = { ...defaultOptions, ...options };
this.tracker = this.options.tracker;
// Initialize resources and metrics
this.initializeResources();
this.initializeMetrics();
// Initialize collections
this.readyQueue = [];
this.pendingFlows = new Map();
this.runningFlows = new Map();
}
initializeResources() {
this.resourceUsage = {
cpu: 0,
memory: 0,
io: 0,
network: 0,
availableCpu: this.options.resourceLimits.availableCpu ?? 4,
availableMemory: this.options.resourceLimits.availableMemory ?? 1024,
maxConcurrentIo: this.options.resourceLimits.maxConcurrentIo ?? 10,
maxConcurrentNetwork: this.options.resourceLimits.maxConcurrentNetwork ?? 10
};
}
initializeMetrics() {
this.metrics = {
averageFlowExecutionTime: 0,
maxFlowExecutionTime: 0,
minFlowExecutionTime: Infinity,
completedFlows: 0,
failedFlows: 0
};
}
/**
* Register a flow with the scheduler
*/
registerFlow(id, flow, options = {}) {
const flowData = {
id,
flow,
priority: options.priority || this.options.defaultPriority,
state: 'pending',
dependencies: new Set(options.dependencies || []),
dependents: new Set(),
estimatedResourceUsage: {
cpu: options.resourceEstimate?.cpu ?? 0.1,
memory: options.resourceEstimate?.memory ?? 10,
io: options.resourceEstimate?.io ?? 0.1,
network: options.resourceEstimate?.network ?? 0.1
},
attempt: 0,
executionTime: undefined,
errors: []
};
this.flowMap.set(id, flowData);
this.pendingFlows.set(id, flowData);
// Update dependency maps
for (const dep of flowData.dependencies) {
const blockers = this.flowsBlocking.get(dep) || new Set();
blockers.add(id);
this.flowsBlocking.set(dep, blockers);
const blockedBy = this.flowsBlockedBy.get(id) || new Set();
blockedBy.add(dep);
this.flowsBlockedBy.set(id, blockedBy);
}
// Update tracker if available
if (this.tracker) {
this.tracker.registerFlow(id);
}
this.emit('flow_registered', { id, priority: flowData.priority });
}
/**
* Add a dependency between flows
*/
addDependency(dependentId, dependencyId) {
const dependent = this.flowMap.get(dependentId);
const dependency = this.flowMap.get(dependencyId);
if (!dependent || !dependency) {
throw new Error('Flow not registered');
}
if (this.wouldCreateCycle(dependentId, dependencyId)) {
throw new Error('Dependency would create cycle');
}
dependent.dependencies.add(dependencyId);
dependency.dependents.add(dependentId);
// Update dependency maps
const blockers = this.flowsBlocking.get(dependencyId) || new Set();
blockers.add(dependentId);
this.flowsBlocking.set(dependencyId, blockers);
const blockedBy = this.flowsBlockedBy.get(dependentId) || new Set();
blockedBy.add(dependencyId);
this.flowsBlockedBy.set(dependentId, blockedBy);
// Update tracker if available
if (this.tracker) {
this.tracker.addDependency(dependentId, dependencyId);
}
this.emit('dependency_added', { dependentId, dependencyId });
}
/**
* Start scheduling flows
*/
start() {
this.lastScheduleTime = performance.now();
// Initial update of ready flows
this.updateReadyFlows();
// Set up periodic scheduling
if (this.options.scheduleInterval > 0) {
this.timer = setInterval(() => {
this.scheduleFlows();
}, this.options.scheduleInterval);
}
// Immediate first scheduling
this.scheduleFlows();
}
/**
* Stop scheduling flows
*/
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
}
/**
* Reset the scheduler to initial state
*/
reset() {
this.stop();
this.readyQueue = [];
this.pendingFlows.clear();
this.runningFlows.clear();
this.completedFlows.clear();
this.failedFlows.clear();
this.flowMap.clear();
this.flowsBlockedBy.clear();
this.flowsBlocking.clear();
// Reset resource usage
this.resetResources();
this.emit('scheduler_reset');
}
resetResources() {
const limits = this.options.resourceLimits;
this.resourceUsage = {
cpu: 0,
memory: 0,
io: 0,
network: 0,
availableCpu: limits?.availableCpu ?? 4,
availableMemory: limits?.availableMemory ?? 1024,
maxConcurrentIo: limits?.maxConcurrentIo ?? 10,
maxConcurrentNetwork: limits?.maxConcurrentNetwork ?? 10
};
this.emit('scheduler_reset');
}
/**
* Update the set of flows that are ready to run
* Uses optimized dependency checking
*/
updateReadyFlows() {
// Check all pending flows
for (const [id, flow] of this.pendingFlows.entries()) {
// Get flows blocking this one with O(1) lookup
const blockers = this.flowsBlockedBy.get(id);
// Flow is ready if it has no blockers or all blockers are completed
if (!blockers || blockers.size === 0 ||
Array.from(blockers).every(blockerId => {
const blocker = this.flowMap.get(blockerId);
return blocker && blocker.state === 'completed';
})) {
// Move from pending to ready
flow.state = 'ready';
flow.readyTime = performance.now();
this.addFlowToReadyQueue(flow);
}
}
// Sort ready queue if needed
if (this.readyQueue.length > 1) {
this.sortReadyQueue();
}
}
/**
* Sort the ready queue based on priority and other factors
* Uses optimized sorting algorithm
*/
sortReadyQueue() {
const now = performance.now();
// Apply priority boosts based on queue time
if (this.options.maxQueueTime > 0) {
for (const flow of this.readyQueue) {
if (flow.readyTime && now - flow.readyTime > this.options.maxQueueTime) {
// Calculate time in queue
flow.timeInQueue = now - flow.readyTime;
// Apply priority boost to prevent starvation
const boostFactor = Math.floor(flow.timeInQueue / this.options.maxQueueTime);
const boost = boostFactor * this.options.priorityBoostAmount;
// Apply boost (up to a maximum reasonable value)
flow.priority = Math.min(flow.priority + boost, 100);
}
}
}
// Sort by multiple factors for optimal scheduling
this.readyQueue.sort((a, b) => {
// Priority is the primary factor (higher values first)
if (a.priority !== b.priority) {
return b.priority - a.priority;
}
// If priorities are equal, use predicted execution time (shorter first)
if (this.options.enablePredictiveScheduling) {
const aTime = this.predictExecutionTime(a);
const bTime = this.predictExecutionTime(b);
if (aTime !== bTime) {
return aTime - bTime; // Shorter first
}
}
// If still tied, use resource efficiency (higher efficiency first)
const aEfficiency = this.calculateResourceEfficiency(a);
const bEfficiency = this.calculateResourceEfficiency(b);
if (aEfficiency !== bEfficiency) {
return bEfficiency - aEfficiency;
}
// Finally, use readiness time (earlier first)
return (a.readyTime || 0) - (b.readyTime || 0);
});
}
/**
* Add flow to ready queue
*/
addFlowToReadyQueue(flow) {
flow.state = 'pending';
flow.readyTime = performance.now();
this.readyQueue.push(flow);
this.readyQueue.sort((a, b) => b.priority - a.priority);
}
/**
* Mark flow as ready
*/
markFlowAsReady(flow) {
flow.state = 'pending';
flow.readyTime = performance.now();
this.addFlowToReadyQueue(flow);
}
/**
* Calculate resource efficiency score for a flow
* Higher is better
*/
calculateResourceEfficiency(flow) {
const usage = flow.estimatedResourceUsage || {
cpu: 0,
memory: 0,
io: 0,
network: 0
};
const { availableCpu, availableMemory, maxConcurrentIo, maxConcurrentNetwork } = this.resourceUsage;
// Calculate resource utilization percentage
const cpuUtil = usage.cpu / availableCpu;
const memUtil = usage.memory / availableMemory;
const ioUtil = usage.io / maxConcurrentIo;
const netUtil = usage.network / maxConcurrentNetwork;
// Calculate efficiency as 1 - max utilization
return 1 - Math.max(cpuUtil, memUtil, ioUtil, netUtil);
}
/**
* Predict execution time for a flow based on historical data
* Returns estimate in milliseconds
*/
predictExecutionTime(flow) {
// Use default values if resource estimates are not provided
const resourceEstimate = flow.estimatedResourceUsage || {
cpu: 1,
memory: 10,
io: 1,
network: 1
};
// Simple linear model based on resource estimates
// CPU time + Memory time + IO time + Network time
return (resourceEstimate.cpu * 1000 + // CPU time in ms (assuming 1 core = 1000ms)
resourceEstimate.memory * 0.1 + // Memory time (1MB = 0.1ms)
resourceEstimate.io * 10 + // IO time (1 operation = 10ms)
resourceEstimate.network * 50 // Network time (1 operation = 50ms)
);
}
/**
* Schedule flows for execution based on resources and priorities
*/
scheduleFlows() {
const now = performance.now();
this.lastScheduleTime = now;
// Check for completed running flows
for (const [id, flow] of this.runningFlows.entries()) {
// If flow is actually done (external signal), mark as completed
if (flow.state === 'completed' || flow.state === 'failed') {
this.runningFlows.delete(id);
this.completedFlows.set(id, flow);
// Release resources
this.updateResourceUsage(flow, false);
// Record execution time for predictive scheduling
if (flow.startTime && flow.endTime && this.options.enablePredictiveScheduling) {
const executionTime = flow.endTime - flow.startTime;
const flowType = flow.flow.constructor.name;
const history = this.flowExecutionHistory.get(flowType);
if (history) {
history.push(executionTime);
// Keep history bounded
if (history.length > 100) {
history.shift();
}
}
}
// Update dependents
this.updateDependentFlows(id);
}
}
// Update ready flows before scheduling
this.updateReadyFlows();
// Schedule flows until we hit resource limits or no more ready flows
while (this.readyQueue.length > 0) {
// Check if we've hit concurrency limit
if (this.runningFlows.size >= this.options.maxConcurrency) {
break;
}
// Check if we should apply backpressure
if (this.options.enableBackpressure && this.isSystemOverloaded()) {
break;
}
// Get highest priority flow
const flow = this.readyQueue.shift();
if (!flow)
break;
// Check if resources are available
if (!this.canScheduleFlow(flow)) {
// Put back at head of queue for next scheduling cycle
this.readyQueue.unshift(flow);
break;
}
// Allocate resources and execute
this.updateResourceUsage(flow, true);
this.executeFlow(flow);
}
// Emit stats event
this.emit('scheduler_stats', {
pendingCount: this.pendingFlows.size,
readyCount: this.readyQueue.length,
runningCount: this.runningFlows.size,
completedCount: this.completedFlows.size,
totalFlows: this.flowMap.size,
resourceUsage: { ...this.resourceUsage }
});
}
/**
* Check if the system is overloaded and should apply backpressure
*/
isSystemOverloaded() {
// Simple overload detection
const cpuPercentage = 1 - (this.resourceUsage.availableCpu /
(this.options.resourceLimits.availableCpu || 4));
const memoryPercentage = 1 - (this.resourceUsage.availableMemory /
(this.options.resourceLimits.availableMemory || 1024));
// System is overloaded if either CPU or memory usage is above 90%
return cpuPercentage > 0.9 || memoryPercentage > 0.9;
}
/**
* Execute a flow
*/
async executeFlow(flow) {
const startTime = Date.now();
try {
await flow.flow.execute();
const duration = Date.now() - startTime;
this.metrics = {
...this.metrics,
averageFlowExecutionTime: (this.metrics.averageFlowExecutionTime * this.metrics.completedFlows + duration) / (this.metrics.completedFlows + 1),
maxFlowExecutionTime: Math.max(this.metrics.maxFlowExecutionTime, duration),
minFlowExecutionTime: Math.min(this.metrics.minFlowExecutionTime, duration),
completedFlows: this.metrics.completedFlows + 1
};
this.markFlowCompleted(flow.id, true);
this.updateDependentFlows(flow.id);
}
catch (error) {
this.metrics = {
...this.metrics,
failedFlows: this.metrics.failedFlows + 1
};
this.markFlowCompleted(flow.id, false);
throw error;
}
}
markFlowCompleted(flowId, success) {
const flow = this.runningFlows.get(flowId);
if (!flow)
return;
if (this.tracker) {
const metrics = flow.metrics || {
startTime: flow.startTime || 0,
endTime: Date.now(),
executionTime: 0,
duration: 0,
resourceUsage: flow.resourceUsage
};
if (success) {
this.tracker.completeFlow(flowId, metrics);
}
else {
// Pass the error object if it exists
const error = metrics.error;
if (error) {
this.tracker.failFlow(flowId, error);
}
else {
this.tracker.failFlow(flowId, new Error('Flow execution failed'));
}
}
}
if (success) {
this.completedFlows.set(flowId, flow);
}
else {
this.failedFlows.set(flowId, flow);
}
this.runningFlows.delete(flowId);
this.emit(success ? 'flow_completed' : 'flow_failed', { flowId });
}
/**
* Update the set of flows that are dependent on a given flow
*/
updateDependentFlows(flowId) {
const dependents = this.flowsBlocking.get(flowId);
if (!dependents)
return;
for (const dependentId of dependents) {
const blockers = this.flowsBlockedBy.get(dependentId);
if (blockers) {
blockers.delete(flowId);
const dependent = this.flowMap.get(dependentId);
if (dependent && blockers.size === 0) {
dependent.state = 'ready';
dependent.readyTime = performance.now();
this.addFlowToReadyQueue(dependent);
this.sortReadyQueue();
}
}
}
}
/**
* Check if adding a dependency would create a cycle
* Using optimized DFS with visited sets
*/
wouldCreateCycle(from, to) {
const visited = new Set();
const stack = [to]; // Start from the dependency
while (stack.length > 0) {
const current = stack.pop();
if (current === from) {
return true; // Would create a cycle
}
if (visited.has(current)) {
continue;
}
visited.add(current);
// Add all dependents to the stack
const dependents = this.flowsBlocking.get(current);
if (dependents) {
for (const dependent of dependents) {
stack.push(dependent);
}
}
}
return false;
}
/**
* Start flow execution
*/
startFlowExecution(flowId) {
const flow = this.flowMap.get(flowId);
if (!flow) {
throw new Error('Flow not found');
}
if (flow.state !== 'ready') {
throw new Error('Flow not ready to start');
}
this.executeFlow(flow);
}
/**
* Get all scheduled flows
*/
getFlows() {
return new Map(this.flowMap);
}
/**
* Get a flow by ID
*/
getFlow(flowId) {
return this.flowMap.get(flowId);
}
/**
* Get current scheduler stats
*/
getStats() {
return {
pendingCount: this.pendingFlows.size,
readyCount: this.readyQueue.length,
runningCount: this.runningFlows.size,
completedCount: this.completedFlows.size,
totalFlows: this.flowMap.size,
resourceUsage: { ...this.resourceUsage }
};
}
updateResourceUsage(flow, isStarting) {
const resourceUsage = flow.resourceUsage || flow.estimatedResourceUsage;
if (!resourceUsage)
return;
// Create a safe copy of the resource usage with default values
const safeUsage = {
cpu: resourceUsage.cpu ?? 0,
memory: resourceUsage.memory ?? 0,
io: resourceUsage.io ?? 0,
network: resourceUsage.network ?? 0
};
if (!this.resourceUsage) {
this.resourceUsage = {
cpu: 0,
memory: 0,
io: 0,
network: 0,
availableCpu: 4,
availableMemory: 1024,
maxConcurrentIo: 10,
maxConcurrentNetwork: 10
};
}
if (isStarting) {
this.resourceUsage.cpu = (this.resourceUsage.cpu ?? 0) + safeUsage.cpu;
this.resourceUsage.memory = (this.resourceUsage.memory ?? 0) + safeUsage.memory;
this.resourceUsage.io = (this.resourceUsage.io ?? 0) + safeUsage.io;
this.resourceUsage.network = (this.resourceUsage.network ?? 0) + safeUsage.network;
}
else {
this.resourceUsage.cpu = Math.max(0, (this.resourceUsage.cpu ?? 0) - safeUsage.cpu);
this.resourceUsage.memory = Math.max(0, (this.resourceUsage.memory ?? 0) - safeUsage.memory);
this.resourceUsage.io = Math.max(0, (this.resourceUsage.io ?? 0) - safeUsage.io);
this.resourceUsage.network = Math.max(0, (this.resourceUsage.network ?? 0) - safeUsage.network);
}
}
handleFlowError(flowId, error) {
const flow = this.runningFlows.get(flowId);
if (!flow)
return;
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Flow execution failed: ${errorMessage}`);
// Update flow metrics with error information
if (flow.metrics) {
flow.metrics.error = error instanceof Error ? error : new Error(errorMessage);
}
this.markFlowCompleted(flowId, false);
}
canScheduleFlow(flow) {
if (!this.resourceUsage) {
return false;
}
const usage = this.calculateResourceUsage(flow);
return ((this.resourceUsage.availableCpu || 0) >= usage.cpu &&
(this.resourceUsage.availableMemory || 0) >= usage.memory &&
(this.resourceUsage.maxConcurrentIo || 0) >= usage.io &&
(this.resourceUsage.maxConcurrentNetwork || 0) >= usage.network);
}
calculateResourceUsage(flow) {
const usage = flow.resourceUsage || flow.estimatedResourceUsage;
return usage || {
cpu: 0,
memory: 0,
io: 0,
network: 0
};
}
getResourceUsage() {
if (!this.resourceUsage) {
this.resourceUsage = {
cpu: 0,
memory: 0,
io: 0,
network: 0,
availableCpu: 4,
availableMemory: 1024,
maxConcurrentIo: 10,
maxConcurrentNetwork: 10
};
}
const memoryUsage = process.memoryUsage();
return {
cpu: 0,
memory: memoryUsage ? memoryUsage.heapUsed / 1024 / 1024 : 0,
io: 0,
network: 0,
availableCpu: this.resourceUsage.availableCpu || 4,
availableMemory: this.resourceUsage.availableMemory || 1024,
maxConcurrentIo: this.resourceUsage.maxConcurrentIo || 10,
maxConcurrentNetwork: this.resourceUsage.maxConcurrentNetwork || 10
};
}
scheduleFlow(flowId) {
const flow = this.getFlow(flowId);
if (!flow)
return;
if (!this.resourceUsage) {
this.initializeResources();
}
if (this.canScheduleFlow(flow)) {
this.updateResourceUsage(flow, true);
this.runningFlows.set(flowId, flow);
this.executeFlow(flow);
}
else {
this.pendingFlows.set(flowId, flow);
}
}
updateFlowMetrics(flowId) {
const flow = this.getFlow(flowId);
if (!flow?.metrics)
return;
const now = performance.now();
flow.metrics.executionTime = now - (flow.metrics.startTime || now);
flow.metrics.duration = flow.metrics.executionTime;
}
getFlowMetrics(flowId) {
const flow = this.getFlow(flowId);
return flow?.metrics || null;
}
getFlowExecutionTime(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics || !metrics.startTime || !metrics.endTime) {
return 0;
}
return metrics.endTime - metrics.startTime;
}
getFlowResourceUsage(flowId) {
const metrics = this.getFlowMetrics(flowId);
return metrics?.resourceUsage || null;
}
}