Search code examples
iosswiftrx-swiftreactivex

RxSwift RetryWhen causes Reentrancy Anomaly


I have been trying to test on retryWhen operator on RxSwift and I have encountered the Reentrancy Anomaly issue, here's the code:

Observable<Int>.create { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onNext(4)
    observer.onError(RequestError.dataError)
    return Disposables.create()
    }
    .retryWhen { error in
        return error.enumerated().flatMap { (index, error) -> Observable<Int> in
        let maxRetry = 1
        print("index: \(index)")
        return index < maxRetry ? Observable.timer(1, scheduler: MainScheduler.instance) : Observable.error(RequestError.tooMany)
        }
    }
    .subscribe(onNext: { value in
        print("This: \(value)")
    }, onError: { error in
        print("ERRRRRRR: \(error)")
    })
    .disposed(by: disposeBag)

With the code above it gives:

This: 1
This: 2
This: 3
This: 4
index: 0
This: 1
This: 2
This: 3
This: 4
index: 1
⚠️ Reentrancy anomaly was detected.
  > Debugging: To debug this issue you can set a breakpoint in /Users/tony.lin/Documents/Snippet/MaterialiseTest/Pods/RxSwift/RxSwift/Rx.swift:97 and observe the call stack.
  > Problem: This behavior is breaking the observable sequence grammar. `next (error | completed)?`
    This behavior breaks the grammar because there is overlapping between sequence events.
    Observable sequence is trying to send an event before sending of previous event has finished.
  > Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,
    or that the system is not behaving in the expected way.
  > Remedy: If this is the expected behavior this message can be suppressed by adding `.observeOn(MainScheduler.asyncInstance)`
    or by enqueing sequence events in some other way.

⚠️ Reentrancy anomaly was detected.
  > Debugging: To debug this issue you can set a breakpoint in /Users/tony.lin/Documents/Snippet/MaterialiseTest/Pods/RxSwift/RxSwift/Rx.swift:97 and observe the call stack.
  > Problem: This behavior is breaking the observable sequence grammar. `next (error | completed)?`
    This behavior breaks the grammar because there is overlapping between sequence events.
    Observable sequence is trying to send an event before sending of previous event has finished.
  > Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,
    or that the system is not behaving in the expected way.
  > Remedy: If this is the expected behavior this message can be suppressed by adding `.observeOn(MainScheduler.asyncInstance)`
    or by enqueing sequence events in some other way.

ERRRRRRR: tooMany

Just wondering if anyone knows the cause of this issue?


Solution

  • As the console comment explains, this warning can be suppressed using .observeOn(MainScheduler.asyncInstance) as in:

    Observable<Int>.from([1, 2, 3, 4]).concat(Observable.error(RequestError.dataError))
        .observeOn(MainScheduler.asyncInstance) // this is the magic that makes it work.
        .retryWhen { error in
            return error.enumerated().flatMap { (index, error) -> Observable<Int> in
                let maxRetry = 1
                print("Index:", index)
                guard index < maxRetry else { throw RequestError.tooMany }
                return Observable.timer(1, scheduler: MainScheduler.instance)
            }
        }
        .subscribe(onNext: { value in
            print("This: \(value)")
        }, onError: { error in
            print("ERRRRRRR: \(error)")
        })
    

    I took the liberty of making a few minor adjustments to your example code to show an alternate way to write what you have.

    Additional Information

    You asked to explain (a) why adding the ObserveOn works and (b) why it is needed.

    What .observeOn(MainScheduler.asyncInstance) does is route the request to an alternate thread where the event can finish and then emit the event again on the main thread. In other words, it's like doing this:

    .observeOn(backgroundScheduler).observeOn(MainScheduler.instance)
    

    Where backgroundScheduler is defined like:

    let backgroundScheduler = SerialDispatchQueueScheduler(qos: .default)
    

    At least that's my understanding.

    As for why it's needed, I can't say. You might have found a bug in the library because using a 1 second delay works fine without the observeOn.