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
JavaScript
"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;