fix(fetcher): handle token timeout gracefully

This commit is contained in:
咕谷酱
2025-11-25 11:08:51 +08:00
parent 7b34bd8b0b
commit db430db01b
2 changed files with 68 additions and 31 deletions

View File

@@ -3,10 +3,13 @@ from typing import Annotated
from app.config import settings
from app.dependencies.database import get_redis
from app.fetcher import Fetcher as OriginFetcher
from app.fetcher._base import TokenAuthError
from app.log import fetcher_logger
from fastapi import Depends
fetcher: OriginFetcher | None = None
logger = fetcher_logger("FetcherDependency")
async def get_fetcher() -> OriginFetcher:
@@ -24,7 +27,14 @@ async def get_fetcher() -> OriginFetcher:
if access_token:
fetcher.access_token = str(access_token)
# Always ensure the access token is valid, regardless of initial state
await fetcher.ensure_valid_access_token()
try:
await fetcher.ensure_valid_access_token()
except TokenAuthError as exc:
logger.warning(
f"Failed to refresh fetcher access token during startup: {exc}. Will retry on demand."
)
except Exception as exc: # noqa: BLE001
logger.exception("Unexpected error while initializing fetcher access token", exc_info=exc)
return fetcher

View File

@@ -5,7 +5,7 @@ import time
from app.dependencies.database import get_redis
from app.log import fetcher_logger
from httpx import AsyncClient, HTTPStatusError
from httpx import AsyncClient, HTTPStatusError, TimeoutException
class TokenAuthError(Exception):
@@ -159,35 +159,62 @@ class BaseFetcher:
return True
return self.token_expiry <= int(time.time()) or not self.access_token
async def grant_access_token(self) -> None:
async with AsyncClient() as client:
response = await client.post(
"https://osu.ppy.sh/oauth/token",
data={
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": "client_credentials",
"scope": "public",
},
)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = int(time.time()) + token_data["expires_in"]
redis = get_redis()
await redis.set(
f"fetcher:access_token:{self.client_id}",
self.access_token,
ex=token_data["expires_in"],
)
await redis.set(
f"fetcher:expire_at:{self.client_id}",
self.token_expiry,
ex=token_data["expires_in"],
)
logger.success(
f"Granted new access token for client {self.client_id}, expires in {token_data['expires_in']} seconds"
)
async def grant_access_token(self, retries: int = 3, backoff: float = 1.0) -> None:
last_error: Exception | None = None
async with AsyncClient(timeout=30.0) as client:
for attempt in range(1, retries + 1):
try:
response = await client.post(
"https://osu.ppy.sh/oauth/token",
data={
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": "client_credentials",
"scope": "public",
},
)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = int(time.time()) + token_data["expires_in"]
redis = get_redis()
await redis.set(
f"fetcher:access_token:{self.client_id}",
self.access_token,
ex=token_data["expires_in"],
)
await redis.set(
f"fetcher:expire_at:{self.client_id}",
self.token_expiry,
ex=token_data["expires_in"],
)
logger.success(
f"Granted new access token for client {self.client_id}, expires in {token_data['expires_in']} seconds"
)
return
except TimeoutException as exc:
last_error = exc
logger.warning(
f"Timed out while requesting access token for client {self.client_id} (attempt {attempt}/{retries})"
)
except HTTPStatusError as exc:
last_error = exc
logger.warning(
f"HTTP error while requesting access token for client {self.client_id}"
f" (status: {exc.response.status_code}, attempt {attempt}/{retries})"
)
except Exception as exc: # noqa: BLE001
last_error = exc
logger.exception(
f"Unexpected error while requesting access token for client {self.client_id}"
f" (attempt {attempt}/{retries})"
)
if attempt < retries:
await asyncio.sleep(backoff * attempt)
raise TokenAuthError("Failed to grant access token after retries") from last_error
async def ensure_valid_access_token(self) -> None:
if self.is_token_expired():