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
text/typescript
/**
* 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();