feat: 添加 EmojiManager 的配置热重载功能及其注销机制
This commit is contained in:
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@@ -196,8 +197,20 @@ def _install_stub_modules(monkeypatch):
|
|||||||
emoji = _EmojiConfig()
|
emoji = _EmojiConfig()
|
||||||
bot = _BotConfig()
|
bot = _BotConfig()
|
||||||
|
|
||||||
|
class _ConfigManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.reload_callbacks = []
|
||||||
|
|
||||||
|
def register_reload_callback(self, callback):
|
||||||
|
self.reload_callbacks.append(callback)
|
||||||
|
|
||||||
|
def unregister_reload_callback(self, callback):
|
||||||
|
if callback in self.reload_callbacks:
|
||||||
|
self.reload_callbacks.remove(callback)
|
||||||
|
|
||||||
config_mod.global_config = _GlobalConfig()
|
config_mod.global_config = _GlobalConfig()
|
||||||
config_mod.model_config = _ModelConfig()
|
config_mod.model_config = _ModelConfig()
|
||||||
|
config_mod.config_manager = _ConfigManager()
|
||||||
|
|
||||||
# src.llm_models.utils_model
|
# src.llm_models.utils_model
|
||||||
llm_mod = _stub_module("src.llm_models.utils_model")
|
llm_mod = _stub_module("src.llm_models.utils_model")
|
||||||
@@ -479,6 +492,62 @@ def test_load_emojis_from_db_empty(monkeypatch):
|
|||||||
assert any("成功加载" in m for m in _messages(logger.info_calls))
|
assert any("成功加载" in m for m in _messages(logger.info_calls))
|
||||||
|
|
||||||
|
|
||||||
|
def test_emoji_manager_registers_reload_callback(monkeypatch):
|
||||||
|
emoji_manager_new = import_emoji_manager_new(monkeypatch)
|
||||||
|
|
||||||
|
assert emoji_manager_new.emoji_manager.reload_runtime_config in emoji_manager_new.config_manager.reload_callbacks
|
||||||
|
|
||||||
|
|
||||||
|
def test_emoji_manager_shutdown_unregisters_reload_callback(monkeypatch):
|
||||||
|
emoji_manager_new = import_emoji_manager_new(monkeypatch)
|
||||||
|
manager = emoji_manager_new.EmojiManager()
|
||||||
|
|
||||||
|
assert manager.reload_runtime_config in emoji_manager_new.config_manager.reload_callbacks
|
||||||
|
|
||||||
|
manager.shutdown()
|
||||||
|
|
||||||
|
assert manager.reload_runtime_config not in emoji_manager_new.config_manager.reload_callbacks
|
||||||
|
|
||||||
|
# 重复调用应保持幂等,不应抛错也不应重复注册
|
||||||
|
manager.shutdown()
|
||||||
|
|
||||||
|
assert manager.reload_runtime_config not in emoji_manager_new.config_manager.reload_callbacks
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reload_runtime_config_wakes_maintenance_loop(monkeypatch):
|
||||||
|
emoji_manager_new = import_emoji_manager_new(monkeypatch)
|
||||||
|
manager = emoji_manager_new.EmojiManager()
|
||||||
|
|
||||||
|
emoji_manager_new.global_config.emoji.steal_emoji = False
|
||||||
|
emoji_manager_new.global_config.emoji.check_interval = 60
|
||||||
|
|
||||||
|
maintenance_runs = 0
|
||||||
|
second_run_event = asyncio.Event()
|
||||||
|
|
||||||
|
def _check_emoji_file_integrity():
|
||||||
|
nonlocal maintenance_runs
|
||||||
|
maintenance_runs += 1
|
||||||
|
if maintenance_runs >= 2:
|
||||||
|
second_run_event.set()
|
||||||
|
|
||||||
|
monkeypatch.setattr(manager, "check_emoji_file_integrity", _check_emoji_file_integrity)
|
||||||
|
monkeypatch.setattr(manager, "remove_untracked_emoji_files", lambda: None)
|
||||||
|
|
||||||
|
task = asyncio.create_task(manager.periodic_emoji_maintenance())
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
assert maintenance_runs >= 1
|
||||||
|
|
||||||
|
manager.reload_runtime_config()
|
||||||
|
|
||||||
|
await asyncio.wait_for(second_run_event.wait(), timeout=0.2)
|
||||||
|
finally:
|
||||||
|
task.cancel()
|
||||||
|
with pytest.raises(asyncio.CancelledError):
|
||||||
|
await task
|
||||||
|
|
||||||
|
|
||||||
def test_load_emojis_from_db_partial_bad_records(monkeypatch):
|
def test_load_emojis_from_db_partial_bad_records(monkeypatch):
|
||||||
emoji_manager_new = import_emoji_manager_new(monkeypatch)
|
emoji_manager_new = import_emoji_manager_new(monkeypatch)
|
||||||
logger = emoji_manager_new.logger
|
logger = emoji_manager_new.logger
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ from src.common.database.database_model import Images, ImageType
|
|||||||
from src.common.database.database import get_db_session, get_db_session_manual
|
from src.common.database.database import get_db_session, get_db_session_manual
|
||||||
from src.common.utils.utils_image import ImageUtils
|
from src.common.utils.utils_image import ImageUtils
|
||||||
from src.prompt.prompt_manager import prompt_manager
|
from src.prompt.prompt_manager import prompt_manager
|
||||||
from src.config.config import global_config
|
from src.config.config import config_manager, global_config, model_config
|
||||||
from src.config.config import model_config
|
|
||||||
from src.llm_models.utils_model import LLMRequest
|
from src.llm_models.utils_model import LLMRequest
|
||||||
|
|
||||||
logger = get_logger("emoji")
|
logger = get_logger("emoji")
|
||||||
@@ -53,9 +52,28 @@ class EmojiManager:
|
|||||||
|
|
||||||
self._emoji_num: int = 0
|
self._emoji_num: int = 0
|
||||||
self.emojis: list[MaiEmoji] = []
|
self.emojis: list[MaiEmoji] = []
|
||||||
|
self._maintenance_wakeup_event = asyncio.Event()
|
||||||
|
self._reload_callback_registered = False
|
||||||
|
|
||||||
|
config_manager.register_reload_callback(self.reload_runtime_config)
|
||||||
|
self._reload_callback_registered = True
|
||||||
|
|
||||||
logger.info("启动表情包管理器")
|
logger.info("启动表情包管理器")
|
||||||
|
|
||||||
|
def reload_runtime_config(self) -> None:
|
||||||
|
"""响应配置热重载,唤醒维护循环以尽快应用最新配置。"""
|
||||||
|
self._maintenance_wakeup_event.set()
|
||||||
|
logger.info("[配置热重载] Emoji 模块配置已更新,将立即应用到维护循环")
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
"""清理 EmojiManager 生命周期资源。"""
|
||||||
|
if not self._reload_callback_registered:
|
||||||
|
return
|
||||||
|
config_manager.unregister_reload_callback(self.reload_runtime_config)
|
||||||
|
self._reload_callback_registered = False
|
||||||
|
self._maintenance_wakeup_event.set()
|
||||||
|
logger.info("[关闭] Emoji 模块已注销配置热重载回调")
|
||||||
|
|
||||||
async def get_emoji_description(
|
async def get_emoji_description(
|
||||||
self, *, emoji_bytes: Optional[bytes] = None, emoji_hash: Optional[str] = None
|
self, *, emoji_bytes: Optional[bytes] = None, emoji_hash: Optional[str] = None
|
||||||
) -> Optional[Tuple[str, List[str]]]:
|
) -> Optional[Tuple[str, List[str]]]:
|
||||||
@@ -640,7 +658,13 @@ class EmojiManager:
|
|||||||
logger.info(f"[定期维护] 删除无法注册的表情包文件: {emoji_file.name}")
|
logger.info(f"[定期维护] 删除无法注册的表情包文件: {emoji_file.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[定期维护] 删除文件 {emoji_file.name} 时出错: {e}")
|
logger.error(f"[定期维护] 删除文件 {emoji_file.name} 时出错: {e}")
|
||||||
await asyncio.sleep(global_config.emoji.check_interval * 60)
|
wait_seconds = max(global_config.emoji.check_interval * 60, 0)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self._maintenance_wakeup_event.wait(), timeout=wait_seconds)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._maintenance_wakeup_event.clear()
|
||||||
|
|
||||||
async def register_emoji_by_filename(self, filename: Path | str) -> bool:
|
async def register_emoji_by_filename(self, filename: Path | str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ async def main():
|
|||||||
system.schedule_tasks(),
|
system.schedule_tasks(),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
emoji_manager.shutdown()
|
||||||
await config_manager.stop_file_watcher()
|
await config_manager.stop_file_watcher()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user