Skip to content

Commit

Permalink
Add giveUpAfter(timeout:) modifier, remove retryOn(*) modifiers (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreMardon authored Sep 1, 2023
1 parent f2a8b8e commit 1a147cd
Show file tree
Hide file tree
Showing 9 changed files with 32 additions and 71 deletions.
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,14 @@ var conditionPublisher: AnyPublisher<Bool, Never>
let coldRetrier = withExponentialBackoff()
// Fetch only when you've got network and your user is authenticated for example
.onlyWhen(conditionPublisher)
// Ensure your retrier fails on some conditions
// Ensure your retrier gives up on some conditions
.giveUpAfter(maxAttempts: 10)
.giveUpAfter(timeout: 30)
.giveUpOnErrors {
$0 is MyFatalError
}
// Ensure your retrier won't give up on some errors
.retryOnErrors {
$0 is MyTmpError
}
```

**All giveUp / retry modifiers are evaluated in reversed order.**

[Exponential backoff](https://aws.amazon.com/fr/blogs/architecture/exponential-backoff-and-jitter/) with
full jitter is the default and recommended algorithm to fetch from a backend.

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

## Retry Policies

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

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

You can add failure conditions using `giveUp*()` functions, and bypass these conditions using `retry*()` functions.

All giveUp / retry modifiers are evaluated in reversed order.
You can add failure conditions using `giveUp*()` functions.

### Homemade policy

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

public struct AttemptFailure {
public let trialStart: Date
public let index: UInt
public let error: Error
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ public extension RetryPolicy {
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: { $0.index >= maxAttempts - 1})
}

func giveUpAfter(timeout: TimeInterval) -> RetryPolicy {
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: {
let nextAttemptStart = Date().addingTimeInterval(retryDelay(for: $0))
return nextAttemptStart >= $0.trialStart.addingTimeInterval(timeout)
})
}

func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> RetryPolicy {
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: { finalErrorCriterium($0.error) })
}
Expand Down
12 changes: 0 additions & 12 deletions Sources/SwiftRetrier/Core/PolicyBuilding/RetryPolicy+RetryOn.swift

This file was deleted.

8 changes: 6 additions & 2 deletions Sources/SwiftRetrier/Core/Retriers/ConditionalRetrier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ public class ConditionalRetrier<Output>: SingleOutputRetrier {
guard let retrier else { return }
retrierSubscription?.cancel()
retrier.cancel()
subject.send(.attemptFailure(AttemptFailure(trialStart: retrier.trialStart,
index: attemptIndex,
error: CancellationError())))
self.retrier = nil
subject.send(.attemptFailure(AttemptFailure(index: attemptIndex, error: CancellationError())))
attemptIndex += 1
}

Expand All @@ -97,7 +99,9 @@ public class ConditionalRetrier<Output>: SingleOutputRetrier {
switch $0 {
// Catch attempt failure to adjust attempt index
case .attemptFailure(let attemptFailure):
event = .attemptFailure(AttemptFailure(index: attemptIndex, error: attemptFailure.error))
event = .attemptFailure(AttemptFailure(trialStart: attemptFailure.trialStart,
index: attemptIndex,
error: attemptFailure.error))
attemptIndex += 1
// Remember final event for future await on value
case .attemptSuccess:
Expand Down
7 changes: 5 additions & 2 deletions Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Combine
/// emits a completion embedding the same error then finishes.
public class SimpleRetrier<Output>: SingleOutputRetrier {

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

Expand Down Expand Up @@ -56,11 +57,13 @@ public class SimpleRetrier<Output>: SingleOutputRetrier {
await finish(with: result)
return result
} catch {
let attemptFailure = AttemptFailure(index: attemptIndex, error: error)
let attemptFailure = AttemptFailure(trialStart: trialStart, index: attemptIndex, error: error)
await sendAttemptFailure(attemptFailure)
try Task.checkCancellation()
let retryDecision = await MainActor.run { [attemptIndex] in
policy.shouldRetry(on: AttemptFailure(index: attemptIndex, error: error))
policy.shouldRetry(on: AttemptFailure(trialStart: trialStart,
index: attemptIndex,
error: error))
}
switch retryDecision {
case .giveUp:
Expand Down
12 changes: 4 additions & 8 deletions Sources/SwiftRetrier/DSL/ColdRepeater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,16 @@ public extension ColdRepeater {
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
}

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

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

func retryOnErrors(matching retryCriterium: @escaping (Error) -> Bool) -> ColdRepeater {
retry(on: { retryCriterium($0.error) })
}

func onlyWhen<P>(
_ conditionPublisher: P
) -> ColdRepeater where P: Publisher, P.Output == Bool, P.Failure == Never {
Expand Down
12 changes: 4 additions & 8 deletions Sources/SwiftRetrier/DSL/ColdRetrier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,16 @@ public extension ColdRetrier {
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
}

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

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

func retryOnErrors(matching retryCriterium: @escaping (Error) -> Bool) -> ColdRetrier {
retry(on: { retryCriterium($0.error) })
}

func onlyWhen<P>(
_ conditionPublisher: P
) -> ColdRetrier where P: Publisher, P.Output == Bool, P.Failure == Never {
Expand Down

0 comments on commit 1a147cd

Please sign in to comment.