244 lines
8.0 KiB
Python
244 lines
8.0 KiB
Python
|
|
#!/usr/bin/env python
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
智能重试策略
|
|||
|
|
包装其他策略并提供智能重试机制
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import time
|
|||
|
|
import logging
|
|||
|
|
from typing import Optional, List
|
|||
|
|
from functools import wraps
|
|||
|
|
|
|||
|
|
from .base import IDownloadStrategy, DownloadTask, DownloadResult, TaskStatus
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RetryStrategy(IDownloadStrategy):
|
|||
|
|
"""智能重试策略,包装其他策略并提供重试机制"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
strategy: IDownloadStrategy,
|
|||
|
|
max_retries: int = 3,
|
|||
|
|
retry_delays: Optional[List[float]] = None,
|
|||
|
|
exponential_backoff: bool = True
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
初始化重试策略
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
strategy: 被包装的策略
|
|||
|
|
max_retries: 最大重试次数
|
|||
|
|
retry_delays: 自定义重试延迟列表
|
|||
|
|
exponential_backoff: 是否使用指数退避
|
|||
|
|
"""
|
|||
|
|
self.strategy = strategy
|
|||
|
|
self.max_retries = max_retries
|
|||
|
|
self.retry_delays = retry_delays or [1, 2, 5, 10, 30]
|
|||
|
|
self.exponential_backoff = exponential_backoff
|
|||
|
|
self.retry_stats = {
|
|||
|
|
'total_retries': 0,
|
|||
|
|
'successful_retries': 0,
|
|||
|
|
'failed_retries': 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def name(self) -> str:
|
|||
|
|
return f"Retry({self.strategy.name})"
|
|||
|
|
|
|||
|
|
def get_priority(self) -> int:
|
|||
|
|
"""继承被包装策略的优先级"""
|
|||
|
|
return self.strategy.get_priority()
|
|||
|
|
|
|||
|
|
async def can_handle(self, task: DownloadTask) -> bool:
|
|||
|
|
"""判断是否可以处理任务"""
|
|||
|
|
return await self.strategy.can_handle(task)
|
|||
|
|
|
|||
|
|
async def download(self, task: DownloadTask) -> DownloadResult:
|
|||
|
|
"""执行下载任务,带重试机制"""
|
|||
|
|
original_retry_count = task.retry_count
|
|||
|
|
last_error = None
|
|||
|
|
|
|||
|
|
for attempt in range(self.max_retries):
|
|||
|
|
try:
|
|||
|
|
# 更新任务状态
|
|||
|
|
if attempt > 0:
|
|||
|
|
task.status = TaskStatus.RETRYING
|
|||
|
|
logger.info(f"任务 {task.task_id} 第 {attempt + 1}/{self.max_retries} 次重试")
|
|||
|
|
|
|||
|
|
# 执行下载
|
|||
|
|
result = await self.strategy.download(task)
|
|||
|
|
|
|||
|
|
if result.success:
|
|||
|
|
if attempt > 0:
|
|||
|
|
self.retry_stats['successful_retries'] += 1
|
|||
|
|
logger.info(f"任务 {task.task_id} 重试成功 (第 {attempt + 1} 次)")
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# 下载失败,准备重试
|
|||
|
|
last_error = result.error_message
|
|||
|
|
|
|||
|
|
# 检查是否应该重试
|
|||
|
|
if not self._should_retry(result, attempt):
|
|||
|
|
logger.warning(f"任务 {task.task_id} 不符合重试条件,停止重试")
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
# 计算延迟时间
|
|||
|
|
delay = self._calculate_delay(attempt)
|
|||
|
|
logger.info(f"任务 {task.task_id} 将在 {delay} 秒后重试")
|
|||
|
|
await asyncio.sleep(delay)
|
|||
|
|
|
|||
|
|
# 增加重试计数
|
|||
|
|
task.retry_count += 1
|
|||
|
|
self.retry_stats['total_retries'] += 1
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
last_error = str(e)
|
|||
|
|
logger.error(f"任务 {task.task_id} 执行异常: {e}")
|
|||
|
|
|
|||
|
|
if attempt < self.max_retries - 1:
|
|||
|
|
delay = self._calculate_delay(attempt)
|
|||
|
|
logger.info(f"任务 {task.task_id} 将在 {delay} 秒后重试")
|
|||
|
|
await asyncio.sleep(delay)
|
|||
|
|
task.retry_count += 1
|
|||
|
|
self.retry_stats['total_retries'] += 1
|
|||
|
|
else:
|
|||
|
|
self.retry_stats['failed_retries'] += 1
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# 所有重试都失败
|
|||
|
|
task.status = TaskStatus.FAILED
|
|||
|
|
self.retry_stats['failed_retries'] += 1
|
|||
|
|
|
|||
|
|
return DownloadResult(
|
|||
|
|
success=False,
|
|||
|
|
task_id=task.task_id,
|
|||
|
|
error_message=f"重试 {self.max_retries} 次后仍然失败: {last_error}",
|
|||
|
|
retry_count=task.retry_count
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _should_retry(self, result: DownloadResult, attempt: int) -> bool:
|
|||
|
|
"""判断是否应该重试"""
|
|||
|
|
# 如果已经达到最大重试次数,不重试
|
|||
|
|
if attempt >= self.max_retries - 1:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 如果没有错误消息,可能是未知错误,应该重试
|
|||
|
|
if not result.error_message:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 检查是否是可重试的错误
|
|||
|
|
retryable_errors = [
|
|||
|
|
'timeout',
|
|||
|
|
'connection',
|
|||
|
|
'network',
|
|||
|
|
'429', # Too Many Requests
|
|||
|
|
'503', # Service Unavailable
|
|||
|
|
'502', # Bad Gateway
|
|||
|
|
'504', # Gateway Timeout
|
|||
|
|
'空响应',
|
|||
|
|
'返回空',
|
|||
|
|
'empty response',
|
|||
|
|
'temporary'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
error_lower = result.error_message.lower()
|
|||
|
|
for error in retryable_errors:
|
|||
|
|
if error in error_lower:
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 检查是否是不可重试的错误
|
|||
|
|
non_retryable_errors = [
|
|||
|
|
'404', # Not Found
|
|||
|
|
'403', # Forbidden
|
|||
|
|
'401', # Unauthorized
|
|||
|
|
'invalid',
|
|||
|
|
'not found',
|
|||
|
|
'deleted',
|
|||
|
|
'已删除',
|
|||
|
|
'不存在'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for error in non_retryable_errors:
|
|||
|
|
if error in error_lower:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 默认重试
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _calculate_delay(self, attempt: int) -> float:
|
|||
|
|
"""计算重试延迟时间"""
|
|||
|
|
if self.exponential_backoff:
|
|||
|
|
# 指数退避:2^attempt 秒,最大30秒
|
|||
|
|
delay = min(2 ** attempt, 30)
|
|||
|
|
else:
|
|||
|
|
# 使用预定义的延迟列表
|
|||
|
|
if attempt < len(self.retry_delays):
|
|||
|
|
delay = self.retry_delays[attempt]
|
|||
|
|
else:
|
|||
|
|
delay = self.retry_delays[-1]
|
|||
|
|
|
|||
|
|
# 添加一些随机性以避免同时重试
|
|||
|
|
import random
|
|||
|
|
jitter = random.uniform(0, 0.3 * delay)
|
|||
|
|
|
|||
|
|
return delay + jitter
|
|||
|
|
|
|||
|
|
def get_stats(self) -> dict:
|
|||
|
|
"""获取重试统计信息"""
|
|||
|
|
return self.retry_stats.copy()
|
|||
|
|
|
|||
|
|
def reset_stats(self):
|
|||
|
|
"""重置统计信息"""
|
|||
|
|
self.retry_stats = {
|
|||
|
|
'total_retries': 0,
|
|||
|
|
'successful_retries': 0,
|
|||
|
|
'failed_retries': 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def with_retry(
|
|||
|
|
max_retries: int = 3,
|
|||
|
|
retry_delays: Optional[List[float]] = None,
|
|||
|
|
exponential_backoff: bool = True
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
装饰器:为异步函数添加重试机制
|
|||
|
|
|
|||
|
|
Usage:
|
|||
|
|
@with_retry(max_retries=3)
|
|||
|
|
async def download_file(url):
|
|||
|
|
...
|
|||
|
|
"""
|
|||
|
|
def decorator(func):
|
|||
|
|
@wraps(func)
|
|||
|
|
async def wrapper(*args, **kwargs):
|
|||
|
|
last_exception = None
|
|||
|
|
delays = retry_delays or [1, 2, 5, 10, 30]
|
|||
|
|
|
|||
|
|
for attempt in range(max_retries):
|
|||
|
|
try:
|
|||
|
|
return await func(*args, **kwargs)
|
|||
|
|
except Exception as e:
|
|||
|
|
last_exception = e
|
|||
|
|
|
|||
|
|
if attempt < max_retries - 1:
|
|||
|
|
if exponential_backoff:
|
|||
|
|
delay = min(2 ** attempt, 30)
|
|||
|
|
else:
|
|||
|
|
delay = delays[attempt] if attempt < len(delays) else delays[-1]
|
|||
|
|
|
|||
|
|
logger.warning(f"函数 {func.__name__} 失败 (尝试 {attempt + 1}/{max_retries}): {e}")
|
|||
|
|
logger.info(f"将在 {delay} 秒后重试")
|
|||
|
|
await asyncio.sleep(delay)
|
|||
|
|
else:
|
|||
|
|
logger.error(f"函数 {func.__name__} 重试 {max_retries} 次后仍然失败")
|
|||
|
|
|
|||
|
|
raise last_exception
|
|||
|
|
|
|||
|
|
return wrapper
|
|||
|
|
return decorator
|