Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CoroutineExceptionHandlerと仲良くなる / Grasping the basics of CoroutineExceptionHandler

kokoakuma
January 24, 2024

CoroutineExceptionHandlerと仲良くなる / Grasping the basics of CoroutineExceptionHandler

2024/01/23に開催された ZOZO Tech Meetup - Android の発表資料です。

kokoakuma

January 24, 2024
Tweet

Other Decks in Programming

Transcript

  1. © ZOZO, Inc. 4 使ってみる 例えば、ViewModelで使うとこんな感じ class TargetViewModel: ViewModel() {

    val handler = CoroutineExceptionHandler { _, throwable -> // handle exception } private val scope: CoroutineScope get() = viewModelScope + handler fun method() { scope.launch { // throw exception } }
  2. © ZOZO, Inc. 7 アウトライン • CoroutineExceptionHandlerの概要 • Coroutinesの例外伝播 •

    CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerが呼ばれる流れ • CoroutineExceptionHandlerのテスト
  3. © ZOZO, Inc. 8 アウトライン • CoroutineExceptionHandlerの概要 • Coroutinesの例外伝播 •

    CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerが呼ばれる流れ • CoroutineExceptionHandlerのテスト
  4. © ZOZO, Inc. 10 CoroutineExceptionHandlerの概要 内容をまとめると • catchされなかった例外(Uncaught Exception)の処理を カスタマイズできる

    • ルートのCoroutineと一緒に使うことで、ルートのCoroutineと子 Coroutineのcatchブロックとして使用できる • Handlerが呼ばれる段階でCoroutineは終了している • ログやエラーメッセージの表示、アプリの再起動等に使用される
  5. © ZOZO, Inc. 11 CoroutineExceptionHandlerの概要 • catchされなかった例外(Uncaught Exception)の処理を カスタマイズできる •

    ルートのCoroutineと一緒に使うことで、ルートのCoroutineと子 Coroutineのcatchブロックとして使用できる • Handlerが呼ばれる段階でCoroutineは終了している • ログやエラーメッセージの表示、アプリの再起動等に使用される launchで作成したCoroutineの例外はどう処理されるんだっけ?🤔
  6. © ZOZO, Inc. 12 アウトライン • CoroutineExceptionHandlerの概要 • Coroutinesの例外伝播 •

    CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerが呼ばれる流れ • CoroutineExceptionHandlerのテスト
  7. © ZOZO, Inc. 14 Coroutinesの例外伝播 基本的にlaunchで作成した場合は、 • 例外が自動的に親に伝播していく • ルートのCoroutineでUncaught

    Exceptionとして扱われる 伝播していく感じの図とか fun method() { scope.launch { // ルートとなる親Coroutine ←ここまで伝播 launch { // 子Coroutine launch { throw Exception() } } } }
  8. © ZOZO, Inc. 16 アウトライン • CoroutineExceptionHandlerの概要 • Coroutinesの例外伝播 •

    CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerが呼ばれる流れ • CoroutineExceptionHandlerのテスト
  9. © ZOZO, Inc. 17 CoroutineExceptionHandlerの実体 CoroutineExceptionHandlerのInterfaceを読んでみる🔍 public interface CoroutineExceptionHandler :

    CoroutineContext.Element { // コメント部分省略 public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler> // コメント部分省略 public fun handleException(context: CoroutineContext, exception: Throwable) } https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/common/src /CoroutineExceptionHandler.kt
  10. © ZOZO, Inc. 18 CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerはCoroutineContext.Elementの一つ • CoroutineExceptionHandlerを示すkeyを持っている •

    handleExceptionというメソッドを持っている (ユーザーが渡す処理に該当するもの) CoroutineContext、Element、Keyはどんなものだっけ?🤔
  11. © ZOZO, Inc. 20 CoroutineExceptionHandlerの実体 CoroutineContext.keyはElementを識別するためのもの CoroutineContextがElementのIndexed Setになっていて、      keyを使用して取得できるようになっている /**

    * Persistent context for the coroutine. It is an indexed set of [Element] instances. * An indexed set is a mix between a set and a map. * Every element in this set has a unique [Key]. */ @SinceKotlin("1.3") public interface CoroutineContext { https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/js/src/Coro utineContext.kt
  12. © ZOZO, Inc. 21 CoroutineExceptionHandlerの実体 CoroutineContextの構造 val context = Job

    + Handler + Dispatchers.IO CombinedContext Job Element CombinedContext Handler Element Dispatchers.IO Element
  13. © ZOZO, Inc. 22 CoroutineExceptionHandlerの実体 CoroutineContextの実装 カスタム演算子のメソッドにおいて、                重複するものを削除した上で新しいElementを渡している public operator

    fun plus(context: CoroutineContext): CoroutineContext = if (context === EmptyCoroutineContext) this else // コメント省略 context.fold(this) { acc, element -> val removed = acc.minusKey(element.key) if (removed === EmptyCoroutineContext) element else { // 以下省略 } } https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/js/src/Coro utineContext.kt
  14. © ZOZO, Inc. 23 CoroutineExceptionHandlerの実体 Handlerを2回渡しても1つしか保持されない val handler1 = CoroutineExceptionHandler

    { _, throwable -> Log.d("1") } val handler2 = CoroutineExceptionHandler { _, throwable -> Log.d("2") } private val scope: CoroutineScope get() = viewModelScope + handler1 + handler2 fun method() { scope.launch { // throw exception } // log result -> 2 }
  15. © ZOZO, Inc. 25 アウトライン • CoroutineExceptionHandlerの概要 • Coroutinesの例外伝播 •

    CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerが呼ばれる流れ • CoroutineExceptionHandlerのテスト
  16. © ZOZO, Inc. 26 CoroutineExceptionHandlerが呼ばれる流れ CoroutineExceptionHandlerはどう呼ばれるのか🔍 class TargetViewModel: ViewModel() {

    val handler = CoroutineExceptionHandler { _, throwable -> Log.d("error") } private val scope: CoroutineScope get() = viewModelScope + handler fun method() { scope.launch { throw Exception() } } }
  17. © ZOZO, Inc. 29 CoroutineExceptionHandlerが呼ばれる流れ Completedという最終的なStateに遷移するために、JobSupportの finalizeFinishingStateというメソッドが呼ばれる private fun finalizeFinishingState(state:

    Finishing, proposedUpdate: Any?): Any? { // 途中省略 // Now handle the final exception if (finalException != null) { val handled = cancelParent(finalException) || handleJobException(finalException) if (handled) (finalState as CompletedExceptionally).makeHandled() } // 以下省略 https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/common/src /JobSupport.kt
  18. © ZOZO, Inc. 30 CoroutineExceptionHandlerが呼ばれる流れ 短絡評価になっていて、親Coroutineが存在しない場合や子Coroutine  自体が例外処理すべき場合にhandleJobExceptionが呼ばれる private fun finalizeFinishingState(state:

    Finishing, proposedUpdate: Any?): Any? { // 途中省略 // Now handle the final exception if (finalException != null) { val handled = cancelParent(finalException) || handleJobException(finalException) if (handled) (finalState as CompletedExceptionally).makeHandled() } // 以下省略 https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/common/src /JobSupport.kt
  19. © ZOZO, Inc. 31 CoroutineExceptionHandlerが呼ばれる流れ StandaloneCoroutineの実装内でhandleCoroutineExceptionが呼ばれる private open class StandaloneCoroutine(

    parentContext: CoroutineContext, active: Boolean ) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) { override fun handleJobException(exception: Throwable): Boolean { handleCoroutineException(context, exception) return true https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/common/src /Builders.common.kt
  20. © ZOZO, Inc. 32 CoroutineExceptionHandlerが呼ばれる流れ CoroutineExceptionHandlerが呼ばれる🎉 @InternalCoroutinesApi public fun handleCoroutineException(context:

    CoroutineContext, exception: Throwable) { // Invoke an exception handler from the context if present try { context[CoroutineExceptionHandler]?.let { it.handleException(context, exception) return // 以下省略 https://cs.android.com/android/platform/superproject/main/+/main:external/kotlinx.coroutines/kotlinx-coroutines-core/common/src /CoroutineExceptionHandler.kt
  21. © ZOZO, Inc. 34 アウトライン • CoroutineExceptionHandlerの概要 • Coroutinesの例外伝播 •

    CoroutineExceptionHandlerの実体 • CoroutineExceptionHandlerが呼ばれる流れ • CoroutineExceptionHandlerのテスト
  22. © ZOZO, Inc. 35 CoroutineExceptionHandlerのテスト Migration Guideの通りにテストを書くとこうなる val handler =

    CoroutineExceptionHandler { _, throwable -> // handle exception } @Test fun testFoo() = runTest { launch(handler) { // ... } // check something } https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md
  23. © ZOZO, Inc. 38 CoroutineExceptionHandlerのテスト 子から親に例外が伝播しているため、例外をcatchできていない😢 runTest()に直接渡す?🤔 @Test fun testFoo()

    = runTest { // 内部でlaunchを呼び、親Coroutineを生成している launch(handler) { // 子Coroutine throw Exception() } } }
  24. © ZOZO, Inc. 40 CoroutineExceptionHandlerのテスト NonCancellableやSupervisorJobを設定して、例外を伝播させない val handler = CoroutineExceptionHandler

    { _, throwable -> // handle exception } @Test fun testFoo() = runTest { launch(NonCancellable + handler) { // ... } // check something } https://github.com/Kotlin/kotlinx.coroutines/issues/3374
  25. © ZOZO, Inc. 42 まとめ • Uncaught Exceptionの処理をカスタマイズできるもの • 実体はCoroutineContext.Elementの一つ

    • Coroutine終了処理→Coroutineの例外ハンドリング →CoroutineExceptionHandlerという流れで呼ばれる • テスト時には例外を伝播させないための工夫が必要