Fixed: back button in stars/ traffic display/ trial in country mode. Added error duplicate handling and request queueing.

This commit is contained in:
Vladless
2026-03-02 00:09:40 +03:00
parent 4fa5c882ba
commit 0f449680ee
10 changed files with 584 additions and 418 deletions
+8 -6
View File
@@ -14,15 +14,17 @@ class CallbackAnswerMiddleware(BaseMiddleware):
event: TelegramObject,
data: dict[str, Any],
) -> Any:
if isinstance(event, CallbackQuery):
if isinstance(event, CallbackQuery) and isinstance(event.message, InaccessibleMessage):
try:
await event.answer()
new_message = await bot.send_message(event.message.chat.id, "")
object.__setattr__(event, "message", new_message)
except Exception:
pass
if isinstance(event.message, InaccessibleMessage):
try:
return await handler(event, data)
finally:
if isinstance(event, CallbackQuery) and not data.get("callback_answered_early"):
try:
new_message = await bot.send_message(event.message.chat.id, "")
object.__setattr__(event, "message", new_message)
await event.answer()
except Exception:
pass
return await handler(event, data)
+99 -26
View File
@@ -4,18 +4,27 @@ from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware, Bot
from aiogram.types import CallbackQuery, Message, TelegramObject
from aiogram.types import CallbackQuery, Message, TelegramObject, Update
from database.db import CONCURRENT_UPDATES_LIMIT, MAX_UPDATE_AGE_SEC
from database.db import (
CONCURRENT_UPDATES_GATE_LIMIT,
CONCURRENT_UPDATES_GATE_WAIT_SEC,
CONCURRENT_UPDATES_LIMIT,
CONCURRENT_UPDATES_WAIT_TIMEOUT_SEC,
MAX_UPDATE_AGE_SEC,
)
from logger import logger
class ConcurrencyLimiterMiddleware(BaseMiddleware):
"""
Регистрируется до SessionMiddleware. Ограничивает число апдейтов, одновременно
получающих сессию, и отсекает апдейты, ждавшие слишком долго.
Регистрируется до SessionMiddleware. Шлюз (gate) ограничивает число апдейтов
в конвейере; семафор — число одновременно обрабатываемых с БД. Лишние
апдейты сразу получают «высокая нагрузка» и не создают тысячи ожидающих задач.
"""
def __init__(self) -> None:
self._gate = asyncio.Semaphore(CONCURRENT_UPDATES_GATE_LIMIT)
self._semaphore = asyncio.Semaphore(CONCURRENT_UPDATES_LIMIT)
async def __call__(
@@ -25,35 +34,99 @@ class ConcurrencyLimiterMiddleware(BaseMiddleware):
data: dict[str, Any],
) -> Any:
data["request_time"] = time.monotonic()
await self._semaphore.acquire()
if isinstance(event, CallbackQuery):
await self._answer_callback_early(event, data)
gate_wait = CONCURRENT_UPDATES_GATE_WAIT_SEC if CONCURRENT_UPDATES_GATE_WAIT_SEC else 0
try:
age = time.monotonic() - data["request_time"]
if age > MAX_UPDATE_AGE_SEC:
await self._reject_stale(event, data)
await asyncio.wait_for(self._gate.acquire(), timeout=gate_wait)
except asyncio.TimeoutError:
logger.warning("[Concurrency] Reject: gate full (очередь переполнена)")
await self._reject_overload(event, data)
return None
try:
try:
await asyncio.wait_for(
self._semaphore.acquire(),
timeout=CONCURRENT_UPDATES_WAIT_TIMEOUT_SEC,
)
except asyncio.TimeoutError:
logger.warning("[Concurrency] Reject: semaphore timeout (все слоты БД заняты)")
await self._reject_overload(event, data)
return None
return await handler(event, data)
try:
age = time.monotonic() - data["request_time"]
if age > MAX_UPDATE_AGE_SEC:
logger.warning("[Concurrency] Reject: update too old (age %.1fs)", age)
await self._reject_stale(event, data)
return None
return await handler(event, data)
finally:
self._semaphore.release()
finally:
self._semaphore.release()
self._gate.release()
async def _answer_callback_early(self, event: CallbackQuery, data: dict[str, Any]) -> None:
"""Отвечает на callback сразу, снимая таймаут «устаревший запрос» при долгой очереди."""
if data.get("callback_answered_early"):
return
bot: Bot = data.get("bot")
if not bot:
return
try:
await bot.answer_callback_query(
event.id,
text="",
show_alert=False,
)
data["callback_answered_early"] = True
except Exception:
pass
async def _reject_stale(self, event: TelegramObject, data: dict[str, Any]) -> None:
if isinstance(event, CallbackQuery):
bot: Bot = data.get("bot")
if bot:
try:
await self._send_reject_message(event, data)
async def _reject_overload(self, event: TelegramObject, data: dict[str, Any]) -> None:
await self._send_reject_message(event, data)
async def _send_reject_message(self, event: TelegramObject, data: dict[str, Any]) -> None:
"""Отправляет пользователю сообщение «высокая нагрузка / нажмите ещё раз»."""
bot: Bot = data.get("bot")
if not bot:
return
text = "Сейчас высокая нагрузка. Попробуйте ещё раз через несколько секунд."
try:
if isinstance(event, Update):
chat_id, callback = self._chat_and_callback_from_update(event)
if chat_id is None:
return
if callback and not data.get("callback_answered_early"):
await bot.answer_callback_query(
callback.id,
text="Время ожидания истекло. Нажмите ещё раз.",
show_alert=False,
)
else:
await bot.send_message(chat_id, text)
elif isinstance(event, CallbackQuery):
if data.get("callback_answered_early"):
if event.message and event.message.chat:
await bot.send_message(event.message.chat.id, text)
else:
await bot.answer_callback_query(
event.id,
text="Время ожидания истекло. Нажмите ещё раз.",
show_alert=False,
)
except Exception:
pass
elif isinstance(event, Message) and event.text and event.chat:
bot: Bot = data.get("bot")
if bot:
try:
await bot.send_message(
event.chat.id,
"Сейчас высокая нагрузка. Отправьте команду ещё раз через пару секунд.",
)
except Exception:
pass
elif isinstance(event, Message) and event.chat:
await bot.send_message(event.chat.id, text)
except Exception:
pass
@staticmethod
def _chat_and_callback_from_update(update: Update) -> tuple[int | None, CallbackQuery | None]:
"""Извлекает chat_id и callback (если есть) из Update для отправки сообщения."""
if update.message and update.message.chat:
return update.message.chat.id, None
if update.callback_query and update.callback_query.message and update.callback_query.message.chat:
return update.callback_query.message.chat.id, update.callback_query
return None, None