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
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:
parent
62cdd750f8
commit
b0173950b8
3 changed files with 62 additions and 21 deletions
|
|
@ -9,13 +9,15 @@ from pathlib import Path
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
Application, CommandHandler, CallbackQueryHandler,
|
Application, CommandHandler, CallbackQueryHandler,
|
||||||
MessageHandler, filters, ContextTypes, ConversationHandler
|
MessageHandler, filters, ContextTypes, ConversationHandler,
|
||||||
|
PicklePersistence
|
||||||
)
|
)
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
TELEGRAM_BOT_TOKEN, PERIOD_OPTIONS, POLL_INTERVAL,
|
TELEGRAM_BOT_TOKEN, PERIOD_OPTIONS, POLL_INTERVAL,
|
||||||
POLL_TIMEOUT, DROP_PENDING_UPDATES, ALLOWED_UPDATES,
|
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 version import BOT_VERSION
|
||||||
from database import Database
|
from database import Database
|
||||||
|
|
@ -1593,12 +1595,26 @@ class LichessBot:
|
||||||
wait_seconds = (next_period_start - now).total_seconds()
|
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}")
|
logger.info(f"⏳ Waiting {wait_seconds:.1f} seconds until next period start ({next_period_start}) for {username}")
|
||||||
await asyncio.sleep(wait_seconds)
|
await asyncio.sleep(wait_seconds)
|
||||||
|
|
||||||
# Используем сохраненное время как начало периода
|
# Используем сохраненное время как начало периода
|
||||||
since_time = last_check_time
|
since_time = last_check_time
|
||||||
# Конец периода - это момент, когда должен был начаться следующий период
|
# Конец периода - это момент, когда должен был начаться следующий период
|
||||||
period_end_approx = next_period_start
|
period_end_approx = next_period_start
|
||||||
logger.info(f"📌 Using saved period: from {since_time} to {period_end_approx}")
|
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:
|
else:
|
||||||
# Первая проверка - ждем period_minutes минут от момента запуска
|
# Первая проверка - ждем period_minutes минут от момента запуска
|
||||||
logger.info(f"⏳ First check: waiting {period_minutes} minutes before first check for {username}")
|
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:
|
if has_games or has_puzzles:
|
||||||
logger.info(f"📊 Activity detected for {gamer['username']}, preparing notification...")
|
logger.info(f"📊 Activity detected for {gamer['username']}, preparing notification...")
|
||||||
user_lang = self.db.get_user_language(user_id)
|
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(
|
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:
|
if not self.application:
|
||||||
|
|
@ -1841,8 +1862,14 @@ def main():
|
||||||
|
|
||||||
bot = LichessBot()
|
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
|
# 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
|
# Setup handlers
|
||||||
bot.setup_handlers(application)
|
bot.setup_handlers(application)
|
||||||
|
|
|
||||||
|
|
@ -283,13 +283,23 @@ class StatsFormatter:
|
||||||
"""Format notification for periodic checks"""
|
"""Format notification for periodic checks"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Format period text
|
# Format period text. period_minutes here is the actual queried span, which
|
||||||
|
# can be much larger than the configured check interval after a backlog
|
||||||
|
# collapse (see periodic_check in bot.py) — fall back to hours/days so a
|
||||||
|
# weeks-old catch-up doesn't get mislabeled as "for 15 minutes".
|
||||||
|
if period_minutes < 60:
|
||||||
if period_minutes == 1:
|
if period_minutes == 1:
|
||||||
period_text = t('period_1_minute', lang)
|
period_text = t('period_1_minute', lang)
|
||||||
elif period_minutes in [2, 3, 4]:
|
elif period_minutes in [2, 3, 4]:
|
||||||
period_text = t('period_2_3_4_minutes', lang, period=period_minutes)
|
period_text = t('period_2_3_4_minutes', lang, period=period_minutes)
|
||||||
else:
|
else:
|
||||||
period_text = t('period_minutes_text', lang, period=period_minutes)
|
period_text = t('period_minutes_text', lang, period=period_minutes)
|
||||||
|
elif period_minutes < 1440:
|
||||||
|
hours = round(period_minutes / 60)
|
||||||
|
period_text = t('period_hours_text', lang, hours=hours)
|
||||||
|
else:
|
||||||
|
days = round(period_minutes / 1440)
|
||||||
|
period_text = t('period_days_text', lang, days=days)
|
||||||
|
|
||||||
result = t('period_notification_title', lang, username=username, period_text=period_text)
|
result = t('period_notification_title', lang, username=username, period_text=period_text)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ TRANSLATIONS = {
|
||||||
'period_1_minute': "for 1 minute",
|
'period_1_minute': "for 1 minute",
|
||||||
'period_2_3_4_minutes': "for {period} minutes",
|
'period_2_3_4_minutes': "for {period} minutes",
|
||||||
'period_minutes_text': "for {period} minutes",
|
'period_minutes_text': "for {period} minutes",
|
||||||
|
'period_hours_text': "for {hours}h",
|
||||||
|
'period_days_text': "for {days}d",
|
||||||
'period_notification_title': "📊 Statistics {username} • {period_text}\n\n",
|
'period_notification_title': "📊 Statistics {username} • {period_text}\n\n",
|
||||||
'period_puzzles_section': "🧩 Puzzles: {total} (✅ {solved} - ❌ {failed})\n\n",
|
'period_puzzles_section': "🧩 Puzzles: {total} (✅ {solved} - ❌ {failed})\n\n",
|
||||||
'no_activity': "📭 No activity for this period",
|
'no_activity': "📭 No activity for this period",
|
||||||
|
|
@ -254,6 +256,8 @@ TRANSLATIONS = {
|
||||||
'period_1_minute': "за 1 минуту",
|
'period_1_minute': "за 1 минуту",
|
||||||
'period_2_3_4_minutes': "за {period} минуты",
|
'period_2_3_4_minutes': "за {period} минуты",
|
||||||
'period_minutes_text': "за {period} минут",
|
'period_minutes_text': "за {period} минут",
|
||||||
|
'period_hours_text': "за {hours}ч",
|
||||||
|
'period_days_text': "за {days}д",
|
||||||
'period_notification_title': "📊 Статистика {username} • {period_text}\n\n",
|
'period_notification_title': "📊 Статистика {username} • {period_text}\n\n",
|
||||||
'period_puzzles_section': "🧩 Пазлы: {total} (✅ {solved} - ❌ {failed})\n\n",
|
'period_puzzles_section': "🧩 Пазлы: {total} (✅ {solved} - ❌ {failed})\n\n",
|
||||||
'no_activity': "📭 Нет активности за этот период",
|
'no_activity': "📭 Нет активности за этот период",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue