All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.internal.ExceptionCollector.kt Maven / Gradle / Ivy

There is a newer version: 1.9.0
Show newest version
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()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy