全栈开发

tenacity 重试监控实战

监控记录重试过程:

tools.py

import traceback
import logging
from tenacity import RetryCallState

# ---------- 全局日志配置 ----------
logging.basicConfig(
    level=logging.INFO,  # 可改为 WARNING 只显示重要信息
    format="%(asctime)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)


# ---------- RetryMonitor 工具类 ----------
class RetryMonitor:
    """Tenacity 重试监控器,使用 logging 输出"""

    def __init__(self, logger: logging.Logger = None):
        self.logger = logger or logging.getLogger(__name__)

    @staticmethod
    def _get_error_detail(exception):
        error_type = type(exception).__name__
        error_msg = str(exception)
        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 before_sleep_callback(self, retry_state: RetryCallState) -> None:
        """重试前打印详细信息(before_sleep 回调)"""
        exception = retry_state.outcome.exception()
        error_type, error_msg, error_location = self._get_error_detail(exception)
        wait_seconds = retry_state.next_action.sleep if retry_state.next_action else 0

        msg = (
            f"🔄 准备第 {retry_state.attempt_number} 次重试\n"
            f"{'=' * 60}\n"
            f"💥 上次错误类型: {error_type}\n"
            f"📝 上次错误信息: {error_msg}\n"
            f"📍 错误发生位置: {error_location}\n"
            f"⏳ 等待 {wait_seconds:.1f} 秒后重试...\n"
            f"{'=' * 60}"
        )
        self.logger.warning(msg)

    def after_attempt_callback(self, retry_state: RetryCallState) -> None:
        """每次失败后打印(after 回调)"""
        exception = retry_state.outcome.exception()
        error_type, error_msg, _ = self._get_error_detail(exception)
        msg = f"❌ 第 {retry_state.attempt_number} 次执行失败 -> {error_type}: {error_msg}"
        self.logger.warning(msg)

test.py

import time
import random
from tenacity import (
    retry, stop_after_attempt, wait_exponential,
    retry_if_exception_type, RetryError
)

from relearn_tenacity.tools import RetryMonitor

# 创建监控器(可全局共享)
monitor = RetryMonitor()


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=monitor.before_sleep_callback,
        after=monitor.after_attempt_callback
    )
    def process_file(self, filename: str) -> dict:
        """处理文件"""
        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)
        }


def run_file_demo():
    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)


if __name__ == '__main__':
    run_file_demo()

输出:

===================================
场景2: 文件处理自动重试
===================================
📄 开始处理文件: data_batch_1.txt
2026-07-23 12:11:08 -   1 次执行失败 -> OSError: IO错误:无法读取文件 data_batch_1.txt,磁盘繁忙
2026-07-23 12:11:08 - 🔄 准备第 1 次重试
============================================================
💥 上次错误类型: OSError
📝 上次错误信息: IO错误:无法读取文件 data_batch_1.txt,磁盘繁忙
📍 错误发生位置: 未知位置
 等待 3.0 秒后重试...
============================================================
📄 开始处理文件: data_batch_1.txt
 data_batch_1.txt 处理成功: 大小=46202字节
--------------------------------------------------
📄 开始处理文件: data_batch_2.txt
 data_batch_2.txt 处理成功: 大小=16225字节
--------------------------------------------------
📄 开始处理文件: data_batch_3.txt
2026-07-23 12:11:14 -   1 次执行失败 -> TimeoutError: 处理超时:文件 data_batch_3.txt 过大,处理超过60秒
2026-07-23 12:11:14 - 🔄 准备第 1 次重试
============================================================
💥 上次错误类型: TimeoutError
📝 上次错误信息: 处理超时:文件 data_batch_3.txt 过大,处理超过60秒
📍 错误发生位置: 未知位置
 等待 3.0 秒后重试...
============================================================
📄 开始处理文件: data_batch_3.txt
2026-07-23 12:11:18 -   2 次执行失败 -> ConnectionError: 连接中断:云存储服务临时不可用
2026-07-23 12:11:18 - 🔄 准备第 2 次重试
============================================================
💥 上次错误类型: ConnectionError
📝 上次错误信息: 连接中断:云存储服务临时不可用
📍 错误发生位置: 未知位置
 等待 4.0 秒后重试...
============================================================
📄 开始处理文件: data_batch_3.txt
 data_batch_3.txt 处理成功: 大小=25740字节
--------------------------------------------------