RxJava 2.x 사용 시 발생하는 UndeliverableException 해결 방법은?



개인 광고 영역

RxJava 2.x 사용 시 발생 가능한 버그를 소개하고, 해결 방법을 소개한다.

RxJava 2.x 버전으로 올리고 나서 UndeliverableException이 발생하는 경우가 생겼다.

이해를 돕기 위해 오류 코드를 그대로 추가하고, 이 오류가 왜 발생하는지와 어떻게 해결할지를 정리한다.

io.reactivex.exceptions.UndeliverableException: The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with. Further reading: https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling | java.lang.UnknownError: UnknownError
	at io.reactivex.plugins.RxJavaPlugins.onError(RxJavaPlugins.java:367)
	at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:69)
	at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnknownError: Error message
	at tech.thdev.coroutinesuiextensions.RxJavaTest$test$1$2.apply(RxJavaTest.kt:33)
	at tech.thdev.coroutinesuiextensions.RxJavaTest$test$1$2.apply(RxJavaTest.kt:10)
	at io.reactivex.internal.operators.single.SingleMap$MapSingleObserver.onSuccess(SingleMap.java:57)
	at io.reactivex.internal.operators.single.SingleObserveOn$ObserveOnSingleObserver.run(SingleObserveOn.java:81)
	at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578)
	at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
	... 7 more

다행히 위 오류는 발생함과 동시에 로그상에 해결 방법을 잘 제시해주고 있다.


라이브러리

이 코드를 확인하기 위한 라이브러리 버전을 명시한다.


이 오류가 발생하는 가능성은?

위 오류를 해석해보면 진행 중 발생한 오류를 보내야 할 대상이 사라졌다는 것이다.

consumer가 canceled/disposed 되어 보낼 곳이 없다는 것이다.

실제 런타임에서 발생한 사례는 아래와 같이 추측할 수 있다.

  • 상황 1 Timeout이 발생할 정도로 서버의 응답이 늦었다.
  • 상황 2 데이터의 전달 오류로 UnknownError가 발생하였다.

위와 같은 상황 1에서 사용자는 아래와 같이 행동할 수 있다.

  • 사용자는 대기가 길어 이미 back 키를 마구 눌러 화면을 떠나버렸다.
  • 라이프 사이클 상 onDestroy 동작하였고, RxJava의 disposable을 동작하는 코드가 동작하였다.

위와 같은 상황을 실제 검증할 수 있었지만, 오류 로그 상 구문에서 이를 명확하게 이해할 수 있었다.

The exception could not be delivered to the consumer because it has already canceled/disposed


코드로 다시 재현해보기

위와 같은 코드를 재현하기란 쉽지 않았다. 그래서 발생한 UndeliverableException을 그냥 내보내는 코드로 이를 대신하려고 한다.

RetrofitFactory.githubApi.contributors("taehwandev", "CoroutinesUIExtensions")
        .observeOn(Schedulers.io())
        .map {
            throw UnknownError("UnknownError")
            it
        }
        .onErrorReturn { // throw UnknownError 동작 후에 여기에서 잡히지 않는다
            println("onErrorReturn")
            mutableListOf()
        }
        .onErrorResumeNext { // throw UnknownError 동작 후에 여기에서 잡히지 않는다
            println("onErrorResumeNext")
            Single.just(mutableListOf())
        }
        .doOnError { // throw UnknownError 동작 후에 여기에서 잡히지 않는다
            println("doOnError")
        }
        .subscribe({
            subject.onNext("success")
        }, { // throw UnknownError 동작 후에 여기에서 잡히지 않는다
            subject.onNext("fail")
        })

map에서 UnknownError를 발생시키면 doOnError/onErrorReturn/onErrorResumeNext로 이동할 것 같았지만 실제론 UndeliverableException 발생 후 앱이 종료되어버렸다.


해결 방법은

해결 방법은 오류 로그 중 Further reading: https://github.com/ReactiveX/RxJava/wiki/What’s-different-in-2.0#error-handling java.lang.UnknownError: UnknownError을 통해 쉬게 확인이 가능하다.

error-handling에서 확인한 Java 코드를 필자가 Kotlin 코드로 변환하여 추가하였다.

RxJava 2.2.7에서 일부 코드가 변경되어 그에 맞게 수정하였다.

RxJavaPlugins.setErrorHandler { e ->
    var error = e
    if (error is UndeliverableException) {
        error = e.cause
    }
    if (error is IOException || error is SocketException) {
        // fine, irrelevant network problem or API that throws on cancellation
        return@setErrorHandler
    }
    if (error is InterruptedException) {
        // fine, some blocking code was interrupted by a dispose call
        return@setErrorHandler
    }
    if (error is NullPointerException || error is IllegalArgumentException) {
        // that's likely a bug in the application
        Thread.currentThread().uncaughtExceptionHandler
                .uncaughtException(Thread.currentThread(), error)
        return@setErrorHandler
    }
    if (error is IllegalStateException) {
        // that's a bug in RxJava or in a custom operator
        Thread.currentThread().uncaughtExceptionHandler
                .uncaughtException(Thread.currentThread(), error)
        return@setErrorHandler
    }
    Log.w("Undeliverable exception received, not sure what to do", error)
}

이 코드는 전역에서 한 번만 설정해주면 되는 부분으로 Application 상속 구조에 적용하면 잘 동작하게 될 것이다.


결과

RxJavaPlugins.setErrorHandler를 통해 간단하게 제어가 가능하게 되었고, RxJava 사용 중 발생한 오류는 아래와 같이 Log가 남고 넘어가게 되었다.

Undeliverable exception received, not sure what to do java.lang.UnknownError: UnknownError

만약 이에 따른 예외 처리가 필요하다면 setErrorHandler를 통해 이를 서버로 로그를 전송할 수 있을 것이다.


마무리

위 코드는 RxJava에서 RxJava2로 마이그레이션 하는 문서에 잘 설명하고 있다.

잘 보고 넘어가면 문제없고, RxJavaPlugins의 setErrorHandler를 잘 적용하였다면 문제없을 것이다.

그리고 보통은 잘 안 나타나는데 필자의 경우 위에서 나열하였던 경우에서 발생함을 확인할 수 있었다.

나중을 위해서 한 번 정리해두었다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments