Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.

This commit is contained in:
2025-09-06 18:35:12 +03:00
parent 50be010026
commit 596a2fa813
111 changed files with 16847 additions and 65 deletions

View File

@@ -0,0 +1,13 @@
"""
Rate limiting сервисы
"""
from .rate_limit_config import RateLimitSettings, get_rate_limit_config, get_adaptive_config
from .rate_limiter import RateLimitConfig, send_with_rate_limit, telegram_rate_limiter
from .rate_limit_service import RateLimitService
__all__ = [
'RateLimitSettings', 'get_rate_limit_config', 'get_adaptive_config',
'RateLimitConfig', 'send_with_rate_limit', 'telegram_rate_limiter',
'RateLimitService'
]

View File

@@ -0,0 +1,150 @@
"""
Конфигурация для rate limiting в AnonBot
"""
import os
from dataclasses import dataclass
from typing import Optional
from dotenv import load_dotenv
# Загружаем переменные окружения
load_dotenv()
@dataclass
class RateLimitSettings:
"""Настройки rate limiting для разных типов сообщений"""
# Основные настройки
messages_per_second: float = float(os.getenv('RATE_LIMIT_MESSAGES_PER_SECOND', '0.5')) # Максимум 0.5 сообщений в секунду на чат
burst_limit: int = int(os.getenv('RATE_LIMIT_BURST_LIMIT', '2')) # Максимум 2 сообщения подряд
retry_after_multiplier: float = float(os.getenv('RATE_LIMIT_RETRY_MULTIPLIER', '1.5')) # Множитель для увеличения задержки при retry
max_retry_delay: float = float(os.getenv('RATE_LIMIT_MAX_RETRY_DELAY', '30.0')) # Максимальная задержка между попытками
max_retries: int = int(os.getenv('RATE_LIMIT_MAX_RETRIES', '3')) # Максимальное количество повторных попыток
# Специальные настройки для разных типов сообщений
voice_message_delay: float = float(os.getenv('RATE_LIMIT_VOICE_DELAY', '2.0')) # Дополнительная задержка для голосовых сообщений
media_message_delay: float = float(os.getenv('RATE_LIMIT_MEDIA_DELAY', '1.5')) # Дополнительная задержка для медиа сообщений
text_message_delay: float = float(os.getenv('RATE_LIMIT_TEXT_DELAY', '1.0')) # Дополнительная задержка для текстовых сообщений
# Настройки для разных типов чатов
private_chat_multiplier: float = float(os.getenv('RATE_LIMIT_PRIVATE_MULTIPLIER', '1.0')) # Множитель для приватных чатов
group_chat_multiplier: float = float(os.getenv('RATE_LIMIT_GROUP_MULTIPLIER', '0.8')) # Множитель для групповых чатов
channel_multiplier: float = float(os.getenv('RATE_LIMIT_CHANNEL_MULTIPLIER', '0.6')) # Множитель для каналов
# Глобальные ограничения
global_messages_per_second: float = float(os.getenv('RATE_LIMIT_GLOBAL_MESSAGES_PER_SECOND', '10.0')) # Максимум 10 сообщений в секунду глобально
global_burst_limit: int = int(os.getenv('RATE_LIMIT_GLOBAL_BURST_LIMIT', '20')) # Максимум 20 сообщений подряд глобально
# Конфигурации для разных сценариев использования
# Основаны на официальных лимитах Telegram Bot API:
# - 1 сообщение в секунду в личных чатах
# - 20 сообщений в минуту в групповых чатах (0.33 в секунду)
# - 30 запросов в секунду глобально
DEVELOPMENT_CONFIG = RateLimitSettings(
messages_per_second=0.8, # Более мягкие ограничения для разработки (80% от лимита)
burst_limit=3, # До 3 сообщений подряд
retry_after_multiplier=1.2,
max_retry_delay=15.0,
max_retries=2,
voice_message_delay=1.5,
media_message_delay=1.2,
text_message_delay=1.0
)
PRODUCTION_CONFIG = RateLimitSettings(
messages_per_second=0.5, # Консервативные ограничения (50% от лимита)
burst_limit=2, # До 2 сообщений подряд
retry_after_multiplier=1.5,
max_retry_delay=30.0,
max_retries=3,
voice_message_delay=2.5, # Дополнительная задержка для голосовых
media_message_delay=2.0, # Дополнительная задержка для медиа
text_message_delay=1.5, # Дополнительная задержка для текста
global_messages_per_second=20.0, # 20 из 30 доступных запросов в секунду
global_burst_limit=15 # До 15 сообщений подряд глобально
)
STRICT_CONFIG = RateLimitSettings(
messages_per_second=0.3, # Очень консервативные ограничения (30% от лимита)
burst_limit=1, # Только 1 сообщение подряд
retry_after_multiplier=2.0,
max_retry_delay=60.0,
max_retries=5,
voice_message_delay=3.0,
media_message_delay=2.5,
text_message_delay=2.0,
global_messages_per_second=10.0, # 10 из 30 доступных запросов в секунду
global_burst_limit=8 # До 8 сообщений подряд глобально
)
def get_rate_limit_config(environment: str = None) -> RateLimitSettings:
"""
Получает конфигурацию rate limiting в зависимости от окружения
Args:
environment: Окружение ('development', 'production', 'strict')
Если не указано, берется из переменной окружения RATE_LIMIT_ENV
Returns:
RateLimitSettings: Конфигурация для указанного окружения
"""
if environment is None:
environment = os.getenv('RATE_LIMIT_ENV', 'production')
configs = {
"development": DEVELOPMENT_CONFIG,
"production": PRODUCTION_CONFIG,
"strict": STRICT_CONFIG
}
return configs.get(environment, PRODUCTION_CONFIG)
def get_adaptive_config(
current_error_rate: float,
base_config: Optional[RateLimitSettings] = None
) -> RateLimitSettings:
"""
Получает адаптивную конфигурацию на основе текущего уровня ошибок
Args:
current_error_rate: Текущий уровень ошибок (0.0 - 1.0)
base_config: Базовая конфигурация
Returns:
RateLimitSettings: Адаптированная конфигурация
"""
if base_config is None:
base_config = PRODUCTION_CONFIG
# Если уровень ошибок высокий, ужесточаем ограничения
if current_error_rate > 0.1: # Более 10% ошибок
return RateLimitSettings(
messages_per_second=base_config.messages_per_second * 0.5,
burst_limit=max(1, base_config.burst_limit - 1),
retry_after_multiplier=base_config.retry_after_multiplier * 1.5,
max_retry_delay=base_config.max_retry_delay * 1.5,
max_retries=base_config.max_retries + 1,
voice_message_delay=base_config.voice_message_delay * 1.5,
media_message_delay=base_config.media_message_delay * 1.3,
text_message_delay=base_config.text_message_delay * 1.2
)
# Если уровень ошибок низкий, можно немного ослабить ограничения
elif current_error_rate < 0.01: # Менее 1% ошибок
return RateLimitSettings(
messages_per_second=base_config.messages_per_second * 1.2,
burst_limit=base_config.burst_limit + 1,
retry_after_multiplier=base_config.retry_after_multiplier * 0.9,
max_retry_delay=base_config.max_retry_delay * 0.8,
max_retries=max(1, base_config.max_retries - 1),
voice_message_delay=base_config.voice_message_delay * 0.8,
media_message_delay=base_config.media_message_delay * 0.9,
text_message_delay=base_config.text_message_delay * 0.9
)
# Возвращаем базовую конфигурацию
return base_config

