重试

Swift 并发的重试算法

详情

该库提供了两个自由函数,一个使用通用时钟,另一个默认使用 ContinuousClock

public func retry<R, E, C>(
  maxAttempts: Int = 3,
  tolerance: C.Duration? = nil,
  clock: C,
  isolation: isolated (any Actor)? = #isolation,
  operation: () async throws(E) -> sending R,
  strategy: (E) -> RetryStrategy<C> = { _ in .backoff(.none) }
) async throws -> R where C: Clock, E: Error { ... }

public func retry<R, E>(
  maxAttempts: Int = 3,
  tolerance: ContinuousClock.Duration? = nil,
  isolation: isolated (any Actor)? = #isolation,
  operation: () async throws(E) -> sending R,
  strategy: (E) -> RetryStrategy<ContinuousClock> = { _ in .backoff(.none) }
) async throws -> R where E: Error { ... }

retry 函数执行一个异步操作,并在遇到错误时最多重试指定次数。您可以根据遇到的错误类型定义自定义重试策略,并使用 BackoffStrategy 控制重试尝试之间的延迟。

退避

此库附带 7 种预构建的退避策略

最小值

为此退避策略强制执行最小延迟持续时间。

此方法确保退避延迟永远不会短于定义的最小持续时间,有助于在重试之间保持基准等待时间。这保留了原始的退避模式,但将低于指定阈值的持续时间提高到最小值。

$g(x) = max(f(x), m)$其中 x 是当前尝试次数,f(x) 是基本退避策略。

extension BackoffStrategy {
  public func min(_ m: C.Duration) -> Self { ... }
}

最大值

限制此退避策略的最大延迟持续时间。

此方法确保退避延迟不超过定义的最大持续时间,有助于避免重试之间等待时间过长。这保留了原始的退避模式,直到指定的上限。

$g(x) = min(f(x), M)$其中 x 是当前尝试次数,f(x) 是基本退避策略。

extension BackoffStrategy {
  public func max(_ M: C.Duration) -> Self { ... }
}

抖动

将抖动应用于延迟持续时间,从而在退避间隔中引入随机性。

此方法在从零到基本持续时间的范围内随机化每次重试尝试的延迟。当多个源在分布式系统中并发重试时,抖动可以帮助减少争用。

$g(x) = random[0, f(x)[$其中 x 是当前尝试次数,f(x) 是基本退避策略。

@available(iOS 18.0, macOS 15.0, macCatalyst 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
extension BackoffStrategy where C.Duration == Duration {
  public func jitter<T>(using generator: T = SystemRandomNumberGenerator()) -> Self where T: RandomNumberGenerator { ... }
}

一种重试尝试之间没有延迟的退避策略。

此策略强制执行零持续时间延迟,使重试立即进行。它适用于需要在没有任何等待时间的情况下尽快进行重试的情况。

$f(x) = 0$其中 x 是当前尝试次数。

extension BackoffStrategy {
  public static var none: Self { ... }
}

常量

一种在每次尝试之间具有恒定延迟的退避策略。

此策略在重试之间应用固定的、不变的延迟,而与尝试次数无关。它非常适合需要在尝试之间使用统一间隔的重试模式。

$f(x) = c$其中 x 是当前尝试次数。

extension BackoffStrategy {
  public static func constant(_ c: C.Duration) -> Self { ... }
}

线性

一种在尝试之间具有线性递增延迟的退避策略。

此策略在每次重试后逐渐增加延迟,从初始延迟开始并线性缩放。适用于需要在一段时间内持续增加延迟的场景。

$f(x) = ax + b$其中 x 是当前尝试次数。

extension BackoffStrategy {
  public static func linear(a: C.Duration, b: C.Duration) -> Self { ... }
}

指数

一种在尝试之间具有指数递增延迟的退避策略。

此策略以指数方式增加延迟,从初始持续时间开始,并在每次重试后应用乘法因子。适用于重试应变得越来越稀疏的情况。

$f(x) = a * b^x$其中 x 是当前尝试次数。

extension BackoffStrategy where C.Duration == Duration {
  public static func exponential(a: C.Duration, b: Double) -> Self { ... }
}

示例

为了充分理解这一点,让我们用 URLSession.shared.data(from: url) 作为示例操作来说明此函数的 3 种自定义用法

自定义用法 1

使用默认值

let (data, response) = try await retry {
  try await URLSession.shared.data(from: url)
}

如果操作成功,则立即返回结果。但是,如果发生任何错误,操作将再尝试 2 次,因为默认的最大尝试次数为 3。在连续的尝试之间,不会有延迟,因为,同样,默认的重试策略是不添加任何退避。

自定义用法 2

使用自定义退避策略和自定义尝试次数

let (data, response) = try await retry(maxAttempts: 5) {
  try await URLSession.shared.data(from: url)
} strategy: { _ in
  return .backoff(.constant(.seconds(2)))
}

与第一个自定义用法类似,如果发生任何错误,操作将再尝试 4 次。但是,在连续的尝试之间,它将等待 2 秒钟,然后再运行操作。

自定义用法 3

使用自定义退避和重试策略

struct TooManyRequests: Error {
  let retryAfter: Double
}

let (data, response) = try await retry {
  let (data, response) = try await URLSession.shared.data(from: url)

  if
    let response = response as? HTTPURLResponse,
    let retryAfter = response.value(forHTTPHeaderField: "Retry-After").flatMap(Double.init),
    response.statusCode == 429
  {
    throw TooManyRequests(retryAfter: retryAfter)
  }

  return (data, response)
} strategy: { error in
  if let error = error as? TooManyRequests {
    return .backoff(.constant(.seconds(error.retryAfter)))
  } else {
    return .stop
  }
}

服务器的常见行为是,如果负载过高,则返回带有建议退避的 HTTP 状态代码 429。与前两个示例相反,我们仅在错误类型为 TooManyRequests 时才重试,任何其他错误都将被重新抛出并停止重试。恒定退避是从自定义错误动态获取的,并以秒为单位传递。

改进