"""命台租约(authoritative_runtime_seat):同一人物唯一权威运行位置。 - 活跃租约(未过期、非本人)存在时,其他持有者获取被拒(SeatConflict); - 租约过期可接管,epoch 递增(旧持有者写入按 epoch 失效——M2 运行时接入); - 事件(seat.acquired / released / contested)由 API 层在事务提交后发射, 避免事务内跨连接写冲突;M1 为 API 级租约,Kernel 前台串行仍为进程内锁,多进程一致在 M2 收口。 """ from __future__ import annotations from datetime import timedelta from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.time import as_utc, utc_now from app.models.jingsheng import RuntimeSeat class SeatConflict(Exception): """命台被其他持有者占用(HJ-1101 语义);调用方须留痕 seat.contested。""" def __init__(self, seat: RuntimeSeat) -> None: super().__init__(f"命台被 {seat.holder} 占用至 {seat.lease_expires_at}") self.seat = seat async def acquire_lease( session: AsyncSession, character_id: str, holder: str, ttl_seconds: int = 60 ) -> RuntimeSeat: """获取或续期命台租约;冲突抛 SeatConflict(不在此发事件,避免事务内跨连接写)。""" seat = await session.scalar(select(RuntimeSeat).where(RuntimeSeat.character_id == character_id)) if seat is None: seat = RuntimeSeat(character_id=character_id, epoch=0) session.add(seat) now = utc_now() if ( seat.holder is not None and seat.holder != holder and seat.lease_expires_at is not None and as_utc(seat.lease_expires_at) > now ): raise SeatConflict(seat) # 仅"从其他持有者手中接管"递增 epoch;无人持有时首次获取沿用当前纪元 takeover = seat.holder is not None and seat.holder != holder seat.holder = holder seat.lease_expires_at = now + timedelta(seconds=ttl_seconds) if takeover: seat.epoch += 1 await session.flush() return seat async def release_lease(session: AsyncSession, character_id: str, holder: str) -> bool: """释放租约;返回是否实际释放(holder 不匹配时无操作)。""" seat = await session.scalar(select(RuntimeSeat).where(RuntimeSeat.character_id == character_id)) if seat is None or seat.holder != holder: return False seat.holder = None seat.lease_expires_at = None await session.flush() return True async def emit_seat_event( event_type: str, character_id: str, holder: str | None, epoch: int | None, *, requester: str | None = None, ) -> None: """事务外发射命台事件(一炁,visibility=internal)。""" from app.domains import one_qi payload: dict = {"holder": holder, "epoch": epoch} if requester is not None: payload["requester"] = requester await one_qi.emit( event_type, character_id=character_id, payload=payload, visibility="internal", source="jingsheng", )