View File

@@ -0,0 +1,142 @@
"""
Сервис для управления rate limiting в AnonBot
"""
from typing import Any, Callable, Dict, Optional
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
from config.constants import MIN_REQUESTS_FOR_ADAPTATION, HIGH_ERROR_RATE_THRESHOLD, LOW_ERROR_RATE_THRESHOLD
from services.infrastructure.logger import get_logger
from .rate_limit_config import RateLimitSettings, get_adaptive_config, get_rate_limit_config
from .rate_limiter import send_with_rate_limit, telegram_rate_limiter
logger = get_logger(__name__)
class RateLimitService:
"""Сервис для управления rate limiting"""
def __init__(self):
self.rate_limiter = telegram_rate_limiter
self.config = get_rate_limit_config()
self.stats = {
'total_requests': 0,
'successful_requests': 0,
'failed_requests': 0,
'retry_after_errors': 0,
'other_errors': 0,
'total_wait_time': 0.0
}
async def send_with_rate_limit(
self,
send_func: Callable,
chat_id: int,
*args,
**kwargs
) -> Any:
"""
Отправляет сообщение с соблюдением rate limit
Args:
send_func: Функция отправки (например, bot.send_message)
chat_id: ID чата
*args, **kwargs: Аргументы для функции отправки
Returns:
Результат выполнения функции отправки
"""
self.stats['total_requests'] += 1
logger.info(f"Обработка rate limit запроса для чата {chat_id}")
try:
result, wait_time = await self.rate_limiter.send_with_rate_limit(send_func, chat_id, *args, **kwargs)
self.stats['successful_requests'] += 1
self.stats['total_wait_time'] += wait_time
logger.info(f"Rate limited сообщение успешно отправлено в чат {chat_id}, время ожидания: {wait_time:.2f}с")
return result
except TelegramRetryAfter as e:
self.stats['failed_requests'] += 1
self.stats['retry_after_errors'] += 1
logger.warning(f"Превышен rate limit для чата {chat_id}: {e}")
raise
except TelegramAPIError as e:
self.stats['failed_requests'] += 1
self.stats['other_errors'] += 1
logger.error(f"Ошибка Telegram API для чата {chat_id}: {e}")
raise
except Exception as e:
self.stats['failed_requests'] += 1
self.stats['other_errors'] += 1
logger.error(f"Неожиданная ошибка в rate limit сервисе для чата {chat_id}: {e}")
raise
def get_stats(self) -> Dict[str, Any]:
"""Получает статистику rate limiting"""
total = self.stats['total_requests']
if total == 0:
return {
'total_requests': 0,
'successful_requests': 0,
'failed_requests': 0,
'success_rate': 0.0,
'error_rate': 0.0,
'retry_after_errors': 0,
'other_errors': 0,
'retry_after_rate': 0.0,
'other_error_rate': 0.0,
'average_wait_time': 0.0
}
return {
'total_requests': total,
'successful_requests': self.stats['successful_requests'],
'failed_requests': self.stats['failed_requests'],
'success_rate': self.stats['successful_requests'] / total,
'error_rate': self.stats['failed_requests'] / total,
'retry_after_errors': self.stats['retry_after_errors'],
'other_errors': self.stats['other_errors'],
'retry_after_rate': self.stats['retry_after_errors'] / total,
'other_error_rate': self.stats['other_errors'] / total,
'average_wait_time': self.stats['total_wait_time'] / total if total > 0 else 0.0
}
def reset_stats(self):
"""Сбрасывает статистику"""
self.stats = {
'total_requests': 0,
'successful_requests': 0,
'failed_requests': 0,
'retry_after_errors': 0,
'other_errors': 0,
'total_wait_time': 0.0
}
logger.info("Статистика rate limit сброшена")
def update_config(self, new_config: RateLimitSettings):
"""Обновляет конфигурацию rate limiting"""
self.config = new_config
logger.info(f"Конфигурация rate limit обновлена: {new_config}")
def get_adaptive_config(self) -> RateLimitSettings:
"""Получает адаптивную конфигурацию на основе текущей статистики"""
error_rate = self.stats['failed_requests'] / max(1, self.stats['total_requests'])
return get_adaptive_config(error_rate, self.config)
def should_adapt_config(self) -> bool:
"""Определяет, нужно ли адаптировать конфигурацию"""
if self.stats['total_requests'] < MIN_REQUESTS_FOR_ADAPTATION: # Недостаточно данных
return False
error_rate = self.stats['failed_requests'] / self.stats['total_requests']
return error_rate > HIGH_ERROR_RATE_THRESHOLD or error_rate < LOW_ERROR_RATE_THRESHOLD # Высокий или низкий уровень ошибок
async def adapt_config_if_needed(self):
"""Адаптирует конфигурацию если необходимо"""
if self.should_adapt_config():
new_config = self.get_adaptive_config()
self.update_config(new_config)
logger.info("Конфигурация rate limit адаптирована на основе текущей производительности")

