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

jvmMain.flow.internal.SafeCollector.kt Maven / Gradle / Ivy

package kotlinx.coroutines.flow.internal

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
import kotlin.coroutines.jvm.internal.*

@Suppress("UNCHECKED_CAST")
private val emitFun =
    FlowCollector::emit as Function3, Any?, Continuation, Any?>

/**
 * A safe collector is an instance of [FlowCollector] that ensures that neither context preservation
 * nor exception transparency invariants are broken. Instances of [SafeCollector] are used in flow
 * operators that provide raw access to the [FlowCollector] e.g. [Flow.transform].
 * Mechanically, each [emit] call captures [currentCoroutineContext], ensures it is not different from the
 * previously caught one and proceeds further. If an exception is thrown from the downstream,
 * it is caught, and any further attempts to [emit] lead to the [IllegalStateException].
 *
 * ### Performance hacks
 *
 * Implementor of [ContinuationImpl] (that will be preserved as ABI nearly forever)
 * in order to properly control `intercepted()` lifecycle.
 * The safe collector implements [ContinuationImpl] to pretend it *is* a state-machine of its own `emit` method.
 * It is [ContinuationImpl] and not any other [Continuation] subclass because only [ContinuationImpl] supports `intercepted()` caching.
 * This is the most performance-sensitive place in the overall flow pipeline, because otherwise safe collector is forced to allocate
 * a state machine on each element being emitted for each intermediate stage where the safe collector is present.
 *
 * See a comment to [emit] for the explanation of what and how is being optimized.
 */
@Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "UNCHECKED_CAST")
internal actual class SafeCollector actual constructor(
    @JvmField internal actual val collector: FlowCollector,
    @JvmField internal actual val collectContext: CoroutineContext
) : FlowCollector, ContinuationImpl(NoOpContinuation, EmptyCoroutineContext), CoroutineStackFrame {

    override val callerFrame: CoroutineStackFrame? get() = completion_ as? CoroutineStackFrame

    override fun getStackTraceElement(): StackTraceElement? = null

    @JvmField // Note, it is non-capturing lambda, so no extra allocation during init of SafeCollector
    internal actual val collectContextSize = collectContext.fold(0) { count, _ -> count + 1 }

    // Either context of the last emission or wrapper 'DownstreamExceptionContext'
    private var lastEmissionContext: CoroutineContext? = null
    // Completion if we are currently suspended or within completion_ body or null otherwise
    private var completion_: Continuation? = null

    /*
     * This property is accessed in two places:
     * - ContinuationImpl invokes this in its `releaseIntercepted` as `context[ContinuationInterceptor]!!`
     * - When we are within a callee, it is used to create its continuation object with this collector as completion_
     */
    override val context: CoroutineContext
        get() = lastEmissionContext ?: EmptyCoroutineContext

    override fun invokeSuspend(result: Result): Any {
        result.onFailure { lastEmissionContext = DownstreamExceptionContext(it, context) }
        completion_?.resumeWith(result as Result)
        return COROUTINE_SUSPENDED
    }

    // Escalate visibility to manually release intercepted continuation
    public actual override fun releaseIntercepted() {
        super.releaseIntercepted()
    }

    /**
     * This is a crafty implementation of state-machine reusing.
     *
     * First it checks that it is not used concurrently (which we explicitly prohibit), and
     * then just caches an instance of the completion_ in order to avoid extra allocation on each emit,
     * making it effectively garbage-free on its hot-path.
     *
     * See `emit` overload.
     */
    actual override suspend fun emit(value: T) {
        // NB: it is a tail-call, so we are sure `uCont` is the completion of the emit's **caller**.
        return suspendCoroutineUninterceptedOrReturn sc@{ uCont ->
            try {
                emit(uCont, value)
            } catch (e: Throwable) {
                // Save the fact that exception from emit (or even check context) has been thrown
                // Note, that this can the first emit and lastEmissionContext may not be saved yet,
                // hence we use `uCont.context` here.
                lastEmissionContext = DownstreamExceptionContext(e, uCont.context)
                throw e
            }
        }
    }

    /**
     * Here we use the following trick:
     * - Perform all the required checks
     * - Having a non-intercepted, non-cancellable caller's `uCont`, we leverage our implementation knowledge
     *   and invoke `collector.emit(T)` as `collector.emit(value: T, completion: Continuation), passing `this`
     *   as the completion. We also setup `this` state, so if the `completion.resume` is invoked, we are
     *   invoking `uCont.resume` properly in accordance with `ContinuationImpl`/`BaseContinuationImpl` internal invariants.
     *
     * Note that in such scenarios, `collector.emit` completion is the current instance of SafeCollector and thus is reused.
     */
    private fun emit(uCont: Continuation, value: T): Any? {
        val currentContext = uCont.context
        currentContext.ensureActive()
        // This check is triggered once per flow on a happy path.
        val previousContext = lastEmissionContext
        if (previousContext !== currentContext) {
            checkContext(currentContext, previousContext, value)
            lastEmissionContext = currentContext
        }
        completion_ = uCont
        val result = emitFun(collector as FlowCollector, value, this as Continuation)
        /*
         * If the callee hasn't suspended, that means that it won't (it's forbidden) call 'resumeWith` (-> `invokeSuspend`)
         * and we don't have to retain a strong reference to it to avoid memory leaks.
         */
        if (result != COROUTINE_SUSPENDED) {
            completion_ = null
        }
        return result
    }

    private fun checkContext(
        currentContext: CoroutineContext,
        previousContext: CoroutineContext?,
        value: T
    ) {
        if (previousContext is DownstreamExceptionContext) {
            exceptionTransparencyViolated(previousContext, value)
        }
        checkContext(currentContext)
    }

    private fun exceptionTransparencyViolated(exception: DownstreamExceptionContext, value: Any?) {
        /*
         * Exception transparency ensures that if a `collect` block or any intermediate operator
         * throws an exception, then no more values will be received by it.
         * For example, the following code:
         * ```
         * val flow = flow {
         *     emit(1)
         *     try {
         *          emit(2)
         *     } catch (e: Exception) {
         *          emit(3)
         *     }
         * }
         * // Collector
         * flow.collect { value ->
         *     if (value == 2) {
         *         throw CancellationException("No more elements required, received enough")
         *     } else {
         *         println("Collected $value")
         *     }
         * }
         * ```
         * is expected to print "Collected 1" and then "No more elements required, received enough" exception,
         * but if exception transparency wasn't enforced, "Collected 1" and "Collected 3" would be printed instead.
         */
        error("""
            Flow exception transparency is violated:
                Previous 'emit' call has thrown exception ${exception.e}, but then emission attempt of value '$value' has been detected.
                Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
                For a more detailed explanation, please refer to Flow documentation.
            """.trimIndent())
    }
}

internal class DownstreamExceptionContext(
    @JvmField val e: Throwable,
    originalContext: CoroutineContext
) : CoroutineContext by originalContext

private object NoOpContinuation : Continuation {
    override val context: CoroutineContext = EmptyCoroutineContext

    override fun resumeWith(result: Result) {
        // Nothing
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy