From 177a10fbd10554f7891b2344f11a629949c7ad59 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sat, 4 Jul 2026 11:43:40 +0800 Subject: [PATCH] fix(storage): inject Redis/MySQL clients into StorageManager singleton --- app/main.py | 19 +++++++++++++++---- app/storage_manager.py | 13 +++++++++++-- tests/test_main_lifespan.py | 38 +++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index 5a6576e..a3043ac 100644 --- a/app/main.py +++ b/app/main.py @@ -39,16 +39,23 @@ async def lifespan(app: FastAPI): # 初始化 Redis 和 MySQL from app.redis_client import init_redis from app.mysql_database import init_mysql + from app.storage_manager import get_storage_manager + + redis_client = None + mysql_engine = None try: - redis_ok = init_redis() is not None + redis_client = init_redis() except Exception: - redis_ok = False + redis_client = None try: - mysql_ok = init_mysql() is not None + mysql_engine = init_mysql() except Exception: - mysql_ok = False + mysql_engine = None + + redis_ok = redis_client is not None + mysql_ok = mysql_engine is not None if redis_ok and mysql_ok: logger.info("存储模式: Redis + MySQL") @@ -57,6 +64,10 @@ async def lifespan(app: FastAPI): else: logger.error("存储模式: SQLite (Redis 和 MySQL 均不可用)") + # 将初始化后的客户端注入 StorageManager 全局单例 + storage_manager = get_storage_manager() + storage_manager.initialize(redis_client, mysql_engine) + if mysql_ok: from app.mysql_database import mysql_engine from app.models import Base as MarketBase, MarketData, SymbolTimestamp diff --git a/app/storage_manager.py b/app/storage_manager.py index bad243b..762e2d0 100644 --- a/app/storage_manager.py +++ b/app/storage_manager.py @@ -85,8 +85,17 @@ class StorageManager: """判断 Redis 或 MySQL 至少一个可用。""" return self.check_redis() or self.check_mysql() - def initialize(self): - """启动时调用,检测各后端可用性并输出日志。""" + def initialize(self, redis_client=None, mysql_engine=None): + """启动时调用,可选注入 Redis/MySQL 客户端并检测各后端可用性。""" + if redis_client is not None: + self.redis_client = redis_client + if mysql_engine is not None: + self.mysql_engine = mysql_engine + + # 重置检测缓存,确保注入新客户端后重新探测后端可用性 + self.last_redis_check = 0 + self.last_mysql_check = 0 + redis_ok = self.check_redis() mysql_ok = self.check_mysql() diff --git a/tests/test_main_lifespan.py b/tests/test_main_lifespan.py index 2fe3fe1..2ab6b07 100644 --- a/tests/test_main_lifespan.py +++ b/tests/test_main_lifespan.py @@ -40,6 +40,11 @@ def patch_lifespan_deps(): for p in patches: p.stop() + # 清理 StorageManager 全局单例,避免测试间状态泄漏 + import app.storage_manager as _sm_module + + _sm_module._storage_manager = None + @pytest.mark.asyncio async def test_lifespan_calls_redis_and_mysql_init(patch_lifespan_deps, caplog): @@ -145,3 +150,36 @@ async def test_lifespan_storage_init_failure_does_not_raise(patch_lifespan_deps, pass assert "存储模式: SQLite (Redis 和 MySQL 均不可用)" in caplog.text + + +def test_storage_manager_initialize_accepts_clients_and_updates_references(): + """initialize 应可选接收 redis_client 和 mysql_engine 并更新内部引用。""" + from app.storage_manager import StorageManager + + manager = StorageManager(redis_client=None, mysql_engine=None) + fake_redis = MagicMock() + fake_mysql = MagicMock() + + manager.initialize(redis_client=fake_redis, mysql_engine=fake_mysql) + + assert manager.redis_client is fake_redis + assert manager.mysql_engine is fake_mysql + + +@pytest.mark.asyncio +async def test_lifespan_injects_redis_and_mysql_into_storage_manager(patch_lifespan_deps): + """lifespan 初始化后应将 Redis/MySQL 客户端注入 StorageManager 单例。""" + from app.main import lifespan, app + from app.storage_manager import StorageManager + + fake_redis = MagicMock() + fake_mysql = MagicMock() + patch_lifespan_deps["init_redis"].return_value = fake_redis + patch_lifespan_deps["init_mysql"].return_value = fake_mysql + + mock_manager = MagicMock(spec=StorageManager) + with patch("app.storage_manager.get_storage_manager", return_value=mock_manager): + async with lifespan(app): + pass + + mock_manager.initialize.assert_called_once_with(fake_redis, fake_mysql)