Last active
August 2, 2023 14:45
-
-
Save daltonclaybrook/2c441ce13562e4bddbfd62fe4dcc05ac to your computer and use it in GitHub Desktop.
A simple exponential backoff operation using async/await
This file contains hidden or 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
// Created by Dalton Claybrook on 8/1/23. | |
import Foundation | |
struct Backoff { | |
enum RetryPolicy { | |
case indefinite | |
case maxAttempts(Int) | |
} | |
/// Whether to retry indefinitely or up to a max number of attempts | |
var retryPolicy: RetryPolicy = .indefinite | |
/// The base time used in the backoff calculation | |
var baseTime: TimeInterval = 1.0 | |
/// The maximum amount of time in seconds to wait before the next attempt | |
var maxTime: TimeInterval = 30 | |
/// When calculating backoff time, the exponent is the number of attempts multiplied by | |
/// this value. Decrease this value to shorten the backoff time. | |
var exponentMultiplier: Double = 1.0 | |
/// Whether to introduce randomness into the backoff | |
var jitter = false | |
/// Function used to sleep the task in between attempts. This can be mocked in tests. | |
var sleepTask: (Duration) async throws -> Void = { | |
try await Task.sleep(for: $0) | |
} | |
} | |
extension Backoff { | |
/// Perform the provided operation with the backoff parameters of the receiver | |
func perform<T>(_ operation: @Sendable () async throws -> T) async throws -> T { | |
let maxAttempts = retryPolicy.maxAttempts | |
var attempts = 0 | |
while true { | |
do { | |
return try await operation() | |
} catch let error { | |
attempts += 1 | |
guard attempts < maxAttempts else { | |
throw BackoffError.exceededMaxAttempts(latestError: error) | |
} | |
var delay = min(maxTime, baseTime * pow(2, Double(attempts - 1) * exponentMultiplier)) | |
if jitter { | |
let halfOfDelay = delay / 2 | |
delay = Double.random(in: halfOfDelay...delay) | |
} | |
let milliseconds = UInt64(delay * 1_000) | |
try await sleepTask(.milliseconds(milliseconds)) | |
} | |
} | |
} | |
} | |
enum BackoffError: Error { | |
case exceededMaxAttempts(latestError: Error) | |
} | |
private extension Backoff.RetryPolicy { | |
var maxAttempts: Int { | |
switch self { | |
case .indefinite: | |
return .max | |
case .maxAttempts(let attempts): | |
assert(attempts > 0, "Max attempts must be greater than zero") | |
return attempts | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment