Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best practice to deal with RxSwift retry and error handling

I read some post says that the best practice to deal with RxSwift is to only pass fatal error to the onError and pass Result to the onNext.

It makes sense to me until I realise that I can't deal with retry anymore since it only happen on onError.

How do I deal with this issue?

Another question is, how do I handle global and local retry mixes together?

A example would be, the iOS receipt validation flow.

1, try to fetch receipt locally

2, if failed, ask Apple server for the latest receipt.

3, send the receipt to our backend to validate.

4, if success, then whole flow complete

5, if failed, check the error code if it's retryable, then go back to 1.

and in the new 1, it will force to ask for new receipt from apple server. then when it reaches 5 again, the whole flow will stop since this is the second attempt already. meaning only retry once.

So in this example, if using state machine and without using rx, I will end up using state machine and shares some global state like isSecondAttempt: Bool, shouldForceFetchReceipt: Bool, etc.

How do I design this flow in rx? with these global shared state designed in the flow.

like image 771
Tony Lin Avatar asked Feb 19 '19 09:02

Tony Lin


1 Answers

I read some post says that the best practice to deal with RxSwift is to only pass fatal error to the onError and pass Result to the onNext.

I don't agree with that sentiment. It is basically saying that you should only use onError if the programmer made a mistake. You should use errors for un-happy paths or to abort a procedure. They are just like throwing except in an async way.

Here's your algorithm as an Rx chain.

enum ReceiptError: Error {
    case noReceipt
    case tooManyAttempts
}

struct Response { 
    // the server response info
}

func getReceiptResonse() -> Observable<Response> {
    return fetchReceiptLocally()
        .catchError { _ in askAppleForReceipt() }
        .flatMapLatest { data in
            sendReceiptToServer(data)
        }
        .retryWhen { error in
            error
                .scan(0) { attempts, error in
                    let max = 1
                    guard attempts < max else { throw ReceiptError.tooManyAttempts }
                    guard isRetryable(error) else { throw error }
                    return attempts + 1
                }
        }
}

Here are the support functions that the above uses:

func fetchReceiptLocally() -> Observable<Data> {
    // return the local receipt data or call `onError`
}

func sendReceiptToServer(_ data: Data) -> Observable<Response> {
    // send the receipt data or `onError` if the server failed to receive or process it correctly.
}

func isRetryable(_ error: Error) -> Bool {
    // is this error the kind that can be retried?
}

func askAppleForReceipt() -> Observable<Data> {
    return Observable.just(Bundle.main.appStoreReceiptURL)
        .map { (url) -> URL in
            guard let url = url else { throw ReceiptError.noReceipt }
            return url
        }
        .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
        .map { try Data(contentsOf: $0) }
}
like image 136
Daniel T. Avatar answered Nov 20 '22 03:11

Daniel T.