UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

270 lines (220 loc) 7.38 kB
/** * Terminal pool management */ import type { Terminal, ITerminalAdapter } from './adapters/base.js'; import type { ILogger } from '../core/logger.js'; import { TerminalError } from '../utils/errors.js'; import { delay } from '../utils/helpers.js'; interface PooledTerminal { terminal: Terminal; useCount: number; lastUsed: Date; inUse: boolean; } /** * Terminal pool for efficient resource management */ export class TerminalPool { private terminals = new Map<string, PooledTerminal>(); private availableQueue: string[] = []; private initializationPromise?: Promise<void>; constructor( private maxSize: number, private recycleAfter: number, private adapter: ITerminalAdapter, private logger: ILogger, ) {} async initialize(): Promise<void> { if (this.initializationPromise) { return this.initializationPromise; } this.initializationPromise = this.doInitialize(); return this.initializationPromise; } private async doInitialize(): Promise<void> { this.logger.info('Initializing terminal pool', { maxSize: this.maxSize, recycleAfter: this.recycleAfter, }); // Pre-create some terminals const preCreateCount = Math.min(2, this.maxSize); const promises: Promise<void>[] = []; for (let i = 0; i < preCreateCount; i++) { promises.push(this.createPooledTerminal()); } await Promise.all(promises); this.logger.info('Terminal pool initialized', { created: preCreateCount, }); } async shutdown(): Promise<void> { this.logger.info('Shutting down terminal pool'); // Destroy all terminals const terminals = Array.from(this.terminals.values()); await Promise.all(terminals.map(({ terminal }) => this.adapter.destroyTerminal(terminal))); this.terminals.clear(); this.availableQueue = []; } async acquire(): Promise<Terminal> { // Try to get an available terminal while (this.availableQueue.length > 0) { const terminalId = this.availableQueue.shift()!; const pooled = this.terminals.get(terminalId); if (pooled && pooled.terminal.isAlive()) { pooled.inUse = true; pooled.lastUsed = new Date(); this.logger.debug('Terminal acquired from pool', { terminalId, useCount: pooled.useCount, }); return pooled.terminal; } // Terminal is dead, remove it if (pooled) { this.terminals.delete(terminalId); } } // No available terminals, create new one if under limit if (this.terminals.size < this.maxSize) { await this.createPooledTerminal(); return this.acquire(); // Recursive call to get the newly created terminal } // Pool is full, wait for a terminal to become available this.logger.info('Terminal pool full, waiting for available terminal'); const startTime = Date.now(); const timeout = 30000; // 30 seconds while (Date.now() - startTime < timeout) { await delay(100); // Check if any terminal became available const available = Array.from(this.terminals.values()).find( (pooled) => !pooled.inUse && pooled.terminal.isAlive(), ); if (available) { available.inUse = true; available.lastUsed = new Date(); return available.terminal; } } throw new TerminalError('No terminal available in pool (timeout)'); } async release(terminal: Terminal): Promise<void> { const pooled = this.terminals.get(terminal.id); if (!pooled) { this.logger.warn('Attempted to release unknown terminal', { terminalId: terminal.id, }); return; } pooled.useCount++; pooled.inUse = false; // Check if terminal should be recycled if (pooled.useCount >= this.recycleAfter || !terminal.isAlive()) { this.logger.info('Recycling terminal', { terminalId: terminal.id, useCount: pooled.useCount, }); // Destroy old terminal this.terminals.delete(terminal.id); await this.adapter.destroyTerminal(terminal); // Create replacement if under limit if (this.terminals.size < this.maxSize) { await this.createPooledTerminal(); } } else { // Return to available queue this.availableQueue.push(terminal.id); this.logger.debug('Terminal returned to pool', { terminalId: terminal.id, useCount: pooled.useCount, }); } } async getHealthStatus(): Promise<{ healthy: boolean; size: number; available: number; recycled: number; }> { const aliveTerminals = Array.from(this.terminals.values()).filter((pooled) => pooled.terminal.isAlive(), ); const available = aliveTerminals.filter((pooled) => !pooled.inUse).length; const recycled = Array.from(this.terminals.values()).filter( (pooled) => pooled.useCount >= this.recycleAfter, ).length; return { healthy: aliveTerminals.length > 0, size: this.terminals.size, available, recycled, }; } async performMaintenance(): Promise<void> { this.logger.debug('Performing terminal pool maintenance'); // Remove dead terminals const deadTerminals: string[] = []; for (const [id, pooled] of this.terminals.entries()) { if (!pooled.terminal.isAlive()) { deadTerminals.push(id); } } // Clean up dead terminals for (const id of deadTerminals) { this.logger.warn('Removing dead terminal from pool', { terminalId: id }); this.terminals.delete(id); const index = this.availableQueue.indexOf(id); if (index !== -1) { this.availableQueue.splice(index, 1); } } // Ensure minimum pool size const currentSize = this.terminals.size; const minSize = Math.min(2, this.maxSize); if (currentSize < minSize) { const toCreate = minSize - currentSize; this.logger.info('Replenishing terminal pool', { currentSize, minSize, creating: toCreate, }); const promises: Promise<void>[] = []; for (let i = 0; i < toCreate; i++) { promises.push(this.createPooledTerminal()); } await Promise.all(promises); } // Check for stale terminals that should be recycled const now = Date.now(); const staleTimeout = 300000; // 5 minutes for (const [id, pooled] of this.terminals.entries()) { if (!pooled.inUse && pooled.terminal.isAlive()) { const idleTime = now - pooled.lastUsed.getTime(); if (idleTime > staleTimeout) { this.logger.info('Recycling stale terminal', { terminalId: id, idleTime, }); // Mark for recycling pooled.useCount = this.recycleAfter; } } } } private async createPooledTerminal(): Promise<void> { try { const terminal = await this.adapter.createTerminal(); const pooled: PooledTerminal = { terminal, useCount: 0, lastUsed: new Date(), inUse: false, }; this.terminals.set(terminal.id, pooled); this.availableQueue.push(terminal.id); this.logger.debug('Created pooled terminal', { terminalId: terminal.id }); } catch (error) { this.logger.error('Failed to create pooled terminal', error); throw error; } } }