refactor: 收敛 factory 模块级副作用并统一 async 路径进度为异步后端

- 将 configure_token_codec/configure_plugin_routes 移入 create_app(),消除 import 期副作用
- runtime 层新增 AsyncCacheProxy/AsyncTTLCache/AsyncProgressHelper,共享 progress region
- search/system/history/scheduler/dashboard 的事件循环路径切换异步进度后端
- 同步进度回调经事件循环提交或线程池执行,避免阻塞事件循环
This commit is contained in:
jxxghp
2026-08-19 09:11:42 +08:00
parent 91d339a8dd
commit 03ada7eb01
9 changed files with 446 additions and 105 deletions
+137
View File
@@ -1158,6 +1158,143 @@ class CacheProxy:
self._cache_backend.close()
class AsyncCacheProxy:
"""
异步缓存代理类,将异步缓存后端的方法直接代理到实例上
与同步 CacheProxy 的唯一差异是方法均为 async,默认绑定构造时指定的 region。
"""
def __init__(self, cache_backend: AsyncCacheBackend, region: str):
"""
初始化异步缓存代理
:param cache_backend: 异步缓存后端实例
:param region: 缓存区域
"""
self._cache_backend = cache_backend
self._region = region
def is_redis(self) -> bool:
"""
检查当前缓存后端是否为 Redis(纯状态判断,无需 await)
"""
return self._cache_backend.is_redis()
async def get(self, key: str, **kwargs) -> Any:
"""
获取缓存值
"""
kwargs.setdefault('region', self._region)
return await self._cache_backend.get(key, **kwargs)
async def set(self, key: str, value: Any, **kwargs) -> None:
"""
设置缓存值
"""
kwargs.setdefault('region', self._region)
await self._cache_backend.set(key, value, **kwargs)
async def delete(self, key: str, **kwargs) -> None:
"""
删除缓存值
"""
kwargs.setdefault('region', self._region)
await self._cache_backend.delete(key, **kwargs)
async def exists(self, key: str, **kwargs) -> bool:
"""
检查缓存键是否存在
"""
kwargs.setdefault('region', self._region)
return await self._cache_backend.exists(key, **kwargs)
async def clear(self, **kwargs) -> None:
"""
清除缓存
"""
kwargs.setdefault('region', self._region)
await self._cache_backend.clear(**kwargs)
async def items(self, **kwargs):
"""
获取所有缓存项
"""
kwargs.setdefault('region', self._region)
async for item in self._cache_backend.items(**kwargs):
yield item
async def keys(self, **kwargs):
"""
获取所有缓存键
"""
kwargs.setdefault('region', self._region)
async for key in self._cache_backend.keys(**kwargs):
yield key
async def values(self, **kwargs):
"""
获取所有缓存值
"""
kwargs.setdefault('region', self._region)
async for value in self._cache_backend.values(**kwargs):
yield value
async def update(self, other: Dict[str, Any], **kwargs) -> None:
"""
更新缓存
"""
kwargs.setdefault('region', self._region)
await self._cache_backend.update(other, **kwargs)
async def pop(self, key: str, default: Any = None, **kwargs) -> Any:
"""
弹出缓存项
"""
kwargs.setdefault('region', self._region)
return await self._cache_backend.pop(key, default, **kwargs)
async def popitem(self, **kwargs) -> Tuple[str, Any]:
"""
弹出最后一个缓存项
"""
kwargs.setdefault('region', self._region)
return await self._cache_backend.popitem(**kwargs)
async def setdefault(self, key: str, default: Any = None, **kwargs) -> Any:
"""
设置默认值
"""
kwargs.setdefault('region', self._region)
return await self._cache_backend.setdefault(key, default, **kwargs)
async def close(self) -> None:
"""
关闭缓存连接
"""
await self._cache_backend.close()
class AsyncTTLCache(AsyncCacheProxy):
"""
基于 TTL 的异步缓存类,与同步 TTLCache 使用同一 region 语义,
内存后端共享进程内存储,Redis 后端共享同一键空间
"""
def __init__(self,
region: Optional[str] = DEFAULT_CACHE_REGION,
maxsize: Optional[int] = DEFAULT_CACHE_SIZE,
ttl: Optional[int] = DEFAULT_CACHE_TTL):
"""
初始化异步 TTL 缓存
:param maxsize: 缓存的最大条目数
:param ttl: 缓存的存活时间,单位秒
:param region: 缓存的区,为 None 时使用默认区
"""
super().__init__(AsyncCache(cache_type='ttl', maxsize=maxsize, ttl=ttl), region)
class TTLCache(CacheProxy):
"""
基于 TTL 的缓存类,兼容 cachetools.TTLCache 接口
+116 -1
View File
@@ -1,7 +1,7 @@
from enum import Enum
from typing import Optional, Union
from app.runtime.cache import TTLCache
from app.runtime.cache import AsyncTTLCache, TTLCache
from app.runtime.localization import LocaleHelper
from app.schemas.types import ProgressKey
@@ -115,3 +115,118 @@ class ProgressHelper:
)
detail["data"] = localized_data
return detail
class AsyncProgressHelper:
"""
处理进度辅助类(异步)
与 ProgressHelper 共用同一个进度 region:内存后端共享进程内存储,
Redis 后端共享同一键空间,因此同步写入、异步读取(或反之)均互通。
供事件循环上的异步调用方使用,避免同步缓存后端阻塞事件循环。
"""
def __init__(self, key: Union[ProgressKey, str]) -> None:
"""为指定业务键绑定独立的异步进度缓存区域。"""
if isinstance(key, Enum):
key = key.value
self._key = key
self._progress = AsyncTTLCache(region="progress", maxsize=1024, ttl=24 * 60 * 60)
async def __reset(self) -> None:
"""
重置进度
"""
await self._progress.set(self._key, {
"enable": False,
"value": 0,
"text": "请稍候...",
"data": {}
})
async def start(self) -> None:
"""
开始进度
"""
await self.__reset()
current = await self._progress.get(self._key)
if not current:
return
current['enable'] = True
await self._progress.set(self._key, current)
async def end(
self,
text: Optional[str] = "",
data: Optional[dict] = None,
value: Optional[Union[float, int]] = 100,
) -> None:
"""
结束进度
"""
current = await self._progress.get(self._key)
if not current:
return
if data is not None:
if not current.get('data'):
current['data'] = {}
current['data'].update(data)
current["enable"] = False
if value is not None:
current["value"] = max(min(float(value), 100), 0)
current["text"] = text or ""
await self._progress.set(self._key, current)
async def update(
self,
value: Optional[Union[float, int]] = None,
text: Optional[str] = None,
data: Optional[dict] = None,
) -> None:
"""
更新进度
"""
current = await self._progress.get(self._key)
if not current or not current.get('enable'):
return
if value is not None:
current['value'] = max(min(float(value), 100), 0)
if text is not None:
current['text'] = text
if data is not None:
if not current.get('data'):
current['data'] = {}
current['data'].update(data)
await self._progress.set(self._key, current)
async def get(self, locale: Optional[str] = None) -> Optional[dict]:
"""
获取当前进度,并按语言补充前端展示字段。
:param locale: 目标语言,未传入时使用当前请求上下文语言
:return: 当前进度字典
"""
current = await self._progress.get(self._key)
if not current:
return current
detail = current.copy()
text = detail.get("text")
if isinstance(text, str):
detail["text_i18n"] = LocaleHelper.translate_text(text, locale=locale)
data = detail.get("data")
if isinstance(data, dict):
localized_data = data.copy()
error = localized_data.get("error")
message = localized_data.get("message")
if isinstance(error, str):
localized_data["error_i18n"] = LocaleHelper.translate_text(
error, locale=locale
)
if isinstance(message, str):
localized_data["message_i18n"] = LocaleHelper.translate_text(
message, locale=locale
)
detail["data"] = localized_data
return detail