fix periodic activity-check algorithm silently losing player activity
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 38s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 38s
Errors from Lichess (timeouts/5xx/invalid tokens) were being disguised as "no activity" (HTTP 200, games_count/puzzles_in_period=0), causing the bot to silently advance its checkpoint past real, undetected activity. Puzzle-fetch failures weren't counted as errors at all, and the periodic task died permanently after 5 consecutive errors with no way to recover short of a manual restart. /setperiod also unconditionally reset the checkpoint, dropping the window between the last check and the command. - API now returns success=false/502 on real errors instead of masking them as zero activity (models.py, stats_service.py, main.py) - Puzzle-fetch errors are now treated the same as game-fetch errors: retry the same window instead of reporting "no puzzles" - Notification delivery failures no longer silently advance the checkpoint - Replaced the hard 5-error kill switch with capped backoff that keeps retrying indefinitely, plus an admin-bot notification if a player's monitoring has been failing for a prolonged period (~2h+) - /setperiod only clears the checkpoint when disabling notifications, preserving continuity when a period is just changed Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
75e62b57a1
commit
4a783225af
4 changed files with 118 additions and 58 deletions
|
|
@ -555,7 +555,12 @@ async def get_games_of_period(
|
|||
since_seconds = since // 1000
|
||||
until_seconds = until // 1000
|
||||
result = await stats_service.get_games_of_period(username, since_seconds, until_seconds, rated_only)
|
||||
if not result.success:
|
||||
# Реальная ошибка при обращении к Lichess — не маскируем её под "0 игр"
|
||||
raise HTTPException(status_code=502, detail=result.message)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в endpoint get_games_of_period: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
|
@ -695,7 +700,12 @@ async def get_puzzle_of_period(
|
|||
|
||||
try:
|
||||
result = await stats_service.get_puzzle_of_period(token, since, until, max)
|
||||
if not result.success:
|
||||
# Реальная ошибка при обращении к Lichess — не маскируем её под "0 пазлов"
|
||||
raise HTTPException(status_code=502, detail=result.message)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в endpoint get_puzzle_of_period: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ class GamesOfPeriodResponse(BaseModel):
|
|||
Содержит метаинформацию о запросе и агрегированную статистику игр.
|
||||
"""
|
||||
message: str = Field(..., description="Сообщение о результате запроса", example="Статистика игр за период")
|
||||
success: bool = Field(True, description="False, если запрос к Lichess завершился ошибкой (а не легитимным нулевым результатом)", example=True)
|
||||
username: str = Field(..., description="Имя пользователя", example="magnus")
|
||||
period_start: int = Field(..., description="Начало периода (Unix timestamp)", example=1640995200)
|
||||
period_end: int = Field(..., description="Конец периода (Unix timestamp)", example=1641081600)
|
||||
|
|
@ -232,6 +233,7 @@ class PuzzleOfPeriodResponse(BaseModel):
|
|||
Содержит метаинформацию о запросе и агрегированную статистику решения задач.
|
||||
"""
|
||||
message: str = Field(..., description="Сообщение о результате запроса", example="Статистика решения задач за период")
|
||||
success: bool = Field(True, description="False, если запрос к Lichess завершился ошибкой (а не легитимным нулевым результатом)", example=True)
|
||||
period_start: int = Field(..., description="Начало периода (Unix timestamp в миллисекундах)", example=1640995200000)
|
||||
period_end: int = Field(..., description="Конец периода (Unix timestamp в миллисекундах)", example=1641081600000)
|
||||
max_puzzles: int = Field(..., description="Максимальное количество задач для получения", example=50)
|
||||
|
|
|
|||
|
|
@ -699,6 +699,7 @@ class StatsService:
|
|||
logger.error(f"Ошибка при получении статистики игр за период: {e}")
|
||||
return GamesOfPeriodResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}",
|
||||
success=False,
|
||||
username=username,
|
||||
period_start=since_timestamp,
|
||||
period_end=until_timestamp,
|
||||
|
|
@ -765,6 +766,7 @@ class StatsService:
|
|||
if activities is None:
|
||||
return PuzzleOfPeriodResponse(
|
||||
message="Неверный токен авторизации или доступ запрещен",
|
||||
success=False,
|
||||
period_start=since_ms,
|
||||
period_end=until_ms,
|
||||
max_puzzles=max_puzzles,
|
||||
|
|
@ -796,6 +798,7 @@ class StatsService:
|
|||
logger.error(f"Ошибка при получении статистики решения задач за период: {e}")
|
||||
return PuzzleOfPeriodResponse(
|
||||
message=f"Ошибка при получении статистики: {str(e)}",
|
||||
success=False,
|
||||
period_start=since_ms,
|
||||
period_end=until_ms,
|
||||
max_puzzles=max_puzzles,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue