""" Oracle Database Connection Pool - Multi-Server Support for ROA2WEB Supports both single-server (backward compatible) and multi-server configurations. Pool-uri sunt create lazy (la prima conexiune pe fiecare server) pentru optimizare. """ import asyncio import oracledb import os from contextlib import asynccontextmanager from typing import Optional, Dict, Any import logging logger = logging.getLogger(__name__) class OracleMultiPool: """ Multi-tenant Oracle connection pool manager. Supports: - Multiple Oracle servers with separate pools: {server_id: pool} - Lazy pool creation (created on first connection) - Backward compatibility (default server when no server_id specified) - Graceful shutdown of all pools """ _instance: Optional['OracleMultiPool'] = None _pools: Dict[str, oracledb.ConnectionPool] _pool_configs: Dict[str, Dict[str, Any]] _pool_lock: asyncio.Lock _legacy_pool: Optional[oracledb.ConnectionPool] # For backward compatibility _initialized: bool def __new__(cls): if cls._instance is None: cls._instance = super(OracleMultiPool, cls).__new__(cls) cls._instance._pools = {} cls._instance._pool_configs = {} cls._instance._pool_lock = asyncio.Lock() cls._instance._legacy_pool = None cls._instance._initialized = False return cls._instance async def initialize(self, **config): """ Initialize pool manager. For backward compatibility, this can: 1. Create a legacy single pool (if called with individual params) 2. Just mark as initialized (if using lazy multi-pool loading) """ if self._initialized: logger.debug("Pool manager already initialized") return # Check if we have DSN or individual parameters (legacy mode) dsn = config.get('dsn', os.getenv('ORACLE_DSN')) user = config.get('user', os.getenv('ORACLE_USER')) if dsn or user: # Legacy single-pool mode - create pool immediately await self._create_legacy_pool(config) self._initialized = True logger.info("Oracle pool manager initialized") async def _create_legacy_pool(self, config: Dict[str, Any]) -> None: """Create legacy single pool for backward compatibility.""" dsn = config.get('dsn', os.getenv('ORACLE_DSN')) if dsn: # Use DSN connection self._legacy_pool = oracledb.create_pool( user=config.get('user', os.getenv('ORACLE_USER')), password=config.get('password', os.getenv('ORACLE_PASSWORD')), dsn=dsn, min=config.get('min_connections', 2), max=config.get('max_connections', 10), increment=config.get('increment', 1), getmode=oracledb.POOL_GETMODE_WAIT ) else: # Use individual parameters (host, port, service_name or sid) service_name = config.get('service_name', os.getenv('ORACLE_SERVICE_NAME')) sid = config.get('sid', os.getenv('ORACLE_SID')) pool_params = { 'user': config.get('user', os.getenv('ORACLE_USER')), 'password': config.get('password', os.getenv('ORACLE_PASSWORD')), 'host': config.get('host', os.getenv('ORACLE_HOST', 'localhost')), 'port': config.get('port', int(os.getenv('ORACLE_PORT', '1526'))), 'min': config.get('min_connections', 2), 'max': config.get('max_connections', 10), 'increment': config.get('increment', 1), 'getmode': oracledb.POOL_GETMODE_WAIT } if service_name: pool_params['service_name'] = service_name logger.info(f"Using SERVICE_NAME: {service_name}") elif sid: pool_params['sid'] = sid logger.info(f"Using SID: {sid}") else: pool_params['service_name'] = 'ROA' logger.info("Using default SERVICE_NAME: ROA") self._legacy_pool = oracledb.create_pool(**pool_params) logger.info(f"Legacy Oracle pool created with {self._legacy_pool.opened} connections") def register_server( self, server_id: str, host: str, port: int, user: str, password: str, sid: Optional[str] = None, service_name: Optional[str] = None, min_connections: int = 2, max_connections: int = 10, **kwargs ) -> None: """ Register a server configuration for lazy pool creation. Pool will be created on first get_connection(server_id) call. """ self._pool_configs[server_id] = { 'host': host, 'port': port, 'user': user, 'password': password, 'sid': sid, 'service_name': service_name, 'min_connections': min_connections, 'max_connections': max_connections, } logger.info(f"Registered server '{server_id}' ({host}:{port}) for lazy pool creation") async def _get_or_create_pool(self, server_id: str) -> oracledb.ConnectionPool: """ Get existing pool or create new one (lazy loading). Thread-safe: uses asyncio.Lock to prevent duplicate pool creation. """ # Fast path: pool already exists if server_id in self._pools: return self._pools[server_id] # Slow path: need to create pool async with self._pool_lock: # Double-check after acquiring lock if server_id in self._pools: return self._pools[server_id] # Check if server is registered if server_id not in self._pool_configs: raise ValueError(f"Server '{server_id}' not registered. Call register_server() first.") config = self._pool_configs[server_id] logger.info(f"Creating pool for server '{server_id}' (lazy initialization)...") pool_params = { 'user': config['user'], 'password': config['password'], 'host': config['host'], 'port': config['port'], 'min': config['min_connections'], 'max': config['max_connections'], 'increment': 1, 'getmode': oracledb.POOL_GETMODE_WAIT } if config.get('service_name'): pool_params['service_name'] = config['service_name'] elif config.get('sid'): pool_params['sid'] = config['sid'] else: pool_params['service_name'] = 'ROA' pool = oracledb.create_pool(**pool_params) self._pools[server_id] = pool logger.info(f"Pool created for server '{server_id}' with {pool.opened} connections") return pool @asynccontextmanager async def get_connection(self, server_id: Optional[str] = None): """ Context manager pentru obținerea unei conexiuni din pool. Args: server_id: ID-ul serverului. Dacă None, folosește legacy pool sau default. Usage: # Multi-server mode async with oracle_pool.get_connection('romfast') as conn: ... # Backward compatible (legacy single pool) async with oracle_pool.get_connection() as conn: ... """ connection = None pool = None try: if server_id is None: # Backward compatibility: use legacy pool if self._legacy_pool is None: # If no legacy pool, try to use 'default' server if 'default' in self._pool_configs: pool = await self._get_or_create_pool('default') else: raise RuntimeError( "No pool available. Either initialize() with config " "or register_server() with server_id='default'." ) else: pool = self._legacy_pool else: pool = await self._get_or_create_pool(server_id) connection = pool.acquire() logger.debug(f"Connection acquired from pool (server_id={server_id})") yield connection finally: if connection is not None: connection.close() logger.debug(f"Connection returned to pool (server_id={server_id})") async def execute_query(self, query: str, parameters=None, server_id: Optional[str] = None): """ Execute a SQL query and return all results. Args: query: SQL query string parameters: Query parameters (dict or tuple) server_id: Server ID for multi-pool mode (optional) """ async with self.get_connection(server_id) as connection: logger.debug(f"Executing query on server '{server_id}': {query[:100]}...") with connection.cursor() as cursor: if parameters: cursor.execute(query, parameters) else: cursor.execute(query) # Check if this is a SELECT statement if query.strip().upper().startswith('SELECT') or query.strip().upper().startswith('WITH'): return cursor.fetchall() else: # For DML statements, return affected row count connection.commit() return cursor.rowcount async def close_pool(self, server_id: Optional[str] = None): """ Close a specific pool or all pools. Args: server_id: Close specific pool. If None, close all pools. """ if server_id is not None: # Close specific pool if server_id in self._pools: self._pools[server_id].close() del self._pools[server_id] logger.info(f"Closed pool for server '{server_id}'") else: # Close all pools (graceful shutdown) if self._legacy_pool is not None: self._legacy_pool.close() self._legacy_pool = None logger.info("Closed legacy pool") for srv_id, pool in list(self._pools.items()): pool.close() logger.info(f"Closed pool for server '{srv_id}'") self._pools.clear() self._initialized = False logger.info("All Oracle pools closed") def get_pool_stats(self, server_id: Optional[str] = None) -> Dict[str, Any]: """ Get statistics for pool(s). Args: server_id: Get stats for specific server. If None, get all stats. Returns: Dict with pool statistics (opened, busy, min, max connections) """ stats = {} if server_id is not None: pool = self._pools.get(server_id) if pool: stats[server_id] = { 'opened': pool.opened, 'busy': pool.busy, 'min': pool.min, 'max': pool.max, } else: # All pools including legacy if self._legacy_pool: stats['legacy'] = { 'opened': self._legacy_pool.opened, 'busy': self._legacy_pool.busy, 'min': self._legacy_pool.min, 'max': self._legacy_pool.max, } for srv_id, pool in self._pools.items(): stats[srv_id] = { 'opened': pool.opened, 'busy': pool.busy, 'min': pool.min, 'max': pool.max, } return stats def is_server_registered(self, server_id: str) -> bool: """Check if a server is registered (config exists).""" return server_id in self._pool_configs def is_pool_active(self, server_id: str) -> bool: """Check if a pool is active (created) for a server.""" return server_id in self._pools def get_registered_servers(self) -> list: """Get list of registered server IDs.""" return list(self._pool_configs.keys()) def get_active_pools(self) -> list: """Get list of server IDs with active pools.""" return list(self._pools.keys()) # Backward compatibility: keep old class name as alias OraclePool = OracleMultiPool # Instance globală pentru folosire în toate aplicațiile oracle_pool = OracleMultiPool()