Initial commit
Dieser Commit ist enthalten in:
259
application/use_cases/detect_rate_limit_use_case.py
Normale Datei
259
application/use_cases/detect_rate_limit_use_case.py
Normale Datei
@ -0,0 +1,259 @@
|
||||
"""
|
||||
Detect Rate Limit Use Case - Erkennt Rate Limits und reagiert entsprechend
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
from domain.services.rate_limit_service import IRateLimitService
|
||||
from domain.value_objects.action_timing import ActionTiming, ActionType
|
||||
from domain.entities.error_event import ErrorEvent, ErrorType, ErrorContext
|
||||
from domain.entities.rate_limit_policy import RateLimitPolicy
|
||||
|
||||
logger = logging.getLogger("detect_rate_limit_use_case")
|
||||
|
||||
|
||||
class DetectRateLimitUseCase:
|
||||
"""
|
||||
Use Case für Rate Limit Erkennung und Reaktion.
|
||||
Analysiert Responses, erkennt Rate Limits und implementiert Backoff-Strategien.
|
||||
"""
|
||||
|
||||
def __init__(self, rate_limit_service: IRateLimitService):
|
||||
self.rate_limit_service = rate_limit_service
|
||||
self.detection_patterns = {
|
||||
'instagram': [
|
||||
"Bitte warte einige Minuten",
|
||||
"Please wait a few minutes",
|
||||
"Try again later",
|
||||
"Versuche es später erneut",
|
||||
"too many requests",
|
||||
"zu viele Anfragen",
|
||||
"We're sorry, but something went wrong",
|
||||
"temporarily blocked",
|
||||
"vorübergehend gesperrt",
|
||||
"Wir haben deine Anfrage eingeschränkt"
|
||||
],
|
||||
'tiktok': [
|
||||
"Too many attempts",
|
||||
"Zu viele Versuche",
|
||||
"Please slow down",
|
||||
"rate limited",
|
||||
"Try again in"
|
||||
],
|
||||
'general': [
|
||||
"429",
|
||||
"rate limit",
|
||||
"throttled",
|
||||
"quota exceeded"
|
||||
]
|
||||
}
|
||||
|
||||
def execute(self, response: Any, context: Optional[Dict[str, Any]] = None) -> Tuple[bool, Optional[ErrorEvent]]:
|
||||
"""
|
||||
Analysiert eine Response auf Rate Limiting.
|
||||
|
||||
Args:
|
||||
response: HTTP Response, Page Content oder Error Message
|
||||
context: Zusätzlicher Kontext (platform, action_type, etc.)
|
||||
|
||||
Returns:
|
||||
Tuple aus (is_rate_limited, error_event)
|
||||
"""
|
||||
# Erkenne Rate Limit
|
||||
is_rate_limited = self._detect_rate_limit(response, context)
|
||||
|
||||
if not is_rate_limited:
|
||||
return False, None
|
||||
|
||||
# Erstelle Error Event
|
||||
error_event = self._create_error_event(response, context)
|
||||
|
||||
# Handle Rate Limit
|
||||
self._handle_rate_limit(error_event, context)
|
||||
|
||||
return True, error_event
|
||||
|
||||
def _detect_rate_limit(self, response: Any, context: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""Erkennt ob Response auf Rate Limiting hindeutet"""
|
||||
# Nutze Service für Basis-Detection
|
||||
if self.rate_limit_service.detect_rate_limit(response):
|
||||
return True
|
||||
|
||||
# Erweiterte Detection basierend auf Platform
|
||||
platform = context.get('platform', 'general') if context else 'general'
|
||||
patterns = self.detection_patterns.get(platform, []) + self.detection_patterns['general']
|
||||
|
||||
# String-basierte Erkennung
|
||||
response_text = self._extract_text(response)
|
||||
if response_text:
|
||||
response_lower = response_text.lower()
|
||||
for pattern in patterns:
|
||||
if pattern.lower() in response_lower:
|
||||
logger.info(f"Rate limit detected: '{pattern}' found in response")
|
||||
return True
|
||||
|
||||
# Status Code Erkennung
|
||||
status = self._extract_status(response)
|
||||
if status in [429, 420, 503]: # Common rate limit codes
|
||||
logger.info(f"Rate limit detected: HTTP {status}")
|
||||
return True
|
||||
|
||||
# Timing-basierte Erkennung
|
||||
if context and 'timing' in context:
|
||||
timing = context['timing']
|
||||
if isinstance(timing, ActionTiming):
|
||||
# Sehr schnelle Fehler können auf Rate Limits hindeuten
|
||||
if not timing.success and timing.duration < 0.5:
|
||||
logger.warning("Possible rate limit: Fast failure detected")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _extract_text(self, response: Any) -> Optional[str]:
|
||||
"""Extrahiert Text aus verschiedenen Response-Typen"""
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
elif hasattr(response, 'text'):
|
||||
try:
|
||||
return response.text
|
||||
except:
|
||||
pass
|
||||
elif hasattr(response, 'content'):
|
||||
try:
|
||||
if callable(response.content):
|
||||
return response.content()
|
||||
return str(response.content)
|
||||
except:
|
||||
pass
|
||||
elif hasattr(response, 'message'):
|
||||
return str(response.message)
|
||||
|
||||
return str(response) if response else None
|
||||
|
||||
def _extract_status(self, response: Any) -> Optional[int]:
|
||||
"""Extrahiert Status Code aus Response"""
|
||||
if hasattr(response, 'status'):
|
||||
return response.status
|
||||
elif hasattr(response, 'status_code'):
|
||||
return response.status_code
|
||||
elif hasattr(response, 'code'):
|
||||
try:
|
||||
return int(response.code)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _create_error_event(self, response: Any, context: Optional[Dict[str, Any]] = None) -> ErrorEvent:
|
||||
"""Erstellt Error Event für Rate Limit"""
|
||||
error_context = ErrorContext(
|
||||
url=context.get('url') if context else None,
|
||||
action=context.get('action_type').value if context and 'action_type' in context else None,
|
||||
step_name=context.get('step_name') if context else None,
|
||||
screenshot_path=context.get('screenshot_path') if context else None,
|
||||
additional_data={
|
||||
'platform': context.get('platform') if context else None,
|
||||
'response_text': self._extract_text(response)[:500] if self._extract_text(response) else None,
|
||||
'status_code': self._extract_status(response),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
return ErrorEvent(
|
||||
error_type=ErrorType.RATE_LIMIT,
|
||||
error_message="Rate limit detected",
|
||||
context=error_context,
|
||||
platform=context.get('platform') if context else None,
|
||||
session_id=context.get('session_id') if context else None,
|
||||
correlation_id=context.get('correlation_id') if context else None
|
||||
)
|
||||
|
||||
def _handle_rate_limit(self, error_event: ErrorEvent, context: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Behandelt erkanntes Rate Limit"""
|
||||
# Extrahiere Wait-Zeit aus Response wenn möglich
|
||||
wait_time = self._extract_wait_time(error_event.context.additional_data.get('response_text', ''))
|
||||
|
||||
if not wait_time:
|
||||
# Verwende exponentielles Backoff
|
||||
retry_count = context.get('retry_count', 0) if context else 0
|
||||
wait_time = self._calculate_backoff(retry_count)
|
||||
|
||||
logger.warning(f"Rate limit detected - waiting {wait_time}s before retry")
|
||||
|
||||
# Update Rate Limit Policy für zukünftige Requests
|
||||
if context and 'action_type' in context:
|
||||
action_type = context['action_type']
|
||||
current_policy = self.rate_limit_service.get_policy(action_type)
|
||||
|
||||
# Erhöhe Delays temporär
|
||||
updated_policy = RateLimitPolicy(
|
||||
min_delay=min(current_policy.min_delay * 1.5, 10.0),
|
||||
max_delay=min(current_policy.max_delay * 2, 60.0),
|
||||
adaptive=current_policy.adaptive,
|
||||
backoff_multiplier=min(current_policy.backoff_multiplier * 1.2, 3.0),
|
||||
max_retries=current_policy.max_retries
|
||||
)
|
||||
|
||||
self.rate_limit_service.update_policy(action_type, updated_policy)
|
||||
|
||||
def _extract_wait_time(self, response_text: str) -> Optional[float]:
|
||||
"""Versucht Wait-Zeit aus Response zu extrahieren"""
|
||||
if not response_text:
|
||||
return None
|
||||
|
||||
import re
|
||||
|
||||
# Patterns für Zeitangaben
|
||||
patterns = [
|
||||
r'wait (\d+) seconds',
|
||||
r'warte (\d+) Sekunden',
|
||||
r'try again in (\d+)s',
|
||||
r'retry after (\d+)',
|
||||
r'(\d+) Minuten warten',
|
||||
r'wait (\d+) minutes'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, response_text.lower())
|
||||
if match:
|
||||
value = int(match.group(1))
|
||||
# Konvertiere Minuten zu Sekunden wenn nötig
|
||||
if 'minute' in pattern or 'minuten' in pattern:
|
||||
value *= 60
|
||||
return float(min(value, 300)) # Max 5 Minuten
|
||||
|
||||
return None
|
||||
|
||||
def _calculate_backoff(self, retry_count: int) -> float:
|
||||
"""Berechnet exponentielles Backoff"""
|
||||
base_wait = 5.0 # 5 Sekunden Basis
|
||||
max_wait = 300.0 # Max 5 Minuten
|
||||
|
||||
# Exponentielles Backoff mit Jitter
|
||||
wait_time = min(base_wait * (2 ** retry_count), max_wait)
|
||||
|
||||
# Füge Jitter hinzu (±20%)
|
||||
import random
|
||||
jitter = wait_time * 0.2 * (random.random() - 0.5)
|
||||
|
||||
return wait_time + jitter
|
||||
|
||||
def analyze_patterns(self, platform: str, timeframe_hours: int = 24) -> Dict[str, Any]:
|
||||
"""Analysiert Rate Limit Muster für eine Plattform"""
|
||||
# Diese Methode würde mit einem Analytics Repository arbeiten
|
||||
# um Muster in Rate Limits zu erkennen
|
||||
|
||||
analysis = {
|
||||
'platform': platform,
|
||||
'timeframe_hours': timeframe_hours,
|
||||
'peak_times': [],
|
||||
'safe_times': [],
|
||||
'recommended_delays': {},
|
||||
'incidents': 0
|
||||
}
|
||||
|
||||
# TODO: Implementiere mit Analytics Repository
|
||||
|
||||
return analysis
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren