UNPKG

multibridge

Version:

A multi-database connection framework with centralized configuration

349 lines (348 loc) 13.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getConnection = getConnection; exports.closeConnection = closeConnection; exports.closeAllConnections = closeAllConnections; exports.getConnectionStats = getConnectionStats; const dbConfig_1 = require("../config/dbConfig"); const tenantContext_1 = require("../context/tenantContext"); const postgres_1 = require("./postgres"); const mysql_1 = require("./mysql"); const mongodb_1 = require("./mongodb"); const cassandra_1 = require("./cassandra"); const loggers_1 = __importDefault(require("../utils/loggers")); const lruCache_1 = require("../utils/lruCache"); const rateLimiter_1 = require("../utils/rateLimiter"); const envConfig_1 = require("../config/envConfig"); const errors_1 = require("../utils/errors"); // LRU Cache for connections with configurable size and TTL const connectionCache = new lruCache_1.LRUCache(envConfig_1.envConfig.CONNECTION_CACHE_MAX_SIZE, envConfig_1.envConfig.CONNECTION_CACHE_TTL_MS || undefined); // Track in-flight connection creations to prevent race conditions const pendingConnections = new Map(); // Rate limiter to prevent excessive connection creation const rateLimiter = new rateLimiter_1.RateLimiter(envConfig_1.envConfig.RATE_LIMIT_MAX_REQUESTS, envConfig_1.envConfig.RATE_LIMIT_WINDOW_MS); function generateCacheKey(appid, orgid, appdbname) { return `${appid}-${orgid}-${appdbname}`; } // Type guard to identify SQL pools function isSQLPool(connection) { return "query" in connection; } async function isSQLConnectionValid(connection, cacheKey) { try { if (isSQLPool(connection)) { // Use explicit typing to help TypeScript with overload resolution if ("execute" in connection) { // MySQL pool await connection.query("SELECT 1"); } else { // PostgreSQL pool await connection.query("SELECT 1"); } return true; } return false; } catch (error) { loggers_1.default.warn(`SQL connection validation failed for ${cacheKey}: ${error.message}`, { cacheKey, error: error.stack, }); return false; } } async function isMongoConnectionValid(connection, cacheKey) { try { await connection.client.db().admin().ping(); return true; } catch (error) { loggers_1.default.warn(`MongoDB connection validation failed for ${cacheKey}: ${error.message}`, { cacheKey, error: error.stack, }); return false; } } async function isCassandraConnectionValid(connection, cacheKey) { try { await connection.execute("SELECT release_version FROM system.local"); return true; } catch (error) { loggers_1.default.warn(`Cassandra connection validation failed for ${cacheKey}: ${error.message}`, { cacheKey, error: error.stack, }); return false; } } async function isConnectionValid(cached, cacheKey) { switch (cached.dbType) { case "postgres": case "mysql": return isSQLConnectionValid(cached.connection, cacheKey); case "mongodb": return isMongoConnectionValid(cached.connection, cacheKey); case "cassandra": return isCassandraConnectionValid(cached.connection, cacheKey); default: return true; } } /** * Check if connection needs validation based on TTL */ function needsValidation(cached) { if (!cached.lastValidated) { return true; } const validationTTL = envConfig_1.envConfig.CONNECTION_VALIDATION_TTL_MS; if (validationTTL <= 0) { return false; // Validation disabled } return Date.now() - cached.lastValidated > validationTTL; } async function _closeAndLog(connection, dbType, cacheKey) { try { switch (dbType) { case "postgres": case "mysql": await connection.end(); loggers_1.default.info(`[MultiBridge] SQL connection closed for ${cacheKey}`); break; case "mongodb": await connection.client.close(); loggers_1.default.info(`[MultiBridge] MongoDB connection closed for ${cacheKey}`); break; case "cassandra": await connection.shutdown(); loggers_1.default.info(`[MultiBridge] Cassandra connection closed for ${cacheKey}`); break; default: loggers_1.default.warn(`[MultiBridge] Unknown DB type or close method missing for ${cacheKey}`); } } catch (error) { loggers_1.default.error(`Error closing connection for ${cacheKey}: ${error.message}`, { cacheKey, dbType, error: error.stack, }); } } /** * Retry connection creation with exponential backoff */ async function _createConnectionWithRetry(dbConfig, schema, attempt = 1) { const maxAttempts = envConfig_1.envConfig.CONNECTION_RETRY_ATTEMPTS; const baseDelay = envConfig_1.envConfig.CONNECTION_RETRY_DELAY_MS; try { return await _createConnection(dbConfig, schema); } catch (error) { // Check if error is retryable const isRetryable = isRetryableError(error); if (!isRetryable || attempt >= maxAttempts) { throw error; } // Calculate exponential backoff delay const delay = baseDelay * Math.pow(2, attempt - 1); loggers_1.default.warn(`Connection creation failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms: ${error.message}`, { attempt, maxAttempts, delay, dbType: dbConfig.db_type, error: error.stack, }); await new Promise((resolve) => setTimeout(resolve, delay)); return _createConnectionWithRetry(dbConfig, schema, attempt + 1); } } /** * Check if an error is retryable */ function isRetryableError(error) { if (!error) return false; const errorMessage = error.message?.toLowerCase() || ""; const retryablePatterns = [ "econnrefused", "etimedout", "timeout", "econnreset", "enotfound", "temporary", "retry", "connection", ]; return retryablePatterns.some((pattern) => errorMessage.includes(pattern)); } async function _createConnection(dbConfig, schema) { const { db_type: dbType } = dbConfig; switch (dbType) { case "postgres": case "mysql": if (!schema) { throw new errors_1.ConnectionError(`Database schema is required for connection type '${dbType}' but was not found.`, { dbType, }); } const sqlConfig = { host: dbConfig.host, port: dbConfig.port, username: dbConfig.username, password: dbConfig.password, database: dbConfig.database, schema: schema, }; return dbType === "postgres" ? (0, postgres_1.createPostgresConnection)(sqlConfig) : (0, mysql_1.createMySQLConnection)(sqlConfig); case "mongodb": return (0, mongodb_1.createMongoDBConnection)({ host: dbConfig.host, port: dbConfig.port, username: dbConfig.username, password: dbConfig.password, database: dbConfig.database, }); case "cassandra": if (!dbConfig.data_center) { throw new errors_1.ConnectionError(`'data_center' is required for Cassandra connections.`, { dbType, }); } return (0, cassandra_1.createCassandraConnection)({ host: dbConfig.host, port: dbConfig.port, username: dbConfig.username, password: dbConfig.password, database: dbConfig.database, dataCenter: dbConfig.data_center, }); default: throw new errors_1.ConnectionError(`Unsupported database type: ${dbType}`, { dbType, }); } } async function getConnection(tenant) { const currentTenant = tenant || (0, tenantContext_1.getTenant)(); if (!currentTenant) { throw new errors_1.TenantContextError("No tenant context available"); } const { appid, orgid, appdbname } = currentTenant; const cacheKey = generateCacheKey(appid, orgid, appdbname); // Check rate limit if (!rateLimiter.check(cacheKey)) { throw new errors_1.ConnectionError(`Rate limit exceeded for connection creation. Max ${envConfig_1.envConfig.RATE_LIMIT_MAX_REQUESTS} requests per ${envConfig_1.envConfig.RATE_LIMIT_WINDOW_MS}ms`, { cacheKey, appid, orgid, appdbname }); } // Check if connection is being created (race condition prevention) const pending = pendingConnections.get(cacheKey); if (pending) { loggers_1.default.debug(`[MultiBridge] Waiting for pending connection creation for ${cacheKey}`); return pending; } // Check cache const cached = connectionCache.get(cacheKey); if (cached) { // Lazy validation: only validate if needed if (!needsValidation(cached)) { loggers_1.default.debug(`[MultiBridge] Reusing cached connection for ${cacheKey} (validation skipped)`); return cached; } // Validate connection if (await isConnectionValid(cached, cacheKey)) { // Update validation timestamp cached.lastValidated = Date.now(); connectionCache.set(cacheKey, cached); loggers_1.default.debug(`[MultiBridge] Reusing valid connection for ${cacheKey}`); return cached; } else { loggers_1.default.warn(`[MultiBridge] Cached connection for ${cacheKey} is stale. Closing and recreating connection.`); await _closeAndLog(cached.connection, cached.dbType, cacheKey); connectionCache.delete(cacheKey); } } // Create new connection with race condition prevention const connectionPromise = (async () => { try { const dbConfig = await (0, dbConfig_1.fetchDBConfig)(appid, orgid); if (!dbConfig) { throw new errors_1.ConnectionError(`No configuration found for appid: ${appid}, orgid: ${orgid}`, { appid, orgid, }); } const schema = appdbname || dbConfig.schema; const connection = await _createConnectionWithRetry(dbConfig, schema); const ret = { connection, dbType: dbConfig.db_type, config: { schema }, lastValidated: Date.now(), }; connectionCache.set(cacheKey, ret); loggers_1.default.info(`[MultiBridge] Created new connection for ${cacheKey}`, { cacheKey, dbType: dbConfig.db_type, appid, orgid, }); return ret; } finally { // Remove from pending connections pendingConnections.delete(cacheKey); } })(); // Store pending connection to prevent duplicates pendingConnections.set(cacheKey, connectionPromise); return connectionPromise; } async function closeConnection(tenant) { const currentTenant = tenant || (0, tenantContext_1.getTenant)(); if (!currentTenant) return; const cacheKey = generateCacheKey(currentTenant.appid, currentTenant.orgid, currentTenant.appdbname); const cached = connectionCache.get(cacheKey); if (cached) { await _closeAndLog(cached.connection, cached.dbType, cacheKey); connectionCache.delete(cacheKey); rateLimiter.reset(cacheKey); } } async function closeAllConnections() { const entries = []; const keys = []; for (const key of connectionCache.keys()) { keys.push(key); } for (const cacheKey of keys) { const data = connectionCache.get(cacheKey); if (data) { entries.push([cacheKey, data]); } } for (const [cacheKey, { connection, dbType }] of entries) { await _closeAndLog(connection, dbType, cacheKey); } connectionCache.clear(); pendingConnections.clear(); rateLimiter.clear(); loggers_1.default.info("[MultiBridge] All connections closed."); } /** * Get connection pool statistics for monitoring */ function getConnectionStats() { return { cachedConnections: connectionCache.size(), pendingConnections: pendingConnections.size, cacheMaxSize: envConfig_1.envConfig.CONNECTION_CACHE_MAX_SIZE, }; }