View File

@@ -0,0 +1,230 @@
"""
Rate limiter для предотвращения Flood control ошибок в Telegram Bot API
"""
import asyncio
import time
from typing import Dict, Optional, Any, Callable
from dataclasses import dataclass
from aiogram.exceptions import TelegramRetryAfter, TelegramAPIError
from services.infrastructure.logger import get_logger
from .rate_limit_config import RateLimitSettings, get_rate_limit_config
logger = get_logger(__name__)
@dataclass
class RateLimitConfig:
"""Конфигурация для rate limiting"""
messages_per_second: float = 0.5 # Максимум 0.5 сообщений в секунду на чат
burst_limit: int = 3 # Максимум 3 сообщения подряд
retry_after_multiplier: float = 1.2 # Множитель для увеличения задержки при retry
max_retry_delay: float = 60.0 # Максимальная задержка между попытками
class ChatRateLimiter:
"""Rate limiter для конкретного чата"""
def __init__(self, config: RateLimitConfig):
self.config = config
self.last_send_time = 0.0
self.burst_count = 0
self.burst_reset_time = 0.0
self.retry_delay = 1.0
async def wait_if_needed(self) -> None:
"""Ждет если необходимо для соблюдения rate limit"""
current_time = time.time()
# Сбрасываем счетчик burst если прошло достаточно времени
if current_time >= self.burst_reset_time:
self.burst_count = 0
self.burst_reset_time = current_time + 1.0
# Проверяем burst limit
if self.burst_count >= self.config.burst_limit:
wait_time = self.burst_reset_time - current_time
if wait_time > 0:
logger.info(f"Достигнут лимит burst, ожидание {wait_time:.2f}с")
await asyncio.sleep(wait_time)
current_time = time.time()
self.burst_count = 0
self.burst_reset_time = current_time + 1.0
# Проверяем минимальный интервал между сообщениями
time_since_last = current_time - self.last_send_time
min_interval = 1.0 / self.config.messages_per_second
if time_since_last < min_interval:
wait_time = min_interval - time_since_last
logger.debug(f"Rate limiting: waiting {wait_time:.2f}s")
await asyncio.sleep(wait_time)
# Обновляем время последней отправки
self.last_send_time = time.time()
self.burst_count += 1
class GlobalRateLimiter:
"""Глобальный rate limiter для всех чатов"""
def __init__(self, config: RateLimitConfig):
self.config = config
self.chat_limiters: Dict[int, ChatRateLimiter] = {}
self.global_last_send = 0.0
self.global_min_interval = 0.1 # Минимум 100ms между любыми сообщениями
def get_chat_limiter(self, chat_id: int) -> ChatRateLimiter:
"""Получает rate limiter для конкретного чата"""
if chat_id not in self.chat_limiters:
self.chat_limiters[chat_id] = ChatRateLimiter(self.config)
return self.chat_limiters[chat_id]
async def wait_if_needed(self, chat_id: int) -> None:
"""Ждет если необходимо для соблюдения глобального и чат-специфичного rate limit"""
current_time = time.time()
# Глобальный rate limit
time_since_global = current_time - self.global_last_send
if time_since_global < self.global_min_interval:
wait_time = self.global_min_interval - time_since_global
logger.info(f"Применен глобальный rate limit для чата {chat_id}, ожидание {wait_time:.2f}с")
await asyncio.sleep(wait_time)
current_time = time.time()
# Чат-специфичный rate limit
chat_limiter = self.get_chat_limiter(chat_id)
await chat_limiter.wait_if_needed()
self.global_last_send = time.time()
class RetryHandler:
"""Обработчик повторных попыток с экспоненциальной задержкой"""
def __init__(self, config: RateLimitConfig):
self.config = config
async def execute_with_retry(
self,
func: Callable,
chat_id: int,
*args,
max_retries: int = 3,
**kwargs
) -> tuple[Any, float]:
"""Выполняет функцию с повторными попытками при ошибках"""
retry_count = 0
current_delay = self.config.retry_after_multiplier
total_wait_time = 0.0
while retry_count <= max_retries:
try:
result = await func(*args, **kwargs)
# Записываем успешный запрос
logger.debug(f"Rate limit запрос успешен для чата {chat_id}")
return result, total_wait_time
except TelegramRetryAfter as e:
retry_count += 1
if retry_count > max_retries:
logger.error(f"Превышено максимальное количество попыток для RetryAfter: {e}")
raise
# Используем время ожидания от Telegram или наше увеличенное
wait_time = max(e.retry_after, current_delay)
wait_time = min(wait_time, self.config.max_retry_delay)
total_wait_time += wait_time
logger.info(f"RetryAfter ошибка для чата {chat_id}, ожидание {wait_time:.2f}с (попытка {retry_count}/{max_retries})")
await asyncio.sleep(wait_time)
current_delay *= self.config.retry_after_multiplier
except TelegramAPIError as e:
retry_count += 1
if retry_count > max_retries:
logger.error(f"Превышено максимальное количество попыток для TelegramAPIError: {e}")
raise
wait_time = min(current_delay, self.config.max_retry_delay)
total_wait_time += wait_time
logger.info(f"TelegramAPIError для чата {chat_id}, ожидание {wait_time:.2f}с (попытка {retry_count}/{max_retries}): {e}")
await asyncio.sleep(wait_time)
current_delay *= self.config.retry_after_multiplier
except Exception as e:
# Для других ошибок не делаем retry
logger.error(f"Ошибка без повторных попыток: {e}")
raise
class TelegramRateLimiter:
"""Основной класс для rate limiting в Telegram боте"""
def __init__(self, config: Optional[RateLimitConfig] = None):
self.config = config or RateLimitConfig()
self.global_limiter = GlobalRateLimiter(self.config)
self.retry_handler = RetryHandler(self.config)
async def send_with_rate_limit(
self,
send_func: Callable,
chat_id: int,
*args,
**kwargs
) -> tuple[Any, float]:
"""Отправляет сообщение с соблюдением rate limit и retry логики"""
async def _send():
await self.global_limiter.wait_if_needed(chat_id)
# Добавляем chat_id в kwargs для функции отправки
send_kwargs = kwargs.copy()
send_kwargs['chat_id'] = chat_id
return await send_func(*args, **send_kwargs)
return await self.retry_handler.execute_with_retry(_send, chat_id)
async def execute_with_rate_limit(
self,
handler_func: Callable,
chat_id: int
) -> tuple[Any, float]:
"""Выполняет обработчик с соблюдением rate limit (без добавления chat_id в kwargs)"""
async def _execute():
await self.global_limiter.wait_if_needed(chat_id)
return await handler_func()
return await self.retry_handler.execute_with_retry(_execute, chat_id)
def _create_rate_limit_config(settings: RateLimitSettings) -> RateLimitConfig:
"""Создает RateLimitConfig из RateLimitSettings"""
return RateLimitConfig(
messages_per_second=settings.messages_per_second,
burst_limit=settings.burst_limit,
retry_after_multiplier=settings.retry_after_multiplier,
max_retry_delay=settings.max_retry_delay
)
# Получаем конфигурацию из настроек
_rate_limit_settings = get_rate_limit_config()
_default_config = _create_rate_limit_config(_rate_limit_settings)
telegram_rate_limiter = TelegramRateLimiter(_default_config)
async def send_with_rate_limit(send_func: Callable, chat_id: int, *args, **kwargs) -> tuple[Any, float]:
"""
Удобная функция для отправки сообщений с rate limiting
Args:
send_func: Функция отправки (например, bot.send_message)
chat_id: ID чата
*args, **kwargs: Аргументы для функции отправки
Returns:
Кортеж (результат выполнения функции отправки, общее время ожидания)
"""
return await telegram_rate_limiter.send_with_rate_limit(send_func, chat_id, *args, **kwargs)