• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 
5 package kotlinx.coroutines.test.internal
6 
7 import kotlinx.coroutines.*
8 import kotlinx.coroutines.internal.*
9 import kotlin.coroutines.*
10 
11 /**
12  * If [addOnExceptionCallback] is called, the provided callback will be evaluated each time
13  * [handleCoroutineException] is executed and can't find a [CoroutineExceptionHandler] to
14  * process the exception.
15  *
16  * When a callback is registered once, even if it's later removed, the system starts to assume that
17  * other callbacks will eventually be registered, and so collects the exceptions.
18  * Once a new callback is registered, the collected exceptions are used with it.
19  *
20  * The callbacks in this object are the last resort before relying on platform-dependent
21  * ways to report uncaught exceptions from coroutines.
22  */
23 internal object ExceptionCollector : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
24     private val lock = SynchronizedObject()
25     private var enabled = false
26     private val unprocessedExceptions = mutableListOf<Throwable>()
27     private val callbacks = mutableMapOf<Any, (Throwable) -> Unit>()
28 
29     /**
30      * Registers [callback] to be executed when an uncaught exception happens.
31      * [owner] is a key by which to distinguish different callbacks.
32      */
<lambda>null33     fun addOnExceptionCallback(owner: Any, callback: (Throwable) -> Unit) = synchronized(lock) {
34         enabled = true // never becomes `false` again
35         val previousValue = callbacks.put(owner, callback)
36         check(previousValue === null)
37         // try to process the exceptions using the newly-registered callback
38         unprocessedExceptions.forEach { reportException(it) }
39         unprocessedExceptions.clear()
40     }
41 
42     /**
43      * Unregisters the callback associated with [owner].
44      */
<lambda>null45     fun removeOnExceptionCallback(owner: Any) = synchronized(lock) {
46         if (enabled) {
47             val existingValue = callbacks.remove(owner)
48             check(existingValue !== null)
49         }
50     }
51 
52     /**
53      * Tries to handle the exception by propagating it to an interested consumer.
54      * Returns `true` if the exception does not need further processing.
55      *
56      * Doesn't throw.
57      */
<lambda>null58     fun handleException(exception: Throwable): Boolean = synchronized(lock) {
59         if (!enabled) return false
60         if (reportException(exception)) return true
61         /** we don't return the result of the `add` function because we don't have a guarantee
62          * that a callback will eventually appear and collect the unprocessed exceptions, so
63          * we can't consider [exception] to be properly handled. */
64         unprocessedExceptions.add(exception)
65         return false
66     }
67 
68     /**
69      * Try to report [exception] to the existing callbacks.
70      */
reportExceptionnull71     private fun reportException(exception: Throwable): Boolean {
72         var executedACallback = false
73         for (callback in callbacks.values) {
74             callback(exception)
75             executedACallback = true
76             /** We don't leave the function here because we want to fan-out the exceptions to every interested consumer,
77              * it's not enough to have the exception processed by one of them.
78              * The reason is, it's less big of a deal to observe multiple concurrent reports of bad behavior than not
79              * to observe the report in the exact callback that is connected to that bad behavior. */
80         }
81         return executedACallback
82     }
83 
84     @Suppress("INVISIBLE_MEMBER")
handleExceptionnull85     override fun handleException(context: CoroutineContext, exception: Throwable) {
86         if (handleException(exception)) {
87             throw ExceptionSuccessfullyProcessed
88         }
89     }
90 
equalsnull91     override fun equals(other: Any?): Boolean = other is ExceptionCollector || other is ExceptionCollectorAsService
92 }
93 
94 /**
95  * A workaround for being unable to treat an object as a `ServiceLoader` service.
96  */
97 internal class ExceptionCollectorAsService: CoroutineExceptionHandler by ExceptionCollector {
98     override fun equals(other: Any?): Boolean = other is ExceptionCollectorAsService || other is ExceptionCollector
99     override fun hashCode(): Int = ExceptionCollector.hashCode()
100 }
101