M1:镜生域后端闭环——灵胚/照心试演/启镜原子闸门/执镜关系/命台租约/魂籍签名导出/死亡冷静期(迁移 001,测试 7 用例)
This commit is contained in:
1 parent
c090893e63
commit
1cd0069d65
24 files changed
+1508
-9
No files matched your search
@@ -0,0 +1,94 @@
|
||||
"""命台租约(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.modules.runtime.events import Event
|
||||
from app.services.event_store import DbEventStore
|
||||
|
||||
from app.domains.jingsheng import service as jingsheng_service
|
||||
|
||||
store: DbEventStore | None = jingsheng_service._event_store # noqa: SLF001
|
||||
if store is None:
|
||||
return
|
||||
payload: dict = {"holder": holder, "epoch": epoch}
|
||||
if requester is not None:
|
||||
payload["requester"] = requester
|
||||
await store.append(
|
||||
Event(
|
||||
type=event_type,
|
||||
source="jingsheng",
|
||||
payload=payload,
|
||||
visibility="internal",
|
||||
run_id=None,
|
||||
character_id=character_id,
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user