• トップ
  • ブログ一覧
  • Koltin Coroutines withContextを使っているとStackTraceを用いた不具合調査が辛いのをどうにかしたい
  • Koltin Coroutines withContextを使っているとStackTraceを用いた不具合調査が辛いのをどうにかしたい

    はじめに

    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")

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background