Swift "Retry" Logic on Request

Swift retry logic on request

Here's a general solution that can be applied to any async function that has no parameters, excepting the callbacks. I simplified the logic by having only success and failure callbacks, a progress should not be that hard to add.

So, assuming that your function is like this:

func startUploading(success: @escaping () -> Void, failure: @escaping (Error) -> Void) {
DDLogDebug("JogUploader: Creating jog: \(self.jog)")

API.sharedInstance.createJog(self.jog,
failure: { error in
failure(error)
}, success: {_ in
success()
})
}

A matching retry function might look like this:

func retry(times: Int, task: @escaping(@escaping () -> Void, @escaping (Error) -> Void) -> Void, success: @escaping () -> Void, failure: @escaping (Error) -> Void) {
task(success,
{ error in
// do we have retries left? if yes, call retry again
// if not, report error
if times > 0 {
retry(times - 1, task: task, success: success, failure: failure)
} else {
failure(error)
}
})
}

and can be called like this:

retry(times: 3, task: startUploading,
success: {
print("Succeeded")
},
failure: { err in
print("Failed: \(err)")
})

The above will retry the startUploading call three times if it keeps failing, otherwise will stop at the first success.

Edit. Functions that do have other params can be simply embedded in a closure:

func updateUsername(username: String, success: @escaping () -> Void, failure: @escaping (Error) -> Void) {
...
}

retry(times: 3, { success, failure in updateUsername(newUsername, success, failure) },
success: {
print("Updated username")
},
failure: {
print("Failed with error: \($0)")
}
)

Update So many @escaping clauses in the retry function declaration might decrease its readability, and increase the cognitive load when it comes to consuming the function. To improve this, we can write a simple generic struct that has the same functionality:

struct Retrier<T> {
let times: UInt
let task: (@escaping (T) -> Void, @escaping (Error) -> Void) -> Void

func callAsFunction(success: @escaping (T) -> Void, failure: @escaping (Error) -> Void) {
let failureWrapper: (Error) -> Void = { error in
// do we have retries left? if yes, call retry again
// if not, report error
if times > 0 {
Retrier(times: times - 1, task: task)(success: success, failure: failure)
} else {
failure(error)
}
}
task(success, failureWrapper)
}

func callAsFunction(success: @escaping () -> Void, failure: @escaping (Error) -> Void) where T == Void {
callAsFunction(success: { _ in }, failure: failure)
}
}

Being callable, the struct can be called like a regular function:

Retrier(times: 3, task: startUploading)(success: { print("success: \($0)") },
failure: { print("failure: \($0)") })

, or can be circulated through the app:

let retrier = Retrier(times: 3, task: startUploading)
// ...
// sometime later
retrier(success: { print("success: \($0)") },
failure: { print("failure: \($0)") })

How can I retry a URLRequest.sharedDataTask until the response is 200?

Consider that the while loop is outside of the asynchronous task, so a couple of 100 tasks can be created before the counter is being decremented.

A better way is to remove the while loop and check for 0 inside the closure – the retriesLeft parameter makes no sense – for example

func retryFunc(url: String, requestType: String, requestJsonData: Any) {
//Function body
// declarations

let task = URLSession.shared.dataTask(with: request) { data, response, error in

if let error = error {
print(error.localizedDescription)
return
}

if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if httpResponse.statusCode == 200 {
print("Got 200")
do {
if let responseJSON = try JSONSerialization.jsonObject(with: data!) as? [String: Any] {
print(responseJSON)
}
} catch { print(error) }

} else if httpResponse.statusCode == 500 {
print("Got 500")
self.numberOfretries -= 1
if self.numberOfretries == 0 { return }

self.retryFunc(url: url, requestType: requestType, requestJsonData: requestJsonData)
}
}
}
task.resume()
}

How to retry network request that has failed

Try this

case .failure(let error):
print("Failure! \(error)")
if(self.counter<3)
{
self.testSampleRestCall()
counter = counter+1
}
else
{
// notify user check if this a thread other than main to wrap code in main queue

}

}

How to retry failed web service calls with time delay in ios app

It would probably be best to create a custom Publisher, with the idea being that you'd keep track of the number of failures, and either return a delayed publisher (that fails) under normal conditions, or a non-delayed publisher for the last retry.

