FileToLink / Thunder /utils /rate_limiter.py
Fyaz Mohammed
1.9.9
1fa270a unverified
Raw
History Blame Contribute Delete
19.9 kB
# Thunder/utils/rate_limiter.py
import time
import math
import asyncio
from collections import deque
from typing import Callable, Dict, Optional, Tuple
from pyrogram import Client
from pyrogram.types import Message
from pyrogram.errors import FloodWait, RPCError
from Thunder.utils.logger import logger
from Thunder.utils.database import db
from Thunder.utils.messages import (
MSG_RATE_LIMIT_QUEUE_PRIORITY,
MSG_RATE_LIMIT_QUEUE_REGULAR,
MSG_RATE_LIMIT_QUEUE_FULL
)
from Thunder.vars import Var
class QueueFullError(Exception):
pass
class RateLimiter:
def __init__(self):
self.request_queue: deque = deque()
self.priority_queue: deque = deque()
self.user_queue_counts: Dict[int, int] = {}
self.request_event: asyncio.Event = asyncio.Event()
self.request_lock: asyncio.Lock = asyncio.Lock()
self.user_requests: Dict[int, deque] = {}
self.global_requests: deque = deque()
self.processing_times: deque = deque(maxlen=100)
self.file_processing_times: Dict[str, deque] = {}
self.average_processing_time: float = 1.0
self.auth_cache: Dict[int, Tuple[bool, float]] = {}
self.auth_cache_ttl_seconds: int = 300
self._initialization_error = False
self._load_configuration()
def _load_configuration(self):
try:
self.max_requests_per_period = Var.MAX_FILES_PER_PERIOD
self.rate_limit_period_seconds = Var.RATE_LIMIT_PERIOD_MINUTES * 60
self.max_queue_size = Var.MAX_QUEUE_SIZE
self.enabled = Var.RATE_LIMIT_ENABLED
self.global_rate_limit_enabled = Var.GLOBAL_RATE_LIMIT
self.max_global_requests_per_minute = Var.MAX_GLOBAL_REQUESTS_PER_MINUTE
if not self._validate_configuration():
logger.warning("Rate limiter disabled due to invalid configuration.")
self.enabled = False
else:
logger.debug(f"Rate limiter initialized: enabled={self.enabled}, "
f"max_requests={self.max_requests_per_period}, "
f"period={self.rate_limit_period_seconds}s, "
f"queue_size={self.max_queue_size}, "
f"global_enabled={self.global_rate_limit_enabled}, "
f"max_global_requests={self.max_global_requests_per_minute}")
except Exception as e:
logger.critical(f"Critical error initializing rate limiter, using safe defaults: {e}", exc_info=True)
self.max_requests_per_period = 5
self.rate_limit_period_seconds = 60
self.max_queue_size = 100
self.enabled = False
self.global_rate_limit_enabled = False
self.max_global_requests_per_minute = 60
self._initialization_error = True
def _validate_configuration(self) -> bool:
is_valid = True
if self.max_requests_per_period <= 0:
logger.error("Invalid MAX_FILES_PER_PERIOD: must be > 0.")
is_valid = False
if self.rate_limit_period_seconds <= 0:
logger.error("Invalid RATE_LIMIT_PERIOD_MINUTES: must be > 0.")
is_valid = False
if self.max_queue_size <= 0:
logger.error("Invalid MAX_QUEUE_SIZE: must be > 0.")
is_valid = False
if self.global_rate_limit_enabled and self.max_global_requests_per_minute <= 0:
logger.error("Invalid MAX_GLOBAL_REQUESTS_PER_MINUTE: must be > 0 when global rate limit is enabled.")
is_valid = False
return is_valid
def is_owner(self, user_id: int) -> bool:
return user_id == Var.OWNER_ID
async def is_authorized_user(self, user_id: int) -> bool:
current_time = time.time()
if user_id in self.auth_cache:
is_auth, timestamp = self.auth_cache[user_id]
if current_time - timestamp < self.auth_cache_ttl_seconds:
return is_auth
try:
authorized_user = await db.authorized_users_col.find_one({"user_id": user_id})
is_auth = bool(authorized_user)
self.auth_cache[user_id] = (is_auth, current_time)
return is_auth
except Exception as e:
logger.error(f"Database error checking authorized user {user_id}: {e}")
return False
async def get_user_priority(self, user_id: int) -> str:
if self.is_owner(user_id):
return 'owner'
if await self.is_authorized_user(user_id):
return 'authorized'
return 'regular'
async def check_limits(self, user_id: int, record: bool = True) -> bool:
if not self.enabled or self._initialization_error or self.is_owner(user_id):
return True
current_time = time.time()
if self.global_rate_limit_enabled:
while self.global_requests and self.global_requests[0] <= current_time - 60:
self.global_requests.popleft()
if len(self.global_requests) >= self.max_global_requests_per_minute:
return False
user_timestamps = self.user_requests.setdefault(user_id, deque())
while user_timestamps and user_timestamps[0] <= current_time - self.rate_limit_period_seconds:
user_timestamps.popleft()
if len(user_timestamps) >= self.max_requests_per_period:
return False
if record:
if self.global_rate_limit_enabled:
self.global_requests.append(current_time)
user_timestamps.append(current_time)
return True
async def _requeue_request(self, request_data: dict, queue_type: str):
async with self.request_lock:
if queue_type == "priority":
self.priority_queue.appendleft(request_data)
else:
self.request_queue.appendleft(request_data)
self.request_event.set()
logger.debug(f"Re-queued request for user {request_data['user_id']} to {queue_type} queue.")
async def add_to_queue(self, func: Callable, user_id: int, file_identifier: Optional[str] = None, *args, **kwargs):
if not self.enabled:
await func(*args, **kwargs)
return
request_data = {
'func': func, 'user_id': user_id, 'args': args, 'kwargs': kwargs,
'timestamp': time.time(), 'user_priority': await self.get_user_priority(user_id),
'file_identifier': file_identifier
}
async with self.request_lock:
total_queued = len(self.request_queue) + len(self.priority_queue)
if total_queued >= self.max_queue_size:
raise QueueFullError("Queue is full")
if request_data['user_priority'] == 'authorized':
self.priority_queue.append(request_data)
queue_name = "priority"
else:
self.request_queue.append(request_data)
queue_name = "regular"
self.user_queue_counts[user_id] = self.user_queue_counts.get(user_id, 0) + 1
logger.debug(f"Added request for user {user_id} to {queue_name} queue. Total queued: {total_queued + 1}")
self.request_event.set()
async def request_executor(self):
logger.debug("Request executor started.")
while True:
try:
await self.request_event.wait()
async with self.request_lock:
queue, queue_type = (self.priority_queue, "priority") if self.priority_queue else (self.request_queue, "regular")
if not queue:
self.request_event.clear()
continue
request_data = queue.popleft()
user_id = request_data['user_id']
processed = False
if not self.is_owner(user_id):
if not await self.check_limits(user_id, record=True):
await self._requeue_request(request_data, queue_type)
await asyncio.sleep(0.5)
continue
logger.debug(f"Processing request for user {user_id} from {queue_type} queue.")
start_time = time.time()
try:
await request_data['func'](*request_data['args'], **request_data['kwargs'])
processing_time = time.time() - start_time
self.processing_times.append(processing_time)
if self.processing_times:
self.average_processing_time = sum(self.processing_times) / len(self.processing_times)
file_identifier = request_data.get('file_identifier')
if file_identifier:
file_times = self.file_processing_times.setdefault(file_identifier, deque(maxlen=100))
file_times.append(processing_time)
processed = True
except FloodWait as e:
logger.warning(f"FloodWait for user {user_id}, waiting {e.value}s before re-queuing.")
await asyncio.sleep(e.value)
await self._requeue_request(request_data, queue_type)
except Exception as e:
logger.error(f"Error processing queued request for user {user_id}: {e}", exc_info=True)
processed = True
finally:
async with self.request_lock:
if processed and user_id in self.user_queue_counts:
self.user_queue_counts[user_id] -= 1
if self.user_queue_counts[user_id] <= 0:
self.user_queue_counts.pop(user_id, None)
except asyncio.CancelledError:
logger.debug("Request executor cancelled, shutting down.")
break
except Exception as e:
logger.critical(f"Critical error in request executor: {e}", exc_info=True)
await asyncio.sleep(5)
async def shutdown(self):
logger.debug("Shutting down rate limiter and clearing queues...")
async with self.request_lock:
self.request_queue.clear()
self.priority_queue.clear()
self.user_queue_counts.clear()
self.request_event.clear()
logger.debug("Rate limiter queues cleared.")
def get_queue_status(self) -> dict:
return {
'regular_queue_size': len(self.request_queue),
'priority_queue_size': len(self.priority_queue),
'total_queued': len(self.request_queue) + len(self.priority_queue),
'max_queue_size': self.max_queue_size,
'active_users_in_queue': len(self.user_queue_counts),
'enabled': self.enabled,
}
async def get_user_queue_position(self, user_id: int) -> dict:
user_priority = await self.get_user_priority(user_id)
position = -1
queue_to_search = self.priority_queue if user_priority == 'authorized' else self.request_queue
for idx, req in enumerate(queue_to_search):
if req.get('user_id') == user_id:
position = idx + 1
break
effective_position = position
if user_priority == 'regular' and position > -1:
effective_position += len(self.priority_queue)
return {
'user_priority': user_priority,
'position_in_own_queue': position if position > -1 else None,
'effective_position': effective_position if effective_position > -1 else None,
'priority_queue_size': len(self.priority_queue),
'regular_queue_size': len(self.request_queue),
'bypasses_rate_limit': user_priority == 'owner'
}
def _get_base_processing_time(self, file_identifier: Optional[str]) -> float:
if file_identifier and file_identifier in self.file_processing_times:
file_times = self.file_processing_times[file_identifier]
if file_times:
return sum(file_times) / len(file_times)
return self.average_processing_time
async def _calculate_queue_wait(self, user_id: int, effective_processing_time: float) -> float:
pos_info = await self.get_user_queue_position(user_id)
items_ahead = (pos_info['effective_position'] - 1) if pos_info['effective_position'] else 0
return items_ahead * effective_processing_time
def _calculate_user_rate_limit_wait(self, user_id: int, future_time: float) -> float:
user_timestamps = self.user_requests.get(user_id, deque())
future_user_timestamps = deque(ts for ts in user_timestamps if ts > future_time - self.rate_limit_period_seconds)
if len(future_user_timestamps) >= self.max_requests_per_period:
reset_time = future_user_timestamps[0] + self.rate_limit_period_seconds
return max(0.0, reset_time - future_time)
return 0.0
def _calculate_global_rate_limit_wait(self, future_time: float) -> float:
if not self.global_rate_limit_enabled:
return 0.0
future_global_requests = deque(ts for ts in self.global_requests if ts > future_time - 60)
if len(future_global_requests) >= self.max_global_requests_per_minute:
oldest_request_time = future_global_requests[0]
reset_time = oldest_request_time + 60
return max(0.0, reset_time - future_time)
return 0.0
async def estimate_wait_time(self, user_id: int, file_identifier: Optional[str] = None) -> float:
if self.is_owner(user_id):
return 0.0
base_processing_time = self._get_base_processing_time(file_identifier)
min_time_per_request = self.rate_limit_period_seconds / self.max_requests_per_period if self.max_requests_per_period > 0 else 0
effective_processing_time = max(base_processing_time, min_time_per_request)
if self.global_rate_limit_enabled and self.max_global_requests_per_minute > 0:
min_time_per_global = 60 / self.max_global_requests_per_minute
effective_processing_time = max(effective_processing_time, min_time_per_global)
queue_wait = await self._calculate_queue_wait(user_id, effective_processing_time)
future_time = time.time() + queue_wait
rate_limit_wait = self._calculate_user_rate_limit_wait(user_id, future_time)
global_wait = self._calculate_global_rate_limit_wait(future_time)
return queue_wait + rate_limit_wait + global_wait
rate_limiter = RateLimiter()
async def request_executor():
await rate_limiter.request_executor()
async def handle_rate_limited_request(bot: Client, message: Message, handler: Callable, *args, **kwargs):
rl_user_id = kwargs.pop('rl_user_id', None)
user_id = rl_user_id if rl_user_id is not None else (message.from_user.id if message and message.from_user else None)
if not isinstance(user_id, int):
logger.error(f"Invalid user_id provided for rate limiting: {user_id}")
return
file_identifier = message.document.file_unique_id if message and message.document else None
if rate_limiter.is_owner(user_id):
logger.debug(f"Owner {user_id} bypassing rate limit.")
await handler(bot, message, *args, **kwargs)
return
if await rate_limiter.check_limits(user_id, record=True):
logger.debug(f"User {user_id} within rate limits, executing immediately.")
await handler(bot, message, *args, **kwargs)
return
is_channel = rl_user_id is not None and rl_user_id < 0
if not is_channel:
try:
user_priority = await rate_limiter.get_user_priority(user_id)
notification_msg = await send_queue_notification(
bot, message, is_priority=(user_priority == 'authorized'), file_identifier=file_identifier
)
kwargs['notification_msg'] = notification_msg
except Exception as e:
logger.error(f"Error sending queue notification for user {user_id}: {e}", exc_info=True)
try:
await rate_limiter.add_to_queue(handler, user_id, file_identifier, bot, message, *args, **kwargs)
logger.debug(f"Request for user {user_id} queued.")
except QueueFullError:
logger.warning(f"Queue full, request for user {user_id} rejected.")
if not is_channel:
await send_queue_full_message(bot, message, file_identifier)
except Exception as e:
logger.error(f"Error adding request to queue for user {user_id}: {e}", exc_info=True)
if not is_channel:
await send_queue_full_message(bot, message, file_identifier)
async def _send_notification(bot: Client, message: Message, template: str, file_identifier: Optional[str], **format_kwargs):
try:
if message.from_user:
user_id = message.from_user.id
wait_seconds = await rate_limiter.estimate_wait_time(user_id, file_identifier)
wait_estimate = max(1, math.ceil(wait_seconds / 60))
text = template.format(wait_estimate=wait_estimate, s="s" if wait_estimate > 1 else "", **format_kwargs)
try:
return await bot.send_message(
chat_id=message.chat.id,
text=text,
reply_to_message_id=message.id
)
except FloodWait as e:
await asyncio.sleep(e.value)
return await bot.send_message(
chat_id=message.chat.id,
text=text,
reply_to_message_id=message.id
)
else:
logger.debug("Skipping notification for channel message (no from_user)")
return None
except (FloodWait, RPCError) as e:
user_id = message.from_user.id if message.from_user else "channel"
logger.warning(f"Error sending notification to user {user_id}: {e}")
except Exception as e:
logger.error(f"Unexpected error sending notification: {e}", exc_info=True)
return None
async def send_queue_notification(bot: Client, message: Message, is_priority: bool, file_identifier: Optional[str]):
if is_priority:
template = MSG_RATE_LIMIT_QUEUE_PRIORITY
params = {}
else:
template = MSG_RATE_LIMIT_QUEUE_REGULAR
time_window = rate_limiter.rate_limit_period_seconds // 60
params = {
"max_requests": rate_limiter.max_requests_per_period,
"time_window": time_window,
"s1": "s" if rate_limiter.max_requests_per_period > 1 else "",
"s2": "s" if time_window > 1 else ""
}
user_id = message.from_user.id if message.from_user else "channel"
logger.debug(f"Sending {'priority' if is_priority else 'regular'} queue notification to user {user_id}")
return await _send_notification(bot, message, template, file_identifier, **params)
async def send_queue_full_message(bot: Client, message: Message, file_identifier: Optional[str]):
user_id = message.from_user.id if message.from_user else "channel"
logger.debug(f"Sending queue full message to user {user_id}")
await _send_notification(bot, message, MSG_RATE_LIMIT_QUEUE_FULL, file_identifier)