@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
259 lines (258 loc) • 7.49 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { EventEmitter } from "events";
import { logger } from "../monitoring/logger.js";
class ParallelExecutor extends EventEmitter {
maxConcurrency;
queueSize;
defaultTimeout;
defaultRetries;
rateLimitPerMinute;
activeCount = 0;
queue = [];
rateLimitTokens;
lastRateLimitReset;
// Metrics
totalExecuted = 0;
totalSucceeded = 0;
totalFailed = 0;
totalDuration = 0;
constructor(maxConcurrency = 5, options = {}) {
super();
this.maxConcurrency = maxConcurrency;
this.queueSize = options.queueSize || 100;
this.defaultTimeout = options.defaultTimeout || 3e5;
this.defaultRetries = options.defaultRetries || 3;
this.rateLimitPerMinute = options.rateLimitPerMinute || 60;
this.rateLimitTokens = this.rateLimitPerMinute;
this.lastRateLimitReset = Date.now();
logger.info("Parallel Executor initialized", {
maxConcurrency,
queueSize: this.queueSize,
rateLimitPerMinute: this.rateLimitPerMinute
});
}
/**
* Execute multiple tasks in parallel
*/
async executeParallel(items, executor, options) {
const results = [];
const batchSize = options?.batchSize || this.maxConcurrency;
const delayBetweenBatches = options?.delayBetweenBatches || 0;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(
(item, index) => this.executeWithTracking(
`parallel-${i + index}`,
() => executor(item, i + index)
)
);
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach((result, index) => {
if (result.status === "fulfilled") {
results.push(result.value);
} else {
results.push({
taskId: `parallel-${i + index}`,
success: false,
error: result.reason,
duration: 0,
attempts: 1
});
}
});
if (delayBetweenBatches > 0 && i + batchSize < items.length) {
await this.delay(delayBetweenBatches);
}
logger.debug("Batch completed", {
batchNumber: Math.floor(i / batchSize) + 1,
totalBatches: Math.ceil(items.length / batchSize),
successRate: results.filter((r) => r.success).length / results.length
});
}
return results;
}
/**
* Execute a single task with tracking and retries
*/
async executeWithTracking(taskId, executor, options) {
const timeout = options?.timeout || this.defaultTimeout;
const maxRetries = options?.retries || this.defaultRetries;
let attempts = 0;
let lastError;
const startTime = Date.now();
while (attempts < maxRetries) {
attempts++;
try {
await this.checkRateLimit();
await this.waitForSlot();
this.activeCount++;
this.emit("task-start", { taskId, attempt: attempts });
const result = await this.executeWithTimeout(executor, timeout);
this.totalSucceeded++;
this.emit("task-success", { taskId, attempts });
return {
taskId,
success: true,
result,
duration: Date.now() - startTime,
attempts
};
} catch (error) {
lastError = error;
logger.warn(`Task failed (attempt ${attempts}/${maxRetries})`, {
taskId,
error: lastError.message
});
this.emit("task-retry", { taskId, attempt: attempts, error: lastError });
if (attempts < maxRetries) {
await this.delay(Math.pow(2, attempts) * 1e3);
}
} finally {
this.activeCount--;
this.totalExecuted++;
this.totalDuration += Date.now() - startTime;
}
}
this.totalFailed++;
this.emit("task-failed", { taskId, attempts, error: lastError });
return {
taskId,
success: false,
error: lastError,
duration: Date.now() - startTime,
attempts
};
}
/**
* Execute task with timeout
*/
async executeWithTimeout(executor, timeout) {
return Promise.race([
executor(),
new Promise(
(_, reject) => setTimeout(() => reject(new Error("Task timeout")), timeout)
)
]);
}
/**
* Check and enforce rate limiting
*/
async checkRateLimit() {
const now = Date.now();
const timeSinceReset = now - this.lastRateLimitReset;
if (timeSinceReset >= 6e4) {
this.rateLimitTokens = this.rateLimitPerMinute;
this.lastRateLimitReset = now;
}
if (this.rateLimitTokens <= 0) {
const waitTime = 6e4 - timeSinceReset;
logger.debug(`Rate limit reached, waiting ${waitTime}ms`);
await this.delay(waitTime);
this.rateLimitTokens = this.rateLimitPerMinute;
this.lastRateLimitReset = Date.now();
}
this.rateLimitTokens--;
}
/**
* Wait for an available execution slot
*/
async waitForSlot() {
while (this.activeCount >= this.maxConcurrency) {
await this.delay(100);
}
}
/**
* Queue a task for execution
*/
async queueTask(task) {
if (this.queue.length >= this.queueSize) {
throw new Error("Execution queue is full");
}
return new Promise((resolve) => {
this.queue.push({
...task,
execute: async () => {
const result = await task.execute();
resolve({
taskId: task.id,
success: true,
result,
duration: 0,
attempts: 1
});
return result;
}
});
this.processQueue();
});
}
/**
* Process queued tasks
*/
async processQueue() {
if (this.activeCount >= this.maxConcurrency || this.queue.length === 0) {
return;
}
this.queue.sort((a, b) => (b.priority || 0) - (a.priority || 0));
const task = this.queue.shift();
if (task) {
this.executeWithTracking(task.id, task.execute, {
timeout: task.timeout,
retries: task.retries
}).then(() => {
this.processQueue();
});
}
}
/**
* Utility delay function
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Get execution metrics
*/
getMetrics() {
return {
activeCount: this.activeCount,
queueLength: this.queue.length,
totalExecuted: this.totalExecuted,
totalSucceeded: this.totalSucceeded,
totalFailed: this.totalFailed,
successRate: this.totalExecuted > 0 ? this.totalSucceeded / this.totalExecuted : 0,
averageDuration: this.totalExecuted > 0 ? this.totalDuration / this.totalExecuted : 0,
rateLimitTokens: this.rateLimitTokens
};
}
/**
* Reset all metrics
*/
resetMetrics() {
this.totalExecuted = 0;
this.totalSucceeded = 0;
this.totalFailed = 0;
this.totalDuration = 0;
}
/**
* Gracefully shutdown executor
*/
async shutdown() {
logger.info("Shutting down Parallel Executor", {
activeCount: this.activeCount,
queueLength: this.queue.length
});
this.queue = [];
while (this.activeCount > 0) {
await this.delay(100);
}
logger.info("Parallel Executor shutdown complete");
}
}
export {
ParallelExecutor
};
//# sourceMappingURL=parallel-executor.js.map