Skip to content

Instantly share code, notes, and snippets.

@daltonclaybrook
Last active August 2, 2023 14:45
Show Gist options
  • Save daltonclaybrook/2c441ce13562e4bddbfd62fe4dcc05ac to your computer and use it in GitHub Desktop.
Save daltonclaybrook/2c441ce13562e4bddbfd62fe4dcc05ac to your computer and use it in GitHub Desktop.
A simple exponential backoff operation using async/await
// 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