全栈开发

tenacity 重试库入门

Demo 1: 基础入门

"""
demo1_basics.py - 感受tenacity的基本魔力
"""
from tenacity import retry, stop_after_attempt, wait_fixed
import time

# ========== 最简单的重试 ==========
@retry
def infinite_retry():
    print("我会一直重试...按Ctrl+C停止")
    raise Exception("永远失败")
    time.sleep(1)

# ========== 限制重试3次 ==========
@retry(stop=stop_after_attempt(3))
def try_3_times():
    print(f"[{time.strftime('%H:%M:%S')}] 尝试执行...")
    raise Exception("失败了")

# ========== 每次等待2秒 ==========
@retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
def wait_2_seconds():
    print(f"[{time.strftime('%H:%M:%S')}] 执行")
    raise Exception("失败")

if __name__ == '__main__':
    print("=" * 40)
    print("测试1: 限制重试3次")
    print("=" * 40)
    try:
        try_3_times()
    except Exception as e:
        print(f"最终失败: {e}")
    
    print("\n" + "=" * 40)
    print("测试2: 每次等待2秒")
    print("=" * 40)
    try:
        wait_2_seconds()
    except Exception as e:
        print(f"最终失败: {e}")

Demo 2: 等待策略对比

"""
demo2_wait_strategies.py - 不同的等待策略效果
"""
from tenacity import (
    retry, stop_after_attempt, 
    wait_fixed, wait_random, wait_exponential
)
import time
import random

# ========== 固定等待 ==========
@retry(stop=stop_after_attempt(4), wait=wait_fixed(2))
def fixed_wait():
    print(f"[{time.strftime('%H:%M:%S')}] 固定等2秒")
    raise Exception("失败")

# ========== 随机等待 ==========
@retry(stop=stop_after_attempt(4), wait=wait_random(min=1, max=5))
def random_wait():
    wait_time = random.randint(1, 5)
    print(f"[{time.strftime('%H:%M:%S')}] 随机等待(模拟)")
    raise Exception("失败")

# ========== 指数退避(生产环境推荐) ==========
@retry(
    stop=stop_after_attempt(5),
    wait=wait_exponential(multiplier=1, min=1, max=30)
)
def exponential_wait():
    print(f"[{time.strftime('%H:%M:%S')}] 指数退避")
    raise Exception("失败")

# ========== 模拟真实场景 ==========
class APIClient:
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=2, min=1, max=10)
    )
    def fetch_data(self):
        print(f"[{time.strftime('%H:%M:%S')}] 调用API...")
        if random.random() < 0.7:  # 70%失败率
            raise ConnectionError("网络超时")
        return {"status": "success"}

if __name__ == '__main__':
    print("1. 固定等待策略")
    print("-" * 30)
    try:
        fixed_wait()
    except:
        pass
    
    print("\n2. 指数退避策略(推荐)")
    print("-" * 30)
    try:
        exponential_wait()
    except:
        pass
    
    print("\n3. 模拟API调用")
    print("-" * 30)
    client = APIClient()
    try:
        result = client.fetch_data()
        print(f"成功: {result}")
    except Exception as e:
        print(f"失败: {e}")

Demo 3: 条件重试

"""
demo3_conditions.py - 智能判断何时重试
"""
from tenacity import (
    retry, stop_after_attempt,
    retry_if_exception_type, retry_if_result
)

# ========== 只重试网络错误 ==========
@retry(
    stop=stop_after_attempt(3),
    retry=retry_if_exception_type((ConnectionError, TimeoutError))
)
def selective_retry():
    """网络错误重试,其他错误不重试"""
    import random
    error_type = random.choice(['network', 'data', 'success'])
    
    if error_type == 'network':
        raise ConnectionError("网络超时 -> 会重试")
    elif error_type == 'data':
        raise ValueError("数据格式错误 -> 不重试")
    else:
        return "成功"

# ========== 根据返回值判断 ==========
@retry(
    stop=stop_after_attempt(5),
    retry=retry_if_result(lambda x: x is None or x == "")
)
def retry_on_bad_result():
    """返回None或空字符串就重试"""
    import random
    result = random.choice([None, "", "有效数据"])
    print(f"返回: {result}")
    return result

# ========== 自定义重试逻辑 ==========
def should_retry(exception):
    """自定义判断函数"""
    error_msg = str(exception)
    # 包含这些关键词才重试
    retry_keywords = ['timeout', '503', 'rate limit', 'temporary']
    return any(keyword in error_msg.lower() for keyword in retry_keywords)

@retry(
    stop=stop_after_attempt(3),
    retry=retry_if_exception(should_retry)
)
def custom_retry_logic():
    """根据错误信息决定是否重试"""
    import random
    errors = [
        "Request timeout",           # 会重试
        "Error 503 Service Down",    # 会重试  
        "Invalid API key",           # 不重试
        "Rate limit exceeded"        # 会重试
    ]
    error = random.choice(errors)
    raise Exception(error)

if __name__ == '__main__':
    print("1. 选择性异常重试")
    print("-" * 40)
    for i in range(3):
        try:
            result = selective_retry()
            print(f"✅ {result}")
        except Exception as e:
            print(f"❌ {e}")
    
    print("\n2. 基于返回值重试")
    print("-" * 40)
    result = retry_on_bad_result()
    print(f"✅ 最终结果: {result}")
    
    print("\n3. 自定义重试条件")
    print("-" * 40)
    for i in range(3):
        try:
            custom_retry_logic()
        except Exception as e:
            print(f"❌ 不重试: {e}")

Demo 4: 实战模拟

"""
demo4_real_world.py - 模拟真实场景
"""
from tenacity import (
    retry, stop_after_attempt, wait_exponential,
    retry_if_exception_type, before_log, after_log
)
import logging
import time
import random

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(message)s'
)
logger = logging.getLogger(__name__)

# ========== 文件上传模拟 ==========
class FileUploader:
    def __init__(self):
        self.uploaded_files = []
    
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        retry=retry_if_exception_type(ConnectionError),
        before=lambda _: logger.info("🔄 准备上传文件..."),
        after=lambda _: logger.info("📁 上传尝试结束")
    )
    def upload(self, filename):
        """模拟文件上传(70%成功率)"""
        logger.info(f"上传中: {filename}")
        
        # 模拟网络波动
        if random.random() < 0.7:
            raise ConnectionError(f"上传{filename}时网络断开")
        
        self.uploaded_files.append(filename)
        logger.info(f"✅ {filename} 上传成功")
        return True

# ========== 网页爬虫模拟 ==========
class WebScraper:
    @retry(
        stop=stop_after_attempt(5),
        wait=wait_exponential(multiplier=2, min=1, max=60),
        retry=retry_if_exception_type(TimeoutError)
    )
    def fetch_page(self, url):
        """模拟爬取网页(防反爬)"""
        logger.info(f"爬取: {url}")
        
        # 模拟各种失败
        fail_type = random.random()
        if fail_type < 0.5:
            raise TimeoutError("请求超时")
        elif fail_type < 0.8:
            raise ConnectionError("连接被拒绝")
        
        return f"<html>{url}的内容</html>"

# ========== 数据库操作模拟 ==========
class DatabaseOperator:
    def __init__(self):
        self.retry_count = 0
    
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=0.5, min=1, max=5)
    )
    def execute_with_retry(self, query):
        """模拟数据库操作"""
        self.retry_count += 1
        logger.info(f"执行SQL(第{self.retry_count}次): {query}")
        
        # 模拟死锁
        if random.random() < 0.6:
            raise Exception("Deadlock detected")
        
        self.retry_count = 0
        return "Query executed successfully"

if __name__ == '__main__':
    print("=" * 60)
    print("🔧 真实场景模拟")
    print("=" * 60)
    
    # 场景1: 文件上传
    print("\n📂 场景1: 批量文件上传")
    print("-" * 40)
    uploader = FileUploader()
    files = ["data1.txt", "data2.txt", "data3.txt"]
    
    for file in files:
        try:
            uploader.upload(file)
        except Exception as e:
            logger.error(f"❌ {file} 上传失败: {e}")
    
    logger.info(f"成功上传: {uploader.uploaded_files}")
    
    # 场景2: 网页爬取
    print("\n🌐 场景2: 网页爬取")
    print("-" * 40)
    scraper = WebScraper()
    
    try:
        content = scraper.fetch_page("https://example.com")
        logger.info(f"✅ 爬取成功: {content}")
    except Exception as e:
        logger.error(f"❌ 爬取失败: {e}")
    
    # 场景3: 数据库操作
    print("\n💾 场景3: 数据库重试")
    print("-" * 40)
    db = DatabaseOperator()
    
    try:
        result = db.execute_with_retry("SELECT * FROM users")
        logger.info(f"✅ {result}")
    except Exception as e:
        logger.error(f"❌ 数据库操作失败: {e}")

