@@ -3,10 +3,51 @@ import Foundation
3
3
import ApolloAPI
4
4
#endif
5
5
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
7
7
public class MaxRetryInterceptor : ApolloInterceptor {
8
8
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
10
51
private var hitCount = 0
11
52
12
53
public var id : String = UUID ( ) . uuidString
@@ -26,17 +67,24 @@ public class MaxRetryInterceptor: ApolloInterceptor {
26
67
///
27
68
/// - Parameter maxRetriesAllowed: How many times a query can be retried, in addition to the initial attempt before
28
69
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
30
78
}
31
79
32
80
public func interceptAsync< Operation: GraphQLOperation > (
33
81
chain: any RequestChain ,
34
82
request: HTTPRequest < Operation > ,
35
83
response: HTTPResponse < Operation > ? ,
36
84
completion: @escaping ( Result < GraphQLResult < Operation . Data > , any Error > ) -> Void ) {
37
- guard self . hitCount <= self . maxRetries else {
85
+ guard self . hitCount <= self . configuration . maxRetries else {
38
86
let error = RetryError . hitMaxRetryCount (
39
- count: self . maxRetries,
87
+ count: self . configuration . maxRetries,
40
88
operationName: Operation . operationName
41
89
)
42
90
@@ -51,11 +99,48 @@ public class MaxRetryInterceptor: ApolloInterceptor {
51
99
}
52
100
53
101
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
+ }
60
145
}
61
146
}
0 commit comments