I see how to use .retry
directly, to resubscribe after an error, like this:
URLSession.shared.dataTaskPublisher(for:url)
.retry(3)
But that seems awfully simple-minded. What if I think that this error might go away if I wait awhile? I could insert a .delay
operator, but then the delay operates even if there is no error. And there doesn't seem to be a way to apply an operator conditionally (i.e. only when there's an error).
I see how I could work around this by writing a RetryWithDelay operator from scratch, and indeed such an operator has been written by third parties. But is there a way to say "delay if there's an error", purely using the operators we're given?
My thought was that I could use .catch
, because its function runs only if there is an error. But the function needs to return a publisher, and what publisher would we use? If we return somePublisher.delay(...)
followed by .retry
, we'd be applying .retry
to the wrong publisher, wouldn't we?
I found a few quirks with the implementations in the accepted answer.
Firstly the first two attempts will be fired off without a delay since the first delay will only take effect after the second attempt.
Secondly if any one of the retry attempts succeed, the output value will also delayed which seems unnecessary.
Thirdly the extension is not flexible enough to allow the user to decide which scheduler it would like the retry attempts to be dispatched to.
After some tinkering back and forth I ended up with a solution like this:
public extension Publisher {
/**
Creates a new publisher which will upon failure retry the upstream publisher a provided number of times, with the provided delay between retry attempts.
If the upstream publisher succeeds the first time this is bypassed and proceeds as normal.
- Parameters:
- retries: The number of times to retry the upstream publisher.
- delay: Delay in seconds between retry attempts.
- scheduler: The scheduler to dispatch the delayed events.
- Returns: A new publisher which will retry the upstream publisher with a delay upon failure.
~~~
let url = URL(string: "https://api.myService.com")!
URLSession.shared.dataTaskPublisher(for: url)
.retryWithDelay(retries: 4, delay: 5, scheduler: DispatchQueue.global())
.sink { completion in
switch completion {
case .finished:
print("Success 😊")
case .failure(let error):
print("The last and final failure after retry attempts: \(error)")
}
} receiveValue: { output in
print("Received value: \(output)")
}
.store(in: &cancellables)
~~~
*/
func retryWithDelay<S>(
retries: Int,
delay: S.SchedulerTimeType.Stride,
scheduler: S
) -> AnyPublisher<Output, Failure> where S: Scheduler {
self
.delayIfFailure(for: delay, scheduler: scheduler)
.retry(retries)
.eraseToAnyPublisher()
}
private func delayIfFailure<S>(
for delay: S.SchedulerTimeType.Stride,
scheduler: S
) -> AnyPublisher<Output, Failure> where S: Scheduler {
self.catch { error in
Future { completion in
scheduler.schedule(after: scheduler.now.advanced(by: delay)) {
completion(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
}
Using .catch
is indeed the answer. We simply make a reference to the data task publisher and use that reference as the head of both pipelines — the outer pipeline that does the initial networking, and the inner pipeline produced by the .catch
function.
Let's start by creating the data task publisher and stop:
let pub = URLSession.shared.dataTaskPublisher(for: url).share()
Now I can form the head of the pipeline:
let head = pub.catch {_ in pub.delay(for: 3, scheduler: DispatchQueue.main)}
.retry(3)
That should do it! head
is now a pipeline that inserts a delay operator only just in case there is an error. We can then proceed to form the rest of the pipeline, based on head
.
Observe that we do indeed change publishers; if there is a failure and the catch
function runs, the pub
which is the upstream of the .delay
becomes the publisher, replacing the pub
we started out with. However, they are the same object (because I said share
), so this is a distinction without a difference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With