UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

403 lines 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PythonProcessPool = void 0; const child_process_1 = require("child_process"); const events_1 = require("events"); const uuid_1 = require("uuid"); const resourceManager_1 = require("./resourceManager"); const circuitBreaker_1 = require("./circuitBreaker"); class PythonProcessPool extends events_1.EventEmitter { constructor(scriptPath, config = {}) { super(); this.connections = new Map(); this.requestQueue = []; this.metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, totalResponseTime: 0, }; this.scriptPath = scriptPath; this.config = { maxProcesses: config.maxProcesses || 6, minProcesses: config.minProcesses || 2, idleTimeout: config.idleTimeout || 60000, requestTimeout: config.requestTimeout || 30000, maxErrorCount: config.maxErrorCount || 5, healthCheckInterval: config.healthCheckInterval || 30000, processRespawnDelay: config.processRespawnDelay || 1000, }; this.initialize(); } async initialize() { for (let i = 0; i < this.config.minProcesses; i++) { await this.createConnection(); } const { id: healthCheckId } = resourceManager_1.resourceManager.createInterval(() => { this.performHealthChecks(); }, this.config.healthCheckInterval, 'Python process pool health check'); this.healthCheckResourceId = healthCheckId; const { id: idleCheckId } = resourceManager_1.resourceManager.createInterval(() => { this.checkIdleConnections(); }, 10000, 'Python process pool idle connection check'); this.idleCheckResourceId = idleCheckId; } async createConnection() { const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker('python-connection-creation', { failureThreshold: 3, recoveryTimeout: 180000, successThreshold: 2, maxRetryAttempts: 1, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Python connection creation: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Python connection creation failure:`, error.message); } }); return await circuitBreaker.execute(async () => { return await this.performConnectionCreation(); }, async () => { console.warn('[Circuit Breaker] Python connection creation is in open state, skipping connection creation'); return null; }); } async performConnectionCreation() { try { const id = (0, uuid_1.v4)(); const pythonProcess = (0, child_process_1.spawn)('python3', [this.scriptPath, '--persistent'], { stdio: ['pipe', 'pipe', 'pipe'], }); const connection = { process: pythonProcess, id, created: Date.now(), lastUsed: Date.now(), pendingRequests: new Map(), healthy: true, errorCount: 0, busy: false, }; let buffer = ''; pythonProcess.stdout.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { this.handleResponse(connection, line); } } }); pythonProcess.stderr.on('data', (data) => { console.error(`Python process ${id} stderr:`, data.toString()); connection.errorCount++; }); pythonProcess.on('error', (error) => { console.error(`Python process ${id} error:`, error); connection.healthy = false; this.handleConnectionError(connection, error); }); pythonProcess.on('exit', (code) => { console.log(`Python process ${id} exited with code ${code}`); connection.healthy = false; this.handleConnectionExit(connection, code); }); const healthCheck = await this.sendRequest(connection, 'ping', []); if (healthCheck !== 'pong') { throw new Error('Health check failed'); } this.connections.set(id, connection); this.emit('connectionCreated', id); return connection; } catch (error) { console.error('Failed to create Python connection:', error); return null; } } handleResponse(connection, line) { try { const response = JSON.parse(line); const requestId = response.id; if (!requestId) { console.warn('Received response without ID:', response); return; } const pendingRequest = connection.pendingRequests.get(requestId); if (!pendingRequest) { console.warn('No pending request for ID:', requestId); return; } clearTimeout(pendingRequest.timeout); const responseTime = Date.now() - pendingRequest.timestamp; this.metrics.totalResponseTime += responseTime; connection.pendingRequests.delete(requestId); connection.busy = connection.pendingRequests.size > 0; connection.lastUsed = Date.now(); if (response.error) { this.metrics.failedRequests++; connection.errorCount++; pendingRequest.reject(new Error(response.error)); } else { this.metrics.successfulRequests++; connection.errorCount = 0; pendingRequest.resolve(response.result); } this.processQueue(); } catch (error) { console.error('Failed to handle response:', error); connection.errorCount++; } } handleConnectionError(connection, error) { for (const [requestId, request] of connection.pendingRequests) { clearTimeout(request.timeout); request.reject(new Error(`Connection error: ${error.message}`)); this.metrics.failedRequests++; } connection.pendingRequests.clear(); this.connections.delete(connection.id); setTimeout(() => { this.maintainMinimumConnections(); }, this.config.processRespawnDelay); } handleConnectionExit(connection, code) { for (const [requestId, request] of connection.pendingRequests) { clearTimeout(request.timeout); request.reject(new Error(`Process exited with code ${code}`)); this.metrics.failedRequests++; } connection.pendingRequests.clear(); this.connections.delete(connection.id); setTimeout(() => { this.maintainMinimumConnections(); }, this.config.processRespawnDelay); } async sendRequest(connection, method, args) { return new Promise((resolve, reject) => { const requestId = (0, uuid_1.v4)(); const request = { id: requestId, method, args, }; const timeout = setTimeout(() => { connection.pendingRequests.delete(requestId); connection.errorCount++; reject(new Error(`Request timeout for method ${method}`)); }, this.config.requestTimeout); connection.pendingRequests.set(requestId, { resolve, reject, timeout, timestamp: Date.now(), }); connection.busy = true; connection.process.stdin.write(JSON.stringify(request) + '\n'); }); } async execute(method, args = []) { const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker(`python-process-execution-${method}`, { failureThreshold: 5, recoveryTimeout: 60000, successThreshold: 3, monitoringWindow: 300000, maxRetryAttempts: 2, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Python execution for ${method}: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Python execution failure for ${method}:`, error.message); } }); return await circuitBreaker.execute(async () => { return await this.performExecution(method, args); }, async () => { console.warn(`[Circuit Breaker] Python execution for ${method} is in open state, returning fallback`); throw new Error(`Python process execution temporarily unavailable for method ${method} due to circuit breaker protection`); }); } async performExecution(method, args = []) { this.metrics.totalRequests++; const connection = this.findAvailableConnection(); if (connection) { try { return await this.sendRequest(connection, method, args); } catch (error) { const retryConnection = this.findAvailableConnection(connection.id); if (retryConnection) { return await this.sendRequest(retryConnection, method, args); } throw error; } } else { return new Promise((resolve, reject) => { this.requestQueue.push({ method, args, resolve, reject }); if (this.connections.size < this.config.maxProcesses) { this.createConnection().then(() => { this.processQueue(); }); } }); } } findAvailableConnection(excludeId) { let bestConnection = null; let minPendingRequests = Infinity; for (const connection of this.connections.values()) { if (connection.id === excludeId) continue; if (!connection.healthy) continue; if (connection.errorCount >= this.config.maxErrorCount) continue; const pendingCount = connection.pendingRequests.size; if (pendingCount === 0) { return connection; } if (pendingCount < minPendingRequests) { minPendingRequests = pendingCount; bestConnection = connection; } } if (bestConnection && minPendingRequests < 3) { return bestConnection; } return null; } processQueue() { while (this.requestQueue.length > 0) { const connection = this.findAvailableConnection(); if (!connection) break; const request = this.requestQueue.shift(); if (!request) break; this.sendRequest(connection, request.method, request.args) .then(request.resolve) .catch(request.reject); } } async performHealthChecks() { const circuitBreaker = circuitBreaker_1.circuitRegistry.getBreaker('python-health-check', { failureThreshold: 3, recoveryTimeout: 120000, successThreshold: 2, maxRetryAttempts: 1, onStateChange: (state, reason) => { console.log(`[Circuit Breaker] Python health check: ${state} - ${reason}`); }, onFailure: (error) => { console.error(`[Circuit Breaker] Python health check failure:`, error.message); } }); await circuitBreaker.execute(async () => { return await this.performActualHealthChecks(); }, async () => { console.warn('[Circuit Breaker] Python health check is in open state, skipping health checks'); this.maintainMinimumConnections(); }); } async performActualHealthChecks() { const checks = []; for (const connection of this.connections.values()) { if (!connection.healthy) continue; const check = this.sendRequest(connection, 'ping', []) .then(result => { if (result !== 'pong') { connection.healthy = false; connection.errorCount++; } }) .catch(() => { connection.healthy = false; connection.errorCount++; }); checks.push(check); } await Promise.allSettled(checks); for (const [id, connection] of this.connections) { if (!connection.healthy || connection.errorCount >= this.config.maxErrorCount) { connection.process.kill(); this.connections.delete(id); } } this.maintainMinimumConnections(); } checkIdleConnections() { const now = Date.now(); const toRemove = []; for (const [id, connection] of this.connections) { if (connection.pendingRequests.size > 0) continue; if (this.connections.size <= this.config.minProcesses) continue; if (now - connection.lastUsed > this.config.idleTimeout) { toRemove.push(id); } } for (const id of toRemove) { const connection = this.connections.get(id); if (connection) { connection.process.kill(); this.connections.delete(id); this.emit('connectionRemoved', id); } } } async maintainMinimumConnections() { const currentCount = this.connections.size; const needed = this.config.minProcesses - currentCount; if (needed > 0) { const creates = []; for (let i = 0; i < needed; i++) { creates.push(this.createConnection()); } await Promise.allSettled(creates); } } getMetrics() { const activeConnections = Array.from(this.connections.values()).filter(c => c.busy).length; const idleConnections = this.connections.size - activeConnections; const healthyProcesses = Array.from(this.connections.values()).filter(c => c.healthy).length; return { activeConnections, idleConnections, totalRequests: this.metrics.totalRequests, successfulRequests: this.metrics.successfulRequests, failedRequests: this.metrics.failedRequests, averageResponseTime: this.metrics.successfulRequests > 0 ? this.metrics.totalResponseTime / this.metrics.successfulRequests : 0, poolUtilization: this.connections.size > 0 ? (activeConnections / this.connections.size) * 100 : 0, healthyProcesses, errorRate: this.metrics.totalRequests > 0 ? (this.metrics.failedRequests / this.metrics.totalRequests) * 100 : 0, }; } async destroy() { if (this.healthCheckResourceId) { await resourceManager_1.resourceManager.cleanup(this.healthCheckResourceId); } if (this.idleCheckResourceId) { await resourceManager_1.resourceManager.cleanup(this.idleCheckResourceId); } for (const connection of this.connections.values()) { connection.process.kill(); } this.connections.clear(); this.requestQueue = []; this.emit('destroyed'); } } exports.PythonProcessPool = PythonProcessPool; //# sourceMappingURL=pythonProcessPool.js.map