Skip to content

Commit 87a8ee7

Browse files
authored
refactor: unify retry execution flow (#49)
1 parent fdeb2d0 commit 87a8ee7

File tree

4 files changed

+339
-74
lines changed

4 files changed

+339
-74
lines changed

Sources/Typhoon/Classes/RetryPolicyService/RetryPolicyService.swift

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,49 @@ import Foundation
77

88
// MARK: - RetryPolicyService
99

10-
/// A class that defines a service for retry policies
10+
/// `RetryPolicyService` provides a high-level API for retrying asynchronous
11+
/// operations using configurable retry strategies.
12+
///
13+
/// The service encapsulates retry logic such as:
14+
/// - limiting the number of retry attempts,
15+
/// - applying delays between retries (e.g. fixed, exponential, or custom),
16+
/// - reacting to errors on each failed attempt.
17+
///
18+
/// This class is typically used for retrying unstable operations like
19+
/// network requests, database calls, or interactions with external services.
20+
///
21+
/// ### Example
22+
/// ```swift
23+
/// let strategy = RetryPolicyStrategy.exponential(
24+
/// maxAttempts: 3,
25+
/// initialDelay: .milliseconds(500)
26+
/// )
27+
///
28+
/// let retryService = RetryPolicyService(strategy: strategy)
29+
///
30+
/// let data = try await retryService.retry(
31+
/// strategy: nil,
32+
/// onFailure: { error in
33+
/// print("Request failed with error: \(error)")
34+
///
35+
/// // Return `true` to continue retrying,
36+
/// // or `false` to stop and rethrow the error.
37+
/// return true
38+
/// }
39+
/// ) {
40+
/// try await apiClient.fetchData()
41+
/// }
42+
/// ```
43+
///
44+
///
45+
/// In this example:
46+
/// - The request will be retried up to 3 times.
47+
/// - The delay between retries grows exponentially.
48+
/// - Each failure is logged before the next attempt.
49+
/// - If all retries are exhausted, `RetryPolicyError.retryLimitExceeded` is thrown.
50+
///
51+
/// - Note: You can override the default strategy per call by passing a custom
52+
/// `RetryPolicyStrategy` into the `retry` method.
1153
public final class RetryPolicyService {
1254
// MARK: Private
1355

@@ -40,9 +82,11 @@ extension RetryPolicyService: IRetryPolicyService {
4082
onFailure: (@Sendable (Error) async -> Bool)?,
4183
_ closure: @Sendable () async throws -> T
4284
) async throws -> T {
43-
for duration in RetrySequence(strategy: strategy ?? self.strategy) {
44-
try Task.checkCancellation()
85+
let effectiveStrategy = strategy ?? self.strategy
4586

87+
var iterator = RetrySequence(strategy: effectiveStrategy).makeIterator()
88+
89+
while true {
4690
do {
4791
return try await closure()
4892
} catch {
@@ -51,11 +95,15 @@ extension RetryPolicyService: IRetryPolicyService {
5195
if !shouldContinue {
5296
throw error
5397
}
54-
}
5598

56-
try await Task.sleep(nanoseconds: duration)
57-
}
99+
guard let duration = iterator.next() else {
100+
throw RetryPolicyError.retryLimitExceeded
101+
}
58102

59-
throw RetryPolicyError.retryLimitExceeded
103+
try Task.checkCancellation()
104+
105+
try await Task.sleep(nanoseconds: duration)
106+
}
107+
}
60108
}
61109
}

Sources/Typhoon/Typhoon.docc/Articles/advanced-retry-strategies.md

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ Master advanced retry patterns and optimization techniques.
77

88
This guide covers advanced usage patterns, performance optimization, and sophisticated retry strategies for complex scenarios.
99

10+
## How Retry Mechanism Works
11+
12+
Understanding the retry flow is crucial for effective error handling:
13+
14+
```swift
15+
// Configuration: retry: 3 means 3 RETRY attempts
16+
let strategy = RetryStrategy.exponential(
17+
retry: 3,
18+
multiplier: 2.0,
19+
duration: .seconds(1)
20+
)
21+
```
22+
23+
**Total Execution Flow:**
24+
25+
| Attempt Type | Attempt # | Delay Before | Description |
26+
|--------------|-----------|--------------|-------------|
27+
| Initial | 1 | 0s | First execution (not a retry) |
28+
| Retry | 2 | 1s | First retry after failure |
29+
| Retry | 3 | 2s | Second retry after failure |
30+
| Retry | 4 | 4s | Third retry after failure |
31+
32+
**Key Points:**
33+
- The `retry` parameter specifies the number of **retry attempts**, not total attempts
34+
- Total attempts = 1 (initial) + N (retries)
35+
- `retry: 3` means **4 total attempts** (1 initial + 3 retries)
36+
- `onFailure` callback is invoked after **every** failed attempt, including the initial one
37+
1038
## Strategy Deep Dive
1139

1240
### Understanding Exponential Backoff
@@ -15,33 +43,36 @@ Exponential backoff progressively increases wait times to avoid overwhelming rec
1543

1644
```swift
1745
let strategy = RetryStrategy.exponential(
18-
retry: 5,
46+
retry: 5, // 5 retry attempts
1947
multiplier: 2.0,
2048
duration: .seconds(1)
2149
)
2250
```
2351

24-
**Calculation:** `delay = baseDuration × multiplier^retryCount`
52+
**Calculation:** `delay = baseDuration × multiplier^(attemptNumber - 1)`
53+
54+
| Attempt Type | Total Attempt | Calculation | Delay Before |
55+
|--------------|---------------|-------------|--------------|
56+
| Initial | 1 | - | 0s (immediate) |
57+
| Retry 1 | 2 | 1 × 2⁰ | 1s |
58+
| Retry 2 | 3 | 1 × 2¹ | 2s |
59+
| Retry 3 | 4 | 1 × 2² | 4s |
60+
| Retry 4 | 5 | 1 × 2³ | 8s |
61+
| Retry 5 | 6 | 1 × 2⁴ | 16s |
2562

26-
| Attempt | Calculation | Delay |
27-
|---------|-------------|-------|
28-
| 1 | 1 × 2⁰ | 1s |
29-
| 2 | 1 × 2¹ | 2s |
30-
| 3 | 1 × 2² | 4s |
31-
| 4 | 1 × 2³ | 8s |
32-
| 5 | 1 × 2⁴ | 16s |
63+
**Total: 6 attempts (1 initial + 5 retries)**
3364

3465
**Multiplier effects:**
3566

3667
```swift
3768
// Aggressive backoff (multiplier: 3.0)
38-
// 1s → 3s → 9s → 27s → 81s
69+
// Initial: 0s → Retry: 1s → 3s → 9s → 27s → 81s
3970

4071
// Moderate backoff (multiplier: 1.5)
41-
// 1s → 1.5s → 2.25s → 3.375s → 5.0625s
72+
// Initial: 0s → Retry: 1s → 1.5s → 2.25s → 3.375s → 5.0625s
4273

4374
// Slow backoff (multiplier: 1.2)
44-
// 1s → 1.2s → 1.44s → 1.728s → 2.074s
75+
// Initial: 0s → Retry: 1s → 1.2s → 1.44s → 1.728s → 2.074s
4576
```
4677

4778
### Jitter: Preventing Thundering Herd
@@ -50,7 +81,7 @@ When multiple clients retry simultaneously, they can overwhelm a recovering serv
5081

5182
```swift
5283
let strategy = RetryStrategy.exponentialWithJitter(
53-
retry: 5,
84+
retry: 5, // 5 retry attempts
5485
jitterFactor: 0.2, // ±20% randomization
5586
maxInterval: .seconds(30), // Cap at 30 seconds
5687
multiplier: 2.0,
@@ -60,17 +91,17 @@ let strategy = RetryStrategy.exponentialWithJitter(
6091

6192
**Without jitter:**
6293
```
63-
Client 1: 0s → 1s → 2s → 4s → 8s
64-
Client 2: 0s → 1s → 2s → 4s → 8s
65-
Client 3: 0s → 1s → 2s → 4s → 8s
94+
Client 1: 0s(init) → 1s → 2s → 4s → 8s → 16s
95+
Client 2: 0s(init) → 1s → 2s → 4s → 8s → 16s
96+
Client 3: 0s(init) → 1s → 2s → 4s → 8s → 16s
6697
All hit server simultaneously! 💥
6798
```
6899

69100
**With jitter:**
70101
```
71-
Client 1: 0s → 0.9s → 2.1s → 3.8s → 8.2s
72-
Client 2: 0s → 1.1s → 1.9s → 4.3s → 7.7s
73-
Client 3: 0s → 0.8s → 2.2s → 3.9s → 8.1s
102+
Client 1: 0s(init) → 0.9s → 2.1s → 3.8s → 8.2s → 15.7s
103+
Client 2: 0s(init) → 1.1s → 1.9s → 4.3s → 7.7s → 16.4s
104+
Client 3: 0s(init) → 0.8s → 2.2s → 3.9s → 8.1s → 15.8s
74105
Traffic spread out! ✅
75106
```
76107

@@ -80,17 +111,19 @@ Prevent delays from growing unbounded:
80111

81112
```swift
82113
.exponentialWithJitter(
83-
retry: 10,
114+
retry: 10, // 10 retry attempts = 11 total
84115
jitterFactor: 0.1,
85-
maxInterval: .seconds(60), // Never wait more than 60 seconds
116+
maxInterval: .seconds(60), // Never wait more than 60 seconds
86117
multiplier: 2.0,
87118
duration: .seconds(1)
88119
)
89120
```
90121

91-
**Without cap:** 1s → 2s → 4s → 8s → 16s → 32s → 64s → 128s → 256s...
122+
**Without cap:**
123+
Initial → 1s → 2s → 4s → 8s → 16s → 32s → 64s → 128s → 256s → 512s
92124

93-
**With 60s cap:** 1s → 2s → 4s → 8s → 16s → 32s → 60s → 60s → 60s...
125+
**With 60s cap:**
126+
Initial → 1s → 2s → 4s → 8s → 16s → 32s → 60s → 60s → 60s → 60s
94127

95128
## Advanced Patterns
96129

@@ -132,7 +165,8 @@ func fetchWithConditionalRetry() async throws -> Data {
132165
} catch let error as RetryPolicyError {
133166
switch error {
134167
case .retryLimitExceeded:
135-
// Retry linit exceeded
168+
// All retry attempts exhausted
169+
print("Retry limit exceeded after multiple attempts")
136170
throw error
137171
}
138172
}
@@ -200,6 +234,7 @@ actor AdaptiveRetryService {
200234
private func selectStrategy() -> RetryPolicyStrategy {
201235
if consecutiveFailures >= maxConsecutiveFailures {
202236
// System under stress - use conservative strategy
237+
// 1 initial + 3 retries with longer delays
203238
return .exponentialWithJitter(
204239
retry: 3,
205240
jitterFactor: 0.3,
@@ -209,6 +244,7 @@ actor AdaptiveRetryService {
209244
)
210245
} else {
211246
// Normal operation - use standard strategy
247+
// 1 initial + 4 retries
212248
return .exponential(
213249
retry: 4,
214250
multiplier: 2.0,

Sources/Typhoon/Typhoon.docc/Articles/quick-start.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ This will:
3333
- Try your operation immediately
3434
- If it fails, wait 1 second and retry
3535
- Repeat up to 3 times
36-
- Throw the last error if all attempts fail
3736

3837
### Network Request Example
3938

@@ -75,7 +74,7 @@ Best for predictable, fixed delays:
7574
.constant(retry: 5, duration: .seconds(2))
7675
```
7776

78-
**Timeline:** 0s → 2s → 2s → 2s → 2s
77+
**Timeline:** 0s (initial) → 2s → 2s → 2s → 2s → 2s
7978

8079
### Exponential Strategy
8180

@@ -86,7 +85,7 @@ Ideal for backing off from failing services:
8685
.exponential(retry: 4, multiplier: 2.0, duration: .seconds(1))
8786
```
8887

89-
**Timeline:** 0s → 1s → 2s → 4s
88+
**Timeline:** 0s (initial) → 1s → 2s → 4s → 8s
9089

9190
### Exponential with Jitter
9291

@@ -103,7 +102,7 @@ Best for preventing thundering herd problems:
103102
)
104103
```
105104

106-
**Timeline:** 0s → ~1s → ~2s → ~4s → ~8s (with randomization)
105+
**Timeline:** 0s (initial) ~1s → ~2s → ~4s → ~8s~16s (with randomization)
107106

108107
## Common Patterns
109108

0 commit comments

Comments
 (0)