Last active
March 4, 2024 17:10
-
-
Save wvteijlingen/d443ee91b0853ac66d732f2c725a829c to your computer and use it in GitHub Desktop.
Swift RetryStrategy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://thuc.space/posts/retry_strategies/