commonMain.internal.ExceptionCollector.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlinx-coroutines-test
Show all versions of kotlinx-coroutines-test
Coroutines support libraries for Kotlin
package kotlinx.coroutines.test.internal
import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
/**
* If [addOnExceptionCallback] is called, the provided callback will be evaluated each time
* [handleCoroutineException] is executed and can't find a [CoroutineExceptionHandler] to
* process the exception.
*
* When a callback is registered once, even if it's later removed, the system starts to assume that
* other callbacks will eventually be registered, and so collects the exceptions.
* Once a new callback is registered, the collected exceptions are used with it.
*
* The callbacks in this object are the last resort before relying on platform-dependent
* ways to report uncaught exceptions from coroutines.
*/
internal object ExceptionCollector : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
private val lock = SynchronizedObject()
private var enabled = false
private val unprocessedExceptions = mutableListOf()
private val callbacks = mutableMapOf Unit>()
/**
* Registers [callback] to be executed when an uncaught exception happens.
* [owner] is a key by which to distinguish different callbacks.
*/
fun addOnExceptionCallback(owner: Any, callback: (Throwable) -> Unit) = synchronized(lock) {
enabled = true // never becomes `false` again
val previousValue = callbacks.put(owner, callback)
check(previousValue === null)
// try to process the exceptions using the newly-registered callback
unprocessedExceptions.forEach { reportException(it) }
unprocessedExceptions.clear()
}
/**
* Unregisters the callback associated with [owner].
*/
fun removeOnExceptionCallback(owner: Any) = synchronized(lock) {
if (enabled) {
val existingValue = callbacks.remove(owner)
check(existingValue !== null)
}
}
/**
* Tries to handle the exception by propagating it to an interested consumer.
* Returns `true` if the exception does not need further processing.
*
* Doesn't throw.
*/
fun handleException(exception: Throwable): Boolean = synchronized(lock) {
if (!enabled) return false
if (reportException(exception)) return true
/** we don't return the result of the `add` function because we don't have a guarantee
* that a callback will eventually appear and collect the unprocessed exceptions, so
* we can't consider [exception] to be properly handled. */
unprocessedExceptions.add(exception)
return false
}
/**
* Try to report [exception] to the existing callbacks.
*/
private fun reportException(exception: Throwable): Boolean {
var executedACallback = false
for (callback in callbacks.values) {
callback(exception)
executedACallback = true
/** We don't leave the function here because we want to fan-out the exceptions to every interested consumer,
* it's not enough to have the exception processed by one of them.
* The reason is, it's less big of a deal to observe multiple concurrent reports of bad behavior than not
* to observe the report in the exact callback that is connected to that bad behavior. */
}
return executedACallback
}
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2
override fun handleException(context: CoroutineContext, exception: Throwable) {
if (handleException(exception)) {
throw ExceptionSuccessfullyProcessed
}
}
override fun equals(other: Any?): Boolean = other is ExceptionCollector || other is ExceptionCollectorAsService
}
/**
* A workaround for being unable to treat an object as a `ServiceLoader` service.
*/
internal class ExceptionCollectorAsService: CoroutineExceptionHandler by ExceptionCollector {
override fun equals(other: Any?): Boolean = other is ExceptionCollectorAsService || other is ExceptionCollector
override fun hashCode(): Int = ExceptionCollector.hashCode()
}