2025-08-26 12:32:21 -07:00
/ * *
* Adaptive Socket Manager
* Automatically manages socket pools and connection settings based on load patterns
* Zero - configuration approach that learns and adapts to workload characteristics
* /
feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00
import { Agent as HttpsAgent } from 'node:https'
2025-08-26 12:32:21 -07:00
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 < string , number > ( )
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 < AdaptiveConfig > {
return { . . . this . config }
}
/ * *
* Get current metrics ( for monitoring )
* /
public getMetrics ( ) : Readonly < LoadMetrics > {
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
}