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("eerrrr")
}
.doOnError {
test.onNext("fail")
}
.subscribe({
test.onNext("success")
}, {
test.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를 잘 적용하였다면 문제없을 것이다.
그리고 보통은 잘 안 나타나는데 필자의 경우 위에서 나열하였던 경우에서 발생함을 확인할 수 있었다.
나중을 위해서 한 번 정리해두었다.