# Oracle 连接池。thick client 进程内只初始化一次。 from __future__ import annotations import os from contextlib import contextmanager from collections.abc import Iterator from .errors import RepoUnavailable _client_ready = False # 班次池只有一条连接。空闲达到这个秒数才探活,坏了就丢掉再借。 PING_INTERVAL = 60 class OraclePool: """oracledb 连接池的薄封装,借还连接、对外异常固定文案。""" def __init__(self, raw_pool): """包住 oracledb.ConnectionPool。调用方不要直接碰 raw_pool。""" self._raw = raw_pool @contextmanager def acquire(self) -> Iterator: """借一条活连接。正常退出归还;未提交失败则丢掉,不放回池里。 探活失败或执行前已死时丢掉,再借一次。班次池只有一条,靠探活和丢弃换连接。 借还失败时抛 RepoUnavailable,消息不含 DSN。 """ conn = self._borrow_live() try: yield conn except Exception: self._drop(conn) raise else: self._release(conn) def run(self, fn) -> None: """执行一笔。未提交失败则丢掉连接,再借一次,只重试这一笔。 fn 收到连接。这一笔里已经调用过 commit 时不再重试,已提交的语句不重做。 班次池只有一条,靠探活和丢弃换连接。 """ conn = self._borrow_live() committed = {"done": False} try: _invoke(conn, fn, committed) except Exception: self._drop(conn) if committed["done"]: raise else: self._release(conn) return retry = self._borrow_live() try: _invoke(retry, fn, {"done": False}) except Exception: self._drop(retry) raise else: self._release(retry) def _borrow_live(self): """借一条连接。探活失败或执行前已死则丢掉,再借一次。""" try: conn = self._raw.acquire() except Exception as exc: raise RepoUnavailable("数据库不可用") from exc if _alive(conn): return conn self._drop(conn) try: conn = self._raw.acquire() except Exception as exc: raise RepoUnavailable("数据库不可用") from exc if _alive(conn): return conn self._drop(conn) raise RepoUnavailable("数据库不可用") def _drop(self, conn) -> None: """丢掉坏连接。池上没有 drop 时退回 close,避免占住唯一的槽。""" drop = getattr(self._raw, "drop", None) if drop is not None: try: drop(conn) return except Exception: pass self._release(conn) def _release(self, conn) -> None: """把连接归还池里。归还失败不再抛出。""" close = getattr(conn, "close", None) if close is None: return try: close() except Exception: pass # 返回连接池下限和上限。 # # oms 默认 1–4,班次(job)默认 1–1。RPA_ORACLE_POOL_MIN / MAX 可覆盖。 # max 小于 min 时抬到与 min 相同。 def pool_limits(role: str = "oms") -> tuple[int, int]: if role == "job": default_min, default_max = 1, 1 else: default_min, default_max = 1, 4 try: min_size = int(os.getenv("RPA_ORACLE_POOL_MIN", str(default_min))) max_size = int(os.getenv("RPA_ORACLE_POOL_MAX", str(default_max))) except ValueError as exc: raise RepoUnavailable("数据库不可用") from exc if max_size < min_size: max_size = min_size return min_size, max_size # 建班次连接池。默认大小 1。缺 HOST / 用户 / 密码抛「数据库不可用」。 def job_pool() -> OraclePool: return create_pool(role="job") def create_pool(*, role: str = "oms") -> OraclePool: """按环境变量建一个连接池。 缺 HOST / USER / PASSWORD、thick 初始化失败或建池失败时, 抛 RepoUnavailable「数据库不可用」,消息不含 DSN 和库路径。 ping_interval 为 60 秒。班次池只有一条连接,靠探活和丢弃换连接,大小仍是 min=1, max=1。 """ user, password, dsn = _credentials() _ensure_client() min_size, max_size = pool_limits(role) try: import oracledb raw = oracledb.create_pool( user=user, password=password, dsn=dsn, min=min_size, max=max_size, increment=1, ping_interval=PING_INTERVAL, ) except RepoUnavailable: raise except Exception as exc: raise RepoUnavailable("数据库不可用") from exc return OraclePool(raw) # 读用户、密码、DSN。缺任一项抛固定文案,不把 host 拼进消息。 def _credentials() -> tuple[str, str, str]: user = os.getenv("RPA_ORACLE_USER", "") password = os.getenv("RPA_ORACLE_PASSWORD", "") host = os.getenv("RPA_ORACLE_HOST", "") if not user or not password or not host: raise RepoUnavailable("数据库不可用") port = os.getenv("RPA_ORACLE_PORT", "1521") service = os.getenv("RPA_ORACLE_SERVICE", "rpa") return user, password, f"{host}:{port}/{service}" def _invoke(conn, fn, committed: dict) -> None: """调用 fn。包住 commit,记下这一笔是否已经提交。""" original = getattr(conn, "commit", None) def commit(): """先记下已提交,再执行原来的 commit。失败也不再重做这一笔。""" committed["done"] = True if original is not None: original() if callable(original): conn.commit = commit try: fn(conn) finally: if callable(original): conn.commit = original def _alive(conn) -> bool: """执行前探活。没有 ping 时看 dead 标记,两者都没有视为可用。""" if getattr(conn, "dead", False): return False ping = getattr(conn, "ping", None) if not callable(ping): return True try: ping() except Exception: return False return True # 进程内只调用一次 init_oracle_client。失败时文案不含 Instant Client 路径。 def _ensure_client() -> None: global _client_ready if _client_ready: return import oracledb if os.getenv("RPA_ORACLE_THICK", "1") == "1": lib_dir = os.getenv("RPA_ORACLE_CLIENT_LIB", "") kwargs = {"lib_dir": lib_dir} if lib_dir else {} try: oracledb.init_oracle_client(**kwargs) except Exception as exc: raise RepoUnavailable("数据库不可用") from exc _client_ready = True