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
控制重试尝试之间的延迟。
参数
maxAttempts
: 重试操作的最大尝试次数。默认为 3。tolerance
: 可选的延迟容差,用于考虑时钟不精确性。默认为 nil
。isolation
: 继承的 Actor 隔离。operation
: 要执行的异步操作。如果发生错误,此函数将根据提供的重试策略重试操作。strategy
: 一个闭包,用于根据错误类型确定处理重试的 RetryStrategy
。默认为 .backoff(.none)
,表示重试之间没有延迟。返回值:操作的结果,如果在允许的尝试次数内成功。
抛出:如果所有重试尝试都失败,或者重试策略指定停止重试,或者 clock
抛出任何错误,则重新抛出最后遇到的错误
前提条件:maxAttempts
必须大于 0。
此库附带 7 种预构建的退避策略
为此退避策略强制执行最小延迟持续时间。
此方法确保退避延迟永远不会短于定义的最小持续时间,有助于在重试之间保持基准等待时间。这保留了原始的退避模式,但将低于指定阈值的持续时间提高到最小值。
x
是当前尝试次数,f(x)
是基本退避策略。
extension BackoffStrategy {
public func min(_ m: C.Duration) -> Self { ... }
}
限制此退避策略的最大延迟持续时间。
此方法确保退避延迟不超过定义的最大持续时间,有助于避免重试之间等待时间过长。这保留了原始的退避模式,直到指定的上限。
x
是当前尝试次数,f(x)
是基本退避策略。
extension BackoffStrategy {
public func max(_ M: C.Duration) -> Self { ... }
}
将抖动应用于延迟持续时间,从而在退避间隔中引入随机性。
此方法在从零到基本持续时间的范围内随机化每次重试尝试的延迟。当多个源在分布式系统中并发重试时,抖动可以帮助减少争用。
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 { ... }
}
一种重试尝试之间没有延迟的退避策略。
此策略强制执行零持续时间延迟,使重试立即进行。它适用于需要在没有任何等待时间的情况下尽快进行重试的情况。
x
是当前尝试次数。
extension BackoffStrategy {
public static var none: Self { ... }
}
一种在每次尝试之间具有恒定延迟的退避策略。
此策略在重试之间应用固定的、不变的延迟,而与尝试次数无关。它非常适合需要在尝试之间使用统一间隔的重试模式。
x
是当前尝试次数。
extension BackoffStrategy {
public static func constant(_ c: C.Duration) -> Self { ... }
}
一种在尝试之间具有线性递增延迟的退避策略。
此策略在每次重试后逐渐增加延迟,从初始延迟开始并线性缩放。适用于需要在一段时间内持续增加延迟的场景。
x
是当前尝试次数。
extension BackoffStrategy {
public static func linear(a: C.Duration, b: C.Duration) -> Self { ... }
}
一种在尝试之间具有指数递增延迟的退避策略。
此策略以指数方式增加延迟,从初始持续时间开始,并在每次重试后应用乘法因子。适用于重试应变得越来越稀疏的情况。
x
是当前尝试次数。
extension BackoffStrategy where C.Duration == Duration {
public static func exponential(a: C.Duration, b: Double) -> Self { ... }
}
为了充分理解这一点,让我们用 URLSession.shared.data(from: url)
作为示例操作来说明此函数的 3 种自定义用法
使用默认值
let (data, response) = try await retry {
try await URLSession.shared.data(from: url)
}
如果操作成功,则立即返回结果。但是,如果发生任何错误,操作将再尝试 2 次,因为默认的最大尝试次数为 3。在连续的尝试之间,不会有延迟,因为,同样,默认的重试策略是不添加任何退避。
使用自定义退避策略和自定义尝试次数
let (data, response) = try await retry(maxAttempts: 5) {
try await URLSession.shared.data(from: url)
} strategy: { _ in
return .backoff(.constant(.seconds(2)))
}
与第一个自定义用法类似,如果发生任何错误,操作将再尝试 4 次。但是,在连续的尝试之间,它将等待 2 秒钟,然后再运行操作。
使用自定义退避和重试策略
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
时才重试,任何其他错误都将被重新抛出并停止重试。恒定退避是从自定义错误动态获取的,并以秒为单位传递。
clock
参数的默认表达式为 ContinuousClock
。