Demo 5: 回调函数和监控

"""
demo5_callbacks.py - 掌握重试过程
"""
from tenacity import (
    retry, stop_after_attempt, wait_fixed,
    before_sleep, after_attempt, before_log,
    RetryCallState
)
import logging
import time

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ========== 监控每次重试 ==========
def on_retry(retry_state: RetryCallState):
    """重试前回调"""
    print(f"⚠️ 重试 #{retry_state.attempt_number}")
    print(f"   等待 {retry_state.next_action.sleep} 秒")
    
    # 访问异常信息
    if retry_state.outcome.failed:
        exception = retry_state.outcome.exception()
        print(f"   原因: {type(exception).__name__}: {exception}")

def on_success(retry_state: RetryCallState):
    """成功回调"""
    if retry_state.attempt_number > 1:
        print(f"🎉 在第 {retry_state.attempt_number} 次尝试后成功!")

@retry(
    stop=stop_after_attempt(5),
    wait=wait_fixed(1),
    before_sleep=on_retry,
    after=on_success
)
def monitored_function():
    """带监控的函数"""
    import random
    if random.random() < 0.8:
        raise Exception("随机失败")
    return "成功!"

# ========== 统计重试信息 ==========
class RetryStats:
    def __init__(self):
        self.total_retries = 0
        self.total_time = 0
        self.start_time = None
    
    def before_retry(self, retry_state):
        if self.start_time is None:
            self.start_time = time.time()
        self.total_retries += 1
    
    def get_stats(self):
        elapsed = time.time() - self.start_time if self.start_time else 0
        return {
            'total_retries': self.total_retries,
            'total_time': f"{elapsed:.2f}s"
        }

stats = RetryStats()

@retry(
    stop=stop_after_attempt(4),
    wait=wait_fixed(0.5),
    before_sleep=stats.before_retry
)
def stats_function():
    """收集统计信息"""
    import random
    if random.random() < 0.7:
        raise Exception("失败")
    return "成功"

# ========== 条件打断 ==========
def should_abort(retry_state):
    """根据时间决定是否继续"""
    elapsed = time.time() - retry_state.start_time
    return elapsed > 10  # 超过10秒就放弃

@retry(
    stop=stop_after_attempt(100),  # 理论上100次
    wait=wait_fixed(1),
    retry=lambda e: True  # 任何异常都重试
)
def abortable_function():
    """可能被打断的函数"""
    elapsed = time.time() - abortable_function.start_time
    if elapsed > 5:
        print(f"⏰ 已过 {elapsed:.1f}秒,主动放弃")
        raise Exception("超时放弃")
    
    print(f"⏳ 执行中... ({elapsed:.1f}秒)")
    raise Exception("继续重试")

abortable_function.start_time = time.time()

if __name__ == '__main__':
    print("=" * 50)
    print("1. 监控重试过程")
    print("=" * 50)
    try:
        result = monitored_function()
        print(f"✅ 结果: {result}")
    except Exception as e:
        print(f"❌ 最终失败: {e}")
    
    print("\n" + "=" * 50)
    print("2. 重试统计")
    print("=" * 50)
    try:
        stats_function()
    except:
        pass
    print(f"📊 统计: {stats.get_stats()}")
    
    print("\n" + "=" * 50)
    print("3. 超时放弃")
    print("=" * 50)
    try:
        abortable_function()
    except Exception as e:
        print(f"❌ {e}")

综合示例:

"""
demo_comprehensive.py - 综合实战:完整的重试监控系统
"""
import time
import random
import traceback
from datetime import datetime
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    before_sleep,
    RetryCallState,
    RetryError
)


# ========== 工具函数 ==========

def format_time() -> str:
    """格式化当前时间"""
    return datetime.now().strftime("%H:%M:%S")


def get_error_detail(exception: Exception) -> str:
    """提取错误详情"""
    error_type = type(exception).__name__
    error_msg = str(exception)

    # 提取traceback的最后一行(关键错误位置)
    tb_lines = traceback.format_exc().split('\n')
    error_location = "未知位置"
    for line in reversed(tb_lines):
        if line.strip().startswith('File'):
            error_location = line.strip()
            break

    return error_type, error_msg, error_location


# ========== 自定义回调函数 ==========

def log_retry_attempt(retry_state: RetryCallState) -> None:
    """
    每次重试前打印详细信息
    这个函数会在每次重试之前自动调用
    """
    # 获取当前重试次数
    attempt_number = retry_state.attempt_number

    # 获取异常信息
    if retry_state.outcome and retry_state.outcome.failed:
        exception = retry_state.outcome.exception()
        error_type, error_msg, error_location = get_error_detail(exception)
    else:
        error_type = "首次尝试"
        error_msg = "无错误"
        error_location = "N/A"

    # 获取等待时间
    wait_seconds = retry_state.next_action.sleep if retry_state.next_action else 0

    # 打印分隔线
    print(f"\n{'=' * 60}")
    print(f"🔄 重试 #{attempt_number}")
    print(f"{'=' * 60}")
    print(f"⏰ 时间: {format_time()}")
    print(f"💥 错误类型: {error_type}")
    print(f"📝 错误信息: {error_msg}")
    print(f"📍 错误位置: {error_location}")
    print(f"⏳ 等待时间: {wait_seconds:.1f} 秒")
    print(f"{'=' * 60}\n")


def log_after_attempt(retry_state: RetryCallState) -> None:
    """每次尝试后打印"""
    if retry_state.outcome and retry_state.outcome.failed:
        # 失败
        pass  # 失败信息已在 before_sleep 中打印
    else:
        # 成功
        total_attempts = retry_state.attempt_number
        if total_attempts > 1:
            print(f"🎉 在第 {total_attempts} 次尝试后终于成功!")
        else:
            print(f"✅ 一次就成功了!")


# ========== 场景模拟 ==========

class NetworkSimulator:
    """模拟各种网络/业务异常"""

    @staticmethod
    def raise_random_error():
        """随机抛出不同类型的异常"""
        error_type = random.random()

        if error_type < 0.3:
            # 30% 概率超时
            raise TimeoutError("请求超时:服务器响应时间超过30秒")
        elif error_type < 0.5:
            # 20% 概率连接错误
            raise ConnectionError("连接被拒绝:目标服务器拒绝连接")
        elif error_type < 0.7:
            # 20% 概率临时性服务器错误
            raise Exception("服务器返回 503 Service Temporarily Unavailable")
        elif error_type < 0.85:
            # 15% 概率限流
            raise Exception("API限流:请求频率过高,请稍后重试")
        else:
            # 15% 概率其他错误
            raise RuntimeError("未知运行时错误:数据解析失败")


# ========== 实战示例1:API调用 ==========

class APIClient:
    """模拟API客户端"""

    def __init__(self, api_name: str):
        self.api_name = api_name
        self.call_count = 0

    @retry(
        stop=stop_after_attempt(5),  # 最多重试5次
        wait=wait_exponential(multiplier=1, min=2, max=30),  # 指数退避
        retry=retry_if_exception_type((  # 只重试这些异常
                TimeoutError,
                ConnectionError,
                Exception
        )),
        before_sleep=log_retry_attempt,  # 重试前打印
        after=log_after_attempt  # 每次尝试后处理
    )
    def call_api(self, endpoint: str, data: dict = None) -> dict:
        """
        调用API接口
        会自动重试:超时、连接错误、503、限流等
        """
        self.call_count += 1
        print(f"🌐 [{self.api_name}] 第{self.call_count}次调用 API: {endpoint}")

        # 模拟网络延迟
        time.sleep(0.5)

        # 70% 概率失败
        if random.random() < 0.7:
            NetworkSimulator.raise_random_error()

        # 成功返回
        self.call_count = 0
        return {
            "status": "success",
            "data": f"{endpoint} 的响应数据",
            "timestamp": format_time()
        }


# ========== 实战示例2:文件处理 ==========

