
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