Skip to content

Commit 93f21b4

Browse files
committed
unify docs and proposal
1 parent 69fbbdb commit 93f21b4

File tree

3 files changed

+70
-17
lines changed

3 files changed

+70
-17
lines changed

Evolution/NNNN-retry-backoff.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,20 @@ If Swift gains the capability to "store" `inout` variables, the jitter variants
204204

205205
## Alternatives considered
206206

207+
### Passing attempt number to `BackoffStrategy `
208+
207209
Another option considered was to pass the current attempt number into the `BackoffStrategy`.
208210

209211
Although this initially seems useful, it conflicts with the idea of strategies being stateful. A strategy is supposed to track its own progression (e.g. by counting invocations or storing the last duration). If the attempt number were provided externally, strategies would become "semi-stateful": mutating because of internal components such as a `RandomNumberGenerator`, but at the same time relying on an external counter instead of their own stored history. This dual model is harder to reason about and less consistent, so it was deliberately avoided.
210212

211213
If adopters require access to the attempt number, they are free to implement this themselves, since the strategy is invoked each time a failure occurs, making it straightforward to maintain an external attempt counter.
212214

215+
### Retry on `AsyncSequence`
216+
217+
An alternative considered was adding retry functionality directly to `AsyncSequence` types, similar to how Combine provides retry on `Publisher`. However, after careful consideration, this was not included in the current proposal due to the lack of compelling real-world use cases.
218+
219+
If specific use cases emerge in the future that demonstrate clear value for async sequence retry functionality, this could be considered in a separate proposal or amended to this proposal.
220+
213221
## Acknowledgments
214222

