본문 바로가기
프로그래밍 놀이터/Kotlin

[coroutine] Exception handling

by 돼지왕왕돼지 2019. 3. 4.

[coroutine] Exception handling


https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/exception-handling.md#exception-handling


actor, android uncaughtexceptionprehandler, Async, cancellation and exceptions, cancellationexception, children terminate, context element, coroutine exception, coroutine exception handling, coroutineexceptionhandler, exception aggregation, Exception Handling, Exception Propagation, exception.suppressed, global coroutine exception handler, globalscope, globalscope default exceptionhandler, jdk version, launch, main coroutine, parent propagation, produce, serviceloader, uncaughtexceptionhandler, unhandled exception


Exception propagation

-

Coroutine builder 는 2가지 flavor 가 있다.

exception 을 자동으로 전파하는 launch 와 actor류, 그리고 user 에게 노출되는(돼왕: api call 을 통해 결과를 얻어갈 수 있는) async 와 produce 류.

자동 전파하는 케이스는 Java 의 uncaughtExceptionHandler 와 비슷하게 unhandled exception 이다. (돼왕: 유저에게 전파되지 않는다.)

반면에 user 에게 노출되는 경우는 user 에게 exception 을 handle 하도록 한다. (돼왕: try-catch 할 수 있다.)

대표적 api 는 await 와 receive 가 있다.



-

runBlocking{
    val job = GlobalScope.launch{
        println(“Throwing exception from launch”)
        throw IndexOutOfBoundsException()
    }
    job.join()
    println(“Joined failed job”)

    val deferred = GlobalScope.async{
        println(“Throwing exception from async”)
        throw ArithmeticException()
    }

    try{
        deferred.await()
        println(“Unreached”)
    } catch(e:ArithmeticException){
        println(“Caught ArithmeticException”)
    }
}

결과는..

Throwing exception from launch

Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException // 돼왕 : GlobalScope 의 기본 exceptionHandler 가 print

Joined failed job

Throwing exception from async // 돼왕 : await 가 불리기 전에 이미 throw ArithmeticException 코드는 탄 상태이다. await 해야 실제 바깥으로 throw 된다.

Caught ArithmeticException


돼왕 : 사실 위의 예제는 defaultUncaughtExceptionHandler 가 장착되어 있을 때의 결과이며, 그것이 없을 때 crash 가 발생한다.





CoroutineExceptionHandler

-

GlobalScope 가 모든 exception 을 print 하는 것을 원치 않는다면, CoroutineExceptionHandler 라는 context element 를 사용하면 된다.

이 녀석은 Thread.uncaughtExceptionHandler 와 비슷하다.



-

JVM 에서 모든 coroutine 에 대한 global exception handler 를 재정의 할 수 있다.

이는 ServiceLoader 에 CoroutineExceptionHandler 를 등록하면 할 수 있다.

Global exception handler 는 Thread.defaultUncaughtExceptionHandler 와 비슷하다.

Android 에서는 uncaughtExceptionPreHandler 가 global coroutine exception handler 로 등록되어 있고, 이 녀석은 더 선행하여 불리는 exceptionHandler 가 없으면 불리게 된다.

CoroutineExceptionHandler 는 user 가 handle 하지 않는 exception 들에 대해서만 작동한다.

그래서 아래와 같이 async builder 를 사용하면 영향력이 없다.


runBlocking{
    val handler = CoroutineExceptionHandler { _, exception ->
        println(“Caught $exception”)
    }

    val job = GlobalScope.launch(handler){
        throw AssertionError()
    }

    val deferred = GlobalScope.async(handler){
        throw ArithmeticException()
    }
    joinAll(job, deferred)
}

결과는..

Caught java.lang.AssertionError // 돼왕 : async 는 join 으로 로직을 다 수행하긴 하지만, await 로 값을 받아오기 전까지 exception 을 전달하지 않는다.





Cancellation and exceptions

-

Coroutine 은 내부적으로 취소시 CancellationException 을 사용한다.

이 exception 들은 모든 handler 에 의해 무시된다.(돼왕 : crash 를 내지 않는다.) 그래서 이들은 debug info 로만 사용되며, catch 로 잡을 수도 있긴 하다. 

(돼왕 : main runBlocking block 에서 실행 될 경우 다른 관점으로 봐야 한다. )

coroutine 은 Job.cancel 를 통해 취소하면, parent 를 cancel 시키지 않고 job 에 종속된 그 녀석만 종료한다.    



-

coroutine 이 CancellationException 이외의 exception 을 마딱뜨리면, parent 를 exception 과 함께 종료시킨다.

이 동작은 override 되어 변경될 수 없고, CoroutineExceptionHandler 구현에 의존하지 않는 안정적인 구조화된 concurrency hierarchy 를 제공한다.

exception 은 모든 children 이 terminate 된 후에 parent 에 의해 처리된다.



-

runBlocking{
    val handler = CoroutineExceptionHandler{ _, exception ->
        println(“Caught $exception”)
    }

    val job = GlobalScope.launch(handler){
        launch{
            try{
                delay(Long.MAX_VALUE)
            } finally{
                withContext(NonCancellable){
                    println(“Children are cancelled, but exception is no handled until all children terminated”)
                    delay(100L)
                    println(“The first child finished its non cancellable block”)
                }
            }
        }

        launch{
            delay(10L)
            println(“Second child throws an exception”)
            throw ArithmeticException()
        }
    }
    job.join()
}

