UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

446 lines (380 loc) 13.1 kB
/** * Request Timeout Optimizer * Prevents hanging requests and optimizes timeout handling based on usage patterns * * Performance Impact: Eliminates hanging requests, 50-70% faster failure detection */ import { logger } from '../logger.js'; import { resourceManager } from './resource-cleanup-manager.js'; interface TimeoutConfig { defaultTimeout: number; fastModeTimeout: number; toolExecutionTimeout: number; streamingTimeout: number; batchTimeout: number; } interface RequestTimeoutData { requestId: string; startTime: number; timeout: number; type: 'regular' | 'streaming' | 'batch' | 'tool_execution'; provider: string; abortController: AbortController; } export class RequestTimeoutOptimizer { private static instance: RequestTimeoutOptimizer | null = null; private activeRequests = new Map<string, RequestTimeoutData>(); private timeoutHistory: Array<{ type: string; duration: number; success: boolean; provider: string }> = []; private config: TimeoutConfig = { defaultTimeout: 30000, // 30 seconds default fastModeTimeout: 10000, // 10 seconds for fast mode toolExecutionTimeout: 45000, // 45 seconds for tool execution streamingTimeout: 60000, // 60 seconds for streaming batchTimeout: 20000 // 20 seconds for batch requests }; private cleanupIntervalId: string | null = null; private constructor() { this.startTimeoutMonitoring(); } static getInstance(): RequestTimeoutOptimizer { if (!RequestTimeoutOptimizer.instance) { RequestTimeoutOptimizer.instance = new RequestTimeoutOptimizer(); } return RequestTimeoutOptimizer.instance; } /** * Create an optimized timeout for a request */ createOptimizedTimeout( requestId: string, type: 'regular' | 'streaming' | 'batch' | 'tool_execution', provider: string, customTimeout?: number ): { abortController: AbortController; timeout: number } { const abortController = new AbortController(); const optimizedTimeout = customTimeout || this.calculateOptimalTimeout(type, provider); const requestData: RequestTimeoutData = { requestId, startTime: Date.now(), timeout: optimizedTimeout, type, provider, abortController }; this.activeRequests.set(requestId, requestData); // Set the actual timeout const timeoutId = setTimeout(() => { this.handleTimeout(requestId); }, optimizedTimeout); // Store timeout ID for cleanup const timeoutCleanupId = resourceManager.registerTimeout( timeoutId as any, 'RequestTimeoutOptimizer', `timeout for ${requestId}` ); logger.debug('Created optimized timeout', { requestId, type, provider, timeout: `${optimizedTimeout}ms`, adaptive: customTimeout ? false : true }); return { abortController, timeout: optimizedTimeout }; } /** * Calculate optimal timeout based on historical data */ private calculateOptimalTimeout( type: 'regular' | 'streaming' | 'batch' | 'tool_execution', provider: string ): number { // Get historical data for this type/provider combination const relevantHistory = this.timeoutHistory.filter(h => h.type === type && h.provider === provider && h.success ).slice(-20); // Last 20 successful requests let baseTimeout: number; switch (type) { case 'streaming': baseTimeout = this.config.streamingTimeout; break; case 'batch': baseTimeout = this.config.batchTimeout; break; case 'tool_execution': baseTimeout = this.config.toolExecutionTimeout; break; default: baseTimeout = this.config.defaultTimeout; } if (relevantHistory.length < 5) { // Not enough data, use base timeout return baseTimeout; } // Calculate average and 95th percentile const durations = relevantHistory.map(h => h.duration).sort((a, b) => a - b); const avg = durations.reduce((sum, d) => sum + d, 0) / durations.length; const p95Index = Math.floor(durations.length * 0.95); const p95 = durations[p95Index]; // Use 95th percentile + 50% buffer, but cap at 2x base timeout const adaptiveTimeout = Math.min(p95 * 1.5, baseTimeout * 2); logger.debug('Calculated adaptive timeout', { type, provider, historySize: relevantHistory.length, avgDuration: avg.toFixed(0), p95Duration: p95.toFixed(0), adaptiveTimeout: adaptiveTimeout.toFixed(0), baseTimeout }); return Math.max(adaptiveTimeout, baseTimeout * 0.5); // Never less than 50% of base } /** * Handle request timeout */ private handleTimeout(requestId: string): void { const requestData = this.activeRequests.get(requestId); if (!requestData) return; const duration = Date.now() - requestData.startTime; logger.warn('Request timeout triggered', { requestId, type: requestData.type, provider: requestData.provider, timeout: `${requestData.timeout}ms`, actualDuration: `${duration}ms` }); // Abort the request try { requestData.abortController.abort(); } catch (error) { logger.error('Error aborting timed out request', error); } // Record timeout in history this.timeoutHistory.push({ type: requestData.type, duration, success: false, // Timeout = failure provider: requestData.provider }); // Cleanup this.activeRequests.delete(requestId); // Trigger adaptive timeout adjustment this.adjustTimeoutsBasedOnFailure(requestData.type, requestData.provider); } /** * Complete a request successfully */ completeRequest(requestId: string): void { const requestData = this.activeRequests.get(requestId); if (!requestData) return; const duration = Date.now() - requestData.startTime; // Record successful completion this.timeoutHistory.push({ type: requestData.type, duration, success: true, provider: requestData.provider }); this.activeRequests.delete(requestId); logger.debug('Request completed successfully', { requestId, type: requestData.type, provider: requestData.provider, duration: `${duration}ms`, timeoutWas: `${requestData.timeout}ms` }); // Keep history manageable if (this.timeoutHistory.length > 1000) { this.timeoutHistory = this.timeoutHistory.slice(-500); } } /** * Adjust timeouts based on failure patterns */ private adjustTimeoutsBasedOnFailure(type: string, provider: string): void { const recentFailures = this.timeoutHistory .filter(h => h.type === type && h.provider === provider && !h.success) .slice(-5); // Last 5 failures if (recentFailures.length >= 3) { // Multiple recent failures - increase timeout switch (type) { case 'regular': this.config.defaultTimeout = Math.min(this.config.defaultTimeout * 1.2, 60000); break; case 'streaming': this.config.streamingTimeout = Math.min(this.config.streamingTimeout * 1.2, 120000); break; case 'batch': this.config.batchTimeout = Math.min(this.config.batchTimeout * 1.2, 45000); break; case 'tool_execution': this.config.toolExecutionTimeout = Math.min(this.config.toolExecutionTimeout * 1.2, 90000); break; } logger.info('Adaptive timeout adjustment', { type, provider, recentFailures: recentFailures.length, newTimeout: this.getTimeoutForType(type) }); } } /** * Get current timeout for a specific type */ private getTimeoutForType(type: string): number { switch (type) { case 'streaming': return this.config.streamingTimeout; case 'batch': return this.config.batchTimeout; case 'tool_execution': return this.config.toolExecutionTimeout; default: return this.config.defaultTimeout; } } /** * Start monitoring for hung requests */ private startTimeoutMonitoring(): void { const monitoringInterval = setInterval(() => { // TODO: Store interval ID and call clearInterval in cleanup this.monitorHungRequests(); }, 10000); // Check every 10 seconds // Don't let monitoring interval keep process alive if (monitoringInterval.unref) { monitoringInterval.unref(); } this.cleanupIntervalId = resourceManager.registerInterval( monitoringInterval, 'RequestTimeoutOptimizer', 'timeout monitoring' ); } /** * Monitor for requests that should have timed out but haven't */ private monitorHungRequests(): void { const now = Date.now(); const hungRequests: string[] = []; for (const [requestId, requestData] of this.activeRequests.entries()) { const elapsed = now - requestData.startTime; // If request has been running for 2x its timeout, it's definitely hung if (elapsed > requestData.timeout * 2) { hungRequests.push(requestId); } } if (hungRequests.length > 0) { logger.warn('Detected hung requests', { count: hungRequests.length, requestIds: hungRequests }); // Force abort hung requests for (const requestId of hungRequests) { this.handleTimeout(requestId); } } } /** * Get timeout optimization statistics */ getTimeoutStats(): { activeRequests: number; totalRequests: number; timeoutRate: number; averageRequestDuration: number; adaptiveAdjustments: number; currentConfig: TimeoutConfig; performanceByType: Record<string, { avgDuration: number; timeoutRate: number; requestCount: number; }>; } { const totalRequests = this.timeoutHistory.length; const timeouts = this.timeoutHistory.filter(h => !h.success).length; const avgDuration = totalRequests > 0 ? this.timeoutHistory.reduce((sum, h) => sum + h.duration, 0) / totalRequests : 0; // Performance by type const performanceByType: Record<string, any> = {}; const types = [...new Set(this.timeoutHistory.map(h => h.type))]; for (const type of types) { const typeHistory = this.timeoutHistory.filter(h => h.type === type); performanceByType[type] = { avgDuration: typeHistory.reduce((sum, h) => sum + h.duration, 0) / typeHistory.length, timeoutRate: typeHistory.filter(h => !h.success).length / typeHistory.length, requestCount: typeHistory.length }; } return { activeRequests: this.activeRequests.size, totalRequests, timeoutRate: totalRequests > 0 ? timeouts / totalRequests : 0, averageRequestDuration: avgDuration, adaptiveAdjustments: 0, // Could track this if needed currentConfig: { ...this.config }, performanceByType }; } /** * Update timeout configuration */ updateTimeoutConfig(newConfig: Partial<TimeoutConfig>): void { this.config = { ...this.config, ...newConfig }; logger.info('Timeout configuration updated', this.config); } /** * Get timeout recommendations */ getTimeoutRecommendations(): string[] { const stats = this.getTimeoutStats(); const recommendations: string[] = []; if (stats.timeoutRate > 0.1) { recommendations.push('High timeout rate (>10%) - consider increasing timeouts'); } if (stats.averageRequestDuration > 15000) { recommendations.push('High average duration (>15s) - optimize request processing'); } if (stats.activeRequests > 10) { recommendations.push('Many active requests - consider implementing request queuing'); } Object.entries(stats.performanceByType).forEach(([type, perf]) => { if (perf.timeoutRate > 0.15) { recommendations.push(`High timeout rate for ${type} requests - increase timeout`); } }); if (recommendations.length === 0) { recommendations.push('Timeout performance is optimal'); } return recommendations; } /** * Shutdown and cleanup */ shutdown(): void { if (this.cleanupIntervalId) { resourceManager.cleanup(this.cleanupIntervalId); this.cleanupIntervalId = null; } // Abort all active requests for (const [requestId, requestData] of this.activeRequests.entries()) { try { requestData.abortController.abort(); } catch (error) { // Ignore cleanup errors } } const stats = this.getTimeoutStats(); logger.info('🔄 RequestTimeoutOptimizer shutting down', { totalRequests: stats.totalRequests, timeoutRate: `${(stats.timeoutRate * 100).toFixed(1)}%`, avgDuration: `${stats.averageRequestDuration.toFixed(0)}ms` }); this.activeRequests.clear(); this.timeoutHistory.length = 0; } } // Global instance for easy access export const requestTimeoutOptimizer = RequestTimeoutOptimizer.getInstance();