@@ -3,10 +3,51 @@ import Foundation
33import ApolloAPI
44#endif
55
6- /// An interceptor to enforce a maximum number of retries of any `HTTPRequest`
6+ /// An interceptor to enforce a maximum number of retries of any `HTTPRequest` with optional exponential backoff support
77public class MaxRetryInterceptor : ApolloInterceptor {
88
9- private let maxRetries : Int
9+ /// A configuration object that defines behavior for retry logic and exponential backoff.
10+ public struct Configuration {
11+ /// Maximum number of retries allowed. Defaults to `3`.
12+ public let maxRetries : Int
13+ /// Initial delay in seconds for exponential backoff. Defaults to `0.3`.
14+ public let baseDelay : TimeInterval
15+ /// Multiplier for exponential backoff calculation. Defaults to `2.0`.
16+ public let multiplier : Double
17+ /// Maximum delay cap in seconds to prevent excessive wait times. Defaults to `20.0`.
18+ public let maxDelay : TimeInterval
19+ /// Whether to enable exponential backoff delays between retries. Defaults to `false`.
20+ public let enableExponentialBackoff : Bool
21+ /// Whether to add jitter to delays to prevent thundering herd problems. Defaults to `true`.
22+ public let enableJitter : Bool
23+
24+ /// Designated initializer
25+ ///
26+ /// - Parameters:
27+ /// - maxRetries: Maximum number of retries allowed.
28+ /// - baseDelay: Initial delay in seconds for exponential backoff.
29+ /// - multiplier: Multiplier for exponential backoff calculation. Should be ≥ 1.0.
30+ /// - maxDelay: Maximum delay cap in seconds to prevent excessive wait times.
31+ /// - enableExponentialBackoff: Whether to enable exponential backoff delays between retries.
32+ /// - enableJitter: Whether to add jitter to delays to prevent thundering herd problems.
33+ public init (
34+ maxRetries: Int = 3 ,
35+ baseDelay: TimeInterval = 0.3 ,
36+ multiplier: Double = 2.0 ,
37+ maxDelay: TimeInterval = 20.0 ,
38+ enableExponentialBackoff: Bool = false ,
39+ enableJitter: Bool = true
40+ ) {
41+ self . maxRetries = maxRetries
42+ self . baseDelay = baseDelay
43+ self . multiplier = multiplier
44+ self . maxDelay = maxDelay
45+ self . enableExponentialBackoff = enableExponentialBackoff
46+ self . enableJitter = enableJitter
47+ }
48+ }
49+
50+ private let configuration : Configuration
1051 private var hitCount = 0
1152
1253 public var id : String = UUID ( ) . uuidString
@@ -26,17 +67,24 @@ public class MaxRetryInterceptor: ApolloInterceptor {
2667 ///
2768 /// - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before
2869 public init ( maxRetriesAllowed: Int = 3 ) {
29- self . maxRetries = maxRetriesAllowed
70+ self . configuration = Configuration ( maxRetries: maxRetriesAllowed)
71+ }
72+
73+ /// Designated initializer with full configuration support.
74+ ///
75+ /// - Parameter configuration: Configuration object defining retry behavior and exponential backoff settings.
76+ public init ( configuration: Configuration ) {
77+ self . configuration = configuration
3078 }
3179
3280 public func interceptAsync< Operation: GraphQLOperation > (
3381 chain: any RequestChain ,
3482 request: HTTPRequest < Operation > ,
3583 response: HTTPResponse < Operation > ? ,
3684 completion: @escaping ( Result < GraphQLResult < Operation . Data > , any Error > ) -> Void ) {
37- guard self . hitCount <= self . maxRetries else {
85+ guard self . hitCount <= self . configuration . maxRetries else {
3886 let error = RetryError . hitMaxRetryCount (
39- count: self . maxRetries,
87+ count: self . configuration . maxRetries,
4088 operationName: Operation . operationName
4189 )
4290
@@ -51,11 +99,48 @@ public class MaxRetryInterceptor: ApolloInterceptor {
5199 }
52100
53101 self . hitCount += 1
54- chain. proceedAsync (
55- request: request,
56- response: response,
57- interceptor: self ,
58- completion: completion
59- )
102+
103+ // Apply exponential backoff delay if enabled and this is a retry (hitCount > 1)
104+ if self . configuration. enableExponentialBackoff && self . hitCount > 1 {
105+ let delay = calculateExponentialBackoffDelay ( )
106+ DispatchQueue . global ( qos: . userInitiated) . asyncAfter ( deadline: . now( ) + delay) { [ weak self] in
107+ guard let self = self else { return }
108+ chain. proceedAsync (
109+ request: request,
110+ response: response,
111+ interceptor: self ,
112+ completion: completion
113+ )
114+ }
115+ } else {
116+ chain. proceedAsync (
117+ request: request,
118+ response: response,
119+ interceptor: self ,
120+ completion: completion
121+ )
122+ }
123+ }
124+
125+ /// Calculates the exponential backoff delay based on current retry attempt.
126+ ///
127+ /// - Returns: The calculated delay in seconds, including jitter if enabled.
128+ private func calculateExponentialBackoffDelay( ) -> TimeInterval {
129+ // Calculate exponential delay: baseDelay * multiplier^(hitCount - 1)
130+ // We use (hitCount - 1) because hitCount includes the initial attempt
131+ let retryAttempt = hitCount - 1
132+ let exponentialDelay = configuration. baseDelay * pow( configuration. multiplier, Double ( retryAttempt) )
133+
134+ // Apply maximum delay cap
135+ let cappedDelay = min ( exponentialDelay, configuration. maxDelay)
136+
137+ // Apply jitter if enabled to prevent thundering herd problems
138+ if configuration. enableJitter {
139+ // Equal jitter: random value between 50% and 100% of calculated delay
140+ let minDelay = cappedDelay / 2
141+ return TimeInterval . random ( in: minDelay... cappedDelay)
142+ } else {
143+ return cappedDelay
144+ }
60145 }
61146}
0 commit comments