fix(score): preload beatmap when creating score token

This commit is contained in:
MingxuanGame
2025-08-18 09:58:11 +00:00
parent 7510b4fae1
commit cc26df253c
2 changed files with 46 additions and 51 deletions

View File

@@ -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,

View File

@@ -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}")