重试回调机制处理

71 阅读8分钟

image.png

无标题-2025-06-10-1021.png JS代码实现如下

  1. 简单易用:只需传入总时长,系统自动计算最优重试次数
  1. 精确控制:完全按照图片中的时间序列执行
  1. 灵活配置:支持总时长和重试次数两种配置方式
/**
 * 重试回调机制处理工具
 * 
 * 实现指数退避重试策略,解决网络不稳定、服务器临时故障等问题
 * 
 * 核心特性:
 * - 快速响应:初始重试间隔短(15秒),快速恢复正常服务
 * - 逐步退避:重试间隔指数增长,避免对故障服务造成额外压力
 * - 稳定重复:达到最大间隔后保持稳定,持续尝试恢复
 * - 资源节约:通过抖动机制避免多个客户端同时重试造成的系统过载
 * 
 * 重试时间序列示例:
 * 15s → 30s → 60s → 120s → 240s → 480s → 960s → 1800s → 3600s → 21600s(6h) → 21600s...
 * 
 * 适用场景:
 * - API调用失败重试
 * - 文件上传/下载重试
 * - 数据库连接重试
 * - 第三方服务调用重试
 * - 网络请求超时重试
 * 
 */

/**
 * 重试配置选项
 * @typedef {Object} RetryOptions
 * @property {number} [maxRetries] - 最大重试次数(可选,如果设置了totalDuration则忽略此项)
 * @property {number} [totalDuration] - 总重试时长(毫秒),自动计算重试次数
 * @property {number} jitter - 抖动因子(0-1),默认0.1,用于避免系统过载
 * @property {boolean} enableJitter - 是否启用抖动,默认true
 * @property {Function} shouldRetry - 自定义重试条件判断函数
 */

// 默认重试配置
var DEFAULT_RETRY_OPTIONS = {
  maxRetries: 15, // 对应完整的重试序列长度
  jitter: 0.1,
  enableJitter: true,
  shouldRetry: function(error, attempt) {
    // 默认重试条件:网络错误、超时错误、5xx服务器错误
    if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') {
      return true;
    }
    if (error.response && error.response.status >= 500) {
      return true;
    }
    return false;
  }
};

/**
 * 获取重试时间序列
 * @returns {Array<number>} 重试时间序列(毫秒)
 */
function getDelaySequence() {
  return [
    15 * 1000,    // 15秒
    15 * 1000,    // 15秒 (重复一次)
    30 * 1000,    // 30秒
    180 * 1000,   // 3分钟
    600 * 1000,   // 10分钟
    1200 * 1000,  // 20分钟
    1800 * 1000,  // 30分钟
    1800 * 1000,  // 30分钟 (重复)
    1800 * 1000,  // 30分钟 (重复)
    3600 * 1000,  // 1小时
    10800 * 1000, // 3小时
    10800 * 1000, // 3小时 (重复)
    10800 * 1000, // 3小时 (重复)
    21600 * 1000, // 6小时
    21600 * 1000  // 6小时 (重复)
  ];
}

/**
 * 根据总时长计算最大重试次数
 * @param {number} totalDuration - 总重试时长(毫秒)
 * @returns {number} 计算出的最大重试次数
 */
function calculateMaxRetriesFromDuration(totalDuration) {
  var delaySequence = getDelaySequence();
  var accumulatedTime = 0;
  var maxRetries = 0;
  
  for (var i = 0; i < delaySequence.length; i++) {
    accumulatedTime += delaySequence[i];
    if (accumulatedTime <= totalDuration) {
      maxRetries = i + 1;
    } else {
      break;
    }
  }
  
  // 如果总时长超过了预定义序列的总和,继续使用最后一个间隔计算
  if (accumulatedTime < totalDuration && delaySequence.length > 0) {
    var lastDelay = delaySequence[delaySequence.length - 1];
    var remainingTime = totalDuration - accumulatedTime;
    var additionalRetries = Math.floor(remainingTime / lastDelay);
    maxRetries += additionalRetries;
  }
  
  return maxRetries;
}

/**
 * 计算下次重试的延迟时间(预定义序列)
 * 
 * 使用预定义的重试时间序列,实现图中描述的连续重试模式:
 * 15s → 15s → 30s → 180s → 600s → 1200s → 1800s → 1800s → 1800s → 3600s → 10800s → 10800s → 10800s → 21600s → 21600s...
 * 
 * @param {number} attempt - 当前重试次数(从0开始)
 * @param {RetryOptions} options - 重试配置
 * @returns {number} 延迟时间(毫秒)
 */
function calculateDelay(attempt, options) {
  var jitter = options.jitter;
  var enableJitter = options.enableJitter;
  
  // 获取预定义的重试时间序列
  var delaySequence = getDelaySequence();
  
  // 获取对应序列中的延迟时间
  var delay;
  if (attempt < delaySequence.length) {
    // 使用序列中的预定义时间
    delay = delaySequence[attempt];
  } else {
    // 超出序列长度后,保持最后一个值(6小时)
    delay = delaySequence[delaySequence.length - 1];
  }
  
  // 添加抖动,避免系统过载
  if (enableJitter && jitter > 0) {
    var jitterAmount = delay * jitter;
    var randomJitter = (Math.random() - 0.5) * 2 * jitterAmount;
    delay += randomJitter;
  }
  
  return Math.max(delay, 0);
}

