Как получить полную трассировку стека исключений при использовании функции await() в CompletableFuture

#kotlin #kotlin-coroutines

Вопрос:

Проблема

Я использую kotlinx.coroutines.future.FutureKt#await для ожидания асинхронного кода. Но когда из этого асинхронного кода возникает какое-либо исключение, исключение не содержит вызова полного стека. Например.:

 fun main() {
    try {
        myFun1Blocking()
    } catch (e: Throwable) {
        e.printStackTrace(System.out)
    }
}

fun myFun1Blocking() {
    runBlocking {
        myFun2Suspend()
    }
}

suspend fun myFun2Suspend() {
    runAsync().await()
}

fun runAsync(): CompletableFuture<Void> {
    return CompletableFuture.runAsync {
        Thread.sleep(2000)
        throw Exception()
    }
}
 

Это приводит к следующему результату:

 java.lang.Exception
    at TestKotlinKt.runAsync$lambda-0(TestKotlin.kt:34)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1728)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
 

Трассировка стека содержит только runAsync часть метода.

Решение № 1

Пытаясь обойти эту проблему, я сначала подумал о том, чтобы поймать ее прямо за пределами ожидания:

 suspend fun <T> CompletionStage<T>.awaitWithException(): T {
    try {
        return await()
    } catch (e: Exception) {
        throw Exception(e)
    }
}
 
 java.lang.Exception: java.lang.Exception
    at TestKotlinKt.awaitWithException(TestKotlin.kt:36)
    at TestKotlinKt$awaitWithException$1.invokeSuspend(TestKotlin.kt)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
    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 TestKotlinKt.myFun1Blocking(TestKotlin.kt:23)
    at TestKotlinKt.main(TestKotlin.kt:16)
    at TestKotlinKt.main(TestKotlin.kt)
Caused by: java.lang.Exception
    at TestKotlinKt.runAsync$lambda-0(TestKotlin.kt:43)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1728)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
 

Лучше, но я все равно потерял вызов myFun2Suspend в стеке.

Решение № 2

Затем я попытался сохранить трассировку стека непосредственно перед ожиданием():

 suspend fun <T> CompletionStage<T>.awaitWithException(printStream: PrintStream): T {
    val throwable = Throwable("Await Exception")
    try {
        return await()
    } catch (e: Exception) {
        throwable.printStackTrace(printStream)
        throw e
    }
}
 
 java.lang.Throwable: Await Exception
    at TestKotlinKt.awaitWithException(TestKotlin.kt:43)
    at TestKotlinKt.myFun2Suspend(TestKotlin.kt:31)
    at TestKotlinKt$myFun1Blocking$1.invokeSuspend(TestKotlin.kt:26)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
    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 TestKotlinKt.myFun1Blocking(TestKotlin.kt:25)
    at TestKotlinKt.main(TestKotlin.kt:18)
    at TestKotlinKt.main(TestKotlin.kt)
AsyncException
    at TestKotlinKt.runAsync$lambda-0(TestKotlin.kt:55)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1728)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
 

Теперь я все вижу.

В целом, мое решение кажется очень банальным. Есть ли что-то лучше?

Ответ №1:

Ссылаясь на документацию Kotlin по отладке, оба ваших решения имеют официальные поддерживаемые аналоги:

  • Установите системное свойство kotlinx.coroutines.debug on равным, чтобы включить режим отладки. Это позволяет восстанавливать трассировку стека, которая является более полной версией решения № 1.
  • Используйте агент отладки Kotlin, чтобы включить трассировку стеков создания, которая является официальной версией решения № 2. Имейте в виду, что это очень дорогая функция, поскольку при каждом создании сопрограммы потребуется сбрасывать трассировки стека.

Теоретически режима отладки kotlin должно быть достаточно, так как исключение должно проходить через «стек» сопрограмм. Это просто не самое красивое решение.

Ответ №2:

Я столкнулся с той же проблемой. Библиотека отладки сопрограмм Kotlin мне никак не помогла. Поэтому, изучив реализацию сопрограмм, я написал собственное решение, основанное на генерации байт-кода и API MethodHandle. Он поддерживает JVM 1.8 и Android API 25 или выше. Я назвал его Stacktrace-декоратором.

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

Моя библиотека заменяет реализацию пробуждения сопрограммы. Он генерирует классы во время выполнения с именами, соответствующими всему стеку вызовов сопрограмм.

Эти классы ничего не делают, кроме как вызывают друг друга в последовательности стека вызовов сопрограммы.

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

Комментарии:

1. Я думаю, что в этом все еще отсутствует объяснение того, как вы это сделали, и дается только описание того, что вы сделали. Без «как» это все равно не более чем ссылка на вашу собственную работу и все равно/снова рискует быть удаленным.