Skip to content

Commit 22b9017

Browse files
gh-action-runnergh-action-runner
authored andcommitted
Squashed 'apollo-ios/' changes from f0c9d1282..038b6e707
038b6e707 feat: Add exponential backoff support to MaxRetryInterceptor (#711) git-subtree-dir: apollo-ios git-subtree-split: 038b6e70731b15f0530fd099575de3011de0df14
1 parent 4311bda commit 22b9017

File tree

1 file changed

+96
-11
lines changed

1 file changed

+96
-11
lines changed

Sources/Apollo/MaxRetryInterceptor.swift

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,51 @@ import Foundation
33
import 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
77
public 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

Comments
 (0)