🪨 Rock-solid, concise and thorough library to retry and repeat async throws
jobs.
var conditionPublisher: AnyPublisher<Bool, Never>
// Fully configurable policy with good defaults. Also available: withConstantDelay(), withNoDelay()
let coldRetrier = withExponentialBackoff()
// Fetch only when you've got network and your user is authenticated for example
.onlyWhen(conditionPublisher)
// Ensure your retrier gives up on some conditions
.giveUpAfter(maxAttempts: 10)
.giveUpAfter(timeout: 30)
.giveUpOnErrors {
$0 is MyFatalError
}
Exponential backoff with full jitter is the default and recommended algorithm to fetch from a backend.
You can chain a call to execute { try await job() }
, but you can also reuse any cold retrier to execute multiple
jobs independently.
let fetcher = coldRetrier.execute {
try await fetchSomething()
}
let poller = coldRetrier
// If you want to poll, well you can
.repeating(withDelay: 30)
.execute {
try await fetchSomethingElse()
}
// you can omit `execute` and call the retrier as a function:
let otherFetcher = coldRetrier { try await fetchSomethingElse() }
// You can always cancel hot retriers
fetcher.cancel()
If you don't repeat, you can wait for a single value in a concurrency context
// This will throw if you cancel the retrier or if any `giveUp*()` function matches
let value = try await withExponentialBackoff()
.onlyWhen(conditionPublisher)
.giveUpAfter(maxAttempts: 10)
.giveUpAfter(timeout: 30)
.giveUpOnErrors {
$0 is MyFatalError
}
.execute {
try await api.fetchValue()
}
.value
Note that you can use cancellableValue
instead of value
. In this case, if the task wrapping the concurrency context
is cancelled, the underlying retrier will be cancelled.
All retriers (including repeaters) expose Combine publishers that publish relevant events.
let cancellable = poller.publisher()
.sink {
switch $0 {
case .attemptSuccess(let value):
print("Fetched something: \(value)")
case .attemptFailure(let failure):
print("An attempt #\(failure.index) failed with \(failure.error)")
case .completion(let error):
print("Poller completed with \(error?.localizedDescription ?? "no error")")
}
}
- The publishers never fail, meaning their completion is always
.finished
and you cansink {}
without handling the completion - Instead,
attemptFailure
,attemptSuccess
andcompletion
events are materialized and sent as values. - Retriers expose
successPublisher()
,failurePublisher()
andcompletionPublisher()
shortcuts. - You can use
publisher(propagateCancellation: true)
to cancel the retrier when you're done listening to it.
- All retriers are cancellable.
- Retriers retry until either:
- their policy gives up
- the job succeeds (except for repeaters that will delay another trial)
- the retrier is cancelled
- their conditionPublisher ends after having published no value or
false
as its last value
- When a policy gives up, the last job error is thrown on any
try await retrier.value
, and also embedded into aRetrierEvent.completion
. - Retriers publishers emit only on
DispatchQueue.main
. - When cancelled, the retrier publishers emit a
RetrierEvent.completion(CancellationError())
value then a.finished
completion and no intermediary attempt result. - All retriers start their tasks immediately on initialization, and just wait for the current main queue cycle to end before executing jobs. This way, if a retrier is created on main queue and cancelled in the same cycle, it's guaranteed to not execute the job even once.
- You can create and cancel retriers on a different
DispatchQueue
or even in an asynchronous context. But in this 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. ConsequentlygiveUpAfter(maxAttempts:)
andgiveUpAfter(timeout:)
checks are applied to the current trial, ignoring previous ones.
It's important to understand that policies are not used to repeat after a success, but only to retry on failure. When repeating, the policy is reused from start after each success.
ExponentialBackoffRetryPolicy is implemented according to state-of-the-art algorithms.
Have a look to the available arguments, and you'll recognize the standard parameters and options.
You can especially choose the jitter type between none
, full
(default) and decorrelated
.
ConstantDelayRetryPolicy does what you expect, just waiting for a fixed amount of time.
You can add failure conditions using giveUp*()
functions.
You can create your own policies that conform RetryPolicy
and they will benefit from the same modifiers.
Have a look at ConstantDelayRetryPolicy.swift
for a basic example.
struct
types.
If a policy needs to know about attempts history, ensure you propagate what's needed when implementing
policyAfter(attemptFailure:, delay:) -> any RetryPolicy
.
To create a DSL entry point using your policy:
public func withMyOwnPolicy() -> ColdRetrier {
let policy = MyOwnPolicy()
return ColdRetrier(policy: policy, conditionPublisher: nil)
}
You can use the classes initializers directly, namely SimpleRetrier
,
ConditionalRetrier
and Repeater
.
Feel free to make any comment, criticism, bug report or feature request using GitHub issues.
You can also directly send me an email at pierre
strange "a" with a long round tail pittscraft.com
.
SwiftRetrier is available under the MIT license. See the LICENSE file for more info.