mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-06 12:20:00 +08:00
性能优化 (#2126)
* 性能优化 * 代码改进 * 优化浏览器代际切换逻辑 * 统一缓存与生命周期 * 添加aiomysql依赖 * 优化插件路径处理逻辑,简化条件判断;在虚拟环境包管理器中添加编码和错误处理参数以增强稳定性 * 🚨 auto fix by pre-commit hooks * 优化Windows下的关闭逻辑 * 代码优化 * bugfix:修复配置重载问题 * bugfix:修复插件加载启动竞态问题 * 收敛事件入口和权限上下文 * 优化 Windows launcher 关闭重启兜底 --------- Co-authored-by: HibiKier <775757368@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
HibiKier
pre-commit-ci[bot]
parent
24c316cd2c
commit
5d92ccd3b0
Vendored
+10
-6
@@ -74,6 +74,7 @@ from .config import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BoundedTTLCache",
|
||||
"Cache",
|
||||
"CacheDict",
|
||||
"CacheManager",
|
||||
@@ -82,6 +83,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
from . import runtime_cache as _runtime_cache # noqa: F401
|
||||
from .bounded_ttl import BoundedTTLCache
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
@@ -400,9 +402,10 @@ class CacheManager:
|
||||
"""清除缓存
|
||||
|
||||
参数:
|
||||
cache_type: 缓存类型,为None时清除所有缓存。
|
||||
注意:受 aiocache 限制,无法按类型精确删除,
|
||||
指定 cache_type 时仅清除整个 backend(行为与不指定相同)。
|
||||
cache_type: 缓存类型。为 None 时清除整个 backend。
|
||||
指定 cache_type 时不再退化为清除整个 backend,避免误删其他类型缓存。
|
||||
需要刷新模型运行态缓存时,应调用对应
|
||||
RuntimeCache.refresh/upsert/remove。
|
||||
|
||||
返回:
|
||||
bool: 是否成功
|
||||
@@ -413,11 +416,12 @@ class CacheManager:
|
||||
|
||||
try:
|
||||
if cache_type:
|
||||
logger.debug(
|
||||
f"清除缓存类型 {cache_type}"
|
||||
"(aiocache 不支持按前缀删除,清除整个 backend)",
|
||||
logger.warning(
|
||||
f"拒绝清除缓存类型 {cache_type}: "
|
||||
"当前后端不支持可靠的按类型清理,已避免清除整个 backend",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
return False
|
||||
await self.cache_backend.clear() # type: ignore
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
import time
|
||||
from typing import Generic, TypeVar
|
||||
import weakref
|
||||
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
def _default_sizeof(value: object) -> int:
|
||||
if isinstance(value, bytes | bytearray | memoryview):
|
||||
return len(value)
|
||||
return 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BoundedTTLCacheStats:
|
||||
name: str
|
||||
items: int
|
||||
max_items: int
|
||||
total_bytes: int
|
||||
max_total_bytes: int | None
|
||||
hits: int
|
||||
misses: int
|
||||
sets: int
|
||||
evictions: int
|
||||
|
||||
def to_dict(self) -> dict[str, int | str | None]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"items": self.items,
|
||||
"max_items": self.max_items,
|
||||
"total_bytes": self.total_bytes,
|
||||
"max_total_bytes": self.max_total_bytes,
|
||||
"hits": self.hits,
|
||||
"misses": self.misses,
|
||||
"sets": self.sets,
|
||||
"evictions": self.evictions,
|
||||
}
|
||||
|
||||
|
||||
class BoundedTTLCache(Generic[K, V]):
|
||||
"""Small async TTL/LRU cache with optional total-byte limit."""
|
||||
|
||||
_instances: weakref.WeakSet["BoundedTTLCache"] = weakref.WeakSet()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
ttl_seconds: float,
|
||||
max_items: int,
|
||||
max_total_bytes: int | None = None,
|
||||
sizeof: Callable[[V], int] | None = None,
|
||||
) -> None:
|
||||
self.name = name.upper()
|
||||
self._ttl_seconds = max(ttl_seconds, 0.0)
|
||||
self._max_items = max(max_items, 1)
|
||||
self._max_total_bytes = (
|
||||
max_total_bytes
|
||||
if isinstance(max_total_bytes, int) and max_total_bytes > 0
|
||||
else None
|
||||
)
|
||||
self._sizeof = sizeof or _default_sizeof
|
||||
self._cache: OrderedDict[K, tuple[float, V, int]] = OrderedDict()
|
||||
self._total_bytes = 0
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
self._sets = 0
|
||||
self._evictions = 0
|
||||
self._lock = asyncio.Lock()
|
||||
self.__class__._instances.add(self)
|
||||
|
||||
def _expire_at(self, now: float) -> float:
|
||||
if self._ttl_seconds <= 0:
|
||||
return sys.float_info.max
|
||||
return now + self._ttl_seconds
|
||||
|
||||
def _value_size(self, value: V) -> int:
|
||||
try:
|
||||
return max(0, int(self._sizeof(value)))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def _remove_key_nolock(self, key: K) -> bool:
|
||||
item = self._cache.pop(key, None)
|
||||
if item is None:
|
||||
return False
|
||||
self._total_bytes -= item[2]
|
||||
if self._total_bytes < 0:
|
||||
self._total_bytes = 0
|
||||
return True
|
||||
|
||||
def _pop_oldest_nolock(self) -> bool:
|
||||
if not self._cache:
|
||||
return False
|
||||
_, (_, _, size) = self._cache.popitem(last=False)
|
||||
self._total_bytes -= size
|
||||
if self._total_bytes < 0:
|
||||
self._total_bytes = 0
|
||||
self._evictions += 1
|
||||
return True
|
||||
|
||||
def _cleanup_nolock(self, now: float) -> None:
|
||||
expired_keys = [
|
||||
key for key, (expire_at, _, _) in self._cache.items() if expire_at <= now
|
||||
]
|
||||
for key in expired_keys:
|
||||
if self._remove_key_nolock(key):
|
||||
self._evictions += 1
|
||||
while len(self._cache) > self._max_items:
|
||||
self._pop_oldest_nolock()
|
||||
if self._max_total_bytes is not None:
|
||||
while self._total_bytes > self._max_total_bytes and self._cache:
|
||||
self._pop_oldest_nolock()
|
||||
|
||||
async def get(self, key: K) -> V | None:
|
||||
now = time.monotonic()
|
||||
async with self._lock:
|
||||
self._cleanup_nolock(now)
|
||||
item = self._cache.get(key)
|
||||
if item is None:
|
||||
self._misses += 1
|
||||
return None
|
||||
expire_at, value, _ = item
|
||||
if expire_at <= now:
|
||||
self._remove_key_nolock(key)
|
||||
self._misses += 1
|
||||
return None
|
||||
self._cache.move_to_end(key)
|
||||
self._hits += 1
|
||||
return value
|
||||
|
||||
async def set(self, key: K, value: V) -> bool:
|
||||
value_size = self._value_size(value)
|
||||
if self._max_total_bytes is not None and value_size > self._max_total_bytes:
|
||||
return False
|
||||
|
||||
now = time.monotonic()
|
||||
async with self._lock:
|
||||
self._remove_key_nolock(key)
|
||||
self._cache[key] = (self._expire_at(now), value, value_size)
|
||||
self._total_bytes += value_size
|
||||
self._sets += 1
|
||||
self._cache.move_to_end(key)
|
||||
self._cleanup_nolock(now)
|
||||
return key in self._cache
|
||||
|
||||
async def delete(self, key: K) -> bool:
|
||||
async with self._lock:
|
||||
return self._remove_key_nolock(key)
|
||||
|
||||
async def clear(self) -> int:
|
||||
async with self._lock:
|
||||
size = len(self._cache)
|
||||
self._cache.clear()
|
||||
self._total_bytes = 0
|
||||
return size
|
||||
|
||||
async def stats(self) -> BoundedTTLCacheStats:
|
||||
now = time.monotonic()
|
||||
async with self._lock:
|
||||
self._cleanup_nolock(now)
|
||||
return BoundedTTLCacheStats(
|
||||
name=self.name,
|
||||
items=len(self._cache),
|
||||
max_items=self._max_items,
|
||||
total_bytes=self._total_bytes,
|
||||
max_total_bytes=self._max_total_bytes,
|
||||
hits=self._hits,
|
||||
misses=self._misses,
|
||||
sets=self._sets,
|
||||
evictions=self._evictions,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def clear_all(cls) -> dict[str, int]:
|
||||
result: dict[str, int] = {}
|
||||
for cache in list(cls._instances):
|
||||
size = await cache.clear()
|
||||
if size:
|
||||
result[cache.name] = result.get(cache.name, 0) + size
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def stats_all(cls) -> dict[str, dict[str, int | str | None]]:
|
||||
result: dict[str, dict[str, int | str | None]] = {}
|
||||
for cache in list(cls._instances):
|
||||
stats = await cache.stats()
|
||||
if not stats.items:
|
||||
continue
|
||||
if cache.name not in result:
|
||||
result[cache.name] = stats.to_dict()
|
||||
continue
|
||||
current = result[cache.name]
|
||||
for key in (
|
||||
"items",
|
||||
"max_items",
|
||||
"total_bytes",
|
||||
"hits",
|
||||
"misses",
|
||||
"sets",
|
||||
"evictions",
|
||||
):
|
||||
current[key] = int(current.get(key) or 0) + int(
|
||||
getattr(stats, key) or 0
|
||||
)
|
||||
current_max_bytes = current.get("max_total_bytes")
|
||||
if current_max_bytes is not None or stats.max_total_bytes is not None:
|
||||
current["max_total_bytes"] = int(current_max_bytes or 0) + int(
|
||||
stats.max_total_bytes or 0
|
||||
)
|
||||
return result
|
||||
+84
-2
@@ -1,9 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
from typing import Any, Generic, TypeVar
|
||||
import weakref
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
DEFAULT_CACHE_MAX_ITEMS = 10000
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheData(Generic[T]):
|
||||
@@ -16,16 +19,21 @@ class CacheData(Generic[T]):
|
||||
class CacheDict(Generic[T]):
|
||||
"""缓存字典类,提供类似普通字典的接口,数据只存储在内存中"""
|
||||
|
||||
def __init__(self, name: str, expire: int = 0):
|
||||
_instances: weakref.WeakSet = weakref.WeakSet()
|
||||
|
||||
def __init__(self, name: str, expire: int = 0, max_items: int | None = None):
|
||||
"""初始化缓存字典
|
||||
|
||||
参数:
|
||||
name: 字典名称
|
||||
expire: 过期时间(秒),默认为0表示永不过期
|
||||
max_items: 最大缓存项数,None 使用统一默认值,0 表示不限制
|
||||
"""
|
||||
self.name = name.upper()
|
||||
self.expire = expire
|
||||
self.max_items = DEFAULT_CACHE_MAX_ITEMS if max_items is None else max_items
|
||||
self._data: dict[str, CacheData[T]] = {}
|
||||
self.__class__._instances.add(self)
|
||||
|
||||
def expire_time(self, key: str) -> float:
|
||||
"""获取字典项的过期时间"""
|
||||
@@ -62,6 +70,7 @@ class CacheDict(Generic[T]):
|
||||
"""
|
||||
expire_time = time.time() + self.expire if self.expire > 0 else 0
|
||||
self._data[key] = CacheData(value=value, expire_time=expire_time)
|
||||
self._enforce_limit()
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
"""删除字典项
|
||||
@@ -122,6 +131,7 @@ class CacheDict(Generic[T]):
|
||||
expire_time = time.time() + self.expire
|
||||
|
||||
self._data[key] = CacheData(value=value, expire_time=expire_time)
|
||||
self._enforce_limit()
|
||||
|
||||
def pop(self, key: str, default: Any = None) -> T:
|
||||
"""删除并返回字典项
|
||||
@@ -146,6 +156,32 @@ class CacheDict(Generic[T]):
|
||||
"""清空字典"""
|
||||
self._data.clear()
|
||||
|
||||
def stats(self) -> dict[str, int]:
|
||||
"""返回当前缓存条目统计。"""
|
||||
self._clean_expired()
|
||||
return {"items": len(self._data), "max_items": self.max_items}
|
||||
|
||||
@classmethod
|
||||
def stats_all(cls) -> dict[str, dict[str, int]]:
|
||||
"""返回所有 CacheDict 实例的条目统计。"""
|
||||
result: dict[str, dict[str, int]] = {}
|
||||
for cache in list(cls._instances):
|
||||
stats = cache.stats()
|
||||
if stats["items"]:
|
||||
result[cache.name] = stats
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def clear_all(cls) -> dict[str, int]:
|
||||
"""清空所有 CacheDict,返回各缓存清理的条目数。"""
|
||||
result: dict[str, int] = {}
|
||||
for cache in list(cls._instances):
|
||||
size = len(cache._data)
|
||||
if size:
|
||||
cache.clear()
|
||||
result[cache.name] = result.get(cache.name, 0) + size
|
||||
return result
|
||||
|
||||
def keys(self) -> list[str]:
|
||||
"""获取所有键
|
||||
|
||||
@@ -187,6 +223,12 @@ class CacheDict(Generic[T]):
|
||||
for key in expired_keys:
|
||||
del self._data[key]
|
||||
|
||||
def _enforce_limit(self) -> None:
|
||||
if self.max_items <= 0:
|
||||
return
|
||||
while len(self._data) > self.max_items:
|
||||
self._data.pop(next(iter(self._data)))
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""获取字典长度
|
||||
|
||||
@@ -211,17 +253,22 @@ class CacheDict(Generic[T]):
|
||||
class CacheList(Generic[T]):
|
||||
"""缓存列表类,提供类似普通列表的接口,数据只存储在内存中"""
|
||||
|
||||
def __init__(self, name: str, expire: int = 0):
|
||||
_instances: weakref.WeakSet = weakref.WeakSet()
|
||||
|
||||
def __init__(self, name: str, expire: int = 0, max_items: int | None = None):
|
||||
"""初始化缓存列表
|
||||
|
||||
参数:
|
||||
name: 列表名称
|
||||
expire: 过期时间(秒),默认为0表示永不过期
|
||||
max_items: 最大缓存项数,None 使用统一默认值,0 表示不限制
|
||||
"""
|
||||
self.name = name.upper()
|
||||
self.expire = expire
|
||||
self.max_items = DEFAULT_CACHE_MAX_ITEMS if max_items is None else max_items
|
||||
self._data: list[CacheData[T]] = []
|
||||
self._expire_time = 0
|
||||
self.__class__._instances.add(self)
|
||||
|
||||
# 如果设置了过期时间,计算整个列表的过期时间
|
||||
if self.expire > 0:
|
||||
@@ -303,6 +350,7 @@ class CacheList(Generic[T]):
|
||||
self.clear()
|
||||
|
||||
self._data.append(CacheData(value=value))
|
||||
self._enforce_limit()
|
||||
|
||||
# 更新过期时间
|
||||
self._update_expire_time()
|
||||
@@ -318,6 +366,7 @@ class CacheList(Generic[T]):
|
||||
self.clear()
|
||||
|
||||
self._data.extend([CacheData(value=v) for v in values])
|
||||
self._enforce_limit()
|
||||
|
||||
# 更新过期时间
|
||||
self._update_expire_time()
|
||||
@@ -334,6 +383,7 @@ class CacheList(Generic[T]):
|
||||
self.clear()
|
||||
|
||||
self._data.insert(index, CacheData(value=value))
|
||||
self._enforce_limit()
|
||||
|
||||
# 更新过期时间
|
||||
self._update_expire_time()
|
||||
@@ -389,6 +439,32 @@ class CacheList(Generic[T]):
|
||||
# 重置过期时间
|
||||
self._update_expire_time()
|
||||
|
||||
def stats(self) -> dict[str, int]:
|
||||
"""返回当前缓存条目统计。"""
|
||||
if self._is_expired():
|
||||
self.clear()
|
||||
return {"items": len(self._data), "max_items": self.max_items}
|
||||
|
||||
@classmethod
|
||||
def stats_all(cls) -> dict[str, dict[str, int]]:
|
||||
"""返回所有 CacheList 实例的条目统计。"""
|
||||
result: dict[str, dict[str, int]] = {}
|
||||
for cache in list(cls._instances):
|
||||
stats = cache.stats()
|
||||
if stats["items"]:
|
||||
result[cache.name] = stats
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def clear_all(cls) -> dict[str, int]:
|
||||
result: dict[str, int] = {}
|
||||
for cache in list(cls._instances):
|
||||
size = len(cache._data)
|
||||
if size:
|
||||
cache.clear()
|
||||
result[cache.name] = result.get(cache.name, 0) + size
|
||||
return result
|
||||
|
||||
def index(self, value: T, start: int = 0, end: int | None = None) -> int:
|
||||
"""查找值的索引
|
||||
|
||||
@@ -438,6 +514,12 @@ class CacheList(Generic[T]):
|
||||
"""更新过期时间"""
|
||||
self._expire_time = time.time() + self.expire if self.expire > 0 else 0
|
||||
|
||||
def _enforce_limit(self) -> None:
|
||||
if self.max_items <= 0:
|
||||
return
|
||||
if len(self._data) > self.max_items:
|
||||
del self._data[: len(self._data) - self.max_items]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""字符串表示
|
||||
|
||||
|
||||
+169
-42
@@ -10,7 +10,13 @@ import uuid
|
||||
|
||||
from zhenxun.services.cache.config import CacheMode
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import LimitCheckType, LimitWatchType, PluginLimitType
|
||||
from zhenxun.utils.enum import (
|
||||
BlockType,
|
||||
LimitCheckType,
|
||||
LimitWatchType,
|
||||
PluginLimitType,
|
||||
PluginType,
|
||||
)
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,24 +24,6 @@ if TYPE_CHECKING:
|
||||
|
||||
LOG_COMMAND = "RuntimeCache"
|
||||
|
||||
PLUGININFO_MEM_REFRESH_INTERVAL = 1800 # 30分钟 - 插件信息很少变化
|
||||
BAN_MEM_REFRESH_INTERVAL = 60
|
||||
BAN_MEM_CLEAN_INTERVAL = 60
|
||||
BAN_MEM_CLEANUP_DB = True
|
||||
BAN_MEM_NEGATIVE_TTL = 5
|
||||
BOT_MEM_REFRESH_INTERVAL = 300 # 5分钟
|
||||
BOT_MEM_NEGATIVE_TTL = 60
|
||||
GROUP_MEM_REFRESH_INTERVAL = 300 # 5分钟 - 群组信息很少变化
|
||||
GROUP_MEM_NEGATIVE_TTL = 60
|
||||
LEVEL_MEM_REFRESH_INTERVAL = 300 # 5分钟 - 用户等级很少变化
|
||||
LEVEL_MEM_NEGATIVE_TTL = 60
|
||||
TASK_MEM_REFRESH_INTERVAL = 900
|
||||
TASK_MEM_NEGATIVE_TTL = 60
|
||||
LIMIT_MEM_REFRESH_INTERVAL = 300 # 5分钟
|
||||
LIMIT_MEM_NEGATIVE_TTL = 30
|
||||
RUNTIME_CACHE_SYNC_ENABLED = True
|
||||
RUNTIME_CACHE_SYNC_CHANNEL = "ZHENXUN_RUNTIME_CACHE_SYNC"
|
||||
|
||||
|
||||
def _coerce_int(value, default: int) -> int:
|
||||
try:
|
||||
@@ -45,6 +33,27 @@ def _coerce_int(value, default: int) -> int:
|
||||
return value_int if value_int >= 0 else default
|
||||
|
||||
|
||||
# RuntimeCache 以模型 save/delete 主动失效为主,周期 refresh 只做兜底。
|
||||
# 这些默认值避免低压力运行时频繁全量扫表。
|
||||
PLUGININFO_MEM_REFRESH_INTERVAL = 3600 # 60分钟
|
||||
BAN_MEM_REFRESH_INTERVAL = 300
|
||||
BAN_MEM_CLEAN_INTERVAL = 60
|
||||
BAN_MEM_CLEANUP_DB = True
|
||||
BAN_MEM_NEGATIVE_TTL = 5
|
||||
BOT_MEM_REFRESH_INTERVAL = 900 # 15分钟
|
||||
BOT_MEM_NEGATIVE_TTL = 60
|
||||
GROUP_MEM_REFRESH_INTERVAL = 900 # 15分钟
|
||||
GROUP_MEM_NEGATIVE_TTL = 60
|
||||
LEVEL_MEM_REFRESH_INTERVAL = 900 # 15分钟
|
||||
LEVEL_MEM_NEGATIVE_TTL = 60
|
||||
TASK_MEM_REFRESH_INTERVAL = 1800
|
||||
TASK_MEM_NEGATIVE_TTL = 60
|
||||
LIMIT_MEM_REFRESH_INTERVAL = 900 # 15分钟
|
||||
LIMIT_MEM_NEGATIVE_TTL = 30
|
||||
RUNTIME_CACHE_SYNC_ENABLED = True
|
||||
RUNTIME_CACHE_SYNC_CHANNEL = "ZHENXUN_RUNTIME_CACHE_SYNC"
|
||||
|
||||
|
||||
INSTANCE_ID = uuid.uuid4().hex
|
||||
_CACHE_READY_EVENT = asyncio.Event()
|
||||
|
||||
@@ -89,6 +98,89 @@ def _parse_block_modules(value: str) -> frozenset[str]:
|
||||
return frozenset(items)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginInfoSnapshot:
|
||||
id: int
|
||||
module: str
|
||||
module_path: str
|
||||
name: str
|
||||
status: bool
|
||||
block_type: BlockType | None
|
||||
load_status: bool
|
||||
author: str | None
|
||||
version: str | None
|
||||
level: int
|
||||
default_status: bool
|
||||
limit_superuser: bool
|
||||
menu_type: str
|
||||
plugin_type: PluginType | None
|
||||
cost_gold: int
|
||||
admin_level: int | None
|
||||
ignore_prompt: bool
|
||||
is_delete: bool
|
||||
parent: str | None
|
||||
is_show: bool
|
||||
ignore_statistics: bool
|
||||
impression: float
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model) -> "PluginInfoSnapshot":
|
||||
return cls(
|
||||
id=int(getattr(model, "id", 0) or 0),
|
||||
module=str(getattr(model, "module", "") or ""),
|
||||
module_path=str(getattr(model, "module_path", "") or ""),
|
||||
name=str(getattr(model, "name", "") or ""),
|
||||
status=bool(getattr(model, "status", True)),
|
||||
block_type=getattr(model, "block_type", None),
|
||||
load_status=bool(getattr(model, "load_status", True)),
|
||||
author=getattr(model, "author", None),
|
||||
version=getattr(model, "version", None),
|
||||
level=int(getattr(model, "level", 0) or 0),
|
||||
default_status=bool(getattr(model, "default_status", True)),
|
||||
limit_superuser=bool(getattr(model, "limit_superuser", False)),
|
||||
menu_type=str(getattr(model, "menu_type", "") or ""),
|
||||
plugin_type=getattr(model, "plugin_type", None),
|
||||
cost_gold=int(getattr(model, "cost_gold", 0) or 0),
|
||||
admin_level=getattr(model, "admin_level", None),
|
||||
ignore_prompt=bool(getattr(model, "ignore_prompt", False)),
|
||||
is_delete=bool(getattr(model, "is_delete", False)),
|
||||
parent=getattr(model, "parent", None),
|
||||
is_show=bool(getattr(model, "is_show", True)),
|
||||
ignore_statistics=bool(getattr(model, "ignore_statistics", False)),
|
||||
impression=float(getattr(model, "impression", 0) or 0),
|
||||
)
|
||||
|
||||
def to_model(self):
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
|
||||
plugin = PluginInfo(
|
||||
id=self.id,
|
||||
module=self.module,
|
||||
module_path=self.module_path,
|
||||
name=self.name,
|
||||
status=self.status,
|
||||
block_type=self.block_type,
|
||||
load_status=self.load_status,
|
||||
author=self.author,
|
||||
version=self.version,
|
||||
level=self.level,
|
||||
default_status=self.default_status,
|
||||
limit_superuser=self.limit_superuser,
|
||||
menu_type=self.menu_type,
|
||||
plugin_type=self.plugin_type,
|
||||
cost_gold=self.cost_gold,
|
||||
admin_level=self.admin_level,
|
||||
ignore_prompt=self.ignore_prompt,
|
||||
is_delete=self.is_delete,
|
||||
parent=self.parent,
|
||||
is_show=self.is_show,
|
||||
ignore_statistics=self.ignore_statistics,
|
||||
impression=self.impression,
|
||||
)
|
||||
plugin._saved_in_db = True
|
||||
return plugin
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BanEntry:
|
||||
user_id: str | None
|
||||
@@ -465,6 +557,20 @@ class RuntimeCacheSync:
|
||||
|
||||
@classmethod
|
||||
async def stop(cls) -> None:
|
||||
cls._ready = False
|
||||
if cls._publish_tasks:
|
||||
tasks = list(cls._publish_tasks)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*tasks, return_exceptions=True),
|
||||
timeout=1.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
for task in tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
finally:
|
||||
cls._publish_tasks.difference_update(tasks)
|
||||
if cls._task and not cls._task.done():
|
||||
cls._task.cancel()
|
||||
cls._task = None
|
||||
@@ -480,7 +586,6 @@ class RuntimeCacheSync:
|
||||
except Exception:
|
||||
pass
|
||||
cls._redis = None
|
||||
cls._ready = False
|
||||
|
||||
@classmethod
|
||||
def publish_event(cls, cache_type: str, action: str, data: dict[str, Any]) -> None:
|
||||
@@ -559,25 +664,43 @@ class RuntimeCacheSync:
|
||||
|
||||
class PluginInfoMemoryCache:
|
||||
_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
|
||||
_by_module: ClassVar[dict[str, "PluginInfo"]] = {}
|
||||
_by_module_path: ClassVar[dict[str, "PluginInfo"]] = {}
|
||||
_by_module: ClassVar[dict[str, PluginInfoSnapshot]] = {}
|
||||
_by_module_path: ClassVar[dict[str, PluginInfoSnapshot]] = {}
|
||||
_loaded: ClassVar[bool] = False
|
||||
_refresh_task: ClassVar[asyncio.Task | None] = None
|
||||
_last_refresh: ClassVar[float] = 0.0
|
||||
|
||||
@classmethod
|
||||
def _to_model(cls, snapshot: PluginInfoSnapshot | None) -> "PluginInfo | None":
|
||||
return snapshot.to_model() if snapshot else None
|
||||
|
||||
@classmethod
|
||||
def _store_snapshot(cls, snapshot: PluginInfoSnapshot) -> None:
|
||||
if snapshot.module:
|
||||
old = cls._by_module.get(snapshot.module)
|
||||
if old and old.module_path != snapshot.module_path:
|
||||
cls._by_module_path.pop(old.module_path, None)
|
||||
cls._by_module[snapshot.module] = snapshot
|
||||
if snapshot.module_path:
|
||||
old = cls._by_module_path.get(snapshot.module_path)
|
||||
if old and old.module != snapshot.module:
|
||||
cls._by_module.pop(old.module, None)
|
||||
cls._by_module_path[snapshot.module_path] = snapshot
|
||||
|
||||
@classmethod
|
||||
async def refresh(cls) -> None:
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
|
||||
async with cls._lock:
|
||||
plugins = await PluginInfo.all()
|
||||
by_module: dict[str, "PluginInfo"] = {}
|
||||
by_module_path: dict[str, "PluginInfo"] = {}
|
||||
by_module: dict[str, PluginInfoSnapshot] = {}
|
||||
by_module_path: dict[str, PluginInfoSnapshot] = {}
|
||||
for plugin in plugins:
|
||||
if plugin.module:
|
||||
by_module[plugin.module] = plugin
|
||||
if plugin.module_path:
|
||||
by_module_path[plugin.module_path] = plugin
|
||||
snapshot = PluginInfoSnapshot.from_model(plugin)
|
||||
if snapshot.module:
|
||||
by_module[snapshot.module] = snapshot
|
||||
if snapshot.module_path:
|
||||
by_module_path[snapshot.module_path] = snapshot
|
||||
cls._by_module = by_module
|
||||
cls._by_module_path = by_module_path
|
||||
cls._loaded = True
|
||||
@@ -596,42 +719,42 @@ class PluginInfoMemoryCache:
|
||||
async def get_by_module(cls, module: str) -> "PluginInfo | None":
|
||||
if not cls._loaded:
|
||||
await cls.ensure_loaded()
|
||||
return cls._by_module.get(module)
|
||||
return cls._to_model(cls._by_module.get(module))
|
||||
|
||||
@classmethod
|
||||
async def get_all(cls) -> dict[str, "PluginInfo"]:
|
||||
if not cls._loaded:
|
||||
await cls.ensure_loaded()
|
||||
return dict(cls._by_module)
|
||||
return {
|
||||
module: snapshot.to_model() for module, snapshot in cls._by_module.items()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_by_module_path(cls, module_path: str) -> "PluginInfo | None":
|
||||
return cls._by_module_path.get(module_path)
|
||||
return cls._to_model(cls._by_module_path.get(module_path))
|
||||
|
||||
@classmethod
|
||||
def set_plugin(cls, plugin) -> None:
|
||||
if not plugin:
|
||||
return
|
||||
if plugin.module:
|
||||
cls._by_module[plugin.module] = plugin
|
||||
if getattr(plugin, "module_path", None):
|
||||
cls._by_module_path[plugin.module_path] = plugin
|
||||
snapshot = PluginInfoSnapshot.from_model(plugin)
|
||||
cls._store_snapshot(snapshot)
|
||||
cls._loaded = True
|
||||
cls._last_refresh = time.time()
|
||||
|
||||
@classmethod
|
||||
def remove_by_module(cls, module: str) -> None:
|
||||
cls._by_module.pop(module, None)
|
||||
snapshot = cls._by_module.pop(module, None)
|
||||
if snapshot and snapshot.module_path:
|
||||
cls._by_module_path.pop(snapshot.module_path, None)
|
||||
|
||||
@classmethod
|
||||
async def upsert_from_model(cls, plugin) -> None:
|
||||
if not plugin:
|
||||
return
|
||||
async with cls._lock:
|
||||
if getattr(plugin, "module", None):
|
||||
cls._by_module[plugin.module] = plugin
|
||||
if getattr(plugin, "module_path", None):
|
||||
cls._by_module_path[plugin.module_path] = plugin
|
||||
snapshot = PluginInfoSnapshot.from_model(plugin)
|
||||
cls._store_snapshot(snapshot)
|
||||
cls._loaded = True
|
||||
cls._last_refresh = time.time()
|
||||
|
||||
@@ -643,9 +766,13 @@ class PluginInfoMemoryCache:
|
||||
return
|
||||
async with cls._lock:
|
||||
if module:
|
||||
cls._by_module.pop(module, None)
|
||||
snapshot = cls._by_module.pop(module, None)
|
||||
if snapshot and snapshot.module_path:
|
||||
cls._by_module_path.pop(snapshot.module_path, None)
|
||||
if module_path:
|
||||
cls._by_module_path.pop(module_path, None)
|
||||
snapshot = cls._by_module_path.pop(module_path, None)
|
||||
if snapshot and snapshot.module:
|
||||
cls._by_module.pop(snapshot.module, None)
|
||||
|
||||
@classmethod
|
||||
async def _refresh_loop(cls, interval: int) -> None:
|
||||
|
||||
Reference in New Issue
Block a user