class FileProcessor:
    """模拟文件处理器"""

    def __init__(self):
        self.processed_count = 0

    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=2, min=3, max=15),
        retry=retry_if_exception_type((
                TimeoutError,
                ConnectionError,
                OSError,
                IOError
        )),
        before_sleep=log_retry_attempt,
        after=log_after_attempt
    )
    def process_file(self, filename: str) -> dict:
        """
        处理文件
        会自动重试:超时、IO错误等
        """
        print(f"📄 开始处理文件: {filename}")

        # 模拟文件操作
        time.sleep(1)

        # 模拟各种失败情况
        fail_probability = random.random()
        if fail_probability < 0.4:
            raise TimeoutError(f"处理超时:文件 {filename} 过大,处理超过60秒")
        elif fail_probability < 0.7:
            raise IOError(f"IO错误:无法读取文件 {filename},磁盘繁忙")
        elif fail_probability < 0.85:
            raise ConnectionError(f"连接中断:云存储服务临时不可用")

        # 成功
        self.processed_count += 1
        return {
            "filename": filename,
            "status": "processed",
            "size": random.randint(1000, 50000)
        }


# ========== 实战示例3:数据库操作 ==========

class DatabaseClient:
    """模拟数据库客户端"""

    def __init__(self):
        self.query_count = 0
        self.connection_pool = 10

    @retry(
        stop=stop_after_attempt(4),
        wait=wait_exponential(multiplier=1, min=1, max=10),
        retry=retry_if_exception_type(Exception),
        before_sleep=log_retry_attempt,
        after=log_after_attempt
    )
    def execute_query(self, sql: str) -> list:
        """
        执行数据库查询
        会自动重试:死锁、连接池耗尽等
        """
        self.query_count += 1
        print(f"💾 [查询#{self.query_count}] 执行: {sql[:50]}...")

        time.sleep(0.3)

        error_random = random.random()
        if error_random < 0.3:
            raise Exception("死锁检测:事务被选为死锁牺牲品,请重试")
        elif error_random < 0.6:
            raise Exception("连接池耗尽:所有连接都在使用中")
        elif error_random < 0.8:
            raise Exception("查询超时:statement_timeout 超过限制")

        # 成功
        self.query_count = 0
        return [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"}
        ]


# ========== 执行演示 ==========

def run_demo():
    """运行完整演示"""
    print("=" * 70)
    print("🚀 Tenacity 综合重试演示系统")
    print("=" * 70)
    print("功能:自动重试 + 详细日志 + 错误追踪")
    print("=" * 70)

    # ---------- 场景1 ----------
    print("\n" + "=" * 35)
    print("场景1: API 调用自动重试")
    print("=" * 35)

    api_client = APIClient("千问API")

    try:
        result = api_client.call_api("/v1/chat", {"prompt": "你好"})
        print(f"\n✅ API调用成功!")
        print(f"返回数据: {result}")
    except RetryError as e:
        print(f"\n❌ API调用最终失败!")
        print(f"总共尝试了 {e.last_attempt.attempt_number} 次")
        print(f"最后一次错误: {e.last_attempt.exception()}")

    time.sleep(2)

    # ---------- 场景2 ----------
    print("\n" + "=" * 35)
    print("场景2: 文件处理自动重试")
    print("=" * 35)

    file_processor = FileProcessor()
    files = ["data_batch_1.txt", "data_batch_2.txt", "data_batch_3.txt"]

    for filename in files:
        try:
            result = file_processor.process_file(filename)
            print(f"✅ {filename} 处理成功: 大小={result['size']}字节")
        except RetryError as e:
            print(f"❌ {filename} 处理失败!")
            original_error = e.last_attempt.exception()
            error_type = type(original_error).__name__
            print(f"   错误类型: {error_type}")
            print(f"   错误信息: {original_error}")
        print("-" * 50)

    time.sleep(2)

    # ---------- 场景3 ----------
    print("\n" + "=" * 35)
    print("场景3: 数据库操作自动重试")
    print("=" * 35)

    db_client = DatabaseClient()
    queries = [
        "SELECT * FROM users WHERE active = 1",
        "UPDATE orders SET status = 'done'",
        "INSERT INTO logs VALUES (...)"
    ]

    for query in queries:
        try:
            results = db_client.execute_query(query)
            print(f"✅ 查询成功,返回 {len(results)} 条记录")
        except RetryError as e:
            print(f"❌ 查询最终失败!")
            print(f"   SQL: {query[:30]}...")
            print(f"   最后一次错误: {e.last_attempt.exception()}")
        print("-" * 50)


if __name__ == '__main__':
    run_demo()