Files
Mutual_Fund/service/client_agent/archive_schedule.py
T

133 lines
4.6 KiB
Python

"""Redis client_agent 会话自动归档调度数据结构。"""
from __future__ import annotations
class SessionArchiveSchedule:
"""管理空闲会话的归档截止时间,不负责实际归档。"""
DUE_KEY = "session:archive:due"
META_KEY = "session:{session_id}:meta"
def __init__(self, redis):
self.redis = redis
@classmethod
def meta_key(cls, session_id: str) -> str:
"""生成与短期记忆共用的会话元数据 Key。"""
return cls.META_KEY.format(session_id=session_id)
async def register(
self,
*,
session_id: str,
created_at: float,
last_activity_at: float,
archive_due_at: float,
absolute_expire_at: float,
) -> None:
"""登记新会话的归档时间和生命周期信息。"""
await self.redis.hset(
self.meta_key(session_id),
mapping={
"created_at": str(created_at),
"last_activity_at": str(last_activity_at),
"archive_due_at": str(archive_due_at),
"absolute_expire_at": str(absolute_expire_at),
"archive_status": "pending",
"archive_retry_count": "0",
},
)
await self.redis.expire(
self.meta_key(session_id),
max(1, int(absolute_expire_at - created_at)),
)
await self.redis.zadd(self.DUE_KEY, {session_id: archive_due_at})
async def touch(
self,
*,
session_id: str,
last_activity_at: float,
archive_due_at: float,
) -> None:
"""更新会话最近活动时间和下一次归档截止时间。"""
await self.redis.hset(
self.meta_key(session_id),
mapping={
"last_activity_at": str(last_activity_at),
"archive_due_at": str(archive_due_at),
"archive_status": "pending",
},
)
await self.redis.zadd(self.DUE_KEY, {session_id: archive_due_at})
async def list_due(self, *, now: float, limit: int = 100) -> list[str]:
"""返回归档截止时间已到的会话。"""
if limit <= 0:
return []
values = await self.redis.zrangebyscore(
self.DUE_KEY, 0, now, start=0, num=limit
)
return [value.decode() if isinstance(value, bytes) else str(value) for value in values]
async def get_meta(self, session_id: str) -> dict:
"""读取会话归档元数据。"""
return await self.redis.hgetall(self.meta_key(session_id))
async def reschedule(self, session_id: str, *, due_at: float) -> None:
"""更新下一次扫描时间,保留当前调度记录。"""
await self.redis.zadd(self.DUE_KEY, {session_id: due_at})
async def rebuild_due_index(self, *, limit: int = 10000) -> int:
"""从会话元数据重建丢失的到期索引。"""
rebuilt = 0
async for raw_key in self.redis.scan_iter(match="session:*:meta", count=100):
key = raw_key.decode() if isinstance(raw_key, bytes) else raw_key
prefix = "session:"
suffix = ":meta"
if not key.startswith(prefix) or not key.endswith(suffix):
continue
session_id = key[len(prefix) : -len(suffix)]
meta = await self.redis.hgetall(key)
due_at = meta.get("archive_due_at")
if due_at is None or meta.get("archive_status") == "success":
continue
await self.redis.zadd(self.DUE_KEY, {session_id: float(due_at)})
rebuilt += 1
if rebuilt >= limit:
break
return rebuilt
async def mark_processing(self, session_id: str) -> None:
"""标记会话进入归档处理状态。"""
await self.redis.hset(
self.meta_key(session_id), mapping={"archive_status": "processing"}
)
async def mark_retry(
self,
session_id: str,
*,
retry_count: int,
error: str,
next_retry_at: float,
) -> None:
"""记录归档失败信息和下一次重试时间。"""
await self.redis.hset(
self.meta_key(session_id),
mapping={
"archive_status": "retry",
"archive_retry_count": str(retry_count),
"last_archive_error": error[:500],
"next_retry_at": str(next_retry_at),
},
)
async def remove(self, session_id: str) -> None:
"""移除归档调度记录,不删除会话消息或短期记忆元数据。"""
await self.redis.zrem(self.DUE_KEY, session_id)
__all__ = ["SessionArchiveSchedule"]