Skip to content

Instantly share code, notes, and snippets.

@wvteijlingen
Last active March 4, 2024 17:10
Show Gist options
  • Save wvteijlingen/d443ee91b0853ac66d732f2c725a829c to your computer and use it in GitHub Desktop.
Save wvteijlingen/d443ee91b0853ac66d732f2c725a829c to your computer and use it in GitHub Desktop.
Swift RetryStrategy
public protocol RetryStrategy {
/// True if the strategy allows to retry a failed action. False if all the retries have been 'used up'.
var shouldRetry: Bool { get }
/// The delay for which to wait before attempting to retry a failed action.
var delay: TimeInterval { get }
/// Returns a copy of this retry strategy with the number of allowable retries lowered by one.
var consumedOnce: Self { get }
}
extension RetryStrategy {
/// Performs the given closure, retrying it if needed, and returns the result of the closure if successful.
/// If the closure throws an error, it is retried as long as the retry strategy allows it.
/// If the closure throws an error and can no longer be retried, the error is retrown and the `attempt` method returns.
public func attempt<T>(closure: () async throws -> T) async throws -> T {
do {
return try await closure()
} catch {
if shouldRetry {
try await Task.sleep(for: .seconds(delay))
return try await consumedOnce.withRetry(closure)
} else {
throw error
}
}
}
}
// MARK: - NoRetryStrategy
extension RetryStrategy where Self == NoRetryStrategy {
public static var noRetry: Self {
NoRetryStrategy()
}
}
public struct NoRetryStrategy: RetryStrategy {
public var shouldRetry: Bool = false
public var delay: TimeInterval = 0
public var consumedOnce: NoRetryStrategy { self }
}
// MARK: - ConstantBackOffStrategy
extension RetryStrategy where Self == ConstantBackOffStrategy {
public static func constantBackoff(attempts: Int, delay: TimeInterval) -> Self {
ConstantBackOffStrategy(attemptsLeft: attempts, delay: delay)
}
}
public struct ConstantBackOffStrategy: RetryStrategy {
public let delay: TimeInterval
public var shouldRetry: Bool {
attemptsLeft > 1
}
public var consumedOnce: Self {
ConstantBackOffStrategy(attemptsLeft: attemptsLeft - 1, delay: delay)
}
private let attemptsLeft: Int
public init(attemptsLeft: Int, delay: TimeInterval) {
self.attemptsLeft = attemptsLeft
self.delay = delay
}
}
// MARK: - LinearBackOffStrategy
extension RetryStrategy where Self == LinearBackOffStrategy {
public static func linearBackoff(attempts: Int, initialDelay: TimeInterval, increase: TimeInterval) -> Self {
LinearBackOffStrategy(attemptsLeft: attempts, delay: initialDelay, increase: increase)
}
}
public struct LinearBackOffStrategy: RetryStrategy {
public let delay: TimeInterval
public var shouldRetry: Bool {
attemptsLeft > 1
}
public var consumedOnce: Self {
LinearBackOffStrategy(attemptsLeft: attemptsLeft - 1, delay: delay + increase, increase: increase)
}
private let attemptsLeft: Int
private let increase: TimeInterval
public init(attemptsLeft: Int, delay: TimeInterval, increase: TimeInterval) {
self.attemptsLeft = attemptsLeft
self.delay = delay
self.increase = increase
}
}
// MARK: - ExponentialBackoffStrategy
extension RetryStrategy where Self == ExponentialBackoffStrategy {
public static func exponentialBackoff(attempts: Int, initialDelay: TimeInterval, multiplier: Double = 2) -> Self {
ExponentialBackoffStrategy(attemptsLeft: attempts, delay: initialDelay, multiplier: multiplier)
}
}
public struct ExponentialBackoffStrategy: RetryStrategy {
public let delay: TimeInterval
public var shouldRetry: Bool {
attemptsLeft > 1
}
public var consumedOnce: Self {
ExponentialBackoffStrategy(attemptsLeft: attemptsLeft - 1, delay: delay * multiplier, multiplier: multiplier)
}
private let attemptsLeft: Int
private let multiplier: Double
public init(attemptsLeft: Int, delay: TimeInterval, multiplier: Double) {
self.attemptsLeft = attemptsLeft
self.delay = delay
self.multiplier = multiplier
}
}
// Usage:
func requestFoo(retryStrategy: some RetryStrategy) async throws -> Foo {
do {
return try await getAFooFromSomewhere()
} catch {
if retryStrategy.shouldRetry {
try await Task.sleep(for: .seconds(retryStrategy.delay))
return try await requestFoo(retryStrategy: retryStrategy.consumedOnce)
} else {
throw error
}
}
// Or:
try await retryStrategy.attempt {
try await getAFooFromSomewhere()
}
}
requestFoo(retryStrategy: .constantBackoff(attempts: 3, delay: 1))
@wvteijlingen
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment