serialize shared-token lichess.org requests to fix accuracy dropping again
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 15s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 15s
games/user and user/activity share one LICHESS_APP_TOKEN process-wide, and Lichess allows only 1 concurrent request per token there (confirmed live: 429 "Please only run 1 request(s) at a time" under concurrent traffic). The existing rate limiter only paced request starts 0.2s apart without holding the lock through the request itself, so concurrent games/period calls (periodic checks vs. on-demand /today etc.) could still collide and get 429'd, silently dropping the accuracy shown in /today, /yesterday, /week (that fetch is best-effort and swallows errors). Hold a dedicated lock for the full request/response cycle on these two calls; puzzle requests use per-user tokens and don't need it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7ef5875f58
commit
31e6fdd23d
2 changed files with 32 additions and 7 deletions
|
|
@ -19,7 +19,7 @@ from typing import List, Dict, Any, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from rate_limiter import get_rate_limiter
|
from rate_limiter import get_rate_limiter, get_shared_token_lock
|
||||||
|
|
||||||
# Настройка логирования для модуля
|
# Настройка логирования для модуля
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -45,6 +45,10 @@ class LichessClient:
|
||||||
self.base_url = "https://lichess.org/api" # Базовый URL Lichess API
|
self.base_url = "https://lichess.org/api" # Базовый URL Lichess API
|
||||||
self.client = httpx.AsyncClient(timeout=30.0) # HTTP клиент с таймаутом
|
self.client = httpx.AsyncClient(timeout=30.0) # HTTP клиент с таймаутом
|
||||||
self.rate_limiter = get_rate_limiter()
|
self.rate_limiter = get_rate_limiter()
|
||||||
|
# Сериализует games/user и user/activity между собой (см. rate_limiter.py) —
|
||||||
|
# обе шарят один LICHESS_APP_TOKEN, и Lichess не пускает по нему 2+
|
||||||
|
# запроса одновременно.
|
||||||
|
self.shared_token_lock = get_shared_token_lock()
|
||||||
# Lichess начал возвращать 404 (вместо честного 401/429) на анонимные
|
# Lichess начал возвращать 404 (вместо честного 401/429) на анонимные
|
||||||
# запросы к games/user и user/activity — тот же валидный токен, отправленный
|
# запросы к games/user и user/activity — тот же валидный токен, отправленный
|
||||||
# для другого юзера, сразу превращает 404 в 429, то есть эндпоинт жив, просто
|
# для другого юзера, сразу превращает 404 в 429, то есть эндпоинт жив, просто
|
||||||
|
|
@ -80,8 +84,11 @@ class LichessClient:
|
||||||
|
|
||||||
headers = {'Authorization': f'Bearer {self.app_token}'} if self.app_token else {}
|
headers = {'Authorization': f'Bearer {self.app_token}'} if self.app_token else {}
|
||||||
|
|
||||||
# Выполняем HTTP GET запрос
|
# Держим shared_token_lock на весь запрос-ответ (не только на паузу
|
||||||
response = await self.client.get(url, headers=headers)
|
# выше) — LICHESS_APP_TOKEN общий с get_games_of_period, и Lichess
|
||||||
|
# не пускает по нему 2+ запроса одновременно.
|
||||||
|
async with self.shared_token_lock:
|
||||||
|
response = await self.client.get(url, headers=headers)
|
||||||
logger.info(f"🔍 Lichess API response status: {response.status_code} for {username}")
|
logger.info(f"🔍 Lichess API response status: {response.status_code} for {username}")
|
||||||
response.raise_for_status() # Проверяем статус ответа
|
response.raise_for_status() # Проверяем статус ответа
|
||||||
|
|
||||||
|
|
@ -152,8 +159,12 @@ class LichessClient:
|
||||||
|
|
||||||
logger.info(f"Запрос игр для {username} с {since_ms} по {until_ms}")
|
logger.info(f"Запрос игр для {username} с {since_ms} по {until_ms}")
|
||||||
|
|
||||||
# Выполняем HTTP GET запрос
|
# Держим shared_token_lock на весь запрос-ответ (не только на паузу
|
||||||
response = await self.client.get(url, params=params, headers=headers)
|
# выше) — LICHESS_APP_TOKEN общий с get_user_activity, и Lichess
|
||||||
|
# не пускает по нему 2+ запроса одновременно (наблюдали 429
|
||||||
|
# "Please only run 1 request(s) at a time" под конкурентной нагрузкой).
|
||||||
|
async with self.shared_token_lock:
|
||||||
|
response = await self.client.get(url, params=params, headers=headers)
|
||||||
response.raise_for_status() # Проверяем статус ответа
|
response.raise_for_status() # Проверяем статус ответа
|
||||||
|
|
||||||
# Парсим NDJSON (Newline Delimited JSON)
|
# Парсим NDJSON (Newline Delimited JSON)
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,17 @@ def get_rate_limiter() -> RateLimiter:
|
||||||
"""Get the global rate limiter instance"""
|
"""Get the global rate limiter instance"""
|
||||||
return _rate_limiter
|
return _rate_limiter
|
||||||
|
|
||||||
|
# wait_if_needed() only paces request STARTS 0.2s apart; it releases the lock
|
||||||
|
# before the request itself runs, so two requests can still be in flight at
|
||||||
|
# once if either takes longer than 0.2s. That's fine for per-user-token calls
|
||||||
|
# (each pair has its own identity), but games/user and user/activity share one
|
||||||
|
# LICHESS_APP_TOKEN process-wide, and Lichess enforces a hard "1 request at a
|
||||||
|
# time" limit per token there (observed as 429 "Please only run 1 request(s)
|
||||||
|
# at a time" under concurrent traffic) — those two calls must hold this lock
|
||||||
|
# for their entire request/response cycle, not just the pre-request wait.
|
||||||
|
_shared_token_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
def get_shared_token_lock() -> asyncio.Lock:
|
||||||
|
"""Lock serializing lichess.org calls made with the shared LICHESS_APP_TOKEN."""
|
||||||
|
return _shared_token_lock
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue