feat(channel): 新增缓存绑定仓储实现
新增带缓存的渠道绑定仓储类,通过装饰代理仓储实现缓存读写、失效逻辑,缓存失效时间默认60秒,空缓存10秒
This commit is contained in:
commit
cf723fcc44
@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from yuxi.channel.domain.exception.cache_error import CacheError
|
||||
from yuxi.channel.domain.model.binding.channel_binding import ChannelBinding
|
||||
from yuxi.channel.domain.port import CachePort
|
||||
from yuxi.channel.domain.repository.binding_repository import BindingRepositoryPort
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CACHE_TTL = 60
|
||||
_NULL_CACHE_TTL = 10
|
||||
_NULL_MARKER = json.dumps({"__null__": True})
|
||||
_CACHE_VERSION = "v2"
|
||||
|
||||
|
||||
class CachingBindingRepository(BindingRepositoryPort):
|
||||
def __init__(self, delegate: BindingRepositoryPort, cache_port: CachePort):
|
||||
self._delegate = delegate
|
||||
self._cache = cache_port
|
||||
|
||||
def _cache_key(self, channel_type: str, account_id: str, group_id: str) -> str:
|
||||
return f"channel:binding:{_CACHE_VERSION}:{channel_type}:{account_id}:{group_id}"
|
||||
|
||||
async def find_active_binding(self, *, channel_type: str, account_id: str, group_id: str) -> ChannelBinding | None:
|
||||
try:
|
||||
cache_key = self._cache_key(channel_type, account_id, group_id)
|
||||
cached = await self._cache.get(cache_key)
|
||||
if cached:
|
||||
data = json.loads(cached)
|
||||
if data.get("__null__"):
|
||||
return None
|
||||
return ChannelBinding(
|
||||
id=data.get("id", 0),
|
||||
channel_type=data.get("channel_type", ""),
|
||||
account_id=data.get("account_id", ""),
|
||||
group_id=data.get("group_id", ""),
|
||||
agent_config_id=data.get("agent_config_id", 0),
|
||||
session_key_strategy=data.get("session_key_strategy", "auto"),
|
||||
is_enabled=data.get("is_enabled", True),
|
||||
created_by=data.get("created_by"),
|
||||
updated_by=data.get("updated_by"),
|
||||
created_at=data.get("created_at"),
|
||||
updated_at=data.get("updated_at"),
|
||||
)
|
||||
except CacheError:
|
||||
logger.warning("Redis cache read failed, falling back to DB")
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
logger.warning("Cache data corrupted for key %s:%s:%s, falling back to DB", channel_type, account_id, group_id)
|
||||
|
||||
result = await self._delegate.find_active_binding(
|
||||
channel_type=channel_type, account_id=account_id, group_id=group_id
|
||||
)
|
||||
|
||||
try:
|
||||
cache_key = self._cache_key(channel_type, account_id, group_id)
|
||||
if result is None:
|
||||
await self._cache.set(cache_key, _NULL_MARKER, ex=_NULL_CACHE_TTL)
|
||||
else:
|
||||
await self._cache.set(
|
||||
cache_key,
|
||||
json.dumps(result.__dict__, ensure_ascii=False, default=str),
|
||||
ex=_CACHE_TTL,
|
||||
)
|
||||
except CacheError:
|
||||
logger.warning("Redis cache write failed, skipping cache")
|
||||
|
||||
return result
|
||||
|
||||
async def create_binding(
|
||||
self,
|
||||
*,
|
||||
channel_type: str,
|
||||
account_id: str,
|
||||
group_id: str,
|
||||
agent_config_id: int,
|
||||
created_by: str | None = None,
|
||||
) -> ChannelBinding:
|
||||
result = await self._delegate.create_binding(
|
||||
channel_type=channel_type,
|
||||
account_id=account_id,
|
||||
group_id=group_id,
|
||||
agent_config_id=agent_config_id,
|
||||
created_by=created_by,
|
||||
)
|
||||
await self._invalidate(channel_type, account_id, group_id)
|
||||
return result
|
||||
|
||||
async def update_binding(
|
||||
self,
|
||||
binding_id: int,
|
||||
*,
|
||||
agent_config_id: int | None = None,
|
||||
is_enabled: bool | None = None,
|
||||
updated_by: str | None = None,
|
||||
) -> ChannelBinding | None:
|
||||
result = await self._delegate.update_binding(
|
||||
binding_id,
|
||||
agent_config_id=agent_config_id,
|
||||
is_enabled=is_enabled,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
if result:
|
||||
await self._invalidate(result.channel_type, result.account_id, result.group_id)
|
||||
return result
|
||||
|
||||
async def delete_binding(self, binding_id: int, *, updated_by: str | None = None) -> ChannelBinding | None:
|
||||
result = await self._delegate.delete_binding(binding_id, updated_by=updated_by)
|
||||
if result:
|
||||
await self._invalidate(result.channel_type, result.account_id, result.group_id)
|
||||
return result
|
||||
|
||||
async def get_binding(self, binding_id: int) -> ChannelBinding | None:
|
||||
return await self._delegate.get_binding(binding_id)
|
||||
|
||||
async def list_bindings(
|
||||
self, *, channel_type: str | None = None, offset: int = 0, limit: int = 50
|
||||
) -> tuple[list[ChannelBinding], int]:
|
||||
return await self._delegate.list_bindings(channel_type=channel_type, offset=offset, limit=limit)
|
||||
|
||||
async def _invalidate(self, channel_type: str, account_id: str, group_id: str) -> None:
|
||||
try:
|
||||
cache_key = self._cache_key(channel_type, account_id, group_id)
|
||||
await self._cache.delete(cache_key)
|
||||
except CacheError:
|
||||
logger.warning("Redis cache invalidation failed, cache will expire via TTL")
|
||||
Loading…
Reference in New Issue
Block a user