"""同账套任务串行锁;独立持有 MySQL 连接,跨分页 commit 仍保持有效。""" import hashlib from contextlib import contextmanager from sqlalchemy import text from sqlalchemy.orm import Session class TPlusBusyError(RuntimeError): """已有任务持有执行锁。""" def lock_name(session: Session) -> str: """生成不含地址、账号或密码的固定长度锁名。""" database = session.get_bind().url.database or "default" return "ys:tplus:read:" + hashlib.sha256(database.encode()).hexdigest()[:24] @contextmanager def sync_lock(session: Session): """只在取得锁后执行;释放失败时使连接失效,防止带锁回池。""" engine = session.get_bind() if engine.dialect.name != "mysql": raise RuntimeError("T+ 执行协调需要 MySQL 命名锁") connection = engine.connect() acquired = False try: acquired = ( connection.execute( text("SELECT GET_LOCK(:name, 0)"), {"name": lock_name(session)} ).scalar_one() == 1 ) if not acquired: raise TPlusBusyError("已有 T+ 任务正在执行,请稍后重试") yield finally: if acquired: try: released = connection.execute( text("SELECT RELEASE_LOCK(:name)"), {"name": lock_name(session)} ).scalar_one() if released != 1: connection.invalidate() except Exception: connection.invalidate() connection.close()