@@ -25,20 +25,29 @@ struct RetryIterator: IteratorProtocol {
2525 /// This value is used when calculating exponential backoff delays.
2626 private var retries : UInt = 0
2727
28+ /// The maximum number of retry attempts allowed.
29+ ///
30+ /// Once the number of attempts reaches this value,
31+ /// the iterator stops producing further delays.
32+ private let maxRetries : UInt
33+
2834 /// The retry policy strategy that defines:
2935 /// - The maximum number of retry attempts.
3036 /// - The algorithm used to calculate delays between retries
3137 /// (constant, exponential, or exponential with jitter).
32- private let strategy : RetryPolicyStrategy
38+ private let delayStrategy : any IRetryDelayStrategy
3339
3440 // MARK: Initialization
3541
36- /// Creates a new `RetryIterator` with the specified retry policy strategy .
42+ /// Creates a new `RetryIterator`.
3743 ///
38- /// - Parameter strategy: A `RetryPolicyStrategy` describing how retry delays
39- /// should be calculated and how many retries are allowed.
40- init ( strategy: RetryPolicyStrategy ) {
41- self . strategy = strategy
44+ /// - Parameters:
45+ /// - maxRetries: The maximum number of retry attempts allowed.
46+ /// - delayStrategy: A strategy that defines how delays between
47+ /// retry attempts are calculated.
48+ init ( maxRetries: UInt , delayStrategy: any IRetryDelayStrategy ) {
49+ self . maxRetries = maxRetries
50+ self . delayStrategy = delayStrategy
4251 }
4352
4453 // MARK: IteratorProtocol
@@ -56,177 +65,8 @@ struct RetryIterator: IteratorProtocol {
5665 /// - Returns: The delay in nanoseconds for the current retry attempt,
5766 /// or `nil` if no more retries are allowed.
5867 mutating func next( ) -> UInt64 ? {
59- guard isValid ( ) else { return nil }
60-
68+ guard retries < maxRetries else { return nil }
6169 defer { retries += 1 }
62-
63- return delay ( )
64- }
65-
66- // MARK: Private
67-
68- /// Determines whether another retry attempt is allowed.
69- ///
70- /// This method compares the current retry count with the maximum
71- /// number of retries defined in the retry strategy.
72- ///
73- /// - Returns: `true` if another retry attempt is allowed;
74- /// `false` otherwise.
75- private func isValid( ) -> Bool {
76- retries < strategy. retries
77- }
78-
79- /// Calculates the delay for the current retry attempt
80- /// based on the selected retry strategy.
81- ///
82- /// - Returns: The computed delay in nanoseconds, or `0`
83- /// if the duration cannot be converted to seconds.
84- private func delay( ) -> UInt64 ? {
85- switch strategy {
86- case let . constant( _, duration) :
87- convertToNanoseconds ( duration)
88-
89- case let . exponential( _, jitterFactor, maxInterval, multiplier, duration) :
90- calculateExponentialDelayWithJitter (
91- duration: duration,
92- multiplier: multiplier,
93- retries: retries,
94- jitterFactor: jitterFactor,
95- maxInterval: maxInterval
96- )
97- }
98- }
99-
100- // MARK: - Helper Methods
101-
102- /// Converts a `DispatchTimeInterval` to nanoseconds.
103- ///
104- /// - Parameter duration: The time interval to convert.
105- /// - Returns: The equivalent duration in nanoseconds, or `0`
106- /// if the interval cannot be represented as seconds.
107- private func convertToNanoseconds( _ duration: DispatchTimeInterval ) -> UInt64 ? {
108- guard let seconds = duration. double else { return . zero }
109- return safeConvertToUInt64 ( seconds * . nanosec)
110- }
111-
112- /// Calculates an exponential backoff delay without jitter.
113- ///
114- /// The delay is calculated as:
115- /// `baseDelay * multiplier ^ retries`
116- ///
117- /// - Parameters:
118- /// - duration: The base delay value.
119- /// - multiplier: The exponential growth multiplier.
120- /// - retries: The current retry attempt index.
121- /// - Returns: The calculated delay in nanoseconds.
122- private func calculateExponentialDelay(
123- duration: DispatchTimeInterval ,
124- multiplier: Double ,
125- retries: UInt
126- ) -> UInt64 ? {
127- guard let seconds = duration. double else { return . zero }
128-
129- let baseNanos = seconds * . nanosec
130- let value = baseNanos * pow( multiplier, Double ( retries) )
131-
132- return safeConvertToUInt64 ( value)
70+ return delayStrategy. delay ( forRetry: retries)
13371 }
134-
135- /// Calculates an exponential backoff delay with jitter and an optional maximum interval.
136- ///
137- /// This method:
138- /// 1. Calculates the exponential backoff delay.
139- /// 2. Applies a random jitter to spread retry attempts over time.
140- /// 3. Caps the result at the provided maximum interval, if any.
141- ///
142- /// - Parameters:
143- /// - duration: The base delay value.
144- /// - multiplier: The exponential growth multiplier.
145- /// - retries: The current retry attempt index.
146- /// - jitterFactor: The percentage of randomness applied to the delay.
147- /// - maxInterval: An optional upper bound for the delay.
148- /// - Returns: The final delay in nanoseconds.
149- private func calculateExponentialDelayWithJitter(
150- duration: DispatchTimeInterval ,
151- multiplier: Double ,
152- retries: UInt ,
153- jitterFactor: Double ,
154- maxInterval: DispatchTimeInterval ?
155- ) -> UInt64 ? {
156- guard let seconds = duration. double else { return . zero }
157-
158- let maxDelayNanos = calculateMaxDelay ( maxInterval)
159- let baseNanos = seconds * . nanosec
160- let exponentialBackoffNanos = baseNanos * pow( multiplier, Double ( retries) )
161-
162- guard exponentialBackoffNanos < maxDelayNanos,
163- exponentialBackoffNanos < Double ( UInt64 . max)
164- else {
165- return safeConvertToUInt64 ( maxDelayNanos)
166- }
167-
168- let delayWithJitter = applyJitter (
169- to: exponentialBackoffNanos,
170- factor: jitterFactor,
171- maxDelay: maxDelayNanos
172- )
173-
174- return safeConvertToUInt64 ( min ( delayWithJitter, maxDelayNanos) )
175- }
176-
177- /// Calculates the maximum allowed delay in nanoseconds.
178- ///
179- /// - Parameter maxInterval: An optional maximum delay value.
180- /// - Returns: The maximum delay in nanoseconds, clamped to `UInt64.max`.
181- private func calculateMaxDelay( _ maxInterval: DispatchTimeInterval ? ) -> Double {
182- guard let maxSeconds = maxInterval? . double else {
183- return Double ( UInt64 . max)
184- }
185-
186- let maxNanos = maxSeconds * . nanosec
187- return min ( maxNanos, Double ( UInt64 . max) )
188- }
189-
190- /// Applies random jitter to a delay value.
191- ///
192- /// Jitter helps prevent synchronized retries (the "thundering herd" problem)
193- /// by randomizing retry timings within a defined range.
194- ///
195- /// - Parameters:
196- /// - value: The base delay value in nanoseconds.
197- /// - factor: The jitter factor defining the randomization range.
198- /// - maxDelay: The maximum allowed delay.
199- /// - Returns: A jittered delay value clamped to valid bounds.
200- private func applyJitter(
201- to value: Double ,
202- factor: Double ,
203- maxDelay: Double
204- ) -> Double {
205- let jitterRange = value * factor
206- let minValue = value - jitterRange
207- let maxValue = min ( value + jitterRange, maxDelay)
208-
209- guard maxValue < Double ( UInt64 . max) else {
210- return maxDelay
211- }
212-
213- let randomized = Double . random ( in: minValue ... maxValue)
214- return max ( 0 , randomized)
215- }
216-
217- private func safeConvertToUInt64( _ value: Double ) -> UInt64 {
218- if value >= Double ( UInt64 . max) {
219- return UInt64 . max
220- }
221- if value <= 0 {
222- return . zero
223- }
224- return UInt64 ( value)
225- }
226- }
227-
228- // MARK: - Constants
229-
230- private extension Double {
231- static let nanosec : Double = 1e+9
23272}
0 commit comments