UNPKG

opendoor-mcp

Version:

Production-grade MCP server with multi-language code execution, VS Code integration, and browser automation

290 lines 11.3 kB
import { Logger } from '../utils/Logger.js'; import { v4 as uuidv4 } from 'uuid'; import Redis from 'ioredis'; import NodeCache from 'node-cache'; export class SessionManager { logger = Logger.getInstance(); redis = null; sessions = new Map(); cache; initialized = false; connectionPool = new Map(); // Connection pooling maxPoolSize = 10; cleanupInterval; constructor() { // Initialize local cache with optimized settings for GCP this.cache = new NodeCache({ stdTTL: 900, // 15 minutes (longer for better performance) checkperiod: 300, // Check for expired keys every 5 minutes maxKeys: 5000, // Reduced for memory efficiency useClones: false // Better performance, but be careful with mutations }); // Start cleanup interval for connection pool this.cleanupInterval = setInterval(() => { this.cleanupConnectionPool(); }, 300000); // Every 5 minutes } async initialize() { if (this.initialized) { return this; } try { // Initialize Redis with optimized settings for GCP this.redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: parseInt(process.env.REDIS_DB || '0'), enableOfflineQueue: false, maxRetriesPerRequest: 2, lazyConnect: true, family: 4, connectTimeout: 5000, commandTimeout: 3000, keepAlive: 30000, // GCP optimizations enableAutoPipelining: true, // Better performance for multiple commands enableReadyCheck: true, keyPrefix: 'mcp:' // Namespace for keys }); // Test connection await this.redis.ping(); this.logger.info('✅ Redis connected successfully'); // Setup Redis event handlers this.redis.on('error', (error) => { this.logger.error('Redis connection error:', error); }); this.redis.on('reconnecting', () => { this.logger.info('Reconnecting to Redis...'); }); } catch (error) { this.logger.warn('Redis connection failed, falling back to memory storage:', error); this.redis = null; } this.initialized = true; return this; } cleanupConnectionPool() { // Clean up old connections in the pool const now = Date.now(); for (const [key, connection] of this.connectionPool.entries()) { if (now - connection.lastUsed > 300000) { // 5 minutes this.connectionPool.delete(key); } } } // Graceful shutdown async shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } if (this.redis) { await this.redis.quit(); } this.cache.flushAll(); this.sessions.clear(); this.connectionPool.clear(); } async createSession(params) { const sessionId = uuidv4(); const session = { id: sessionId, type: params.type, language: params.language, status: 'creating', memory: params.memory || '5g', endpoints: {}, createdAt: new Date(), lastAccessedAt: new Date(), clientId: params.clientId }; // Store session await this.storeSession(session); this.logger.info(`Created session ${sessionId} for client ${params.clientId}`); return session; } async getSession(sessionId) { try { // Try cache first (fastest) const cachedSession = this.cache.get(sessionId); if (cachedSession) { // Update last accessed time cachedSession.lastAccessedAt = new Date(); this.cache.set(sessionId, cachedSession); return cachedSession; } // Try Redis second if (this.redis) { const sessionData = await this.redis.get(`session:${sessionId}`); if (sessionData) { const session = JSON.parse(sessionData); session.createdAt = new Date(session.createdAt); session.lastAccessedAt = new Date(session.lastAccessedAt); // Cache for future requests this.cache.set(sessionId, session); return session; } } // Fallback to memory const memorySession = this.sessions.get(sessionId); if (memorySession) { memorySession.lastAccessedAt = new Date(); this.cache.set(sessionId, memorySession); } return memorySession || null; } catch (error) { this.logger.error(`Error getting session ${sessionId}:`, error); return null; } } async updateSession(sessionId, updates) { const session = await this.getSession(sessionId); if (!session) { return null; } const updatedSession = { ...session, ...updates, lastAccessedAt: new Date() }; await this.storeSession(updatedSession); return updatedSession; } async updateSessionStatus(sessionId, status) { await this.updateSession(sessionId, { status }); this.logger.info(`Session ${sessionId} status updated to ${status}`); } async setSessionEndpoints(sessionId, endpoints) { await this.updateSession(sessionId, { endpoints }); this.logger.info(`Session ${sessionId} endpoints updated:`, endpoints); } async setContainerId(sessionId, containerId) { await this.updateSession(sessionId, { containerId }); this.logger.info(`Session ${sessionId} container ID set to ${containerId}`); } async destroySession(sessionId) { try { // Remove from all storage layers await Promise.all([ // Remove from cache Promise.resolve(this.cache.del(sessionId)), // Remove from Redis this.redis ? this.redis.del(`session:${sessionId}`) : Promise.resolve(), // Remove from memory Promise.resolve(this.sessions.delete(sessionId)) ]); this.logger.info(`Session ${sessionId} destroyed`); } catch (error) { this.logger.error(`Error destroying session ${sessionId}:`, error); throw error; } } async listSessions(clientId) { try { const sessions = []; const processedSessions = new Set(); // Get from cache first const cacheKeys = this.cache.keys(); for (const sessionId of cacheKeys) { const session = this.cache.get(sessionId); if (session && (!clientId || session.clientId === clientId)) { sessions.push(session); processedSessions.add(sessionId); } } // Get from Redis for sessions not in cache if (this.redis) { const keys = await this.redis.keys('session:*'); const pipeline = this.redis.pipeline(); for (const key of keys) { const sessionId = key.replace('session:', ''); if (!processedSessions.has(sessionId)) { pipeline.get(key); } } const results = await pipeline.exec(); if (results) { for (let i = 0; i < results.length; i++) { const [error, sessionData] = results[i]; if (!error && sessionData) { const session = JSON.parse(sessionData); if (!clientId || session.clientId === clientId) { session.createdAt = new Date(session.createdAt); session.lastAccessedAt = new Date(session.lastAccessedAt); sessions.push(session); // Cache for future requests this.cache.set(session.id, session); } } } } } else { // Use in-memory sessions for those not already processed for (const [sessionId, session] of this.sessions.entries()) { if (!processedSessions.has(sessionId) && (!clientId || session.clientId === clientId)) { sessions.push(session); } } } return sessions; } catch (error) { this.logger.error('Error listing sessions:', error); return []; } } async cleanupExpiredSessions() { const sessions = await this.listSessions(); const now = new Date(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours for (const session of sessions) { const age = now.getTime() - session.lastAccessedAt.getTime(); if (age > maxAge) { this.logger.info(`Cleaning up expired session ${session.id}`); await this.destroySession(session.id); } } } async cleanup() { try { await this.cleanupExpiredSessions(); // Close Redis connection if (this.redis) { await this.redis.quit(); this.redis = null; } // Clear all storage this.cache.flushAll(); this.sessions.clear(); this.logger.info('✅ Session manager cleanup completed'); } catch (error) { this.logger.error('Error during session manager cleanup:', error); } } async storeSession(session) { try { const sessionTTL = 24 * 60 * 60; // 24 hours // Store in all layers simultaneously for redundancy and performance await Promise.all([ // Store in cache (fastest access) Promise.resolve(this.cache.set(session.id, session, 600)), // 10 minute cache TTL // Store in Redis (persistence) this.redis ? this.redis.setex(`session:${session.id}`, sessionTTL, JSON.stringify(session)) : Promise.resolve(), // Store in memory (fallback) Promise.resolve(this.sessions.set(session.id, session)) ]); } catch (error) { this.logger.error(`Error storing session ${session.id}:`, error); throw error; } } } //# sourceMappingURL=SessionManager.js.map