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
Firebase Datadescription Returns Empty Array
How to Make a Uiview Grow in Uiview.Animate() with an Accurate Corner Radius
Fetch an Email Body in Mailcore2 Osx with Swift
How to Generate a Random Unicode Character in Swift
Different Path Url for Filemanager Everytime I Open the App
Url Constructor Doesn't Work with Some Characters
Unrecognized Selector Sent to Instance When No Related Entities Found in Core Data
Swift - Pass JSON Data to Other Views
Why Won't My Collection View Cells Display in the iPhone Simulator
How to Use Core Data Value from Picker? #Swiftui #Coredata
Var Declaration with Type VS Without
Check If the Username Exist in Firebase
How to Know When to Call a Closure with () or Without Parentheses in Handler:
Check If an Int Value Is Greater or Equal to Another Int? Value
How to Ensure Make Sure I'm Not Accessing Data Until It's Loaded In