Fixed: back button in stars/ traffic display/ trial in country mode. Added error duplicate handling and request queueing.
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user