UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

428 lines (427 loc) 17 kB
"use strict"; /** * @fileoverview Memory Manager - Infrastructure Layer * @version 1.0.0 * @since 2025-07-30 * @lastUpdated 2025-07-30 * @module MemoryManager * @description Centralized memory management system for Clean Architecture. * Implements SECURITY_002 memory resource controls to prevent DoS and resource exhaustion. * @contributors Claude Code Agent * @dependencies None (core infrastructure service) * @requirements SECURITY_002 (Memory Resource Controls), REQ-PERF-001 * @testCoverage Unit tests for memory limits, monitoring, and garbage collection */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MemoryOperationTimeoutError = exports.MemoryExhaustionError = exports.MemoryManagerError = exports.MemoryManager = void 0; /** * Memory Manager Service * * @description memory management with monitoring, limits, and automatic cleanup. * Prevents memory exhaustion attacks and ensures stable resource usage. * * @example * ```typescript * const memoryManager = MemoryManager.getInstance(); * await memoryManager.initialize({ * maxHeapSizeBytes: 512 * 1024 * 1024, // 512MB * monitoringIntervalMs: 30000 * }); * * // Check memory before operation * memoryManager.enforceMemoryLimits('quiz-operation'); * ``` * * @since 2025-07-30 * @author Claude Code Agent * @requirements SECURITY_002 (Memory Resource Controls) */ class MemoryManager { constructor() { this.isInitialized = false; this.config = { maxHeapSizeBytes: 256 * 1024 * 1024, // 256MB default maxOldSpaceSizeBytes: 200 * 1024 * 1024, // 200MB default warningThresholdPercent: 80, criticalThresholdPercent: 90, monitoringIntervalMs: 30000, // 30 seconds enableGarbageCollection: true, maxConcurrentOperations: 100, operationTimeoutMs: 60000, // 1 minute }; this.activeOperations = new Map(); this.memoryStats = { currentHeapUsed: 0, currentHeapTotal: 0, maxHeapUsed: 0, warningEvents: 0, criticalEvents: 0, gcEvents: 0, operationsBlocked: 0, operationsCompleted: 0, startTime: Date.now(), }; this.listeners = []; } /** * Get singleton instance */ static getInstance() { if (!MemoryManager.instance) { MemoryManager.instance = new MemoryManager(); } return MemoryManager.instance; } /** * Initialize memory manager */ async initialize(config) { if (this.isInitialized) { console.warn('⚠️ Memory Manager already initialized'); return; } this.config = { ...this.config, ...config }; // Configure Node.js memory limits this.configureNodeMemoryLimits(); // Start monitoring this.startMemoryMonitoring(); // Setup garbage collection monitoring this.setupGarbageCollectionMonitoring(); this.isInitialized = true; console.log('🧠 Memory Manager initialized with limits:', { maxHeapMB: Math.round(this.config.maxHeapSizeBytes / 1024 / 1024), warningThreshold: `${this.config.warningThresholdPercent}%`, monitoringInterval: `${this.config.monitoringIntervalMs}ms`, }); } /** * Enforce memory limits before operation */ enforceMemoryLimits(operationType = 'default') { if (!this.isInitialized) { throw new MemoryManagerError('Memory Manager not initialized'); } const currentMemory = process.memoryUsage(); const currentHeapPercent = (currentMemory.heapUsed / this.config.maxHeapSizeBytes) * 100; // Check if we're at critical memory usage if (currentHeapPercent >= this.config.criticalThresholdPercent) { this.memoryStats.operationsBlocked++; this.emitEvent('memory-critical', { currentHeapPercent, operationType, action: 'operation-blocked', }); throw new MemoryExhaustionError(`Memory usage critical (${currentHeapPercent.toFixed(1)}%). Operation blocked: ${operationType}`); } // Check operation-specific limits const operationLimit = MemoryManager.OPERATION_MEMORY_LIMITS[operationType] || MemoryManager.OPERATION_MEMORY_LIMITS['default']; if (currentMemory.heapUsed + operationLimit > this.config.maxHeapSizeBytes) { this.memoryStats.operationsBlocked++; throw new MemoryExhaustionError(`Insufficient memory for operation ${operationType}. Required: ${Math.round(operationLimit / 1024 / 1024)}MB`); } // Check concurrent operations limit if (this.activeOperations.size >= this.config.maxConcurrentOperations) { this.memoryStats.operationsBlocked++; throw new MemoryExhaustionError(`Maximum concurrent operations exceeded: ${this.config.maxConcurrentOperations}`); } // Warning threshold if (currentHeapPercent >= this.config.warningThresholdPercent) { this.memoryStats.warningEvents++; this.emitEvent('memory-warning', { currentHeapPercent, operationType, action: 'warning-issued', }); console.warn(`⚠️ Memory usage high (${currentHeapPercent.toFixed(1)}%) for operation: ${operationType}`); } } /** * Track memory operation */ trackOperation(operationId, operationType, estimatedMemoryMB = 5) { const operation = { id: operationId, type: operationType, startTime: Date.now(), estimatedMemoryBytes: estimatedMemoryMB * 1024 * 1024, startMemoryUsage: process.memoryUsage().heapUsed, }; this.activeOperations.set(operationId, operation); // Set timeout for operation cleanup setTimeout(() => { if (this.activeOperations.has(operationId)) { console.warn(`⚠️ Memory operation timeout: ${operationId} (${operationType})`); this.completeOperation(operationId); } }, this.config.operationTimeoutMs); return operation; } /** * Complete memory operation */ completeOperation(operationId) { const operation = this.activeOperations.get(operationId); if (operation) { const endMemoryUsage = process.memoryUsage().heapUsed; const actualMemoryUsed = Math.max(0, endMemoryUsage - operation.startMemoryUsage); const duration = Date.now() - operation.startTime; // Update statistics this.memoryStats.operationsCompleted++; // Log if memory usage was significantly different from estimate if (actualMemoryUsed > operation.estimatedMemoryBytes * 1.5) { console.warn(`⚠️ Operation used more memory than estimated:`, { operationId, estimated: Math.round(operation.estimatedMemoryBytes / 1024 / 1024) + 'MB', actual: Math.round(actualMemoryUsed / 1024 / 1024) + 'MB', duration: duration + 'ms', }); } this.activeOperations.delete(operationId); } } /** * Force garbage collection if available */ forceGarbageCollection() { if (!this.config.enableGarbageCollection) { return false; } try { if (global.gc) { const beforeMemory = process.memoryUsage().heapUsed; global.gc(); const afterMemory = process.memoryUsage().heapUsed; const freedMemory = beforeMemory - afterMemory; this.memoryStats.gcEvents++; console.log(`🗑️ Garbage collection freed ${Math.round(freedMemory / 1024 / 1024)}MB`); this.emitEvent('garbage-collection', { freedMemoryMB: Math.round(freedMemory / 1024 / 1024), beforeMemoryMB: Math.round(beforeMemory / 1024 / 1024), afterMemoryMB: Math.round(afterMemory / 1024 / 1024), }); return true; } } catch (error) { console.error('❌ Garbage collection failed:', error); } return false; } /** * Get current memory statistics */ getStatistics() { const currentMemory = process.memoryUsage(); return { ...this.memoryStats, currentHeapUsed: currentMemory.heapUsed, currentHeapTotal: currentMemory.heapTotal, maxHeapUsed: Math.max(this.memoryStats.maxHeapUsed, currentMemory.heapUsed), activeOperations: this.activeOperations.size, heapUsagePercent: (currentMemory.heapUsed / this.config.maxHeapSizeBytes) * 100, uptime: Date.now() - this.memoryStats.startTime, }; } /** * Get memory health status */ getHealthStatus() { const currentMemory = process.memoryUsage(); const heapUsagePercent = (currentMemory.heapUsed / this.config.maxHeapSizeBytes) * 100; let status; if (heapUsagePercent >= this.config.criticalThresholdPercent) { status = 'critical'; } else if (heapUsagePercent >= this.config.warningThresholdPercent) { status = 'warning'; } else { status = 'healthy'; } return { status, heapUsagePercent, heapUsedMB: Math.round(currentMemory.heapUsed / 1024 / 1024), heapTotalMB: Math.round(currentMemory.heapTotal / 1024 / 1024), maxHeapMB: Math.round(this.config.maxHeapSizeBytes / 1024 / 1024), activeOperations: this.activeOperations.size, maxConcurrentOperations: this.config.maxConcurrentOperations, recommendations: this.getMemoryRecommendations(heapUsagePercent), }; } /** * Add memory event listener */ addEventListener(listener) { this.listeners.push(listener); } /** * Remove memory event listener */ removeEventListener(listener) { const index = this.listeners.indexOf(listener); if (index > -1) { this.listeners.splice(index, 1); } } /** * Shutdown memory manager */ async shutdown() { console.log('🔄 Shutting down Memory Manager...'); // Stop monitoring if (this.monitoringTimer) { clearInterval(this.monitoringTimer); } // Complete any active operations for (const operationId of this.activeOperations.keys()) { this.completeOperation(operationId); } // Final garbage collection this.forceGarbageCollection(); this.isInitialized = false; console.log('✅ Memory Manager shut down'); } /** * Configure Node.js memory limits */ configureNodeMemoryLimits() { const maxHeapMB = Math.round(this.config.maxHeapSizeBytes / 1024 / 1024); const maxOldSpaceMB = Math.round(this.config.maxOldSpaceSizeBytes / 1024 / 1024); // Note: These would need to be set via command line arguments in production // --max-heap-size=256 --max-old-space-size=200 console.log(`💾 Memory limits configured: ${maxHeapMB}MB heap, ${maxOldSpaceMB}MB old space`); } /** * Start memory monitoring */ startMemoryMonitoring() { this.monitoringTimer = setInterval(() => { const currentMemory = process.memoryUsage(); const heapUsagePercent = (currentMemory.heapUsed / this.config.maxHeapSizeBytes) * 100; // Update statistics this.memoryStats.currentHeapUsed = currentMemory.heapUsed; this.memoryStats.currentHeapTotal = currentMemory.heapTotal; this.memoryStats.maxHeapUsed = Math.max(this.memoryStats.maxHeapUsed, currentMemory.heapUsed); // Check thresholds if (heapUsagePercent >= this.config.criticalThresholdPercent) { this.memoryStats.criticalEvents++; this.emitEvent('memory-critical', { currentHeapPercent: heapUsagePercent, action: 'monitoring-alert', }); console.error(`🚨 CRITICAL: Memory usage ${heapUsagePercent.toFixed(1)}%`); // Force garbage collection at critical levels this.forceGarbageCollection(); } else if (heapUsagePercent >= this.config.warningThresholdPercent) { this.memoryStats.warningEvents++; this.emitEvent('memory-warning', { currentHeapPercent: heapUsagePercent, action: 'monitoring-alert', }); console.warn(`⚠️ WARNING: Memory usage ${heapUsagePercent.toFixed(1)}%`); } // Log periodic status (only in development) if (process.env.NODE_ENV === 'development') { console.log(`🧠 Memory: ${Math.round(currentMemory.heapUsed / 1024 / 1024)}MB (${heapUsagePercent.toFixed(1)}%) | Operations: ${this.activeOperations.size}`); } }, this.config.monitoringIntervalMs); } /** * Setup garbage collection monitoring */ setupGarbageCollectionMonitoring() { if (typeof process.on === 'function') { // Monitor garbage collection events (if available) try { const v8 = require('v8'); if (v8.writeHeapSnapshot) { // GC monitoring could be added here with appropriate Node.js flags console.log('🔍 GC monitoring available'); } } catch (error) { // V8 module not available, continue without GC monitoring } } } /** * Emit memory event to listeners */ emitEvent(eventType, data) { const event = { type: eventType, timestamp: new Date(), data, }; this.listeners.forEach(listener => { try { listener(event); } catch (error) { console.error('❌ Memory event listener error:', error); } }); } /** * Get memory optimization recommendations */ getMemoryRecommendations(heapUsagePercent) { const recommendations = []; if (heapUsagePercent > 90) { recommendations.push('CRITICAL: Restart application to free memory'); recommendations.push('Consider increasing max heap size'); recommendations.push('Reduce concurrent operations'); } else if (heapUsagePercent > 80) { recommendations.push('Run garbage collection'); recommendations.push('Reduce memory-intensive operations'); recommendations.push('Monitor for memory leaks'); } else if (heapUsagePercent > 60) { recommendations.push('Monitor memory growth trends'); recommendations.push('Consider optimizing data structures'); } if (this.activeOperations.size > this.config.maxConcurrentOperations * 0.8) { recommendations.push('High concurrent operation load - consider queuing'); } return recommendations; } } exports.MemoryManager = MemoryManager; // Memory limits for different operation types MemoryManager.OPERATION_MEMORY_LIMITS = { 'quiz-creation': 10 * 1024 * 1024, // 10MB 'quiz-operation': 5 * 1024 * 1024, // 5MB 'file-upload': 20 * 1024 * 1024, // 20MB 'database-query': 15 * 1024 * 1024, // 15MB 'image-processing': 50 * 1024 * 1024, // 50MB 'export-operation': 25 * 1024 * 1024, // 25MB default: 5 * 1024 * 1024, // 5MB default }; /** * Memory Manager Error Types */ class MemoryManagerError extends Error { constructor(message) { super(message); this.name = 'MemoryManagerError'; } } exports.MemoryManagerError = MemoryManagerError; class MemoryExhaustionError extends MemoryManagerError { constructor(message) { super(message); this.name = 'MemoryExhaustionError'; } } exports.MemoryExhaustionError = MemoryExhaustionError; class MemoryOperationTimeoutError extends MemoryManagerError { constructor(operationId, timeoutMs) { super(`Memory operation timed out: ${operationId} after ${timeoutMs}ms`); this.name = 'MemoryOperationTimeoutError'; } } exports.MemoryOperationTimeoutError = MemoryOperationTimeoutError;