О дебаге Kotlin-корутин

от автора

Привет, Хабр. Хочу поделиться своим решением одной из проблем использования корутин в Kotlin.

Корутины в Kotlin — одна из значимых фич языка, которая позволяет писать асинхронных код в синхронном стиле. Корутины прекрасны во всём, до тех пор пока не возникает необходимость их дебажить.

Одна из типичных проблем корутин — обрезанный стек вызова в исключениях. Например, рассмотрим такой код:

import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking  data class UserDTO(     val id: Long,     val login: String )  suspend fun getUserByToken(token: String): UserDTO {     delay(100)     val id = 123L //предположим, что здесь обращение в кэш, который по токену пользователя находит его id     return getUserById(id) }  suspend fun getUserById(id: Long): UserDTO {     delay(100) //тут происходит обращение в БД     //предположим, данные в кэше и в БД рассинхронизированы и пользователя с данным id нет, поэтому мы кидаем исключение     throw Exception("user not found") }  fun main() {     try {         runBlocking {             getUserByToken("secretToken")         }     } catch (e: Exception) {         e.printStackTrace()     } }

Данных код выведет такой стектрейс:

java.lang.Exception: user not found   at MainKt.getUserById(main.kt:18)   at MainKt$getUserById$1.invokeSuspend(main.kt)   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)   at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)   at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)   at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)   at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)   at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)   at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)   at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)   at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)   at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)   at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)   at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)   at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)   at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)   at MainKt.main(main.kt:23)   at MainKt.main(main.kt)

Как можно заметить, в данном трейсе отсутствует вызов функции getUserByToken. В более сложных примерах могут отсутвовать большее число функций.

Куда же делся пропавший вызов? Ответ прост и кроется в реализации корутин: при «пробуждении» (вызове метода resumeWith) корутина не восстанавливает весь свой стек вызова, а лишь вызывает одну функцию, которая находится на вершине стека всей корутины. После завершения этой функции вызывается следующая по порядку и т.д.

Соответственно, если в корутине кидается исключение, в её стектрейс попадает только функция на вершине стека этой самой корутины.

Причина ясна. Ничего нельзя поделать, отладка — сложная штука, жизнь — боль 🙁

Но погодите, команда Kotlin’а о нас подумала и сделала специальный дебаг-режим для корутин, в котором данная проблема решается.

Запускает наш пример в этом самом дебаг-режиме(для этого, например, достаточно добавить ключ -ea в строке запуска JVM) и видим… что ничего не изменилось.

В чем же причина? После долгого вчитывания в код Kotlin stdlib понимаем, что механика восстановления стектрейса работает только для случая, когда исключение передаётся при «пробуждении корутины с исключением» (вызовом resumeWith c исключением). Исключения, созданные в коде самой корутины, (как в нашем случае) никак не изменяются. 🙁

Я написал библиотеку, реализующую другой подход для восстановления стека корутин:

  1. В райтайме генерируем классы-заглушки, которые не делают ничего кроме как могут вызывать методы друг друга в любом порядке.

  2. Подменяем реализацию «пробуждения»: перед «обычным пробуждением» вызываем наши классы-заглушки в порядке стека корутины.

В результате стек вызова корутины всегда ровно такой, какой и должен быть и созданные исключения полностью его содержат.

Библиотека называется Stacktrace-decoroutinator, активно использует кодогенерацию в рантайме и MethodHandle API. Поэтому работает для JVM >= 1.8 и для Android API >= 26.

Модифицируем наш пример, добавим зависимость dev.reformator.stacktracedecoroutinator:stacktrace-decoroutinator-jvm:2.1.0 и в методе main инициализацию:

import dev.reformator.stacktracedecoroutinator.runtime.DecoroutinatorRuntime import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking  data class UserDTO(     val id: Long,     val login: String )  suspend fun getUserByToken(token: String): UserDTO {     delay(100)     val id = 123L //сдесь должно быть обращение в кеш, который по токену пользователя находит его id     return getUserById(id) }  suspend fun getUserById(id: Long): UserDTO {     delay(100) //тут происходит обращение в БД     //предположим, данные в кеше и в БД рассинхронизированы и пользователя с данным id нет, поэтому мы кидаем исключение     throw Exception("user not found") }  fun main() {     DecoroutinatorRuntime.load() // загружаем Stacktrace-decoroutinator     try {         runBlocking {             getUserByToken("secretToken")         }     } catch (e: Exception) {         e.printStackTrace()     } }

Запускает, видим стектрейс с пропавшим методом getUserByToken:

java.lang.Exception: user not found   at MainKt.getUserById(main.kt:19)   at MainKt$getUserById$1.invokeSuspend(main.kt)   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.decoroutinatorResumeWith$lambda-1(continuation-stdlib.kt:47)   at MainKt.getUserByToken(main.kt:13)   at MainKt$main$1.invokeSuspend(main.kt:26)   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.decoroutinatorResumeWith(continuation-stdlib.kt:177)   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(continuation-stdlib.kt:21)   at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)   at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)   at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)   at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)   at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)   at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)   at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)   at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)   at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)   at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)   at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)   at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)   at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)   at MainKt.main(main.kt:25)   at MainKt.main(main.kt)

В ближайшее время собираюсь использовать свою библиотеку в продакшн среде. Буду рад, если кто-нибуть присоединится)


ссылка на оригинал статьи https://habr.com/ru/post/599655/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *