AutoRpa/rpa_oracle/pool.py

218 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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