UNPKG

codecrucible-synth

Version:

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

664 lines (583 loc) 17.9 kB
/** * Rate Limiting and Timeout Handling System * * Provides comprehensive rate limiting, timeout management, and * external API call optimization with retry mechanisms. */ import { logger } from '../logger.js'; import { ErrorFactory, ErrorCategory, ErrorSeverity, ServiceResponse, ErrorResponse, ErrorHandler, } from '../error-handling/structured-error-system.js'; // Rate limiting configuration export interface RateLimitConfig { maxRequests: number; windowMs: number; blockDuration?: number; skipSuccessfulRequests?: boolean; skipFailedRequests?: boolean; keyGenerator?: (identifier: string) => string; } // Timeout configuration export interface TimeoutConfig { connectionTimeout: number; responseTimeout: number; totalTimeout: number; retryTimeout: number; } // Retry configuration export interface RetryConfig { maxRetries: number; baseDelay: number; maxDelay: number; backoffMultiplier: number; retryCondition?: (error: any) => boolean; } // API call configuration export interface APICallConfig { identifier: string; rateLimit?: RateLimitConfig; timeout?: TimeoutConfig; retry?: RetryConfig; priority?: 'low' | 'normal' | 'high' | 'critical'; cacheEnabled?: boolean; cacheTTL?: number; } // Rate limit bucket interface RateLimitBucket { tokens: number; lastRefill: number; blocked: boolean; blockExpiry?: number; } // Request queue item interface QueuedRequest { id: string; config: APICallConfig; requestFn: () => Promise<any>; resolve: (value: any) => void; reject: (error: any) => void; priority: number; timestamp: number; timeoutHandle?: NodeJS.Timeout; } // Cache entry interface CacheEntry { data: any; timestamp: number; ttl: number; } /** * Rate Limiting Manager */ export class RateLimitManager { private buckets = new Map<string, RateLimitBucket>(); private defaultConfig: RateLimitConfig = { maxRequests: 60, windowMs: 60000, // 1 minute blockDuration: 300000, // 5 minutes skipSuccessfulRequests: false, skipFailedRequests: false, }; /** * Check if request is allowed under rate limit */ async checkRateLimit( identifier: string, config?: Partial<RateLimitConfig> ): Promise<ServiceResponse<boolean>> { const rateLimitConfig = { ...this.defaultConfig, ...config }; const key = rateLimitConfig.keyGenerator ? rateLimitConfig.keyGenerator(identifier) : identifier; try { const bucket = this.getBucket(key, rateLimitConfig); // Check if currently blocked if (bucket.blocked && bucket.blockExpiry && Date.now() < bucket.blockExpiry) { const remainingTime = bucket.blockExpiry - Date.now(); return ErrorHandler.createErrorResponse( ErrorFactory.createError( `Rate limit exceeded for ${identifier}`, ErrorCategory.SYSTEM, ErrorSeverity.MEDIUM, { context: { identifier, remainingBlockTime: remainingTime, rateLimitConfig, }, userMessage: 'Too many requests, please wait before trying again', suggestedActions: [`Wait ${Math.ceil(remainingTime / 1000)} seconds before retrying`], retryable: true, } ) ); } // Refill tokens based on time elapsed this.refillBucket(bucket, rateLimitConfig); // Check if tokens available if (bucket.tokens < 1) { // Block if no tokens available bucket.blocked = true; bucket.blockExpiry = Date.now() + (rateLimitConfig.blockDuration || 300000); logger.warn(`Rate limit bucket exhausted for ${identifier}`, { bucket: { ...bucket }, config: rateLimitConfig, }); return ErrorHandler.createErrorResponse( ErrorFactory.createError( `Rate limit bucket exhausted for ${identifier}`, ErrorCategory.SYSTEM, ErrorSeverity.MEDIUM, { context: { identifier, rateLimitConfig }, userMessage: 'Request rate limit exceeded', suggestedActions: ['Reduce request frequency', 'Wait before retrying'], retryable: true, } ) ); } // Consume token bucket.tokens -= 1; return ErrorHandler.createSuccessResponse(true); } catch (error) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( `Rate limit check failed: ${(error as Error).message}`, ErrorCategory.SYSTEM, ErrorSeverity.HIGH, { context: { identifier }, originalError: error as Error, userMessage: 'Rate limiting system error', suggestedActions: ['Try again', 'Contact support if issue persists'], } ) ); } } /** * Record request completion (for rate limit adjustment) */ recordRequest(identifier: string, success: boolean, config?: Partial<RateLimitConfig>): void { const rateLimitConfig = { ...this.defaultConfig, ...config }; // Skip recording based on configuration if ( (success && rateLimitConfig.skipSuccessfulRequests) || (!success && rateLimitConfig.skipFailedRequests) ) { return; } // This could be used for adaptive rate limiting logger.debug(`Request recorded for ${identifier}`, { success, timestamp: Date.now() }); } /** * Reset rate limit for identifier */ resetRateLimit(identifier: string): void { const key = identifier; this.buckets.delete(key); logger.info(`Rate limit reset for ${identifier}`); } /** * Get current rate limit status */ getRateLimitStatus(identifier: string): { tokens: number; blocked: boolean; blockExpiry?: number; nextRefill: number; } { const bucket = this.buckets.get(identifier); if (!bucket) { return { tokens: this.defaultConfig.maxRequests, blocked: false, nextRefill: Date.now() + this.defaultConfig.windowMs, }; } return { tokens: bucket.tokens, blocked: bucket.blocked, blockExpiry: bucket.blockExpiry, nextRefill: bucket.lastRefill + this.defaultConfig.windowMs, }; } private getBucket(key: string, config: RateLimitConfig): RateLimitBucket { let bucket = this.buckets.get(key); if (!bucket) { bucket = { tokens: config.maxRequests, lastRefill: Date.now(), blocked: false, }; this.buckets.set(key, bucket); } return bucket; } private refillBucket(bucket: RateLimitBucket, config: RateLimitConfig): void { const now = Date.now(); const timePassed = now - bucket.lastRefill; if (timePassed >= config.windowMs) { // Full refill bucket.tokens = config.maxRequests; bucket.lastRefill = now; bucket.blocked = false; bucket.blockExpiry = undefined; } else { // Partial refill based on time passed const tokensToAdd = (timePassed / config.windowMs) * config.maxRequests; bucket.tokens = Math.min(config.maxRequests, bucket.tokens + tokensToAdd); if (bucket.tokens >= 1) { bucket.blocked = false; bucket.blockExpiry = undefined; } } } } /** * Timeout and Retry Manager */ export class TimeoutRetryManager { private defaultTimeoutConfig: TimeoutConfig = { connectionTimeout: 10000, responseTimeout: 30000, totalTimeout: 60000, retryTimeout: 5000, }; private defaultRetryConfig: RetryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, retryCondition: (error: any) => { // Retry on network errors, timeouts, and 5xx HTTP errors return ( error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED' || (error.response && error.response.status >= 500) || error.timeout === true ); }, }; /** * Execute request with timeout and retry logic */ async executeWithTimeoutAndRetry<T>( requestFn: () => Promise<T>, config: { timeout?: Partial<TimeoutConfig>; retry?: Partial<RetryConfig>; identifier?: string; } = {} ): Promise<ServiceResponse<T>> { const timeoutConfig = { ...this.defaultTimeoutConfig, ...config.timeout }; const retryConfig = { ...this.defaultRetryConfig, ...config.retry }; const identifier = config.identifier || 'anonymous'; let lastError: any = null; let attempt = 0; while (attempt <= retryConfig.maxRetries) { try { const result = await this.executeWithTimeout(requestFn, timeoutConfig, identifier); logger.debug(`Request succeeded for ${identifier}`, { attempt: attempt + 1, totalAttempts: retryConfig.maxRetries + 1, }); return ErrorHandler.createSuccessResponse(result); } catch (error) { lastError = error; attempt++; logger.warn(`Request attempt ${attempt} failed for ${identifier}`, { error: (error as Error).message, attempt, maxRetries: retryConfig.maxRetries, }); // Check if we should retry if ( attempt <= retryConfig.maxRetries && retryConfig.retryCondition && retryConfig.retryCondition(error) ) { const delay = this.calculateDelay(attempt, retryConfig); logger.info(`Retrying request for ${identifier} in ${delay}ms`, { attempt: attempt + 1, delay, }); await this.sleep(delay); } else { break; } } } // All retries exhausted return ErrorHandler.createErrorResponse( ErrorFactory.createError( `Request failed after ${attempt} attempts: ${lastError?.message || 'Unknown error'}`, ErrorCategory.NETWORK, ErrorSeverity.HIGH, { context: { identifier, attempts: attempt, lastError: lastError?.message, timeoutConfig, retryConfig, }, originalError: lastError, userMessage: 'Network request failed after multiple attempts', suggestedActions: [ 'Check network connection', 'Verify service availability', 'Try again later', ], retryable: true, } ) ); } private async executeWithTimeout<T>( requestFn: () => Promise<T>, config: TimeoutConfig, identifier: string ): Promise<T> { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { reject( ErrorFactory.createError( `Request timeout after ${config.totalTimeout}ms`, ErrorCategory.NETWORK, ErrorSeverity.MEDIUM, { context: { identifier, timeout: config.totalTimeout }, userMessage: 'Request timed out', suggestedActions: ['Try again with longer timeout', 'Check network connection'], retryable: true, metadata: { timeout: true }, } ) ); }, config.totalTimeout); requestFn() .then(result => { clearTimeout(timeoutHandle); resolve(result); }) .catch(error => { clearTimeout(timeoutHandle); reject(error); }); }); } private calculateDelay(attempt: number, config: RetryConfig): number { const delay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1); const jitter = Math.random() * 0.1 * delay; // Add 10% jitter return Math.min(delay + jitter, config.maxDelay); } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } } /** * API Call Queue Manager with Priority Handling */ export class APICallQueue { private queue: QueuedRequest[] = []; private cache = new Map<string, CacheEntry>(); private processing = false; private rateLimitManager = new RateLimitManager(); private timeoutRetryManager = new TimeoutRetryManager(); private requestCounter = 0; /** * Queue an API call with configuration */ async queueAPICall<T>(requestFn: () => Promise<T>, config: APICallConfig): Promise<T> { // Check cache first if (config.cacheEnabled) { const cached = this.getCachedResult(config.identifier); if (cached) { logger.debug(`Cache hit for ${config.identifier}`); return cached; } } // Check rate limit const rateLimitCheck = await this.rateLimitManager.checkRateLimit( config.identifier, config.rateLimit ); if (!rateLimitCheck.success) { throw (rateLimitCheck as ErrorResponse).error; } return new Promise((resolve, reject) => { const request: QueuedRequest = { id: `req_${++this.requestCounter}_${Date.now()}`, config, requestFn, resolve, reject, priority: this.getPriorityValue(config.priority || 'normal'), timestamp: Date.now(), }; this.queue.push(request); this.sortQueue(); logger.debug(`Queued API call: ${config.identifier}`, { requestId: request.id, queueLength: this.queue.length, priority: config.priority, }); this.processQueue(); }); } /** * Get queue status */ getQueueStatus(): { queueLength: number; processing: boolean; cacheSize: number; queuedRequests: Array<{ id: string; identifier: string; priority: string; age: number; }>; } { return { queueLength: this.queue.length, processing: this.processing, cacheSize: this.cache.size, queuedRequests: this.queue.map(req => ({ id: req.id, identifier: req.config.identifier, priority: req.config.priority || 'normal', age: Date.now() - req.timestamp, })), }; } /** * Clear queue and cache */ clear(): void { // Cancel pending requests this.queue.forEach(req => { if (req.timeoutHandle) { clearTimeout(req.timeoutHandle); } req.reject(new Error('Queue cleared')); }); this.queue = []; this.cache.clear(); this.processing = false; logger.info('API call queue cleared'); } private async processQueue(): Promise<void> { if (this.processing || this.queue.length === 0) { return; } this.processing = true; try { while (this.queue.length > 0) { const request = this.queue.shift()!; try { await this.executeRequest(request); } catch (error) { logger.error(`Failed to execute request ${request.id}`, error); } } } finally { this.processing = false; } } private async executeRequest(request: QueuedRequest): Promise<void> { const { config, requestFn, resolve, reject } = request; try { logger.debug(`Executing API call: ${config.identifier}`, { requestId: request.id, }); const result = await this.timeoutRetryManager.executeWithTimeoutAndRetry(requestFn, { timeout: config.timeout, retry: config.retry, identifier: config.identifier, }); if (!result.success) { // Record failed request for rate limiting this.rateLimitManager.recordRequest(config.identifier, false, config.rateLimit); reject((result as ErrorResponse).error); return; } // Record successful request this.rateLimitManager.recordRequest(config.identifier, true, config.rateLimit); // Cache result if enabled if (config.cacheEnabled && config.cacheTTL) { this.setCachedResult(config.identifier, result.data, config.cacheTTL); } resolve(result.data); } catch (error) { this.rateLimitManager.recordRequest(config.identifier, false, config.rateLimit); reject(error); } } private sortQueue(): void { this.queue.sort((a, b) => { // Sort by priority (higher first), then by timestamp (older first) if (a.priority !== b.priority) { return b.priority - a.priority; } return a.timestamp - b.timestamp; }); } private getPriorityValue(priority: string): number { const priorities = { low: 1, normal: 2, high: 3, critical: 4, }; return priorities[priority as keyof typeof priorities] || 2; } private getCachedResult(identifier: string): any | null { const entry = this.cache.get(identifier); if (!entry) return null; if (Date.now() - entry.timestamp > entry.ttl) { this.cache.delete(identifier); return null; } return entry.data; } private setCachedResult(identifier: string, data: any, ttl: number): void { this.cache.set(identifier, { data, timestamp: Date.now(), ttl, }); // Clean up old cache entries periodically if (this.cache.size % 100 === 0) { this.cleanupCache(); } } private cleanupCache(): void { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > entry.ttl) { this.cache.delete(key); } } } } // Global instances export const globalRateLimitManager = new RateLimitManager(); export const globalTimeoutRetryManager = new TimeoutRetryManager(); export const globalAPICallQueue = new APICallQueue(); // Convenience function for making rate-limited API calls export async function makeRateLimitedAPICall<T>( requestFn: () => Promise<T>, config: APICallConfig ): Promise<T> { return globalAPICallQueue.queueAPICall(requestFn, config); }