From cc26df253c31a20475001e889e139fe9d4f7da3b Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Mon, 18 Aug 2025 09:58:11 +0000 Subject: [PATCH] fix(score): preload beatmap when creating score token --- app/router/v2/score.py | 46 ++++++++++++++++++++++++++++++-- app/signalr/hub/spectator.py | 51 ++---------------------------------- 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/app/router/v2/score.py b/app/router/v2/score.py index c57bea6..3590697 100644 --- a/app/router/v2/score.py +++ b/app/router/v2/score.py @@ -50,7 +50,17 @@ from app.storage.local import LocalStorageService from .router import router -from fastapi import Body, Depends, Form, HTTPException, Path, Query, Request, Security +from fastapi import ( + BackgroundTasks, + Body, + Depends, + Form, + HTTPException, + Path, + Query, + Request, + Security, +) from fastapi.responses import FileResponse, RedirectResponse from httpx import HTTPError from pydantic import BaseModel @@ -99,6 +109,7 @@ async def submit_score( # 智能预取beatmap缓存(异步进行,不阻塞主流程) try: from app.service.beatmap_cache_service import get_beatmap_cache_service + cache_service = get_beatmap_cache_service(redis, fetcher) await cache_service.smart_preload_for_score(beatmap) except Exception as e: @@ -167,6 +178,34 @@ async def submit_score( return resp +async def _preload_beatmap_for_pp_calculation(beatmap_id: int) -> None: + """ + 预缓存beatmap文件以加速PP计算 + 当玩家开始游玩时异步预加载beatmap原始文件到Redis缓存 + """ + # 检查是否启用了beatmap预加载功能 + if not settings.enable_beatmap_preload: + return + + try: + # 异步获取fetcher和redis连接 + fetcher = await get_fetcher() + redis = get_redis() + + # 检查是否已经缓存,避免重复下载 + cache_key = f"beatmap:raw:{beatmap_id}" + if await redis.exists(cache_key): + logger.debug(f"Beatmap {beatmap_id} already cached, skipping preload") + return + + await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id) + logger.debug(f"Successfully preloaded beatmap {beatmap_id} for PP calculation") + + except Exception as e: + # 预缓存失败不应该影响正常游戏流程 + logger.warning(f"Failed to preload beatmap {beatmap_id}: {e}") + + class BeatmapScores(BaseModel): scores: list[ScoreResp] user_score: BeatmapUserScore | None = None @@ -311,6 +350,7 @@ async def get_user_all_beatmap_scores( description="**客户端专属**\n为指定谱面创建一次性的成绩提交令牌。", ) async def create_solo_score( + background_task: BackgroundTasks, beatmap_id: int = Path(description="谱面 ID"), version_hash: str = Form("", description="游戏版本哈希"), beatmap_hash: str = Form(description="谱面文件哈希"), @@ -319,6 +359,7 @@ async def create_solo_score( db: AsyncSession = Depends(get_db), ): assert current_user.id is not None + background_task.add_task(_preload_beatmap_for_pp_calculation, beatmap_id) async with db: score_token = ScoreToken( user_id=current_user.id, @@ -360,6 +401,7 @@ async def submit_solo_score( description="**客户端专属**\n为房间游玩项目创建成绩提交令牌。", ) async def create_playlist_score( + background_task: BackgroundTasks, room_id: int, playlist_id: int, beatmap_id: int = Form(description="谱面 ID"), @@ -415,7 +457,7 @@ async def create_playlist_score( status_code=400, detail="Playlist item has already been played" ) # 这里应该不用验证mod了吧。。。 - + background_task.add_task(_preload_beatmap_for_pp_calculation, beatmap_id) score_token = ScoreToken( user_id=current_user.id, beatmap_id=beatmap_id, diff --git a/app/signalr/hub/spectator.py b/app/signalr/hub/spectator.py index 5bee18e..12e7fe7 100644 --- a/app/signalr/hub/spectator.py +++ b/app/signalr/hub/spectator.py @@ -223,8 +223,8 @@ class SpectatorHub(Hub[StoreClientState]): ) logger.info(f"[SpectatorHub] {client.user_id} began playing {state.beatmap_id}") - # 预缓存beatmap文件以加速后续PP计算 - await self._preload_beatmap_for_pp_calculation(state.beatmap_id) + # # 预缓存beatmap文件以加速后续PP计算 + # await self._preload_beatmap_for_pp_calculation(state.beatmap_id) await self.broadcast_group_call( self.group_id(user_id), @@ -450,50 +450,3 @@ class SpectatorHub(Hub[StoreClientState]): if (target_client := self.get_client_by_id(str(target_id))) is not None: await self.call_noblock(target_client, "UserEndedWatching", user_id) logger.info(f"[SpectatorHub] {user_id} ended watching {target_id}") - - async def _preload_beatmap_for_pp_calculation(self, beatmap_id: int) -> None: - """ - 预缓存beatmap文件以加速PP计算 - 当玩家开始游玩时异步预加载beatmap原始文件到Redis缓存 - """ - # 检查是否启用了beatmap预加载功能 - if not settings.enable_beatmap_preload: - return - - try: - # 异步获取fetcher和redis连接 - from app.dependencies.database import get_redis - from app.dependencies.fetcher import get_fetcher - - fetcher = get_fetcher() - redis = get_redis() - - # 检查是否已经缓存,避免重复下载 - cache_key = f"beatmap:raw:{beatmap_id}" - if await redis.exists(cache_key): - logger.debug(f"Beatmap {beatmap_id} already cached, skipping preload") - return - - # 在后台异步预缓存beatmap文件,存储任务引用防止被回收 - task = asyncio.create_task( - self._fetch_beatmap_background(fetcher, redis, beatmap_id) - ) - # 任务完成后自动清理,避免内存泄漏 - task.add_done_callback(lambda t: None) - - except Exception as e: - # 预缓存失败不应该影响正常游戏流程 - logger.warning(f"Failed to preload beatmap {beatmap_id}: {e}") - - async def _fetch_beatmap_background(self, fetcher, redis, beatmap_id: int) -> None: - """ - 后台获取beatmap文件 - """ - try: - # 使用fetcher的get_or_fetch_beatmap_raw方法预缓存 - await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id) - logger.debug( - f"Successfully preloaded beatmap {beatmap_id} for PP calculation" - ) - except Exception as e: - logger.debug(f"Failed to preload beatmap {beatmap_id}: {e}")