问题 - 索引中该目录记为小写 `scripts/`,工作区实为 `Scripts/`。因 core.ignorecase=true, 本地 `git diff -- Scripts/bin/x.py` 会**静默返回空**(看似无改动), 且在大小写敏感环境(Linux CI / Docker 挂载)会检出 `scripts/`, 与全仓所有 `Scripts/...` 引用不匹配。 - 该目录此前 10 个脚本(check_python_version.py、perf_snapshot.py、 install_collector_launchd.py、mysql_toggle.sh 等)从未入库, 而 CI 第 31 行正调用 `Scripts/bin/check_python_version.py --strict` —— 全新检出必然失败。 改动 - 索引路径统一为 `Scripts/`(工作区文件未移动,仅索引改名)。 - 补齐上述 10 个未跟踪脚本及其测试。 影响 - 修正前索引 35 条(全为小写路径),修正后 45 条。
135 lines
4.1 KiB
Python
135 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""db_pool.py — 轻量 MySQL 连接池(基于 pymysql + queue,无需额外依赖)"""
|
|
|
|
import pymysql
|
|
from queue import Empty, Queue
|
|
from threading import Lock
|
|
|
|
from common import MYSQL_CFG
|
|
|
|
_pool = None
|
|
_lock = Lock()
|
|
|
|
# 连接超时配置(避免无密码/错误配置时挂起)
|
|
_CONNECT_TIMEOUT = 5 # 秒
|
|
_READ_TIMEOUT = 10 # 秒
|
|
|
|
# 从池中取连接的等待上限。
|
|
# 池容量 8;若取连接无 timeout,一旦有调用方未归还导致池被耗尽,
|
|
# 所有请求都会永久阻塞在 _pool.get() 上(进程只剩线程堆积、不报错),
|
|
# 设上限才能把"静默挂死"降级为"可观测的报错"。
|
|
_POOL_GET_TIMEOUT = 10 # 秒
|
|
|
|
|
|
def _connect_cfg():
|
|
"""返回带超时参数的连接配置"""
|
|
cfg = dict(MYSQL_CFG)
|
|
cfg.setdefault("connect_timeout", _CONNECT_TIMEOUT)
|
|
cfg.setdefault("read_timeout", _READ_TIMEOUT)
|
|
return cfg
|
|
|
|
|
|
def _init_pool(max_conn=8):
|
|
"""初始化连接池(双检锁:裸判 _pool is None 在并发首调时会建出多个池)"""
|
|
global _pool
|
|
if _pool is None:
|
|
with _lock:
|
|
if _pool is None:
|
|
q = Queue(maxsize=max_conn)
|
|
for _ in range(max_conn):
|
|
q.put(None) # None 表示需要时才创建
|
|
_pool = q
|
|
|
|
|
|
def get_conn(max_retries=3, retry_delay=2):
|
|
"""从池中获取连接(惰性创建 + 自动重连 + 重试)"""
|
|
global _pool
|
|
if _pool is None:
|
|
_init_pool()
|
|
try:
|
|
conn = _pool.get(timeout=_POOL_GET_TIMEOUT)
|
|
except Empty:
|
|
raise TimeoutError(
|
|
f"连接池已耗尽(容量 {_pool.maxsize}),等待 {_POOL_GET_TIMEOUT}s 未取到连接。"
|
|
"通常意味着有调用方持连接未归还(异常路径缺 put_conn)。"
|
|
)
|
|
cfg = _connect_cfg()
|
|
|
|
last_err = None
|
|
for attempt in range(max_retries):
|
|
try:
|
|
if conn is None:
|
|
conn = pymysql.connect(**cfg)
|
|
else:
|
|
conn.ping(reconnect=True)
|
|
return conn
|
|
except Exception as e:
|
|
last_err = e
|
|
# 坏连接必须关掉再置 None,别带着它继续重试
|
|
if conn is not None:
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
conn = None
|
|
if attempt < max_retries - 1:
|
|
import time
|
|
time.sleep(retry_delay)
|
|
|
|
# 全部重试失败:必须把已占用的池位归还,否则每失败一次就永久少一个连接,
|
|
# 池会被失败请求逐步耗尽。
|
|
put_conn(None)
|
|
raise last_err or Exception("数据库连接失败")
|
|
|
|
|
|
def put_conn(conn):
|
|
"""归还连接到池中
|
|
|
|
归还前必须 rollback:写路径(INSERT/UPDATE)若半途抛错,连接会带着
|
|
未提交事务与行锁回到池里,下一个请求拿到它就会读到脏数据甚至死锁。
|
|
"""
|
|
global _pool
|
|
if _pool is None:
|
|
_init_pool()
|
|
try:
|
|
if conn and conn.open:
|
|
try:
|
|
conn.rollback()
|
|
except Exception:
|
|
# 回滚都失败说明连接已不可用,丢弃之,用 None 占位
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
_pool.put_nowait(None)
|
|
return
|
|
_pool.put_nowait(conn)
|
|
else:
|
|
_pool.put_nowait(None)
|
|
except Exception:
|
|
# 池满了,直接关闭
|
|
try:
|
|
if conn:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class DBContext:
|
|
"""上下文管理器,自动获取和归还连接"""
|
|
def __enter__(self):
|
|
self.conn = get_conn()
|
|
return self.conn
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
# 异常出栈时显式回滚(put_conn 内也有兜底 rollback),
|
|
# 避免把未提交事务留在连接上带回池。
|
|
if exc_type is not None and self.conn is not None:
|
|
try:
|
|
self.conn.rollback()
|
|
except Exception:
|
|
pass
|
|
put_conn(self.conn)
|
|
self.conn = None
|
|
return False
|