Skip to content

Commit 2406360

Browse files
committed
add docs
1 parent 24475e9 commit 2406360

File tree

2 files changed

+240
-12
lines changed

2 files changed

+240
-12
lines changed

Sources/AsyncAlgorithms/Retry/Backoff.swift

Lines changed: 158 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
#if compiler(<6.2)
2-
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
3-
extension Duration {
4-
@usableFromInline var attoseconds: Int128 {
5-
return Int128(_low: _low, _high: _high)
6-
}
7-
@usableFromInline init(attoseconds: Int128) {
8-
self.init(_high: attoseconds._high, low: attoseconds._low)
9-
}
10-
}
11-
#endif
12-
1+
#if compiler(>=6.2)
2+
/// A protocol for defining backoff strategies that generate delays between retry attempts.
3+
///
4+
/// Backoff strategies are stateful and generate progressively changing delays based on their
5+
/// internal algorithm. Each call to `nextDuration()` returns the delay for the next retry attempt.
6+
///
7+
/// ## Example
8+
///
9+
/// ```swift
10+
/// var strategy = Backoff.exponential(factor: 2, initial: .milliseconds(100))
11+
/// strategy.nextDuration() // 100ms
12+
/// strategy.nextDuration() // 200ms
13+
/// strategy.nextDuration() // 400ms
14+
/// ```
1315
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
1416
public protocol BackoffStrategy<Duration> {
1517
associatedtype Duration: DurationProtocol
@@ -135,49 +137,193 @@ public protocol BackoffStrategy<Duration> {
135137

136138
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
137139
public enum Backoff {
140+
/// Creates a constant backoff strategy that always returns the same delay.
141+
///
142+
/// Formula: `f(n) = constant`
143+
///
144+
/// - Parameter constant: The fixed duration to wait between retry attempts.
145+
/// - Returns: A backoff strategy that always returns the constant duration.
138146
@inlinable public static func constant<Duration: DurationProtocol>(_ constant: Duration) -> some BackoffStrategy<Duration> {
139147
return ConstantBackoffStrategy(constant: constant)
140148
}
149+
150+
/// Creates a constant backoff strategy that always returns the same delay.
151+
///
152+
/// Formula: `f(n) = constant`
153+
///
154+
/// - Parameter constant: The fixed duration to wait between retry attempts.
155+
/// - Returns: A backoff strategy that always returns the constant duration.
156+
///
157+
/// ## Example
158+
///
159+
/// ```swift
160+
/// var backoff = Backoff.constant(.milliseconds(100))
161+
/// backoff.nextDuration() // 100ms
162+
/// backoff.nextDuration() // 100ms
163+
/// ```
141164
@inlinable public static func constant(_ constant: Duration) -> some BackoffStrategy<Duration> {
142165
return ConstantBackoffStrategy(constant: constant)
143166
}
167+
168+
/// Creates a linear backoff strategy where delays increase by a fixed increment.
169+
///
170+
/// Formula: `f(n) = initial + increment * n`
171+
///
172+
/// - Parameters:
173+
/// - increment: The amount to increase the delay by on each attempt.
174+
/// - initial: The initial delay for the first retry attempt.
175+
/// - Returns: A backoff strategy with linearly increasing delays.
144176
@inlinable public static func linear<Duration: DurationProtocol>(increment: Duration, initial: Duration) -> some BackoffStrategy<Duration> {
145177
return LinearBackoffStrategy(increment: increment, initial: initial)
146178
}
179+
180+
/// Creates a linear backoff strategy where delays increase by a fixed increment.
181+
///
182+
/// Formula: `f(n) = initial + increment * n`
183+
///
184+
/// - Parameters:
185+
/// - increment: The amount to increase the delay by on each attempt.
186+
/// - initial: The initial delay for the first retry attempt.
187+
/// - Returns: A backoff strategy with linearly increasing delays.
188+
///
189+
/// ## Example
190+
///
191+
/// ```swift
192+
/// var backoff = Backoff.linear(increment: .milliseconds(100), initial: .milliseconds(100))
193+
/// backoff.nextDuration() // 100ms
194+
/// backoff.nextDuration() // 200ms
195+
/// backoff.nextDuration() // 300ms
196+
/// ```
147197
@inlinable public static func linear(increment: Duration, initial: Duration) -> some BackoffStrategy<Duration> {
148198
return LinearBackoffStrategy(increment: increment, initial: initial)
149199
}
200+
201+
/// Creates an exponential backoff strategy where delays grow exponentially.
202+
///
203+
/// Formula: `f(n) = initial * factor^n`
204+
///
205+
/// - Parameters:
206+
/// - factor: The multiplication factor for each retry attempt.
207+
/// - initial: The initial delay for the first retry attempt.
208+
/// - Returns: A backoff strategy with exponentially increasing delays.
150209
@inlinable public static func exponential<Duration: DurationProtocol>(factor: Int, initial: Duration) -> some BackoffStrategy<Duration> {
151210
return ExponentialBackoffStrategy(factor: factor, initial: initial)
152211
}
212+
213+
/// Creates an exponential backoff strategy where delays grow exponentially.
214+
///
215+
/// Formula: `f(n) = initial * factor^n`
216+
///
217+
/// - Parameters:
218+
/// - factor: The multiplication factor for each retry attempt.
219+
/// - initial: The initial delay for the first retry attempt.
220+
/// - Returns: A backoff strategy with exponentially increasing delays.
221+
///
222+
/// ## Example
223+
///
224+
/// ```swift
225+
/// var backoff = Backoff.exponential(factor: 2, initial: .milliseconds(100))
226+
/// backoff.nextDuration() // 100ms
227+
/// backoff.nextDuration() // 200ms
228+
/// backoff.nextDuration() // 400ms
229+
/// ```
153230
@inlinable public static func exponential(factor: Int, initial: Duration) -> some BackoffStrategy<Duration> {
154231
return ExponentialBackoffStrategy(factor: factor, initial: initial)
155232
}
156233
}
157234

158235
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
159236
extension Backoff {
237+
/// Creates a decorrelated jitter backoff strategy that uses randomized delays.
238+
///
239+
/// Formula: `f(n) = random(base, f(n - 1) * factor)` where `f(0) = base`
240+
///
241+
/// Jitter prevents the "thundering herd" problem where multiple clients retry
242+
/// simultaneously, reducing server load spikes and improving system stability.
243+
///
244+
/// - Parameters:
245+
/// - factor: The multiplication factor for calculating the upper bound of randomness.
246+
/// - base: The base duration used as the minimum delay and initial reference.
247+
/// - generator: The random number generator to use. Defaults to `SystemRandomNumberGenerator()`.
248+
/// - Returns: A backoff strategy with decorrelated jitter.
160249
@inlinable public static func decorrelatedJitter<RNG: RandomNumberGenerator>(factor: Int, base: Duration, using generator: RNG = SystemRandomNumberGenerator()) -> some BackoffStrategy<Duration> {
161250
return DecorrelatedJitterBackoffStrategy(base: base, factor: factor, generator: generator)
162251
}
163252
}
164253

165254
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
166255
extension BackoffStrategy {
256+
/// Applies a minimum duration constraint to this backoff strategy.
257+
///
258+
/// Formula: `f(n) = max(minimum, g(n))` where `g(n)` is the base strategy
259+
///
260+
/// This modifier ensures that no delay returned by the strategy is less than
261+
/// the specified minimum duration.
262+
///
263+
/// - Parameter minimum: The minimum duration to enforce.
264+
/// - Returns: A backoff strategy that never returns delays shorter than the minimum.
265+
///
266+
/// ## Example
267+
///
268+
/// ```swift
269+
/// var backoff = Backoff
270+
/// .exponential(factor: 2, initial: .milliseconds(100))
271+
/// .minimum(.milliseconds(200))
272+
/// backoff.nextDuration() // 200ms (enforced minimum)
273+
/// ```
167274
@inlinable public func minimum(_ minimum: Duration) -> some BackoffStrategy<Duration> {
168275
return MinimumBackoffStrategy(base: self, minimum: minimum)
169276
}
277+
278+
/// Applies a maximum duration constraint to this backoff strategy.
279+
///
280+
/// Formula: `f(n) = min(maximum, g(n))` where `g(n)` is the base strategy
281+
///
282+
/// This modifier ensures that no delay returned by the strategy exceeds
283+
/// the specified maximum duration, effectively capping exponential growth.
284+
///
285+
/// - Parameter maximum: The maximum duration to enforce.
286+
/// - Returns: A backoff strategy that never returns delays longer than the maximum.
287+
///
288+
/// ## Example
289+
///
290+
/// ```swift
291+
/// var backoff = Backoff
292+
/// .exponential(factor: 2, initial: .milliseconds(100))
293+
/// .maximum(.seconds(5))
294+
/// // Delays will cap at 5 seconds instead of growing indefinitely
295+
/// ```
170296
@inlinable public func maximum(_ maximum: Duration) -> some BackoffStrategy<Duration> {
171297
return MaximumBackoffStrategy(base: self, maximum: maximum)
172298
}
173299
}
174300

175301
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
176302
extension BackoffStrategy where Duration == Swift.Duration {
303+
/// Applies full jitter to this backoff strategy.
304+
///
305+
/// Formula: `f(n) = random(0, g(n))` where `g(n)` is the base strategy
306+
///
307+
/// Jitter prevents the "thundering herd" problem where multiple clients retry
308+
/// simultaneously, reducing server load spikes and improving system stability.
309+
///
310+
/// - Parameter generator: The random number generator to use. Defaults to `SystemRandomNumberGenerator()`.
311+
/// - Returns: A backoff strategy with full jitter applied.
177312
@inlinable public func fullJitter<RNG: RandomNumberGenerator>(using generator: RNG = SystemRandomNumberGenerator()) -> some BackoffStrategy<Duration> {
178313
return FullJitterBackoffStrategy(base: self, generator: generator)
179314
}
315+
316+
/// Applies equal jitter to this backoff strategy.
317+
///
318+
/// Formula: `f(n) = random(g(n) / 2, g(n))` where `g(n)` is the base strategy
319+
///
320+
/// Jitter prevents the "thundering herd" problem where multiple clients retry
321+
/// simultaneously, reducing server load spikes and improving system stability.
322+
///
323+
/// - Parameter generator: The random number generator to use. Defaults to `SystemRandomNumberGenerator()`.
324+
/// - Returns: A backoff strategy with equal jitter applied.
180325
@inlinable public func equalJitter<RNG: RandomNumberGenerator>(using generator: RNG = SystemRandomNumberGenerator()) -> some BackoffStrategy<Duration> {
181326
return EqualJitterBackoffStrategy(base: self, generator: generator)
182327
}
183328
}
329+
#endif

Sources/AsyncAlgorithms/Retry/Retry.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#if compiler(>=6.2)
12
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
23
public struct RetryAction<Duration: DurationProtocol> {
34
@usableFromInline enum Action {
@@ -8,14 +9,58 @@ public struct RetryAction<Duration: DurationProtocol> {
89
@usableFromInline init(action: Action) {
910
self.action = action
1011
}
12+
13+
/// Indicates that retrying should stop immediately and the error should be rethrown.
1114
@inlinable public static var stop: Self {
1215
return .init(action: .stop)
1316
}
17+
18+
/// Indicates that retrying should continue after waiting for the specified duration.
19+
///
20+
/// - Parameter duration: The duration to wait before the next retry attempt.
21+
/// - Returns: A retry action that will cause the retry operation to wait.
1422
@inlinable public static func backoff(_ duration: Duration) -> Self {
1523
return .init(action: .backoff(duration))
1624
}
1725
}
1826

27+
/// Executes an asynchronous operation with retry logic and customizable backoff strategies.
28+
///
29+
/// This function attempts to execute the provided operation up to `maxAttempts` times.
30+
/// Between failed attempts, it consults the strategy function to determine whether to
31+
/// continue retrying with a delay or stop immediately.
32+
///
33+
/// The retry logic follows this sequence:
34+
/// 1. Execute the operation
35+
/// 2. If successful, return the result
36+
/// 3. If failed and this was not the final attempt:
37+
/// - Call the strategy closure with the error
38+
/// - If the strategy returns `.stop`, rethrow the error immediately
39+
/// - If the strategy returns `.backoff`, suspend for the given duration
40+
/// - Return to step 1
41+
/// 4. If failed on the final attempt, rethrow the error without consulting the strategy
42+
///
43+
/// - Parameters:
44+
/// - maxAttempts: The maximum number of attempts to make. Must be greater than 0.
45+
/// - tolerance: The tolerance for the sleep operation between retries.
46+
/// - clock: The clock to use for timing delays between retries.
47+
/// - isolation: The actor isolation to maintain during execution.
48+
/// - operation: The asynchronous operation to retry.
49+
/// - strategy: A closure that determines the retry action based on the error.
50+
/// Defaults to immediate retry with no delay.
51+
/// - Returns: The result of the successful operation.
52+
/// - Throws: The error from the operation if all retry attempts fail or if the strategy returns `.stop`.
53+
///
54+
/// ## Example
55+
///
56+
/// ```swift
57+
/// var backoff = Backoff.exponential(factor: 2, initial: .milliseconds(100))
58+
/// let result = try await retry(maxAttempts: 3, clock: ContinuousClock()) {
59+
/// try await someNetworkOperation()
60+
/// } strategy: { error in
61+
/// .backoff(backoff.nextDuration())
62+
/// }
63+
/// ```
1964
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
2065
@inlinable public func retry<Result, ErrorType, ClockType>(
2166
maxAttempts: Int,
@@ -42,6 +87,42 @@ public struct RetryAction<Duration: DurationProtocol> {
4287
return try await operation()
4388
}
4489

90+
/// Executes an asynchronous operation with retry logic and customizable backoff strategies.
91+
///
92+
/// This function attempts to execute the provided operation up to `maxAttempts` times.
93+
/// Between failed attempts, it consults the strategy function to determine whether to
94+
/// continue retrying with a delay or stop immediately.
95+
///
96+
/// The retry logic follows this sequence:
97+
/// 1. Execute the operation
98+
/// 2. If successful, return the result
99+
/// 3. If failed and this was not the final attempt:
100+
/// - Call the strategy closure with the error
101+
/// - If the strategy returns `.stop`, rethrow the error immediately
102+
/// - If the strategy returns `.backoff`, suspend for the given duration
103+
/// - Return to step 1
104+
/// 4. If failed on the final attempt, rethrow the error without consulting the strategy
105+
///
106+
/// - Parameters:
107+
/// - maxAttempts: The maximum number of attempts to make. Must be greater than 0.
108+
/// - tolerance: The tolerance for the sleep operation between retries.
109+
/// - isolation: The actor isolation to maintain during execution.
110+
/// - operation: The asynchronous operation to retry.
111+
/// - strategy: A closure that determines the retry action based on the error.
112+
/// Defaults to immediate retry with no delay.
113+
/// - Returns: The result of the successful operation.
114+
/// - Throws: The error from the operation if all retry attempts fail or if the strategy returns `.stop`.
115+
///
116+
/// ## Example
117+
///
118+
/// ```swift
119+
/// var backoff = Backoff.exponential(factor: 2, initial: .milliseconds(100))
120+
/// let result = try await retry(maxAttempts: 3) {
121+
/// try await someNetworkOperation()
122+
/// } strategy: { error in
123+
/// .backoff(backoff.nextDuration())
124+
/// }
125+
/// ```
45126
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
46127
@inlinable public func retry<Result, ErrorType>(
47128
maxAttempts: Int,
@@ -58,3 +139,4 @@ public struct RetryAction<Duration: DurationProtocol> {
58139
strategy: strategy
59140
)
60141
}
142+
#endif

0 commit comments

Comments
 (0)