bugfix:修复sqlite部分场景下锁竞争问题;新增插件恶意触发配置文件 (#2144)

* bugfix:修复sqlite部分场景下锁竞争问题;新增插件恶意触发配置文件

* 文件没同步完
This commit is contained in:
Copaan
2026-06-24 09:11:03 +08:00
committed by GitHub
parent 73cbe2a609
commit f4d2342693
39 changed files with 1577 additions and 829 deletions
+9 -21
View File
@@ -52,7 +52,6 @@ from typing import Any, ClassVar, Generic, TypeVar, cast, get_type_hints
from typing_extensions import Self
from aiocache import Cache as AioCache
from aiocache import SimpleMemoryCache
from aiocache.base import BaseCache
from aiocache.serializers import JsonSerializer
import nonebot
@@ -109,6 +108,10 @@ driver = nonebot.get_driver()
cache_config = nonebot.get_plugin_config(Config)
def _redis_cache_enabled() -> bool:
return cache_config.cache_mode == CacheMode.REDIS and bool(cache_config.redis_host)
class CacheException(Exception):
"""缓存相关异常"""
@@ -190,15 +193,10 @@ class CacheManager:
def cache_backend(self) -> BaseCache | AioCache:
"""获取缓存后端"""
if self._cache_backend is None:
ttl = cache_config.redis_expire
if cache_config.cache_mode == CacheMode.NONE:
ttl = 0
logger.info("缓存功能已禁用,使用非持久化内存缓存", LOG_COMMAND)
elif cache_config.cache_mode == CacheMode.REDIS and cache_config.redis_host:
if _redis_cache_enabled():
try:
from aiocache import RedisCache
# 使用Redis缓存
self._cache_backend = RedisCache(
serializer=JsonSerializer(),
namespace=CACHE_KEY_PREFIX,
@@ -215,19 +213,11 @@ class CacheManager:
return self._cache_backend
except ImportError as e:
logger.error(
"导入aiocache[redis]失败,将默认使用内存缓存...",
"导入aiocache[redis]失败,CacheRoot 模型缓存已禁用",
LOG_COMMAND,
e=e,
)
else:
logger.info("使用内存缓存", LOG_COMMAND)
# 默认使用内存缓存
self._cache_backend = SimpleMemoryCache(
serializer=JsonSerializer(),
namespace=CACHE_KEY_PREFIX,
timeout=30,
ttl=ttl,
)
raise CacheException("CacheRoot 后端未启用")
return self._cache_backend
async def invalidate_cache(
@@ -815,11 +805,9 @@ class Cache(Generic[T]):
@driver.on_startup
async def _():
CacheRoot.enabled = cache_config.cache_mode != CacheMode.NONE
CacheRoot.enabled = _redis_cache_enabled()
if CacheRoot.enabled:
logger.info("缓存系统已启用", LOG_COMMAND)
else:
logger.info("缓存系统已禁用", LOG_COMMAND)
logger.info("CacheRoot Redis 模型缓存已启用", LOG_COMMAND)
@driver.on_shutdown
-282
View File
@@ -248,285 +248,3 @@ class CacheDict(Generic[T]):
# 清理过期的键
self._clean_expired()
return f"CacheDict({self.name}, {len(self._data)} items)"
class CacheList(Generic[T]):
"""缓存列表类,提供类似普通列表的接口,数据只存储在内存中"""
_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:
self._expire_time = time.time() + self.expire
def __getitem__(self, index: int) -> T:
"""获取列表项
参数:
index: 列表索引
返回:
T: 列表值
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
raise IndexError(f"列表索引 {index} 超出范围")
if 0 <= index < len(self._data):
return self._data[index].value
raise IndexError(f"列表索引 {index} 超出范围")
def __setitem__(self, index: int, value: T):
"""设置列表项
参数:
index: 列表索引
value: 列表值
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
# 确保索引有效
while len(self._data) <= index:
raise IndexError(f"列表索引 {index} 超出范围")
self._data[index] = CacheData(value=value)
# 更新过期时间
self._update_expire_time()
def __delitem__(self, index: int):
"""删除列表项
参数:
index: 列表索引
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
raise IndexError(f"列表索引 {index} 超出范围")
if not 0 <= index < len(self._data):
raise IndexError(f"列表索引 {index} 超出范围")
del self._data[index]
# 更新过期时间
self._update_expire_time()
def __len__(self) -> int:
"""获取列表长度
返回:
int: 列表长度
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
return len(self._data)
def append(self, value: T):
"""添加列表项
参数:
value: 列表值
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
self._data.append(CacheData(value=value))
self._enforce_limit()
# 更新过期时间
self._update_expire_time()
def extend(self, values: list[T]):
"""扩展列表
参数:
values: 要添加的值列表
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
self._data.extend([CacheData(value=v) for v in values])
self._enforce_limit()
# 更新过期时间
self._update_expire_time()
def insert(self, index: int, value: T):
"""插入列表项
参数:
index: 插入位置
value: 列表值
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
self._data.insert(index, CacheData(value=value))
self._enforce_limit()
# 更新过期时间
self._update_expire_time()
def pop(self, index: int = -1) -> T:
"""删除并返回列表项
参数:
index: 列表索引,默认为最后一项
返回:
Any: 列表值
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
raise IndexError("从空列表中弹出")
if not self._data:
raise IndexError("从空列表中弹出")
item = self._data.pop(index)
# 更新过期时间
self._update_expire_time()
return item.value
def remove(self, value: T):
"""删除第一个匹配的列表项
参数:
value: 要删除的值
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
raise ValueError(f"{value} 不在列表中")
# 查找匹配的项
for i, item in enumerate(self._data):
if item.value == value:
del self._data[i]
# 更新过期时间
self._update_expire_time()
return
raise ValueError(f"{value} 不在列表中")
def clear(self) -> None:
"""清空列表"""
self._data.clear()
# 重置过期时间
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:
"""查找值的索引
参数:
value: 要查找的值
start: 起始索引
end: 结束索引
返回:
int: 索引位置
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
raise ValueError(f"{value} 不在列表中")
end = end if end is not None else len(self._data)
for i in range(start, min(end, len(self._data))):
if self._data[i].value == value:
return i
raise ValueError(f"{value} 不在列表中")
def count(self, value: T) -> int:
"""计算值出现的次数
参数:
value: 要计数的值
返回:
int: 出现次数
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
return 0
# sourcery skip: simplify-constant-sum
return sum(1 for item in self._data if item.value == value)
def _is_expired(self) -> bool:
"""检查整个列表是否过期"""
return self._expire_time > 0 and self._expire_time < time.time()
def _update_expire_time(self):
"""更新过期时间"""
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:
"""字符串表示
返回:
str: 字符串表示
"""
# 检查整个列表是否过期
if self._is_expired():
self.clear()
return f"CacheList({self.name}, {len(self._data)} items)"
+78 -8
View File
@@ -11,6 +11,7 @@ import uuid
from zhenxun.services.cache.config import CacheMode
from zhenxun.services.log import logger
from zhenxun.services.message_load import is_db_unhealthy
from zhenxun.utils.enum import (
BlockType,
LimitCheckType,
@@ -57,6 +58,7 @@ RUNTIME_CACHE_SYNC_ENABLED = True
RUNTIME_CACHE_SYNC_CHANNEL = "ZHENXUN_RUNTIME_CACHE_SYNC"
RUNTIME_CACHE_LOAD_RETRY_SECONDS = 1.0
RUNTIME_CACHE_STARTUP_REFRESH_SKIP_SECONDS = 5.0
RUNTIME_CACHE_DB_TIMEOUT_SECONDS = 3.0
INSTANCE_ID = uuid.uuid4().hex
@@ -749,6 +751,9 @@ class RuntimeCacheMutation:
@classmethod
async def ensure_loaded(cls, cache_cls: type, label: str) -> None:
if is_db_unhealthy():
cls.mark_error(cache_cls, RuntimeError("database unhealthy"))
return
if getattr(cache_cls, "_loaded", False):
return
now = time.monotonic()
@@ -756,6 +761,9 @@ class RuntimeCacheMutation:
return
lock = cls._load_locks.setdefault(label, asyncio.Lock())
async with lock:
if is_db_unhealthy():
cls.mark_error(cache_cls, RuntimeError("database unhealthy"))
return
if getattr(cache_cls, "_loaded", False):
return
now = time.monotonic()
@@ -768,7 +776,27 @@ class RuntimeCacheMutation:
cls._retry_after[label] = (
time.monotonic() + RUNTIME_CACHE_LOAD_RETRY_SECONDS
)
raise
return
@staticmethod
async def read_db(cache_cls: type, coro, *, operation: str):
from zhenxun.services.db_context import with_db_timeout
if is_db_unhealthy():
RuntimeCacheMutation.mark_error(
cache_cls, RuntimeError("database unhealthy")
)
return None
try:
return await with_db_timeout(
coro,
timeout=RUNTIME_CACHE_DB_TIMEOUT_SECONDS,
operation=operation,
source="runtime_cache",
)
except Exception as exc:
RuntimeCacheMutation.mark_error(cache_cls, exc)
return None
@staticmethod
def mark_refreshed(cache_cls: type) -> None:
@@ -834,7 +862,13 @@ class PluginInfoMemoryCache:
from zhenxun.models.plugin_info import PluginInfo
async with cls._lock:
plugins = await PluginInfo.all()
plugins = await RuntimeCacheMutation.read_db(
cls,
PluginInfo.all(),
operation="PluginInfoMemoryCache.refresh",
)
if plugins is None:
return
by_module: dict[str, PluginInfoSnapshot] = {}
by_module_path: dict[str, PluginInfoSnapshot] = {}
for plugin in plugins:
@@ -1016,7 +1050,13 @@ class BotMemoryCache:
from zhenxun.models.bot_console import BotConsole
async with cls._lock:
records = await BotConsole.all()
records = await RuntimeCacheMutation.read_db(
cls,
BotConsole.all(),
operation="BotMemoryCache.refresh",
)
if records is None:
return
cls._by_id = {str(r.bot_id): BotSnapshot.from_model(r) for r in records}
RuntimeCacheMutation.clear_negative_all(cls)
RuntimeCacheMutation.mark_refreshed(cls)
@@ -1201,7 +1241,13 @@ class GroupMemoryCache:
from zhenxun.models.group_console import GroupConsole
async with cls._lock:
records = await GroupConsole.all()
records = await RuntimeCacheMutation.read_db(
cls,
GroupConsole.all(),
operation="GroupMemoryCache.refresh",
)
if records is None:
return
by_key: dict[tuple[str, str], GroupSnapshot] = {}
for record in records:
entry = GroupSnapshot.from_model(record)
@@ -1374,7 +1420,13 @@ class LevelUserMemoryCache:
from zhenxun.models.level_user import LevelUser
async with cls._lock:
records = await LevelUser.all()
records = await RuntimeCacheMutation.read_db(
cls,
LevelUser.all(),
operation="LevelUserMemoryCache.refresh",
)
if records is None:
return
by_key: dict[tuple[str, str], LevelUserSnapshot] = {}
by_user_max: dict[str, int] = {}
for record in records:
@@ -1607,7 +1659,13 @@ class TaskInfoMemoryCache:
from zhenxun.models.task_info import TaskInfo
async with cls._lock:
records = await TaskInfo.all()
records = await RuntimeCacheMutation.read_db(
cls,
TaskInfo.all(),
operation="TaskInfoMemoryCache.refresh",
)
if records is None:
return
by_module: dict[str, TaskInfoSnapshot] = {}
by_name: dict[str, TaskInfoSnapshot] = {}
for record in records:
@@ -1790,7 +1848,13 @@ class PluginLimitMemoryCache:
from zhenxun.models.plugin_limit import PluginLimit
async with cls._lock:
records = await PluginLimit.filter(status=True).all()
records = await RuntimeCacheMutation.read_db(
cls,
PluginLimit.filter(status=True).all(),
operation="PluginLimitMemoryCache.refresh",
)
if records is None:
return
by_id: dict[int, PluginLimitSnapshot] = {}
by_module: dict[str, list[PluginLimitSnapshot]] = {}
for record in records:
@@ -2015,7 +2079,13 @@ class BanMemoryCache:
async with cls._lock:
now_ts = time.time()
records = await BanConsole.all()
records = await RuntimeCacheMutation.read_db(
cls,
BanConsole.all(),
operation="BanMemoryCache.refresh",
)
if records is None:
return
by_user: dict[str, BanEntry] = {}
by_group: dict[str, BanEntry] = {}
by_user_group: dict[tuple[str, str], BanEntry] = {}