UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

581 lines 20.3 kB
/** * Container Worker Pool * Docker-based worker pool for high-throughput headless execution. * * ADR-020: Headless Worker Integration Architecture - Phase 3 * - Manages pool of Docker containers for isolated worker execution * - Supports dynamic scaling based on workload * - Provides container lifecycle management * - Integrates with WorkerQueue for task distribution * * Key Features: * - Container pooling with configurable size * - Health checking and auto-recovery * - Resource limits (CPU, memory) * - Volume mounting for workspace access * - Network isolation per worker type */ import { EventEmitter } from 'events'; import { spawn, exec } from 'child_process'; import { promisify } from 'util'; import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; const execAsync = promisify(exec); // ============================================ // Constants // ============================================ const DEFAULT_CONFIG = { maxContainers: 3, minContainers: 1, image: 'ghcr.io/ruvnet/claude-flow-headless:latest', resources: { cpus: '2', memory: '4g', }, healthCheckIntervalMs: 30000, idleTimeoutMs: 300000, // 5 minutes workspacePath: '/workspace', statePath: '.claude-flow/container-pool', defaultSandbox: 'strict', }; // ============================================ // ContainerWorkerPool Class // ============================================ /** * ContainerWorkerPool - Manages Docker containers for headless worker execution */ export class ContainerWorkerPool extends EventEmitter { config; projectRoot; containers = new Map(); taskQueue = []; healthCheckTimer; idleCheckTimer; dockerAvailable = null; initialized = false; isShuttingDown = false; exitHandlersRegistered = false; constructor(projectRoot, config) { super(); this.projectRoot = projectRoot; this.config = { ...DEFAULT_CONFIG, ...config }; // Ensure state directory exists const stateDir = join(projectRoot, this.config.statePath); if (!existsSync(stateDir)) { mkdirSync(stateDir, { recursive: true }); } } // ============================================ // Public API // ============================================ /** * Initialize the container pool */ async initialize() { if (this.initialized) { return true; } // Check Docker availability this.dockerAvailable = await this.checkDockerAvailable(); if (!this.dockerAvailable) { this.emit('warning', { message: 'Docker not available - container pool disabled' }); return false; } // Pull image if needed await this.ensureImage(); // Create minimum containers await this.scaleToMinimum(); // Start health check timer this.startHealthChecks(); // Start idle check timer this.startIdleChecks(); // Register exit handlers for cleanup this.registerExitHandlers(); this.initialized = true; this.emit('initialized', { containers: this.containers.size }); return true; } /** * Register process exit handlers to clean up containers */ registerExitHandlers() { if (this.exitHandlersRegistered) return; const cleanup = async () => { if (!this.isShuttingDown) { await this.shutdown(); } }; process.once('SIGTERM', cleanup); process.once('SIGINT', cleanup); process.once('beforeExit', cleanup); this.exitHandlersRegistered = true; } /** * Execute a worker in a container */ async execute(options) { if (!this.initialized) { await this.initialize(); } if (!this.dockerAvailable) { return this.createErrorResult(options.workerType, 'Docker not available'); } // Try to get a ready container const container = this.getReadyContainer(); if (container) { return this.executeInContainer(container, options); } // No ready containers - check if we can create more if (this.containers.size < this.config.maxContainers) { const newContainer = await this.createContainer(); if (newContainer) { return this.executeInContainer(newContainer, options); } } // Queue the task return new Promise((resolve, reject) => { this.taskQueue.push({ options, resolve, reject, queuedAt: new Date(), }); this.emit('taskQueued', { workerType: options.workerType, queuePosition: this.taskQueue.length, }); }); } /** * Scale pool for batch execution */ async scaleForBatch(workerCount) { const targetSize = Math.min(workerCount, this.config.maxContainers); const currentSize = this.containers.size; if (targetSize > currentSize) { const toCreate = targetSize - currentSize; const createPromises = []; for (let i = 0; i < toCreate; i++) { createPromises.push(this.createContainer()); } await Promise.all(createPromises); this.emit('scaled', { from: currentSize, to: this.containers.size }); } } /** * Get pool status */ getStatus() { const containers = Array.from(this.containers.values()); return { totalContainers: containers.length, readyContainers: containers.filter(c => c.state === 'ready').length, busyContainers: containers.filter(c => c.state === 'busy').length, unhealthyContainers: containers.filter(c => c.state === 'unhealthy').length, queuedTasks: this.taskQueue.length, containers, dockerAvailable: this.dockerAvailable ?? false, lastHealthCheck: undefined, // Will be set by health check }; } /** * Shutdown the pool */ async shutdown() { if (this.isShuttingDown) return; this.isShuttingDown = true; // Stop timers if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); this.healthCheckTimer = undefined; } if (this.idleCheckTimer) { clearInterval(this.idleCheckTimer); this.idleCheckTimer = undefined; } // Reject queued tasks for (const task of this.taskQueue) { task.reject(new Error('Pool shutting down')); } this.taskQueue = []; // Terminate all containers with timeout const terminatePromises = []; for (const [id] of this.containers) { terminatePromises.push(this.terminateContainer(id).catch(() => { // Ignore errors during shutdown })); } // Wait for all containers with 30s timeout await Promise.race([ Promise.all(terminatePromises), new Promise(resolve => setTimeout(resolve, 30000)), ]); this.initialized = false; this.emit('shutdown', {}); } // ============================================ // Private Methods - Container Lifecycle // ============================================ /** * Check if Docker is available (async) */ async checkDockerAvailable() { try { await execAsync('docker --version', { timeout: 5000 }); await execAsync('docker info', { timeout: 10000 }); return true; } catch { return false; } } /** * Ensure the container image exists (async) */ async ensureImage() { try { await execAsync(`docker image inspect ${this.config.image}`, { timeout: 10000 }); } catch { // Image not found, try to pull this.emit('imagePull', { image: this.config.image }); try { await execAsync(`docker pull ${this.config.image}`, { timeout: 300000 }); } catch (error) { this.emit('warning', { message: `Failed to pull image: ${error}` }); // Continue anyway - might work with local image } } } /** * Create a new container */ async createContainer() { const id = `cf-worker-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const name = `claude-flow-worker-${id}`; const containerInfo = { id, name, state: 'creating', createdAt: new Date(), executionCount: 0, healthCheckFailures: 0, }; this.containers.set(id, containerInfo); this.emit('containerCreating', { id, name }); try { // Build docker run command const args = [ 'run', '-d', '--name', name, '--cpus', this.config.resources.cpus, '--memory', this.config.resources.memory, '-v', `${this.projectRoot}:${this.config.workspacePath}:ro`, '-v', `${join(this.projectRoot, this.config.statePath)}:/root/.claude-flow`, '-w', this.config.workspacePath, ]; // Add environment variables const env = { ...this.config.env, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '', CLAUDE_CODE_HEADLESS: 'true', CLAUDE_CODE_SANDBOX_MODE: this.config.defaultSandbox, }; for (const [key, value] of Object.entries(env)) { if (value) { args.push('-e', `${key}=${value}`); } } // Add network if specified if (this.config.network) { args.push('--network', this.config.network); } // Add image and entrypoint to keep container running args.push(this.config.image, 'tail', '-f', '/dev/null'); // Create the container (async) const { stdout } = await execAsync(`docker ${args.join(' ')}`, { timeout: 60000 }); const containerId = stdout.trim(); containerInfo.state = 'ready'; this.emit('containerCreated', { id, name, containerId }); return containerInfo; } catch (error) { this.containers.delete(id); this.emit('containerError', { id, error: String(error) }); return null; } } /** * Terminate a container (async) */ async terminateContainer(id) { const container = this.containers.get(id); if (!container) return; container.state = 'terminated'; try { await execAsync(`docker rm -f ${container.name}`, { timeout: 30000 }); } catch { // Ignore removal errors } this.containers.delete(id); this.emit('containerTerminated', { id, name: container.name }); } /** * Get a ready container */ getReadyContainer() { for (const container of this.containers.values()) { if (container.state === 'ready') { return container; } } return null; } /** * Scale to minimum containers */ async scaleToMinimum() { const current = this.containers.size; const needed = this.config.minContainers - current; if (needed > 0) { const createPromises = []; for (let i = 0; i < needed; i++) { createPromises.push(this.createContainer()); } await Promise.all(createPromises); } } // ============================================ // Private Methods - Execution // ============================================ /** * Execute worker in a specific container */ async executeInContainer(container, options) { const startTime = Date.now(); const executionId = `${options.workerType}_${startTime}_${Math.random().toString(36).slice(2, 8)}`; container.state = 'busy'; container.workerType = options.workerType; container.lastUsedAt = new Date(); this.emit('executionStart', { executionId, containerId: container.id, workerType: options.workerType }); try { // Build the command to run inside container const command = this.buildWorkerCommand(options); // Execute in container with timeout const timeoutMs = options.timeoutMs || 300000; const output = await this.execInContainer(container.name, command, timeoutMs); container.state = 'ready'; container.executionCount++; const result = { success: true, output: output, parsedOutput: this.tryParseJson(output), durationMs: Date.now() - startTime, model: options.model || 'sonnet', sandboxMode: options.sandbox || this.config.defaultSandbox, workerType: options.workerType, timestamp: new Date(), executionId, }; this.emit('executionComplete', result); // Process queue this.processQueue(); return result; } catch (error) { container.state = 'ready'; const result = this.createErrorResult(options.workerType, error instanceof Error ? error.message : String(error)); result.executionId = executionId; result.durationMs = Date.now() - startTime; this.emit('executionError', result); // Process queue this.processQueue(); return result; } } /** * Execute command in container */ async execInContainer(containerName, command, timeoutMs) { return new Promise((resolve, reject) => { const args = ['exec', containerName, ...command]; const child = spawn('docker', args, { stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; let timedOut = false; const timeout = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); setTimeout(() => { if (!child.killed) { child.kill('SIGKILL'); } }, 5000); }, timeoutMs); child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { clearTimeout(timeout); if (timedOut) { reject(new Error(`Execution timed out after ${timeoutMs}ms`)); return; } if (code !== 0) { reject(new Error(stderr || `Process exited with code ${code}`)); return; } resolve(stdout); }); child.on('error', (error) => { clearTimeout(timeout); reject(error); }); }); } /** * Build worker command for container execution */ buildWorkerCommand(options) { // Use npx to run claude-flow daemon trigger return [ 'npx', 'claude-flow@v3alpha', 'daemon', 'trigger', '-w', options.workerType, '--headless', ]; } /** * Process queued tasks */ processQueue() { while (this.taskQueue.length > 0) { const container = this.getReadyContainer(); if (!container) break; const task = this.taskQueue.shift(); if (task) { this.executeInContainer(container, task.options) .then(task.resolve) .catch(task.reject); } } } // ============================================ // Private Methods - Health & Maintenance // ============================================ /** * Start health check timer */ startHealthChecks() { this.healthCheckTimer = setInterval(async () => { await this.runHealthChecks(); }, this.config.healthCheckIntervalMs); } /** * Run health checks on all containers */ async runHealthChecks() { for (const [id, container] of this.containers) { if (container.state === 'terminated') continue; try { // Check if container is running (async) const { stdout } = await execAsync(`docker inspect -f '{{.State.Running}}' ${container.name}`, { timeout: 10000 }); const output = stdout.trim(); if (output !== 'true') { container.healthCheckFailures++; if (container.healthCheckFailures >= 3) { container.state = 'unhealthy'; this.emit('containerUnhealthy', { id, name: container.name }); // Remove and replace await this.terminateContainer(id); if (this.containers.size < this.config.minContainers) { await this.createContainer(); } } } else { container.healthCheckFailures = 0; } } catch { container.healthCheckFailures++; } } this.emit('healthCheckComplete', { containers: this.containers.size }); } /** * Start idle check timer */ startIdleChecks() { this.idleCheckTimer = setInterval(async () => { await this.runIdleChecks(); }, 60000); // Check every minute } /** * Terminate idle containers above minimum */ async runIdleChecks() { const now = Date.now(); const readyContainers = Array.from(this.containers.values()) .filter(c => c.state === 'ready') .sort((a, b) => (a.lastUsedAt?.getTime() || 0) - (b.lastUsedAt?.getTime() || 0)); // Keep minimum containers const toTerminate = readyContainers.slice(this.config.minContainers); for (const container of toTerminate) { const lastUsed = container.lastUsedAt?.getTime() || container.createdAt.getTime(); if (now - lastUsed > this.config.idleTimeoutMs) { await this.terminateContainer(container.id); this.emit('containerIdleTerminated', { id: container.id, name: container.name }); } } } // ============================================ // Private Methods - Utilities // ============================================ /** * Try to parse JSON from output */ tryParseJson(output) { try { const jsonMatch = output.match(/\{[\s\S]*\}/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); } return JSON.parse(output.trim()); } catch { return undefined; } } /** * Create an error result */ createErrorResult(workerType, error) { return { success: false, output: '', durationMs: 0, model: 'unknown', sandboxMode: this.config.defaultSandbox, workerType, timestamp: new Date(), executionId: `error_${Date.now()}`, error, }; } } // Export default export default ContainerWorkerPool; //# sourceMappingURL=container-worker-pool.js.map