struct RetryWithDelay<Upstream: Publisher, S: Scheduler>: Publisher {

typealias Output = Upstream.Output
typealias Failure = Upstream.Failure

let upstream: Upstream
let retries: Int
let interval: S.SchedulerTimeType.Stride
let scheduler: S

func receive<Sub>(subscriber: Sub)
where Sub : Subscriber,
Upstream.Failure == Sub.Failure,
Upstream.Output == Sub.Input {

var retries = self.retries
let p = upstream
.catch { err -> AnyPublisher<Output, Failure> in
retries -= 1
return Fail(error: err)
.delay(for: retries > 0 ? interval : .zero, scheduler: scheduler)
.eraseToAnyPublisher()
}
.retry(self.retries)

p.subscribe(subscriber)
}
}

For convenience, you can create an operator, like you did in the question:

public extension Publisher {
func retryWithDelay<S>(
retries: Int,
delay: S.SchedulerTimeType.Stride,
scheduler: S
) -> RetryWithDelay<Self, S> where S: Scheduler {

RetryWithDelay(upstream: self,
retries: retries, delay: delay, scheduler: scheduler)
}
}

Swift combine retry only for some error types

Basically, you need a retryIf operator, so you can provide a closure to tell Combine which errors should be retried, and which not. I'm not aware of such an operator, but it's not hard to build one for yourself.

The idiomatic way is to extend the Publishers namespace with a new type for your operator, and then extend Publisher to add support for that operator so that yo can chain it along with other operators.

The implementation could look like this:

extension Publishers {
struct RetryIf<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure

let publisher: P
let times: Int
let condition: (P.Failure) -> Bool

func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }

publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
if condition(error) {
return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
} else {
return Fail(error: error).eraseToAnyPublisher()
}
}.receive(subscriber: subscriber)
}
}
}

extension Publisher {
func retry(times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
}

Usage:

func createRequest(for message: Message) -> AnyPublisher<ResponseMessage, Error> {
Deferred {
Future<ResponseMessage, Error> { promise in
// future code
}
}
.retry(times: 3) { error in
if case let MessageBusError.messageError(responseError) = error, responseError.isRecoverable {
return true
}
return false
}
.eraseToAnyPublisher()
}

Note that I wrapped your Future within a Deferred one, otherwise the retry operator would be meaningless, as the closure will not be executed multiple times. More details about that behaviour here: Swift. Combine. Is there any way to call a publisher block more than once when retry?.


Alternatively, you can write the Publisher extension like this:

extension Publisher {
func retry(_ times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}

func retry(_ times: Int, unless condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
retry(times, if: { !condition($0) })
}
}

, which enables some funky stuff, like this:

extension Error {
var isRecoverable: Bool { ... }
var isUnrecoverable: Bool { ... }
}

// retry at most 3 times while receiving recoverable errors
// bail out the first time when encountering an error that is
// not recoverable
somePublisher
.retry(3, if: \.isRecoverable)

// retry at most 3 times, bail out the first time when
// an unrecoverable the error is encountered
somePublisher
.retry(3, unless: \.isUnrecoverable)

Or even funkier, ruby-style:

extension Int {
var times: Int { self }
}

somePublisher
.retry(3.times, unless: \.isUnrecoverable)

Is there a way to do Alamofire requests with retries

One of the bits of syntactic sugar you get with Swift is you can use this:

public func updateEvents(someNormalParam: Bool = true, someBlock: (Void->Void))

Like this:

updateEvents(someNormalParam: false) {...}

Note the block is outside the () of the updateEvents function, contrary to where you'd normally expect it. It works only if the block is the last thing in the declaration of the function.

That means if you happen to have a block such as your Alamofire request, you can effectively wrap it with your retry functionality. One slightly complicating issue is you want to call a block within the block. Not a big deal:

func retryWrapper(alamoBlock: (Void->Request)) {
alamoblock().responseJSON() {
//Your retry logic here
}
}

And you use it like so:

retryWrapper() {
Alamofire.request(method, targetUrl, parameters: parameters, encoding: encoding)
}

Meaning all you have to do is find your Alamofire calls and wrap them in { } and put retryWrapper() before. The retry logic itself is only there once.



Related Topics



Leave a reply



Submit