215-
Thanks to [Philippe Hausler](https://github.com/phausler), [Franz Busch](https://github.com/FranzBusch) and [Honza Dvorsky](https://github.com/czechboy0) for their thoughtful feedback and suggestions that helped refine the API design and improve its clarity and usability.
223+
Thanks to [Philippe Hausler](https://github.com/phausler), [Franz Busch](https://github.com/FranzBusch) and [Honza Dvorsky](https://github.com/czechboy0) for their thoughtful feedback and suggestions that helped refine the API design and improve its clarity and usability.

Sources/AsyncAlgorithms/Retry/Backoff.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
#if compiler(>=6.2)
22
/// A protocol for defining backoff strategies that generate delays between retry attempts.
33
///
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.
4+
/// Each call to `nextDuration()` returns the delay for the next retry attempt. Strategies are
5+
/// naturally stateful. For instance, they may track the number of invocations or the previously
6+
/// returned duration to calculate the next delay.
7+
///
8+
/// - Precondition: Strategies should only increase or stay the same over time, never decrease.
9+
/// Decreasing delays may cause issues with modifiers like jitter which expect non-decreasing values.
610
///
711
/// ## Example
812
///
@@ -141,6 +145,8 @@ public enum Backoff {
141145
///
142146
/// Formula: `f(n) = constant`
143147
///
148+
/// - Precondition: `constant` must be greater than or equal to zero.
149+
///
144150
/// - Parameter constant: The fixed duration to wait between retry attempts.
145151
/// - Returns: A backoff strategy that always returns the constant duration.
146152
@inlinable public static func constant<Duration: DurationProtocol>(_ constant: Duration) -> some BackoffStrategy<Duration> {
@@ -151,6 +157,8 @@ public enum Backoff {
151157
///
152158
/// Formula: `f(n) = constant`
153159
///
160+
/// - Precondition: `constant` must be greater than or equal to zero.
161+
///
154162
/// - Parameter constant: The fixed duration to wait between retry attempts.
155163
/// - Returns: A backoff strategy that always returns the constant duration.
156164
///
@@ -169,6 +177,8 @@ public enum Backoff {
169177
///
170178
/// Formula: `f(n) = initial + increment * n`
171179
///
180+
/// - Precondition: `initial` and `increment` must be greater than or equal to zero.
181+
///
172182
/// - Parameters:
173183
/// - increment: The amount to increase the delay by on each attempt.
174184
/// - initial: The initial delay for the first retry attempt.
@@ -181,6 +191,8 @@ public enum Backoff {
181191
///
182192
/// Formula: `f(n) = initial + increment * n`
183193
///
194+
/// - Precondition: `initial` and `increment` must be greater than or equal to zero.
195+
///
184196
/// - Parameters:
185197
/// - increment: The amount to increase the delay by on each attempt.
186198
/// - initial: The initial delay for the first retry attempt.
@@ -202,6 +214,8 @@ public enum Backoff {
202214
///
203215
/// Formula: `f(n) = initial * factor^n`
204216
///
217+
/// - Precondition: `initial` must be greater than or equal to zero.
218+
///
205219
/// - Parameters:
206220
/// - factor: The multiplication factor for each retry attempt.
207221
/// - initial: The initial delay for the first retry attempt.
@@ -214,6 +228,8 @@ public enum Backoff {
214228
///
215229
/// Formula: `f(n) = initial * factor^n`
216230
///
231+
/// - Precondition: `initial` must be greater than or equal to zero.
232+
///
217233
/// - Parameters:
218234
/// - factor: The multiplication factor for each retry attempt.
219235
/// - initial: The initial delay for the first retry attempt.
@@ -238,9 +254,11 @@ extension Backoff {
238254
///
239255
/// Formula: `f(n) = random(base, f(n - 1) * factor)` where `f(0) = base`
240256
///
241-
/// Jitter prevents the "thundering herd" problem where multiple clients retry
257+
/// Jitter prevents the thundering herd problem where multiple clients retry
242258
/// simultaneously, reducing server load spikes and improving system stability.
243259
///
260+
/// - Precondition: `factor` must be greater than or equal to 1, and `base` must be greater than or equal to zero.
261+
///
244262
/// - Parameters:
245263
/// - factor: The multiplication factor for calculating the upper bound of randomness.
246264
/// - base: The base duration used as the minimum delay and initial reference.
@@ -304,7 +322,7 @@ extension BackoffStrategy where Duration == Swift.Duration {
304322
///
305323
/// Formula: `f(n) = random(0, g(n))` where `g(n)` is the base strategy
306324
///
307-
/// Jitter prevents the "thundering herd" problem where multiple clients retry
325+
/// Jitter prevents the thundering herd problem where multiple clients retry
308326
/// simultaneously, reducing server load spikes and improving system stability.
309327
///
310328
/// - Parameter generator: The random number generator to use. Defaults to `SystemRandomNumberGenerator()`.
@@ -317,7 +335,7 @@ extension BackoffStrategy where Duration == Swift.Duration {
317335
///
318336
/// Formula: `f(n) = random(g(n) / 2, g(n))` where `g(n)` is the base strategy
319337
///
320-
/// Jitter prevents the "thundering herd" problem where multiple clients retry
338+
/// Jitter prevents the thundering herd problem where multiple clients retry
321339
/// simultaneously, reducing server load spikes and improving system stability.
322340
///
323341
/// - Parameter generator: The random number generator to use. Defaults to `SystemRandomNumberGenerator()`.

Sources/AsyncAlgorithms/Retry/Retry.swift

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,15 @@ public struct RetryAction<Duration: DurationProtocol> {
1818
/// Indicates that retrying should continue after waiting for the specified duration.
1919
///
2020
/// - Parameter duration: The duration to wait before the next retry attempt.
21-
/// - Returns: A retry action that will cause the retry operation to wait.
2221
@inlinable public static func backoff(_ duration: Duration) -> Self {
2322
return .init(action: .backoff(duration))
2423
}
2524
}
2625

2726
/// Executes an asynchronous operation with retry logic and customizable backoff strategies.
2827
///
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.
28+
/// This function executes an asynchronous operation up to a specified number of attempts,
29+
/// with customizable delays and error-based retry decisions between attempts.
3230
///
3331
/// The retry logic follows this sequence:
3432
/// 1. Execute the operation
@@ -40,8 +38,23 @@ public struct RetryAction<Duration: DurationProtocol> {
4038
/// - Return to step 1
4139
/// 4. If failed on the final attempt, rethrow the error without consulting the strategy
4240
///
41+
/// Given this sequence, there are four termination conditions (when retrying will be stopped):
42+
/// - The operation completes without throwing an error
43+
/// - The operation has been attempted `maxAttempts` times
44+
/// - The strategy closure returns `.stop`
45+
/// - The clock throws
46+
///
47+
/// ## Cancellation
48+
///
49+
/// `retry` does not introduce special cancellation handling. If your code cooperatively
50+
/// cancels by throwing, ensure your strategy returns `.stop` for that error. Otherwise,
51+
/// retries continue unless the clock throws on cancellation (which, at the time of writing,
52+
/// both `ContinuousClock` and `SuspendingClock` do).
53+
///
54+
/// - Precondition: `maxAttempts` must be greater than 0.
55+
///
4356
/// - Parameters:
44-
/// - maxAttempts: The maximum number of attempts to make. Must be greater than 0.
57+
/// - maxAttempts: The maximum number of attempts to make.
4558
/// - tolerance: The tolerance for the sleep operation between retries.
4659
/// - clock: The clock to use for timing delays between retries.
4760
/// - isolation: The actor isolation to maintain during execution.
@@ -58,7 +71,7 @@ public struct RetryAction<Duration: DurationProtocol> {
5871
/// let result = try await retry(maxAttempts: 3, clock: ContinuousClock()) {
5972
/// try await someNetworkOperation()
6073
/// } strategy: { error in
61-
/// .backoff(backoff.nextDuration())
74+
/// return .backoff(backoff.nextDuration())
6275
/// }
6376
/// ```
6477
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@@ -89,9 +102,8 @@ public struct RetryAction<Duration: DurationProtocol> {
89102

90103
/// Executes an asynchronous operation with retry logic and customizable backoff strategies.
91104
///
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.
105+
/// This function executes an asynchronous operation up to a specified number of attempts,
106+
/// with customizable delays and error-based retry decisions between attempts.
95107
///
96108
/// The retry logic follows this sequence:
97109
/// 1. Execute the operation
@@ -103,8 +115,23 @@ public struct RetryAction<Duration: DurationProtocol> {
103115
/// - Return to step 1
104116
/// 4. If failed on the final attempt, rethrow the error without consulting the strategy
105117
///
118+
/// Given this sequence, there are four termination conditions (when retrying will be stopped):
119+
/// - The operation completes without throwing an error
120+
/// - The operation has been attempted `maxAttempts` times
121+
/// - The strategy closure returns `.stop`
122+
/// - The clock throws
123+
///
124+
/// ## Cancellation
125+
///
126+
/// `retry` does not introduce special cancellation handling. If your code cooperatively
127+
/// cancels by throwing, ensure your strategy returns `.stop` for that error. Otherwise,
128+
/// retries continue unless the clock throws on cancellation (which, at the time of writing,
129+
/// both `ContinuousClock` and `SuspendingClock` do).
130+
///
131+
/// - Precondition: `maxAttempts` must be greater than 0.
132+
///
106133
/// - Parameters:
107-
/// - maxAttempts: The maximum number of attempts to make. Must be greater than 0.
134+
/// - maxAttempts: The maximum number of attempts to make.
108135
/// - tolerance: The tolerance for the sleep operation between retries.
109136
/// - isolation: The actor isolation to maintain during execution.
110137
/// - operation: The asynchronous operation to retry.
@@ -120,7 +147,7 @@ public struct RetryAction<Duration: DurationProtocol> {
120147
/// let result = try await retry(maxAttempts: 3) {
121148
/// try await someNetworkOperation()
122149
/// } strategy: { error in
123-
/// .backoff(backoff.nextDuration())
150+
/// return .backoff(backoff.nextDuration())
124151
/// }
125152
/// ```
126153
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)

0 commit comments

Comments
 (0)