Skip to content

Commit 79ebc5f

Browse files
committed
refactor: unify retry execution flow
Removes code duplication by merging the initial attempt and subsequent retries into a single `while` loop Changes: - Replaces the separate initial call and `for-in` loop with a manual iterator approach. - Centralizes the closure execution, error handling, and cancellation checks into one block. - Improves code readability and maintainability without altering external behavior.
1 parent 363fd0a commit 79ebc5f

4 files changed

Lines changed: 296 additions & 73 deletions

File tree

Sources/Typhoon/Classes/RetryPolicyService/RetryPolicyService.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ extension RetryPolicyService: IRetryPolicyService {
4040
onFailure: (@Sendable (Error) async -> Bool)?,
4141
_ closure: @Sendable () async throws -> T
4242
) async throws -> T {
43-
for duration in RetrySequence(strategy: strategy ?? self.strategy) {
44-
try Task.checkCancellation()
43+
let effectiveStrategy = strategy ?? self.strategy
4544

45+
var iterator = RetrySequence(strategy: effectiveStrategy).makeIterator()
46+
47+
while true {
4648
do {
4749
return try await closure()
4850
} catch {
@@ -51,11 +53,15 @@ extension RetryPolicyService: IRetryPolicyService {
5153
if !shouldContinue {
5254
throw error
5355
}
54-
}
5556

56-
try await Task.sleep(nanoseconds: duration)
57-
}
57+
guard let duration = iterator.next() else {
58+
throw RetryPolicyError.retryLimitExceeded
59+
}
5860

59-
throw RetryPolicyError.retryLimitExceeded
61+
try Task.checkCancellation()
62+
63+
try await Task.sleep(nanoseconds: duration)
64+
}
65+
}
6066
}
6167
}

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)