Hanbit the Developer

Kotlin Documentation | Coroutine exceptions handlings 본문

Mobile/Kotlin

Kotlin Documentation | Coroutine exceptions handlings

hanbikan 2023. 5. 26. 14:39

Kotlin Documentation 시리즈에 대해

Category: Official libraries - Coroutines

문서 링크: https://kotlinlang.org/docs/exception-handling.html


Exception propagation

코루틴 빌더는 두 유형으로 나뉜다: 예외를 자동으로 전파하는 방식(launch, actor), 예외를 유저에게 노출(async, produce)하는 방식.(https://chat.openai.com/share/295078a5-2ad9-428d-a3ff-44ac4deafe02) 코루틴 빌더가 루트 코루틴을 생성하는 데 쓰였다면, 전자에 해당하는 빌더는 예외를 uncaught exception으로 취급한다.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val job = GlobalScope.launch { // root coroutine with launch
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // root coroutine with async
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IndexOutOfBoundsException
	at FileKt$main$1$job$1.invokeSuspend(File.kt:7)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Cancelling}@5ea80522, Dispatchers.Default]
Joined failed job
Throwing exception from async
Caught ArithmeticException

CoroutineExceptionHandler

uncaught exception을 출력하는 기본 동작을 커스터마이즈할 수 있다. 루트 코루틴에서의 CoroutineExceptionHandler 컨텍스트 요소가 루트 코루틴과 모든 자식 코루틴에 대해 일반적인 catch block으로 쓰일 수 있다. 핸들러가 호출된 시점에서 이미 예외와 함께 코루틴이 종료되었기 때문에 CoroutineExceptionHandler에서 예외를 복구할 수는 없다. 일반적으로 예외를 로깅하거나 에러 메시지를 보여주거나 어플리케이션을 종료 또는 재시작하는 데에 쓰인다.

CoroutineExceptionHandler는 uncaught exception에 대해서만 수행된다. 특히 (다른 Job 컨텍스트에서 생성된) 모든 자식 코루틴은 자신의 예외를 부모 코루틴으로 위임하며, 이러한 과정이 루트 코루틴까지 반복된다. 따라서 자식 코루틴에 지정된 CoroutineExceptionHandler는 사용되지 않는다. 추가로 async 빌더는 모든 예외를 잡고 그것들을 Deferred 객체로 표현하기 때문에 CoroutineExceptionHandler가 효과가 없다.

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
    throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
CoroutineExceptionHandler got java.lang.AssertionError

Cancellation and exceptions

코루틴은 내부적으로 취소를 위해 CancellationException를 사용하고 있으며 이 예외는 모든 핸들러들에 의해 무시된다. 따라서 catch block으로 얻을 수 있는 추가적인 디버깅 정보를 위한 코드로서 쓰여야만 한다. Job.cancel로 코루틴이 취소되었을 때 부모 코루틴이 취소되지는 않는다.

val job = launch {
    val child = launch {
        try {
            delay(Long.MAX_VALUE)
        } finally {
            println("Child is cancelled")
        }
    }
    yield()
    println("Cancelling child")
    child.cancel()
    child.join()
    yield()
    println("Parent is not cancelled")
}
job.join()
Cancelling child
Child is cancelled
Parent is not cancelled

코루틴이 CancellationException이 아닌 다른 예외를 만나면 해당 예외와 함께 부모 코루틴도 취소한다. 이는 오버라이드 될 수 없으며 이를 통해 structured concurrency의 안정화된 계층 구조를 제공한다.

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) {
    launch { // the first child
        try {
            delay(Long.MAX_VALUE)
        } finally {
            withContext(NonCancellable) {
                println("Children are cancelled, but exception is not handled until all children terminate")
                delay(100)
                println("The first child finished its non cancellable block")
            }
        }
    }
    launch { // the second child
        delay(10)
        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
CoroutineExceptionHandler got java.lang.ArithmeticException

Exceptions aggregation

여러 자식 코루틴이 실패했을 때의 원칙은 “첫번째 예외가 승리한다.”이다. 첫번째 예외 이후 모든 예외들은 suppressed로서 첫번째 예외에 붙는다.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

Supervision

코루틴 취소는 양방향(자식 코루틴이 부모 코루틴을 취소시킬 수도 있고 그 반대도 가능하다.)으로 이루어졌다. 이번에는 단방향 취소가 필요한 경우를 다룬다.

스코프 내에서 작동하는 작업이 있는 UI 컴포넌트가 좋은 예시이다. UI의 태스크 중 하나가 실패하더라도 UI 컴포넌트 전체를 취소할 필요가 없다.

또 다른 예시는, 여러 자식 작업들을 생성하고 이 작업들의 실행을 감독할 필요가 있는 서버 프로세스이다.

Supervision job

SupervisorJob이 이런 목적을 위해 사용된다. Job과 유사하지만 취소가 downwards로 전파된다는 특징이 있다.

val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
    // launch the first child -- its exception is ignored for this example (don't do this in practice!)
    val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
        println("The first child is failing")
        throw AssertionError("The first child is cancelled")
    }
    // launch the second child
    val secondChild = launch {
        firstChild.join()
        // Cancellation of the first child is not propagated to the second child
        println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
        try {
            delay(Long.MAX_VALUE)
        } finally {
            // But cancellation of the supervisor is propagated
            println("The second child is cancelled because the supervisor was cancelled")
        }
    }
    // wait until the first child fails & completes
    firstChild.join()
    println("Cancelling the supervisor")
    supervisor.cancel()
    secondChild.join()
}
The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because the supervisor was cancelled

Supervision scope

coroutineScope 대신 supervisorScope를 사용할 수도 있다. 이는 취소를 단방향으로 전파하고, 자기 자신이 실패할 때만 모든 자식을 취소한다. 또한 coroutineScope가 하는 것처럼 모든 자식이 완료될 때까지 기다린다.

try {
    supervisorScope {
        val child = launch {
            try {
                println("The child is sleeping")
                delay(Long.MAX_VALUE)
            } finally {
                println("The child is cancelled")
            }
        }
        // Give our child a chance to execute and print using yield 
        yield()
        println("Throwing an exception from the scope")
        throw AssertionError()
    }
} catch(e: AssertionError) {
    println("Caught an assertion error")
}
The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error

Exceptions in supervised coroutines

또 다른 일반적인 작업과 supervisor 작업의 차이점은 예외 처리이다. 모든 자식은 예외 처리 매커니즘을 통해 스스로 예외를 처리해야 한다. 이 차이점은 자식 코루틴이 실패해도 부모 코루틴에 전달되지 않기 때문에 생긴다. 이는 supervisorScope 내에서 실행된 코루틴은 자신의 스코프에 세팅된 CoroutineExceptionHandler를 사용한다는 의미이다.(기존에는 최상위 CoroutineScope에 넣은 CoroutineExceptionHandler만 유효했다.)

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
supervisorScope {
    val child = launch(handler) {
        println("The child throws an exception")
        throw AssertionError()
    }
    println("The scope is completing")
}
println("The scope is completed")
The scope is completing
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completed