10

I want to make a network request more than one time when some error occurs using retry() from Swift/Combine. The block inside the publisher is called once only which means one only one request is made for a real app when error happens. My code is:

import UIKit
import Combine
import PlaygroundSupport

enum TestFailureCondition: Error {
    case invalidServerResponse
}

var backgroundQueue: DispatchQueue = DispatchQueue(label: "backgroundQueue")

var failPublisher: AnyPublisher<(Data, URLResponse), Error> {
    Future<(Data, URLResponse), Error> { promise in
        print("Attempt to call")
        backgroundQueue.asyncAfter(deadline: .now() + Double.random(in: 1..<3)) {
            promise(.failure(TestFailureCondition.invalidServerResponse))
        }
    }
    .eraseToAnyPublisher()
}

let cancellable = failPublisher
.print("(1)>")
.retry(3)
.print("(2)>")
.sink(receiveCompletion: { fini in
    print(" ** .sink() received the completion:", String(describing: fini))


    PlaygroundPage.current.finishExecution()
}, receiveValue: { stringValue in
    print(" ** .sink() received \(stringValue)")
})

PlaygroundPage.current.needsIndefiniteExecution = true

I expect that backgroundQueue.asyncAfter(deadline) is called three time before some error happens. Does anyone know why?

Dmitry
  • 171
  • 3
  • 12
  • As per my experiments `retry` really make its sense only with `URLSession` publishers, because it retries by recreating subscriptions, so depends on how publisher manages its internal state, between these retries. – Asperi Dec 07 '19 at 13:18
  • I am not sure then I understand what retry() actually does. I assumed that recreating a subscription means it needs to call a publisher block again, but it seems not the case. Anyway the Apple's documentation is not clear regarding this. – Dmitry Dec 09 '19 at 08:58

2 Answers2

17

Future runs its body once, as soon as it is created, even if nothing subscribes to it. It saves the result and completes all subscriptions with that same result. So using the retry operator on a Future won't make the Future run its body again on failure.

You want each subscription to run the body again. You can do that by wrapping your Future in a Deferred. The Deferred will create a new Future for every subscription.

var failPublisher: AnyPublisher<(Data, URLResponse), Error> {
    Deferred {
        Future<(Data, URLResponse), Error> { promise in
            print("Attempt to call")
            backgroundQueue.asyncAfter(deadline: .now() + Double.random(in: 1..<3)) {
                promise(.failure(TestFailureCondition.invalidServerResponse))
            }
        }
    }
    .eraseToAnyPublisher()
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
3

I managed to achieve the expected behaviour by utilising tryCatch() function and making another request: Link

The link contains two ways to achieve the same behaviour including Deferred {} mentioned above.

Dmitry
  • 171
  • 3
  • 12