import { Meteor } from 'meteor/meteor'; import { mongodbDriverManager } from './mongodbDriverManager'; /** * MongoDB Connection Manager * * This module handles MongoDB connections with automatic driver selection * based on detected MongoDB server version and wire protocol compatibility. * * Features: * - Automatic driver selection based on MongoDB version * - Connection retry with different drivers on wire protocol errors * - Fallback mechanism for unsupported versions * - Connection pooling and management */ class MongoDBConnectionManager { constructor() { this.connections = new Map(); this.connectionConfigs = new Map(); this.retryAttempts = 3; this.retryDelay = 1000; // 1 second } /** * Create a MongoDB connection with automatic driver selection * @param {string} connectionString - MongoDB connection string * @param {Object} options - Connection options * @returns {Promise} - MongoDB connection object */ async createConnection(connectionString, options = {}) { const connectionId = this.generateConnectionId(connectionString); // Check if we already have a working connection if (this.connections.has(connectionId)) { const existingConnection = this.connections.get(connectionId); if (existingConnection.status === 'connected') { return existingConnection; } } // Try to connect with automatic driver selection return await this.connectWithDriverSelection(connectionString, options, connectionId); } /** * Connect with automatic driver selection and retry logic * @param {string} connectionString - MongoDB connection string * @param {Object} options - Connection options * @param {string} connectionId - Connection identifier * @returns {Promise} - MongoDB connection object */ async connectWithDriverSelection(connectionString, options, connectionId) { let lastError = null; let currentDriver = null; // First, try with the default driver (if we have a detected version) if (mongodbDriverManager.detectedVersion) { currentDriver = mongodbDriverManager.getDriverForVersion(mongodbDriverManager.detectedVersion); } else { // Start with the most recent driver currentDriver = 'mongodb8legacy'; } // Try connection with different drivers for (let attempt = 0; attempt < this.retryAttempts; attempt++) { try { console.log(`Attempting MongoDB connection with driver: ${currentDriver} (attempt ${attempt + 1})`); const connection = await this.connectWithDriver(currentDriver, connectionString, options); // Record successful connection mongodbDriverManager.recordConnectionAttempt( currentDriver, mongodbDriverManager.detectedVersion || 'unknown', true ); // Store connection this.connections.set(connectionId, { connection, driver: currentDriver, version: mongodbDriverManager.detectedVersion || 'unknown', status: 'connected', connectionString, options, createdAt: new Date() }); return connection; } catch (error) { lastError = error; console.error(`Connection attempt ${attempt + 1} failed with driver ${currentDriver}:`, error.message); // Try to detect MongoDB version from error const detectedVersion = mongodbDriverManager.detectVersionFromError(error); if (detectedVersion && detectedVersion !== 'unknown') { mongodbDriverManager.detectedVersion = detectedVersion; currentDriver = mongodbDriverManager.getDriverForVersion(detectedVersion); console.log(`Detected MongoDB version ${detectedVersion}, switching to driver ${currentDriver}`); } else { // Try next fallback driver const nextDriver = mongodbDriverManager.getNextFallbackDriver(); if (nextDriver) { currentDriver = nextDriver; console.log(`Trying fallback driver: ${currentDriver}`); } else { console.error('No more fallback drivers available'); break; } } // Record failed attempt mongodbDriverManager.recordConnectionAttempt( currentDriver, detectedVersion || 'unknown', false, error ); // Wait before retry if (attempt < this.retryAttempts - 1) { await this.delay(this.retryDelay * (attempt + 1)); } } } // All attempts failed throw new Error(`Failed to connect to MongoDB after ${this.retryAttempts} attempts. Last error: ${lastError?.message}`); } /** * Connect using a specific driver * @param {string} driverName - Driver package name * @param {string} connectionString - MongoDB connection string * @param {Object} options - Connection options * @returns {Promise} - MongoDB connection object */ async connectWithDriver(driverName, connectionString, options) { try { // Dynamically import the driver const driver = await import(driverName); const MongoClient = driver.MongoClient; // Set default options const defaultOptions = { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 10, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, ...options }; // Create connection const client = new MongoClient(connectionString, defaultOptions); await client.connect(); // Test the connection await client.db('admin').admin().ping(); return client; } catch (error) { throw new Error(`Failed to connect with driver ${driverName}: ${error.message}`); } } /** * Get connection by ID * @param {string} connectionId - Connection identifier * @returns {Object|null} - Connection object or null */ getConnection(connectionId) { return this.connections.get(connectionId) || null; } /** * Close a connection * @param {string} connectionId - Connection identifier * @returns {Promise} - Whether connection was closed successfully */ async closeConnection(connectionId) { const connection = this.connections.get(connectionId); if (connection && connection.connection) { try { await connection.connection.close(); this.connections.delete(connectionId); console.log(`Closed MongoDB connection: ${connectionId}`); return true; } catch (error) { console.error(`Error closing MongoDB connection ${connectionId}:`, error.message); return false; } } return false; } /** * Close all connections * @returns {Promise} - Number of connections closed */ async closeAllConnections() { let closedCount = 0; const connectionIds = Array.from(this.connections.keys()); for (const connectionId of connectionIds) { if (await this.closeConnection(connectionId)) { closedCount++; } } console.log(`Closed ${closedCount} MongoDB connections`); return closedCount; } /** * Get connection statistics * @returns {Object} - Connection statistics */ getConnectionStats() { const connections = Array.from(this.connections.values()); const connected = connections.filter(conn => conn.status === 'connected').length; const disconnected = connections.length - connected; return { total: connections.length, connected, disconnected, connections: connections.map(conn => ({ id: this.getConnectionIdFromConnection(conn), driver: conn.driver, version: conn.version, status: conn.status, createdAt: conn.createdAt })) }; } /** * Generate a unique connection ID * @param {string} connectionString - MongoDB connection string * @returns {string} - Unique connection ID */ generateConnectionId(connectionString) { // Create a hash of the connection string for unique ID let hash = 0; for (let i = 0; i < connectionString.length; i++) { const char = connectionString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return `mongodb_${Math.abs(hash)}`; } /** * Get connection ID from connection object * @param {Object} connection - Connection object * @returns {string} - Connection ID */ getConnectionIdFromConnection(connection) { return this.generateConnectionId(connection.connectionString); } /** * Utility function to delay execution * @param {number} ms - Milliseconds to delay * @returns {Promise} - Promise that resolves after delay */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Reset all connections and driver manager state */ reset() { this.connections.clear(); this.connectionConfigs.clear(); mongodbDriverManager.reset(); } } // Create singleton instance const mongodbConnectionManager = new MongoDBConnectionManager(); // Export for use in other modules export { mongodbConnectionManager, MongoDBConnectionManager }; // MongoDB Connection Manager initialized (status available in Admin Panel)