opendoor-mcp
Version:
Production-grade MCP server with multi-language code execution, VS Code integration, and browser automation
290 lines • 11.3 kB
JavaScript
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