stop infinite retry loop on rejected Lichess tokens
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 12s

A gamer's periodic check would get permanently stuck if their stored token
was revoked/expired: our stats API collapsed both "Lichess rejected the
token" (401/403, permanent) and genuine transient errors into the same 502
response, so the bot treated an invalid token exactly like a network blip —
retrying the same window forever at a capped 300s backoff, never advancing
the checkpoint (observed in prod: Dor1zz stuck for 100+ consecutive errors
over 8+ hours, admin alerts firing every 25 failures).

Preserve the distinction that already existed one layer down (lichess_client.py
already tells 401/403 apart from other failures) instead of collapsing it in
stats_service.py: add PuzzleOfPeriodResponse.auth_failed, have main.py return
401 specifically for that case, and have the bot raise a distinct
InvalidTokenError instead of returning None. On InvalidTokenError, the bot now
clears the token for that pair, notifies the user to reconnect via /addtoken,
and continues tracking games normally instead of stalling forever.
This commit is contained in:
vrubelroman 2026-07-05 07:37:14 +00:00
parent 8080921141
commit 619c00aa06
7 changed files with 70 additions and 16 deletions

View file

@ -23,7 +23,7 @@ from config import (
)
from version import BOT_VERSION
from database import Database
from lichess_api import LichessAPI
from lichess_api import LichessAPI, InvalidTokenError
from formatters import StatsFormatter
from i18n import t
from admin_bot import get_admin_bot, init_admin_bot
@ -1713,19 +1713,38 @@ class LichessBot:
continue
if gamer.get('token'):
# Ошибки получения пазлов обрабатываются так же, как ошибки игр:
# Транзитные ошибки получения пазлов обрабатываются так же, как ошибки игр:
# исключение всплывает во внешний обработчик, чекпоинт не продвигается,
# то же окно ретраится на следующей итерации (не считаем "пазлов не было").
# InvalidTokenError — отдельный случай: Lichess отклонил токен насовсем
# (401), ретраить бессмысленно — это раньше приводило к вечному циклу
# ретраев без продвижения чекпоинта (см. историю с Dor1zz). Вместо этого
# отключаем пазлы для этой пары и продолжаем отслеживать партии как обычно.
logger.info(f"📥 Adding puzzles request to queue for {gamer['username']}")
puzzles_data = await self.request_queue.add_request(
self.lichess_api.get_puzzles_period,
gamer['token'], since_timestamp, until_timestamp_approx, 150
)
if puzzles_data is None:
raise RuntimeError("Puzzles period API returned no data")
# Обновляем фактическое время после получения ответа по пазлам
request_end_time = datetime.now()
logger.info(f"✅ Puzzles API response received for {gamer['username']} at {request_end_time}")
try:
puzzles_data = await self.request_queue.add_request(
self.lichess_api.get_puzzles_period,
gamer['token'], since_timestamp, until_timestamp_approx, 150
)
except InvalidTokenError:
logger.warning(f"🔑 Token rejected by Lichess for {gamer['username']} (user {user_id}); disabling puzzle tracking for this pair")
self.db.clear_user_gamer_token(user_id, gamer['id'])
gamer['token'] = None
puzzles_data = None
try:
user_lang = self.db.get_user_language(user_id)
await self.application.bot.send_message(
chat_id=user_id,
text=t('token_rejected_notice', user_lang, username=gamer['username'])
)
except Exception as notify_err:
logger.error(f"Failed to notify user {user_id} about rejected token: {notify_err}")
else:
if puzzles_data is None:
raise RuntimeError("Puzzles period API returned no data")
# Обновляем фактическое время после получения ответа по пазлам
request_end_time = datetime.now()
logger.info(f"✅ Puzzles API response received for {gamer['username']} at {request_end_time}")
# Сбрасываем счетчик ошибок при успешном запросе
consecutive_errors = 0