결과는..

Second child throws an exception

Children are cancelled, but exception is not handled until all children terminate

The first child finished its non cancellable block

Caught ArithmeticException

위 예제에서 CoroutineExceptionHandler 는 GlobalScope 안에서 생성된 녀석에 항상 install 되어 있다.

그래서 main runBlocking scope 에서 launch 되는 coroutine 에 exception handler 를 설치하는 것은 큰 의미가 없다.

왜냐면 main coroutine 은 installed handler 에 상관없이 child 가 exception 으로 종료될 때 항상 cancel 되기 때문이다.








Exceptions aggregation

-

여러개의 children coroutine 에서 exception 을 던지면 어떻게 될까?

일반적인 규칙은 "처음 던져진 exception 이 이긴다” 이다.

그러나 이 경우 exception 이 유실될 수 있다.



-

runBlocking{
    val handler = CoroutineExceptionHandler { _, exception ->
        println(“Caught $exception with suppressed ${exception.suppressed.contentToString()}”)
    }

    val job = GloablScope.launch(handler){
        launch{
            try{
                delay(Long.MAX_VALUE)
            } finally{
                throw ArithmeticException()
            }
        }

        launch{
            delay(100L)
            throw IOException()
        }

        delay(Long.MAX_VALUE)
    }
    job.join()
}

결과는..

Caught java.io.IOException with suppressed [java.lang.ArithmeticException]

suppressed 는 JDK7 이상에서 작동한다.



-

Cancellation exception 은 기본적으로 투명하고 wrap 되지 않는다.

runBlocking{
    val handler = CoroutineExceptionHandler { _, exception ->
        println(“Caught original $exception”)
    }

    val job = GlobalScope.launch(handler){
        val inner = launch{
                        launch{
                            launch{
                                throw IOException()
                            }
                        }
                    }

        try{
            inner.join()
        } catch(e:CancellationException){
            println(“Rethrowing CancellationException with original cause”)
            throw e
        }
    }
    job.join()
}

결과는..

Rethrowing CancellationException with original cause

Caught original java.io.IOException





Supervision


-

취소는 전체 coroutine hierarchy 에 propagation 되며 양방향의 관계를 가지고 있다.

그러나 한쪽방향의 취소가 필요하다면?


이런 요구사항의 좋은 예는 scope 내에서 job 을 가진 UI component 이다.

만약 UI 의 child task 가 실패한다면, 모든 UI component 를 취소시킬 필요는 없다.

그러나 해당 UI component 가 destroy 된다면, 모든 child job 들의 결과는 필요 없으므로 다 취소시키는 것이 맞다.


다른 예제는 서버 process 로 여러 child job 을 생성한 경우이며, 그들의 작업을 관리할 필요가 있을 때이다.

그들이 실패했다면, 실패한 모든 child job 을 재시도시킬 수 있다.



** Supervision job


-

SupervisorJob 이 이런 목적으로 사용될 수 있다.

이 녀석은 일반 Job 과 비슷하지만, 취소가 오직 아래쪽 방향으로만 전파된다.

fun main() = runBlocking{
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)){
        // supervisor 가 없었다면 이 exceptionHandler 에 걸리지도 않는다.
        // supervisor 가 있기 떄문에 여기서 exceptionHandler 로 자체처리 해야 crash 가 나지 않는다.
        val firstChild = launch(CoroutineExceptionHandler { _, _ -> }){ 
            println("First child is failing")
            throw AssertionError("First child is cancelled")
        }

        val secondChild = launch {
            firstChild.join()
            println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
            try{
                delay(Long.MAX_VALUE)
            } finally{
                println("Second child is cancelled because supervisor is cancelled")
            }
        }

        firstChild.join()
        println("Cancelling supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

// First child is failing
// First child is cancelled: true, but second one is till active
// Cancelling supervisor
// Second child is cancelled because supervisor is cancelled


* Supervision scope


-

scoped 병렬을 위해 supervisorScope 가 coroutineScope 대신 사용될 수 있다.

이 녀석은 한쪽 방향으로만 취소를 전달한다. 스스로가 취소되었을 때 child 들에게 취소를 전달한다.

그리고 이 녀석은 coroutineScope 가 그랬듯이 child 의 완료를 기다린다.

fun main() = runBlocking{
    try{
        supervisorScope{
            val child = launch{
                try{
                    println("Child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally{
                    println("Child is cancelled")
                }
            }
            yield()
            println("Throwing exception from scope")
            throw AssertionError()
        } catch(e:AssertionError){
            println("Caught assertion error")
        }
    }
}

// Child is sleeping
// Throwing exception from scope
// Child is cancelled
// Caught assertion error


** Exceptions in supervised coroutines


-

supervisor job 과 일반 job 의 다른 특별한 특징은 exception 처리에 있다.

모든 child 는 스스로 exception 을 처리해야 한다.

이 차이는 child 의 실패가 parent 로 전파되지 않기 때문이다.

fun main() = runBlocking{
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }
    supervisorScope{
        val child = launch(handler){
            println("Child throws an exception")
            throw AssertionError()
        }
        println("Scope is completing")
    }
    println("Scope is completed")
}

// Scope is completing
// Child throws an exception
// Caught java.lang.AssertionError
// Scope is completed





댓글0