🪨 Rock-solid, concise and thorough library to retry and repeat async throws
jobs.
Swift 6 mode and MainActor
friendly 🥳
var conditionPublisher: AnyPublisher<Bool, Never>
// Fully configurable policy with good defaults. Also available: withConstantDelay(), withNoDelay()
let retrier = 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 directly chain a call to job { try await job() }
to create a cold job retrier,
but you can also reuse any retrier to create multiple job retriers.
let fetcher = retrier.job {
try await fetchSomething()
}
let poller = retrier
// If you want to poll, well you can
.repeating(withDelay: 30)
.job {
try await fetchSomethingElse()
}
Once the job is set, you can add event handlers to your (still cold ❄️) retrier.
let fetcherWithEventHandler = fetcher.handleRetrierEvents {
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("Fetcher completed with \(error?.localizedDescription ?? "no error")")
}
}.handleRetrierEvents {
// Do something fun 🤡
}
All job retriers are cold publishers and:
- each subscription will create a new independent retrying stream 🔥
- cancelling the subscription cancels the retrier
Once in the Combine world, you'll know what to do (else check next paragraph).
let cancellable = fetcher
.sink { event in
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")")
}
}
let cancellable = fetcher
// Retrieve success values
.success()
.sink { fetchedValue in
// Do something with values
}
failure()
andcompletion()
filters are also available- 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. - You can use
success()
,failure()
andcompletion()
shortcuts.
If you don't repeat, you can wait for a single value in a concurrency context and:
- each awaiting will create a new independent retrying stream
- cancelling the task that is awaiting the value cancels the retrier
// 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
}
.job {
try await api.fetchValue()
}
.value
- 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 (via its subscription or its awaiting task cancellation)
- 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
. - Publishers emit only on
DispatchQueue.main
- Everything here is
MainActor
friendly - 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() -> Retrier {
let policy = MyOwnPolicy()
return Retrier(policy: policy, conditionPublisher: nil)
}
- most (if not all) usecases imply unlimited demand using
sink(receiveCompletion:)
orassign(to:)
operators - given the asynchronous nature of jobs, there's a very good chance backpressure won't be a problem
Still backpressure is properly managed: No event will be sent to the subscriber if there's no demand, and there will be no attempt to execute a job until the subscriber provides a positive demand.
In practice, you shouldn't care about that except if you implement your own Subscriber
.
- Now, retriers are cold until consumed because the old behavior was mostly useless and complicated - even dangerous
- All cancellations are propagated by default because I never found any use to preventing that but observed mistakes frequently. Up to you to not cancel if you don't want to.
execute()
->job()
because this modifier doesn't trigger execution anymore- forget about
publisher()
, the job retrier/repeater IS the publisher because there's no reason to add an extra step
I'm convinced these changes are for the best, and the API should not change much in the future.
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.