Slide 1

Slide 1 text

CoroutineExceptionHandlerと 仲良くなる 株式会社ZOZO
 ZOZOTOWN開発本部 ZOZOTOWN開発1部 Android1ブロック
 愛川 功樹 Copyright © ZOZO, Inc. 1

Slide 2

Slide 2 text

© ZOZO, Inc. 株式会社ZOZO ZOZOTOWN開発本部 ZOZOTOWN開発1部 Android1ブロック 愛川 功樹 2023年10月に入社 温泉旅行が好き 2

Slide 3

Slide 3 text

© ZOZO, Inc. 3 CoroutineExceptionHandlerを使っていますか? 自分は今まで使ったことがなく、 例外を良い感じに処理できるものという理解🤔

Slide 4

Slide 4 text

© 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 } }

Slide 5

Slide 5 text

© ZOZO, Inc. 5 使ってみる 使い方はシンプルで分かりやすいものの、もう少し仲良くなりたい

Slide 6

Slide 6 text

© ZOZO, Inc. 6 今回のゴール CoroutineExceptionHandlerをざっくりと理解して、仲良くなる

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

© ZOZO, Inc. 9 CoroutineExceptionHandlerの概要 Kotlinのドキュメントを読む🔍 https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

© ZOZO, Inc. 13 Coroutinesの例外伝播 Kotlinのドキュメントを読む🔍 https://kotlinlang.org/docs/exception-handling.html#exception-propagation

Slide 14

Slide 14 text

© ZOZO, Inc. 14 Coroutinesの例外伝播 基本的にlaunchで作成した場合は、 ● 例外が自動的に親に伝播していく ● ルートのCoroutineでUncaught Exceptionとして扱われる 伝播していく感じの図とか fun method() { scope.launch { // ルートとなる親Coroutine ←ここまで伝播 launch { // 子Coroutine launch { throw Exception() } } } }

Slide 15

Slide 15 text

© ZOZO, Inc. 15 Coroutinesの例外伝播 CoroutineExceptionHandlerの概要と例外伝播の概要は分かった ただ、CoroutineExceptionHandlerの実体は何なんだろう?🤔

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

© ZOZO, Inc. 17 CoroutineExceptionHandlerの実体 CoroutineExceptionHandlerのInterfaceを読んでみる🔍 public interface CoroutineExceptionHandler : CoroutineContext.Element { // コメント部分省略 public companion object Key : CoroutineContext.Key // コメント部分省略 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

Slide 18

Slide 18 text

© ZOZO, Inc. 18 CoroutineExceptionHandlerの実体 ● CoroutineExceptionHandlerはCoroutineContext.Elementの一つ ● CoroutineExceptionHandlerを示すkeyを持っている ● handleExceptionというメソッドを持っている (ユーザーが渡す処理に該当するもの) CoroutineContext、Element、Keyはどんなものだっけ?🤔

Slide 19

Slide 19 text

© ZOZO, Inc. 19 CoroutineExceptionHandlerの実体 CoroutineContextはCoroutineが実行される際の定義となるもので、 CoroutineContext.Element(要素)が合わさったもの 主なElement: JobやDispatcher https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

Slide 20

Slide 20 text

© 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

Slide 21

Slide 21 text

© ZOZO, Inc. 21 CoroutineExceptionHandlerの実体 CoroutineContextの構造 val context = Job + Handler + Dispatchers.IO CombinedContext Job Element CombinedContext Handler Element Dispatchers.IO Element

Slide 22

Slide 22 text

© 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

Slide 23

Slide 23 text

© 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 }

Slide 24

Slide 24 text

© ZOZO, Inc. 24 CoroutineExceptionHandlerの実体 CoroutineContextはCoroutineContext.Elementの集合であり、 CoroutineExceptionHandlerはElementの一つであると分かった🎉 では、どう呼ばれるのか?🤔

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

© 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() } } }

Slide 27

Slide 27 text

© ZOZO, Inc. 27 CoroutineExceptionHandlerが呼ばれる流れ 例外が返却された後の流れを追ってみる🔍 1. 結果を元にCoroutineを終了する 2. Coroutineの終了時の処理が呼ばれる 3. CoroutineのhandleJobExceptionが呼ばれる 4. CoroutineExceptionHandler#handleExceptionが呼ばれる

Slide 28

Slide 28 text

© ZOZO, Inc. 28 CoroutineExceptionHandlerが呼ばれる流れ 例外が返却された後の流れを追ってみる🔍 1. 結果を元にCoroutineを終了する 2. Coroutineの終了時の処理が呼ばれる←ここから追う 3. CoroutineのhandleJobExceptionが呼ばれる 4. CoroutineExceptionHandler#handleExceptionが呼ばれる

Slide 29

Slide 29 text

© 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

Slide 30

Slide 30 text

© 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

Slide 31

Slide 31 text

© ZOZO, Inc. 31 CoroutineExceptionHandlerが呼ばれる流れ StandaloneCoroutineの実装内でhandleCoroutineExceptionが呼ばれる private open class StandaloneCoroutine( parentContext: CoroutineContext, active: Boolean ) : AbstractCoroutine(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

Slide 32

Slide 32 text

© 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

Slide 33

Slide 33 text

© ZOZO, Inc. 33 CoroutineExceptionHandlerが呼ばれる流れ 終了時の処理からCoroutineExceptionHandlerが呼ばれるまでの    流れが分かった🎉

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

© 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

Slide 36

Slide 36 text

© ZOZO, Inc. 36 CoroutineExceptionHandlerのテスト 例外をcatchできず、テストが失敗する? 🤔

Slide 37

Slide 37 text

© ZOZO, Inc. 37 CoroutineExceptionHandlerのテスト CoroutineExceptionHandlerの説明に戻る https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler

Slide 38

Slide 38 text

© ZOZO, Inc. 38 CoroutineExceptionHandlerのテスト 子から親に例外が伝播しているため、例外をcatchできていない😢 runTest()に直接渡す?🤔 @Test fun testFoo() = runTest { // 内部でlaunchを呼び、親Coroutineを生成している launch(handler) { // 子Coroutine throw Exception() } } }

Slide 39

Slide 39 text

© ZOZO, Inc. 39 CoroutineExceptionHandlerのテスト runTestにはCoroutineExceptionHandlerを渡せない @Test fun testFoo() = runTest(handler) { // test }

Slide 40

Slide 40 text

© 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

Slide 41

Slide 41 text

© ZOZO, Inc. 41 CoroutineExceptionHandlerのテスト CoroutineExceptionHandlerが例外を処理できるようになり、成功🎉

Slide 42

Slide 42 text

© ZOZO, Inc. 42 まとめ ● Uncaught Exceptionの処理をカスタマイズできるもの ● 実体はCoroutineContext.Elementの一つ ● Coroutine終了処理→Coroutineの例外ハンドリング →CoroutineExceptionHandlerという流れで呼ばれる ● テスト時には例外を伝播させないための工夫が必要

Slide 43

Slide 43 text

No content