/** * Adaptive Socket Manager * Automatically manages socket pools and connection settings based on load patterns * Zero-configuration approach that learns and adapts to workload characteristics */ import { Agent as HttpsAgent } from 'node:https' import { NodeHttpHandler } from '@smithy/node-http-handler' import { createModuleLogger } from './logger.js' interface LoadMetrics { requestsPerSecond: number pendingRequests: number socketUtilization: number errorRate: number latencyP50: number latencyP95: number memoryUsage: number } interface AdaptiveConfig { maxSockets: number maxFreeSockets: number keepAliveTimeout: number connectionTimeout: number socketTimeout: number batchSize: number } /** * Adaptive Socket Manager that automatically scales based on load patterns */ export class AdaptiveSocketManager { private logger = createModuleLogger('AdaptiveSocketManager') // Current configuration private config: AdaptiveConfig = { maxSockets: 100, // Start conservative maxFreeSockets: 20, keepAliveTimeout: 60000, connectionTimeout: 10000, socketTimeout: 60000, batchSize: 10 } // Performance tracking private metrics: LoadMetrics = { requestsPerSecond: 0, pendingRequests: 0, socketUtilization: 0, errorRate: 0, latencyP50: 0, latencyP95: 0, memoryUsage: 0 } // Historical data for learning private history: LoadMetrics[] = [] private maxHistorySize = 100 // Adaptation state private lastAdaptationTime = 0 private adaptationInterval = 5000 // Check every 5 seconds private consecutiveHighLoad = 0 private consecutiveLowLoad = 0 // Request tracking private requestStartTimes = new Map() private requestLatencies: number[] = [] private errorCount = 0 private successCount = 0 private lastMetricReset = Date.now() // Socket pool instances private currentAgent: HttpsAgent | null = null private currentHandler: NodeHttpHandler | null = null /** * Get or create an optimized HTTP handler */ public getHttpHandler(): NodeHttpHandler { // Adapt configuration if needed this.adaptIfNeeded() // Create new handler if configuration changed if (!this.currentHandler || this.shouldRecreateHandler()) { this.currentAgent = new HttpsAgent({ keepAlive: true, maxSockets: this.config.maxSockets, maxFreeSockets: this.config.maxFreeSockets, timeout: this.config.keepAliveTimeout, scheduling: 'fifo' // Fair scheduling for high-volume scenarios }) this.currentHandler = new NodeHttpHandler({ httpsAgent: this.currentAgent, connectionTimeout: this.config.connectionTimeout, socketTimeout: this.config.socketTimeout }) this.logger.debug('Created new HTTP handler with config:', this.config) } return this.currentHandler } /** * Get current batch size recommendation */ public getBatchSize(): number { this.adaptIfNeeded() return this.config.batchSize } /** * Track request start */ public trackRequestStart(requestId: string): void { this.requestStartTimes.set(requestId, Date.now()) this.metrics.pendingRequests++ } /** * Track request completion */ public trackRequestComplete(requestId: string, success: boolean): void { const startTime = this.requestStartTimes.get(requestId) if (startTime) { const latency = Date.now() - startTime this.requestLatencies.push(latency) this.requestStartTimes.delete(requestId) // Keep latency array bounded if (this.requestLatencies.length > 1000) { this.requestLatencies = this.requestLatencies.slice(-500) } } if (success) { this.successCount++ } else { this.errorCount++ } this.metrics.pendingRequests = Math.max(0, this.metrics.pendingRequests - 1) } /** * Check if we should adapt configuration */ private adaptIfNeeded(): void { const now = Date.now() if (now - this.lastAdaptationTime < this.adaptationInterval) { return } this.lastAdaptationTime = now this.updateMetrics() this.analyzeAndAdapt() } /** * Update current metrics */ private updateMetrics(): void { const now = Date.now() const timeSinceReset = (now - this.lastMetricReset) / 1000 // Calculate requests per second const totalRequests = this.successCount + this.errorCount this.metrics.requestsPerSecond = timeSinceReset > 0 ? totalRequests / timeSinceReset : 0 // Calculate error rate this.metrics.errorRate = totalRequests > 0 ? this.errorCount / totalRequests : 0 // Calculate latency percentiles if (this.requestLatencies.length > 0) { const sorted = [...this.requestLatencies].sort((a, b) => a - b) const p50Index = Math.floor(sorted.length * 0.5) const p95Index = Math.floor(sorted.length * 0.95) this.metrics.latencyP50 = sorted[p50Index] || 0 this.metrics.latencyP95 = sorted[p95Index] || 0 } // Calculate socket utilization this.metrics.socketUtilization = this.metrics.pendingRequests / this.config.maxSockets // Memory usage if (typeof process !== 'undefined' && process.memoryUsage) { const memUsage = process.memoryUsage() this.metrics.memoryUsage = memUsage.heapUsed / memUsage.heapTotal } // Add to history this.history.push({ ...this.metrics }) if (this.history.length > this.maxHistorySize) { this.history.shift() } // Reset counters periodically if (timeSinceReset > 60) { this.lastMetricReset = now this.successCount = 0 this.errorCount = 0 } } /** * Analyze metrics and adapt configuration */ private analyzeAndAdapt(): void { const wasConfig = { ...this.config } // Detect high load conditions const isHighLoad = this.detectHighLoad() const isLowLoad = this.detectLowLoad() const hasErrors = this.metrics.errorRate > 0.01 // More than 1% errors if (isHighLoad) { this.consecutiveHighLoad++ this.consecutiveLowLoad = 0 if (this.consecutiveHighLoad >= 2) { // Wait for 2 consecutive high load readings this.scaleUp() } } else if (isLowLoad) { this.consecutiveLowLoad++ this.consecutiveHighLoad = 0 if (this.consecutiveLowLoad >= 6) { // Wait longer before scaling down this.scaleDown() } } else { // Reset counters if load is normal this.consecutiveHighLoad = Math.max(0, this.consecutiveHighLoad - 1) this.consecutiveLowLoad = Math.max(0, this.consecutiveLowLoad - 1) } // Handle error conditions if (hasErrors) { this.handleErrors() } // Log significant changes if (JSON.stringify(wasConfig) !== JSON.stringify(this.config)) { this.logger.info('Adapted configuration', { from: wasConfig, to: this.config, metrics: this.metrics }) } } /** * Detect high load conditions */ private detectHighLoad(): boolean { return ( this.metrics.socketUtilization > 0.7 || // Sockets heavily used this.metrics.pendingRequests > this.config.maxSockets * 0.8 || // Many pending requests this.metrics.latencyP95 > 5000 || // High latency this.metrics.requestsPerSecond > 100 // High request rate ) } /** * Detect low load conditions */ private detectLowLoad(): boolean { return ( this.metrics.socketUtilization < 0.2 && // Sockets barely used this.metrics.pendingRequests < 5 && // Few pending requests this.metrics.latencyP95 < 1000 && // Low latency this.metrics.requestsPerSecond < 10 && // Low request rate this.metrics.memoryUsage < 0.5 // Low memory usage ) } /** * Scale up resources for high load */ private scaleUp(): void { // Increase socket limits progressively const scaleFactor = this.metrics.errorRate > 0.05 ? 1.5 : 2.0 // Scale more aggressively if no errors this.config.maxSockets = Math.min( 2000, // Hard limit to prevent resource exhaustion Math.ceil(this.config.maxSockets * scaleFactor) ) this.config.maxFreeSockets = Math.min( 200, Math.ceil(this.config.maxSockets * 0.1) // Keep 10% as free sockets ) // Increase batch size for better throughput this.config.batchSize = Math.min( 100, Math.ceil(this.config.batchSize * 1.5) ) // Adjust timeouts for high load this.config.keepAliveTimeout = 120000 // Keep connections alive longer this.config.connectionTimeout = 15000 // Allow more time for connections this.config.socketTimeout = 90000 // Allow more time for responses this.logger.debug('Scaled up for high load', { sockets: this.config.maxSockets, batchSize: this.config.batchSize }) } /** * Scale down resources for low load */ private scaleDown(): void { // Only scale down if memory pressure is low if (this.metrics.memoryUsage > 0.7) { return } // Decrease socket limits conservatively this.config.maxSockets = Math.max( 50, // Minimum sockets Math.floor(this.config.maxSockets * 0.7) ) this.config.maxFreeSockets = Math.max( 10, Math.floor(this.config.maxSockets * 0.2) // Keep 20% as free sockets ) // Decrease batch size this.config.batchSize = Math.max( 5, Math.floor(this.config.batchSize * 0.7) ) // Adjust timeouts for low load this.config.keepAliveTimeout = 60000 this.config.connectionTimeout = 10000 this.config.socketTimeout = 60000 this.logger.debug('Scaled down for low load', { sockets: this.config.maxSockets, batchSize: this.config.batchSize }) } /** * Handle error conditions by adjusting configuration */ private handleErrors(): void { const errorRate = this.metrics.errorRate if (errorRate > 0.1) { // More than 10% errors // Severe errors - back off aggressively this.config.maxSockets = Math.max(50, Math.floor(this.config.maxSockets * 0.5)) this.config.batchSize = Math.max(1, Math.floor(this.config.batchSize * 0.3)) this.config.connectionTimeout = Math.min(30000, this.config.connectionTimeout * 2) this.config.socketTimeout = Math.min(120000, this.config.socketTimeout * 2) this.logger.warn('High error rate detected, backing off', { errorRate, newConfig: this.config }) } else if (errorRate > 0.05) { // More than 5% errors // Moderate errors - reduce load slightly this.config.batchSize = Math.max(1, Math.floor(this.config.batchSize * 0.7)) this.config.connectionTimeout = Math.min(20000, this.config.connectionTimeout * 1.2) } } /** * Check if we should recreate the handler */ private shouldRecreateHandler(): boolean { if (!this.currentAgent) return true // Recreate if socket configuration changed significantly const currentMaxSockets = (this.currentAgent as any).maxSockets const socketsDiff = Math.abs(currentMaxSockets - this.config.maxSockets) return socketsDiff > currentMaxSockets * 0.5 // 50% change threshold } /** * Get current configuration (for monitoring) */ public getConfig(): Readonly { return { ...this.config } } /** * Get current metrics (for monitoring) */ public getMetrics(): Readonly { return { ...this.metrics } } /** * Predict optimal configuration based on historical data */ public predictOptimalConfig(): AdaptiveConfig { if (this.history.length < 10) { return this.config // Not enough data to predict } // Analyze recent history const recentHistory = this.history.slice(-20) const avgRPS = recentHistory.reduce((sum, m) => sum + m.requestsPerSecond, 0) / recentHistory.length const maxRPS = Math.max(...recentHistory.map(m => m.requestsPerSecond)) const avgLatency = recentHistory.reduce((sum, m) => sum + m.latencyP95, 0) / recentHistory.length // Predict optimal socket count based on request patterns const optimalSockets = Math.min(2000, Math.max(50, Math.ceil(maxRPS * 2))) // Predict optimal batch size based on latency const optimalBatchSize = avgLatency < 1000 ? 50 : avgLatency < 3000 ? 20 : 10 return { maxSockets: optimalSockets, maxFreeSockets: Math.ceil(optimalSockets * 0.15), keepAliveTimeout: avgRPS > 50 ? 120000 : 60000, connectionTimeout: avgLatency > 3000 ? 20000 : 10000, socketTimeout: avgLatency > 3000 ? 90000 : 60000, batchSize: optimalBatchSize } } /** * Reset to default configuration */ public reset(): void { this.config = { maxSockets: 100, maxFreeSockets: 20, keepAliveTimeout: 60000, connectionTimeout: 10000, socketTimeout: 60000, batchSize: 10 } this.consecutiveHighLoad = 0 this.consecutiveLowLoad = 0 this.history = [] this.requestLatencies = [] this.errorCount = 0 this.successCount = 0 // Force recreation of handler this.currentAgent = null this.currentHandler = null this.logger.info('Reset to default configuration') } } // Global singleton instance let globalSocketManager: AdaptiveSocketManager | null = null /** * Get the global socket manager instance */ export function getGlobalSocketManager(): AdaptiveSocketManager { if (!globalSocketManager) { globalSocketManager = new AdaptiveSocketManager() } return globalSocketManager }