
Koltin Coroutines withContextを使っているとStackTraceを用いた不具合調査が辛いのをどうにかしたい

IT技術
はじめに
Androidの非同期処理でKoltin Coroutinesを用いた実装を数年続けてきたことで
非同期処理を最適化するために
withContextを積極的に使うことを意識して実装するようになりました。
withContextによるスレッドの切り替えは
オーバーヘッドも少なく、気軽に使うことができるのが魅力の一つです。
しかし、withContextを積極的に使った実装をするようになったことで
StackTraceを用いた不具合調査がしづらくなってしまいました。
「不具合調査が辛い」とはどういうことか
端的に言うと。
withContextを使うと「StackTraceが破壊され処理の呼び出し元がわからなくなる」
という問題が辛いです。
例を挙げましょう。
1 // 適当なRpository
2class HogeRepository(
3 private val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
4) {
5 suspend fun getHoge(): String = withContext(dispatcherIO) {
6 throw RuntimeException("適当な例外")
7 }
8}
これをViewModelで利用したとしましょう。
1 // 適当なViewModelから呼び出す
2class MainViewModel(
3 private val repository: HogeRepository = HogeRepository()
4) : ViewModel() {
5
6 fun onClickButton() {
7 viewModelScope.launch {
8 // 先のRepositoryのメソッド呼び出す
9 repository.getHoge()
10 }
11 }
12}
この状態でMainViewModel.onClickButton()
を実行して、アプリをクラッシュさせます。
出力されたスタックトレースを見てみましょう。
はい。(行数は例とズレているので気にしないでください)
「HogeRepository.getHoge()
内部(性格にはwithContext内部)で例外が発生した」
という情報しかわかりません。
「MainViewModel.onClickButton()
から呼ばれた」
という情報が読み取れません。
これでは、不具合調査が難しくなってしまう可能性があります。
困ること
まあ、当たり前といえば当たり前の話ですね。
suspend関数のJVMレベルでの挙動では
「一時的に抜けてあとから再開する」という挙動をしており
物理的なコールスタックが分断されているので、
スタックトレースがこうなってしまうのは仕方ないことです。
いやー
呼び出し箇所が1箇所なら、これでもあまり困りませんが。
以下のように、同じ処理を複数箇所から呼び出されている場合は致命的です。
1 // 複数箇所から呼び出すケース
2class MainViewModel(
3 private val repository: HogeRepository = HogeRepository()
4) : ViewModel() {
5
6 fun onClickButton1() {
7 viewModelScope.launch {
8 repository.getHoge() // ここでクラッシュ?
9 }
10 }
11
12 fun onClickButton2() {
13 viewModelScope.launch {
14 repository.getHoge() // それとも、こっちでクラッシュ?
15 }
16 }
17}
「特定の呼び出し元からのみクラッシュする」のようなケースで
どこから呼ばれてクラッシュしたのかわからないと辛いですね。
困った困った。
ではどうするか?
正直、ベストな回答を持っていません。
今のところは、try-catchでcatchした例外をログ出力するのがよいのでは?と思っています。
1 // try-catchして例外発生場所を把握できるようにする。
2class MainViewModel(
3 private val repository: HogeRepository = HogeRepository()
4) : ViewModel() {
5
6 fun onClickButton1() {
7 viewModelScope.launch {
8 try {
9 repository.getHoge()
10 } catch (e: CancellationException) {
11 throw e // Jobキャンセル例外はキャッチしたらダメです
12 } catch (e: RuntimeException) {
13 Log.e("hoge", "onClickButton1", e)
14 // 握りつぶしたくないならthrow
15 }
16 }
17 }
18
19 fun onClickButton2() {
20 viewModelScope.launch {
21 try {
22 repository.getHoge()
23 } catch (e: CancellationException) {
24 throw e // Jobキャンセル例外はキャッチしたらダメです
25 } catch (e: RuntimeException) {
26 Log.e("hoge", "onClickButton2", e)
27 // 握りつぶしたくないならthrow
28 }
29 }
30 }
31}
一応、スタックトレースから呼び出し元がわかるようになった。
ただ、これをRepository呼び出す毎に書くのはちょっとナンセンスな気もしていて。
どうするのかいいんでしょう?
もし、今後よさそうな答えを得たときに追記します。
おまけ
開発中であれば、以下でスタックトレースで追えるので活用できることもあるのかも?
System.setProperty("kotlinx.coroutines.debug", "on")
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