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
JavaScript
/**
* 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