web-dev-qa-db-fra.com

Exception levée par deferred.await () dans une exécution bloquée considérée comme non prise en charge, même après avoir été capturée

Ce code:

fun main() {
    runBlocking {
        try {
            val deferred = async { throw Exception() }
            deferred.await()
        } catch (e: Exception) {
            println("Caught $e")
        }
    }
    println("Completed")
}

résultats dans cette sortie:

Caught Java.lang.Exception
Exception in thread "main" Java.lang.Exception
    at org.mtopol.TestKt$main$1$deferred$1.invokeSuspend(test.kt:11)
    ...

Ce comportement n'a pas de sens pour moi. L'exception a été interceptée et gérée, et malgré tout, elle échappe au niveau supérieur en tant qu'exception non gérée.

Ce comportement est-il documenté et attendu? Cela viole toutes mes intuitions sur la manière dont la gestion des exceptions est censée fonctionner.

J'ai adapté cette question à partir d'un fil de discussion sur le forum Kotlin .


La documentation Kotlin suggère d'utiliser supervisorScope si nous ne voulons pas annuler toutes les routines secondaires en cas d'échec. Donc je peux écrire

fun main() {
    runBlocking {
        supervisorScope {
            try {
                launch {
                    delay(1000)
                    println("Done after delay")
                }
                val job = launch {
                    throw Exception()
                }
                job.join()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
    }
    println("Completed")
}

La sortie est maintenant

Exception in thread "main" Java.lang.Exception
    at org.mtopol.TestKt$main$2$1$job$1.invokeSuspend(test.kt:16)
    ...
    at org.mtopol.TestKt.main(test.kt:8)
    ...

Done after delay
Completed

Encore une fois, ce n’est pas le comportement que je veux. Ici, une coroutine launched a échoué avec une exception non gérée, invalidant le travail des autres routines, mais elles se poursuivent sans interruption.

Le comportement que je trouverais raisonnable est d’annuler l’annulation lorsqu’une coroutine échoue de manière imprévue (c’est-à-dire non gérée). Attraper une exception de await signifie qu'il n'y a pas eu d'erreur globale, mais juste une exception localisée gérée dans le cadre de la logique métier.

8
Marko Topolnik

Après avoir étudié les raisons pour lesquelles Kotlin a provoqué ce comportement, j’ai trouvé que, si les exceptions n’étaient pas propagées de cette façon, il serait compliqué d’écrire du code bien conçu qui sera annulé à temps. Par exemple:

runBlocking {
    val deferredA = async {
        Thread.sleep(10_000)
        println("Done after delay")
        1
    }
    val deferredB = async<Int> { throw Exception() }
    println(deferredA.await() + deferredB.await())
}

Étant donné que a est le premier résultat attendu, ce code continue à s'exécuter pendant 10 secondes, puis génère une erreur et ne produit aucun travail utile. Dans la plupart des cas, nous souhaitons tout annuler dès qu'un composant échoue. Nous pourrions le faire comme ceci:

val (a, b) = awaitAll(deferredA, deferredB)
println(a + b)

Ce code est moins élégant: nous sommes obligés d’attendre tous les résultats au même endroit et nous perdons la sécurité du type car awaitAll renvoie une liste du sur-type commun de tous les arguments. Si nous avons des

suspend fun suspendFun(): Int {
    delay(10_000)
    return 2
}

et nous voulons écrire

val c = suspendFun()
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b + c)

Nous sommes privés de la possibilité de renflouer avant que suspendFun ne soit terminé. Nous pourrions travailler comme ceci:

val deferredC = async { suspendFun() }
val (a, b, c) = awaitAll(deferredA, deferredB, deferredC)
println(a + b + c)

mais ceci est fragile car vous devez faire attention à ce que vous le fassiez pour chaque appel pouvant être suspendu. C'est aussi contre la doctrine Kotlin du "séquentiel par défaut"

En conclusion: la conception actuelle, bien que paradoxale au premier abord, est logique en tant que solution pratique. De plus, cela renforce la règle de ne pas utiliser async-await sauf si vous effectuez une décomposition parallèle d'une tâche.

3
Marko Topolnik

Cela peut être résolu en modifiant légèrement le code pour que la valeur deferred soit exécutée de manière explicite en utilisant la même CoroutineContext que la portée runBlocking, par exemple.

runBlocking {
    try {
        val deferred = withContext(this.coroutineContext) {
            async {
                throw Exception()
            }
        }
        deferred.await()
    } catch (e: Exception) {
        println("Caught $e")
    }
}
println("Completed")

MISE À JOUR APRÈS QUESTION ORIGINALE MISE À JOUR

Est-ce que cela fournit ce que vous voulez:

runBlocking {
    supervisorScope {
        try {
            val a = async {
                delay(1000)
                println("Done after delay")
            }
            val b = async { throw Exception() }
            awaitAll(a, b)
        } catch (e: Exception) {
            println("Caught $e")
            // Optional next line, depending on whether you want the async with the delay in it to be cancelled.
            coroutineContext.cancelChildren()
        }
    }
}

Ceci est tiré de this comment qui traite de la décomposition parallèle.

1
Yoni Gibbs

Bien que toutes les réponses soient exactes à cet endroit, mais laissez-moi vous éclairer un peu plus, ce qui pourrait aider d'autres utilisateurs. Il est documenté ici ( Doc officiel ) que: - 

Si une coroutine rencontre une exception autre que CancellationException, il annule son parent à cette exception près. Ce comportement ne peut pas être substitué et est utilisé pour fournir des hiérarchies de coroutines stables pour concurrence simultanée structurée qui ne dépendent pas de CoroutineExceptionHandler implémentation. L'exception originale est géré par le parent (In GlobalScope) lorsque tous ses enfants se terminent.

Cela n’a aucun sens d’installer un gestionnaire d’exceptions sur une coroutine qui est lancé dans le cadre du principal runBlocking , depuis le principal coroutine sera toujours annulé lorsque son enfant aura terminé avec une exception malgré le gestionnaire installé.

J'espère que cela aidera.

1
Pravin Divraniya

Une CoroutineScope normale (créée par runBlocking) annule immédiatement toutes les routines enfants lorsque l'un d'eux lève une exception. Ce comportement est documenté ici: https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#cancellation-and-exceptions

Vous pouvez utiliser une supervisorScope pour obtenir le comportement souhaité. Si une coroutine enfant échoue à l'intérieur d'un périmètre de supervision, les autres enfants ne seront pas immédiatement annulés. Les enfants ne seront annulés que si l'exception n'est pas gérée.

Pour plus d'informations, voir ici: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html

fun main() {
    runBlocking {
        supervisorScope {
            try {
                val deferred = async { throw Exception() }
                deferred.await()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
    }
    println("Completed")
}
0
marstran