133 lines
4.6 KiB
Python
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"]
|