Skip to content

Commit 1a147cd

Browse files
authored
Add giveUpAfter(timeout:) modifier, remove retryOn(*) modifiers (#23)
1 parent f2a8b8e commit 1a147cd

9 files changed

Lines changed: 32 additions & 71 deletions

File tree

README.md

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,14 @@ var conditionPublisher: AnyPublisher<Bool, Never>
1111
let coldRetrier = withExponentialBackoff()
1212
// Fetch only when you've got network and your user is authenticated for example
1313
.onlyWhen(conditionPublisher)
14-
// Ensure your retrier fails on some conditions
14+
// Ensure your retrier gives up on some conditions
1515
.giveUpAfter(maxAttempts: 10)
16+
.giveUpAfter(timeout: 30)
1617
.giveUpOnErrors {
1718
$0 is MyFatalError
1819
}
19-
// Ensure your retrier won't give up on some errors
20-
.retryOnErrors {
21-
$0 is MyTmpError
22-
}
2320
```
2421

25-
**All giveUp / retry modifiers are evaluated in reversed order.**
26-
2722
[Exponential backoff](https://aws.amazon.com/fr/blogs/architecture/exponential-backoff-and-jitter/) with
2823
full jitter is the default and recommended algorithm to fetch from a backend.
2924

@@ -60,12 +55,10 @@ If you don't repeat, you can wait for a single value in a concurrency context
6055
let value = try await withExponentialBackoff()
6156
.onlyWhen(conditionPublisher)
6257
.giveUpAfter(maxAttempts: 10)
58+
.giveUpAfter(timeout: 30)
6359
.giveUpOnErrors {
6460
$0 is MyFatalError
6561
}
66-
.retryOnErrors {
67-
$0 is MyTmpError
68-
}
6962
.execute {
7063
try await api.fetchValue()
7164
}
@@ -120,6 +113,7 @@ case, guarantees such as the previous one are no longer valid.
120113
- Condition publishers events will be processed on `DispatchQueue.main`, but won't be delayed if they're already
121114
emitted on it.
122115
- After a retrier is interrupted then resumed by its `conditionPublisher`, its policy is reused from start.
116+
Consequently `giveUpAfter(maxAttempts:)` and `giveUpAfter(timeout:)` checks are applied to the current trial, ignoring previous ones.
123117

124118
## Retry Policies
125119

@@ -134,9 +128,7 @@ You can especially choose the jitter type between `none`, `full` (default) and `
134128

135129
**ConstantDelayRetryPolicy** does what you expect, just waiting for a fixed amount of time.
136130

137-
You can add failure conditions using `giveUp*()` functions, and bypass these conditions using `retry*()` functions.
138-
139-
All giveUp / retry modifiers are evaluated in reversed order.
131+
You can add failure conditions using `giveUp*()` functions.
140132

141133
### Homemade policy
142134

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22

33
public struct AttemptFailure {
4+
public let trialStart: Date
45
public let index: UInt
56
public let error: Error
67
}

Sources/SwiftRetrier/Core/PolicyBuilding/RetryOnPolicyWrapper.swift

Lines changed: 0 additions & 26 deletions
This file was deleted.

Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+GiveUpOn.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ public extension RetryPolicy {
1010
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: { $0.index >= maxAttempts - 1})
1111
}
1212

13+
func giveUpAfter(timeout: TimeInterval) -> RetryPolicy {
14+
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: {
15+
let nextAttemptStart = Date().addingTimeInterval(retryDelay(for: $0))
16+
return nextAttemptStart >= $0.trialStart.addingTimeInterval(timeout)
17+
})
18+
}
19+
1320
func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> RetryPolicy {
1421
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: { finalErrorCriterium($0.error) })
1522
}

Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+RetryOn.swift

Lines changed: 0 additions & 12 deletions
This file was deleted.

Sources/SwiftRetrier/Core/Retriers/ConditionalRetrier.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ public class ConditionalRetrier<Output>: SingleOutputRetrier {
7878
guard let retrier else { return }
7979
retrierSubscription?.cancel()
8080
retrier.cancel()
81+
subject.send(.attemptFailure(AttemptFailure(trialStart: retrier.trialStart,
82+
index: attemptIndex,
83+
error: CancellationError())))
8184
self.retrier = nil
82-
subject.send(.attemptFailure(AttemptFailure(index: attemptIndex, error: CancellationError())))
8385
attemptIndex += 1
8486
}
8587

