Skip to content

Instantly share code, notes, and snippets.

@Aayush9029
Last active January 18, 2025 22:50
Show Gist options
  • Select an option

  • Save Aayush9029/d7ad6cadb3f1ef8c4310b2e19e164aa4 to your computer and use it in GitHub Desktop.

Select an option

Save Aayush9029/d7ad6cadb3f1ef8c4310b2e19e164aa4 to your computer and use it in GitHub Desktop.
Network Retry Client
import Dependencies
import Foundation
import os
import XCTestDynamicOverlay
// Add Logger
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "RetryClient", category: "Retry")
public struct RetryClient {
public var retry: @Sendable (RetryConfiguration, @Sendable () async throws -> Void) async -> Void
// Consolidated retry logic (non-throwing version)
private static func retryOperation<T>(
configuration: RetryConfiguration,
task: @Sendable () async throws -> T
) async -> T? {
var lastError: Error?
var currentBackoff = configuration.backoffDuration
logger.info("Starting retry task with configuration: maxAttempts=\(configuration.maxAttempts), initialBackoff=\(configuration.backoffDuration)s")
for attempt in 1 ... configuration.maxAttempts {
do {
logger.debug("Attempting execution (attempt \(attempt)/\(configuration.maxAttempts))")
let result = try await task()
logger.info("Task succeeded on attempt \(attempt)")
return result
} catch {
lastError = error
logger.error("Attempt \(attempt) failed: \(error.localizedDescription)")
if attempt < configuration.maxAttempts {
logger.debug("Waiting \(String(format: "%.2f", currentBackoff))s before next attempt")
try? await Task.sleep(for: .seconds(currentBackoff))
currentBackoff *= configuration.backoffMultiplier
}
}
}
// Log the final error if we couldn't succeed after all retries
if let error = lastError {
logger.error("Final error after \(configuration.maxAttempts) attempts: \(error.localizedDescription)")
}
return nil
}
// Public method for retry task
public func retryTask<T>(
configuration: RetryConfiguration = .default,
task: @Sendable () async throws -> T
) async throws -> T {
if let result = await RetryClient.retryOperation(configuration: configuration, task: task) {
return result
} else {
throw RetryError.maxAttemptsExceeded
}
}
}
// MARK: - Dependency Registration
extension RetryClient: DependencyKey {
public static let liveValue = Self(
retry: { configuration, operation in
// Use the static retryOperation method since it's now accessible without needing 'self'
await retryOperation(configuration: configuration) {
try await operation()
}
}
)
}
public extension DependencyValues {
var retryClient: RetryClient {
get { self[RetryClient.self] }
set { self[RetryClient.self] = newValue }
}
}
enum RetryError: Error {
case maxAttemptsExceeded
}
public struct RetryConfiguration {
let maxAttempts: Int
/// - backoffDuration: second initial delay between retries
let backoffDuration: TimeInterval
/// - backoffMultiplier: increase in delay for each subsequent retry
let backoffMultiplier: Double
public static let `default` = RetryConfiguration(
maxAttempts: 3,
backoffDuration: 5.0,
backoffMultiplier: 3.0
)
public static let aggressive = RetryConfiguration(
maxAttempts: 5,
backoffDuration: 2.0,
backoffMultiplier: 1.5
)
public static let conservative = RetryConfiguration(
maxAttempts: 3,
backoffDuration: 10.0,
backoffMultiplier: 5.0
)
public init(
maxAttempts: Int,
backoffDuration: TimeInterval,
backoffMultiplier: Double
) {
self.maxAttempts = maxAttempts
self.backoffDuration = backoffDuration
self.backoffMultiplier = backoffMultiplier
}
}
@Aayush9029
Copy link
Author

Aayush9029 commented Jan 1, 2025

let response = try await retryClient.retryTask(configuration: .default) { ... }

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