fix silent addgamer drop on restart and periodic-check backlog buildup
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 13s

Persist user_data (PicklePersistence) so an in-flight /addgamer username
prompt survives a bot restart instead of being silently swallowed by
handle_username when the in-memory awaiting flag is gone.

Collapse periodic-check backlog into a single request spanning the whole
missed gap instead of replaying it one period_minutes window at a time —
with enough tracked gamers sharing one RequestQueue, per-window replay
could never catch up and the checkpoint fell further behind indefinitely.
Notification period label now reflects the actual queried span (minutes/
hours/days) instead of the configured interval, so a weeks-old catch-up
no longer gets mislabeled as "for 15 minutes".
This commit is contained in:
vrubelroman 2026-07-04 18:33:45 +00:00
parent 62cdd750f8
commit b0173950b8
3 changed files with 62 additions and 21 deletions

View file

@ -8,14 +8,16 @@ from pathlib import Path
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application, CommandHandler, CallbackQueryHandler,
MessageHandler, filters, ContextTypes, ConversationHandler
Application, CommandHandler, CallbackQueryHandler,
MessageHandler, filters, ContextTypes, ConversationHandler,
PicklePersistence
)
from config import (
TELEGRAM_BOT_TOKEN, PERIOD_OPTIONS, POLL_INTERVAL,
TELEGRAM_BOT_TOKEN, PERIOD_OPTIONS, POLL_INTERVAL,
POLL_TIMEOUT, DROP_PENDING_UPDATES, ALLOWED_UPDATES,
LICHESS_STATS_API_BASE_URL, ADMINPANEL_TELEGRAM_BOT_TOKEN
LICHESS_STATS_API_BASE_URL, ADMINPANEL_TELEGRAM_BOT_TOKEN,
DATABASE_PATH
)
from version import BOT_VERSION
from database import Database
@ -1587,18 +1589,32 @@ class LichessBot:
# Рассчитываем, когда должен начаться следующий период
next_period_start = last_check_time + timedelta(minutes=period_minutes)
now = datetime.now()
# Если следующий период еще не наступил, ждем
if next_period_start > now:
wait_seconds = (next_period_start - now).total_seconds()
logger.info(f"⏳ Waiting {wait_seconds:.1f} seconds until next period start ({next_period_start}) for {username}")
await asyncio.sleep(wait_seconds)
# Используем сохраненное время как начало периода
since_time = last_check_time
# Конец периода - это момент, когда должен был начаться следующий период
period_end_approx = next_period_start
logger.info(f"📌 Using saved period: from {since_time} to {period_end_approx}")
# Используем сохраненное время как начало периода
since_time = last_check_time
# Конец периода - это момент, когда должен был начаться следующий период
period_end_approx = next_period_start
logger.info(f"📌 Using saved period: from {since_time} to {period_end_approx}")
else:
# Мы отстаём от расписания (бэклог): если догонять по period_minutes
# за раз, при большом отставании (недели простоя, много отслеживаемых
# игроков через один общий RequestQueue) чекпоинт никогда не догонит
# текущее время — отставание только растёт. Вместо этого закрываем
# весь пропущенный промежуток одним запросом и сразу прыгаем к "сейчас".
since_time = last_check_time
period_end_approx = now
if period_end_approx - next_period_start > timedelta(minutes=period_minutes):
logger.warning(
f"{username} is behind schedule by "
f"{(now - next_period_start).total_seconds():.0f}s; "
f"collapsing backlog into one request from {since_time} to {period_end_approx}"
)
logger.info(f"📌 Using saved period: from {since_time} to {period_end_approx}")
else:
# Первая проверка - ждем period_minutes минут от момента запуска
logger.info(f"⏳ First check: waiting {period_minutes} minutes before first check for {username}")
@ -1729,8 +1745,13 @@ class LichessBot:
if has_games or has_puzzles:
logger.info(f"📊 Activity detected for {gamer['username']}, preparing notification...")
user_lang = self.db.get_user_language(user_id)
# Label the notification with the actual queried window, not the
# configured period: after a backlog collapse (see above) they can
# differ by days/weeks, and showing "for 15 minutes" on activity
# that's actually from weeks ago is misleading.
actual_span_minutes = max(1, round((period_end_approx - since_time).total_seconds() / 60))
notification = StatsFormatter.format_period_notification(
gamer['username'], games_data, puzzles_data, period_minutes, lang=user_lang
gamer['username'], games_data, puzzles_data, actual_span_minutes, lang=user_lang
)
if not self.application:
@ -1840,9 +1861,15 @@ def main():
init_admin_bot()
bot = LichessBot()
# Persist user_data (e.g. awaiting_addgamer_username) to disk so an in-flight
# /addgamer conversation survives a bot restart instead of silently dropping
# the next message the user sends.
persistence_path = os.path.join(os.path.dirname(DATABASE_PATH), "bot_persistence.pickle")
persistence = PicklePersistence(filepath=persistence_path)
# Create application with Long Polling configuration
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
application = Application.builder().token(TELEGRAM_BOT_TOKEN).persistence(persistence).build()
# Setup handlers
bot.setup_handlers(application)