@@ -97,7 +99,9 @@ public class ConditionalRetrier<Output>: SingleOutputRetrier {
9799
switch $0 {
98100
// Catch attempt failure to adjust attempt index
99101
case .attemptFailure(let attemptFailure):
100-
event = .attemptFailure(AttemptFailure(index: attemptIndex, error: attemptFailure.error))
102+
event = .attemptFailure(AttemptFailure(trialStart: attemptFailure.trialStart,
103+
index: attemptIndex,
104+
error: attemptFailure.error))
101105
attemptIndex += 1
102106
// Remember final event for future await on value
103107
case .attemptSuccess:

Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Combine
1212
/// emits a completion embedding the same error then finishes.
1313
public class SimpleRetrier<Output>: SingleOutputRetrier {
1414

15+
public let trialStart = Date()
1516
private let subject = PassthroughSubject<RetrierEvent<Output>, Never>()
1617
private var task: Task<Output, Error>!
1718

@@ -56,11 +57,13 @@ public class SimpleRetrier<Output>: SingleOutputRetrier {
5657
await finish(with: result)
5758
return result
5859
} catch {
59-
let attemptFailure = AttemptFailure(index: attemptIndex, error: error)
60+
let attemptFailure = AttemptFailure(trialStart: trialStart, index: attemptIndex, error: error)
6061
await sendAttemptFailure(attemptFailure)
6162
try Task.checkCancellation()
6263
let retryDecision = await MainActor.run { [attemptIndex] in
63-
policy.shouldRetry(on: AttemptFailure(index: attemptIndex, error: error))
64+
policy.shouldRetry(on: AttemptFailure(trialStart: trialStart,
65+
index: attemptIndex,
66+
error: error))
6467
}
6568
switch retryDecision {
6669
case .giveUp:

Sources/SwiftRetrier/DSL/ColdRepeater.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,16 @@ public extension ColdRepeater {
1919
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
2020
}
2121

22-
func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRepeater {
23-
let policy = policy.giveUpOnErrors(matching: finalErrorCriterium)
22+
func giveUpAfter(timeout: TimeInterval) -> ColdRepeater {
23+
let policy = policy.giveUpAfter(timeout: timeout)
2424
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
2525
}
2626

27-
func retry(on retryCriterium: @escaping (AttemptFailure) -> Bool) -> ColdRepeater {
28-
let policy = RetryOnPolicyWrapper(wrapped: policy, retryCriterium: retryCriterium)
27+
func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRepeater {
28+
let policy = policy.giveUpOnErrors(matching: finalErrorCriterium)
2929
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
3030
}
3131

32-
func retryOnErrors(matching retryCriterium: @escaping (Error) -> Bool) -> ColdRepeater {
33-
retry(on: { retryCriterium($0.error) })
34-
}
35-
3632
func onlyWhen<P>(
3733
_ conditionPublisher: P
3834
) -> ColdRepeater where P: Publisher, P.Output == Bool, P.Failure == Never {

Sources/SwiftRetrier/DSL/ColdRetrier.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,16 @@ public extension ColdRetrier {
1818
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
1919
}
2020

21-
func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRetrier {
22-
let policy = policy.giveUpOnErrors(matching: finalErrorCriterium)
21+
func giveUpAfter(timeout: TimeInterval) -> ColdRetrier {
22+
let policy = policy.giveUpAfter(timeout: timeout)
2323
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
2424
}
2525

26-
func retry(on retryCriterium: @escaping (AttemptFailure) -> Bool) -> ColdRetrier {
27-
let policy = RetryOnPolicyWrapper(wrapped: policy, retryCriterium: retryCriterium)
26+
func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRetrier {
27+
let policy = policy.giveUpOnErrors(matching: finalErrorCriterium)
2828
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
2929
}
3030

31-
func retryOnErrors(matching retryCriterium: @escaping (Error) -> Bool) -> ColdRetrier {
32-
retry(on: { retryCriterium($0.error) })
33-
}
34-
3531
func onlyWhen<P>(
3632
_ conditionPublisher: P
3733
) -> ColdRetrier where P: Publisher, P.Output == Bool, P.Failure == Never {

0 commit comments

Comments
 (0)