/**
 * 延迟执行函数
 * @param {number} ms - 延迟时间(毫秒)
 * @returns {Promise} Promise对象
 */
function delay(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

/**
 * 重试回调机制处理函数
 * @param {Function} asyncFunction - 需要重试的异步函数
 * @param {RetryOptions} options - 重试配置选项
 * @returns {Promise} 返回执行结果的Promise
 */
export function retryWithBackoff(asyncFunction, options) {
  options = options || {};
  var config = Object.assign({}, DEFAULT_RETRY_OPTIONS, options);
  
  // 如果设置了总时长,自动计算最大重试次数
  if (options.totalDuration && !options.maxRetries) {
    config.maxRetries = calculateMaxRetriesFromDuration(options.totalDuration);
    console.log('[重试机制] 根据总时长 ' + options.totalDuration + 'ms 计算出最大重试次数: ' + config.maxRetries);
  }
  
  var lastError;
  
  return new Promise(function(resolve, reject) {
    function attemptExecution(attempt) {
      Promise.resolve(asyncFunction())
        .then(function(result) {
          // 成功执行,返回结果
          console.log('[重试机制] 第' + (attempt + 1) + '次尝试成功');
          resolve(result);
        })
        .catch(function(error) {
          lastError = error;
          
          // 检查是否应该重试
          if (attempt === config.maxRetries || !config.shouldRetry(error, attempt)) {
            console.error('[重试机制] 第' + (attempt + 1) + '次尝试失败,不再重试:', error.message);
            reject(error);
            return;
          }
          
          // 计算延迟时间
          var delayTime = calculateDelay(attempt, config);
          
          console.warn('[重试机制] 第' + (attempt + 1) + '次尝试失败,' + delayTime + 'ms后重试:', error.message);
          
          // 延迟后重试
          delay(delayTime).then(function() {
            attemptExecution(attempt + 1);
          });
        });
    }
    
    attemptExecution(0);
  });
}

/**
 * 创建带重试机制的函数包装器
 * @param {Function} asyncFunction - 需要包装的异步函数
 * @param {RetryOptions} options - 重试配置选项
 * @returns {Function} 包装后的函数
 */
export function withRetry(asyncFunction, options) {
  return function() {
    var args = Array.prototype.slice.call(arguments);
    return retryWithBackoff(function() {
      return asyncFunction.apply(null, args);
    }, options);
  };
}

/**
 * 预设的重试配置
 * 
 * 使用 totalDuration 来控制重试总时长,自动计算重试次数
 * 所有预设都使用相同的重试时间序列:
 * 15s → 15s → 30s → 180s → 600s → 1200s → 1800s → 1800s → 1800s → 3600s → 10800s → 10800s → 10800s → 21600s → 21600s...
 */
export var RETRY_PRESETS = {
  // 快速重试:适用于轻量级操作,总时长1分钟
  // 自动计算:15s + 15s + 30s = 60s,重试3次
  FAST: {
    totalDuration: 60 * 1000 // 1分钟
  },
  
  // 标准重试:适用于一般API调用,总时长30分钟
  // 自动计算:15s + 15s + 30s + 180s + 600s + 1200s = 2040s (34分钟),重试6次
  STANDARD: {
    totalDuration: 30 * 60 * 1000 // 30分钟
  },
  
  // 长时间重试:适用于重要的后台任务,总时长2小时
  // 自动计算到2小时内的所有重试
  LONG_RUNNING: {
    totalDuration: 2 * 60 * 60 * 1000 // 2小时
  },
  
  // 完整重试:使用完整序列,总时长24小时
  // 包含完整的重试序列,最终稳定在6小时间隔
  FULL: {
    totalDuration: 24 * 60 * 60 * 1000 // 24小时
  },
  
  // 网络请求重试:专门针对网络请求,总时长1分钟
  NETWORK: {
    totalDuration: 60 * 1000, // 1分钟
    shouldRetry: function(error) {
      return error.code === 'NETWORK_ERROR' || 
             error.code === 'TIMEOUT' ||
             (error.response && error.response.status >= 500);
    }
  }
};

/**
 * 重试状态管理器
 */
export function RetryManager() {
  this.activeRetries = new Map();
}

RetryManager.prototype = {
  /**
   * 执行带重试的异步操作
   * @param {string} key - 操作标识符
   * @param {Function} asyncFunction - 异步函数
   * @param {RetryOptions} options - 重试配置
   * @returns {Promise} 执行结果
   */
  execute: function(key, asyncFunction, options) {
    var self = this;
    
    // 如果已有相同key的重试在进行,返回现有的Promise
    if (this.activeRetries.has(key)) {
      console.log('[重试管理器] 操作"' + key + '"已在重试中,返回现有Promise');
      return this.activeRetries.get(key);
    }
    
    // 创建新的重试Promise
    var retryPromise = retryWithBackoff(asyncFunction, options)
      .finally(function() {
        // 完成后清理
        self.activeRetries.delete(key);
      });
    
    this.activeRetries.set(key, retryPromise);
    return retryPromise;
  },
  
  /**
   * 取消指定的重试操作
   * @param {string} key - 操作标识符
   */
  cancel: function(key) {
    if (this.activeRetries.has(key)) {
      this.activeRetries.delete(key);
      console.log('[重试管理器] 已取消操作"' + key + '"的重试');
    }
  },
  
  /**
   * 获取当前活跃的重试操作数量
   * @returns {number} 活跃重试数量
   */
  getActiveCount: function() {
    return this.activeRetries.size;
  },
  
  /**
   * 清理所有重试操作
   */
  clear: function() {
    this.activeRetries.clear();
    console.log('[重试管理器] 已清理所有重试操作');
  }
};

// 导出默认的重试管理器实例
export var defaultRetryManager = new RetryManager();

/**
 * 辅助函数:根据总时长预览重试次数和时间点
 * @param {number} totalDuration - 总时长(毫秒)
 * @returns {Object} 包含重试次数和时间点信息
 */
export function previewRetrySchedule(totalDuration) {
  var maxRetries = calculateMaxRetriesFromDuration(totalDuration);
  var delaySequence = getDelaySequence();
  var schedule = [];
  var accumulatedTime = 0;
  
  for (var i = 0; i < maxRetries && i < delaySequence.length; i++) {
    accumulatedTime += delaySequence[i];
    schedule.push({
      attempt: i + 1,
      delay: delaySequence[i],
      delayFormatted: formatDuration(delaySequence[i]),
      accumulatedTime: accumulatedTime,
      accumulatedTimeFormatted: formatDuration(accumulatedTime)
    });
  }
  
  // 如果超出了预定义序列,继续计算
  if (maxRetries > delaySequence.length) {
    var lastDelay = delaySequence[delaySequence.length - 1];
    for (var j = delaySequence.length; j < maxRetries; j++) {
      accumulatedTime += lastDelay;
      schedule.push({
        attempt: j + 1,
        delay: lastDelay,
        delayFormatted: formatDuration(lastDelay),
        accumulatedTime: accumulatedTime,
        accumulatedTimeFormatted: formatDuration(accumulatedTime)
      });
    }
  }
  
  return {
    totalDuration: totalDuration,
    totalDurationFormatted: formatDuration(totalDuration),
    maxRetries: maxRetries,
    schedule: schedule
  };
}

/**
 * 格式化时长显示
 * @param {number} ms - 毫秒数
 * @returns {string} 格式化后的时长字符串
 */
function formatDuration(ms) {
  var seconds = Math.floor(ms / 1000);
  var minutes = Math.floor(seconds / 60);
  var hours = Math.floor(minutes / 60);
  
  if (hours > 0) {
    return hours + '小时' + (minutes % 60 > 0 ? (minutes % 60) + '分钟' : '');
  } else if (minutes > 0) {
    return minutes + '分钟' + (seconds % 60 > 0 ? (seconds % 60) + '秒' : '');
  } else {
    return seconds + '秒';
  }
}

/**
 * 使用示例:
 * 
 * // 基础使用 - 网络请求重试1分钟总时长
 * retryWithBackoff(function() {
 *   return fetch('/api/data');
 * }, RETRY_PRESETS.NETWORK);
 * 
 * // 标准重试 - 30分钟总时长,自动计算重试次数
 * var apiCall = withRetry(function(url) {
 *   return fetch(url);
 * }, RETRY_PRESETS.STANDARD);
 * 
 * // 长时间重试 - 2小时总时长
 * defaultRetryManager.execute('important-task', function() {
 *   return fetch('/api/critical-operation');
 * }, RETRY_PRESETS.LONG_RUNNING);
 * 
 * // 完整重试 - 24小时总时长,最终稳定在6小时间隔
 * retryWithBackoff(function() {
 *   return fetch('/api/critical-system');
 * }, RETRY_PRESETS.FULL);
 * 
 * // 自定义总时长 - 重试5分钟
 * retryWithBackoff(function() {
 *   return fetch('/api/custom-check');
 * }, { totalDuration: 5 * 60 * 1000 }); // 5分钟
 * 
 * // 仍然支持手动设置重试次数
 * retryWithBackoff(function() {
 *   return fetch('/api/quick-check');
 * }, { maxRetries: 2 });
 * 
 * // 预览重试计划
 * var schedule = previewRetrySchedule(30 * 60 * 1000); // 30分钟
 * console.log('总重试次数:', schedule.maxRetries);
 * console.log('重试计划:', schedule.schedule);
 * // 输出示例:
 * // 总重试次数: 6
 * // 重试计划: [
 * //   { attempt: 1, delay: 15000, delayFormatted: '15秒', accumulatedTime: 15000, accumulatedTimeFormatted: '15秒' },
 * //   { attempt: 2, delay: 15000, delayFormatted: '15秒', accumulatedTime: 30000, accumulatedTimeFormatted: '30秒' },
 * //   ...
 * // ]
 */