Skip to content

Commit 4c069fc

Browse files
SokolovaMariaelizarov
authored andcommitted
Reactor coroutine context propagation in more places
* Propagation of the coroutine context of await calls into Mono/Flux builder * Publisher.asFlow propagates coroutine context from `collect` call to the Publisher * Flow.asFlux transform Fixes #284
1 parent 1156e1c commit 4c069fc

22 files changed

+347
-134
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt

+18-9
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,29 @@ public final class kotlinx/coroutines/reactive/ChannelKt {
1414
public static synthetic fun openSubscription$default (Lorg/reactivestreams/Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel;
1515
}
1616

17+
public abstract interface class kotlinx/coroutines/reactive/ContextInjector {
18+
public abstract fun injectCoroutineContext (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher;
19+
}
20+
1721
public final class kotlinx/coroutines/reactive/ConvertKt {
1822
public static final fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher;
1923
public static synthetic fun asPublisher$default (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lorg/reactivestreams/Publisher;
2024
}
2125

26+
public final class kotlinx/coroutines/reactive/FlowKt {
27+
public static final fun asFlow (Lorg/reactivestreams/Publisher;)Lkotlinx/coroutines/flow/Flow;
28+
public static final fun asFlow (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/flow/Flow;
29+
public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Lorg/reactivestreams/Publisher;
30+
}
31+
32+
public final class kotlinx/coroutines/reactive/FlowSubscription : org/reactivestreams/Subscription {
33+
public final field flow Lkotlinx/coroutines/flow/Flow;
34+
public final field subscriber Lorg/reactivestreams/Subscriber;
35+
public fun <init> (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;)V
36+
public fun cancel ()V
37+
public fun request (J)V
38+
}
39+
2240
public final class kotlinx/coroutines/reactive/PublishKt {
2341
public static final fun publish (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lorg/reactivestreams/Publisher;
2442
public static final fun publish (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lorg/reactivestreams/Publisher;
@@ -44,12 +62,3 @@ public final class kotlinx/coroutines/reactive/PublisherCoroutine : kotlinx/coro
4462
public fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
4563
}
4664

47-
public final class kotlinx/coroutines/reactive/flow/FlowAsPublisherKt {
48-
public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lorg/reactivestreams/Publisher;
49-
}
50-
51-
public final class kotlinx/coroutines/reactive/flow/PublisherAsFlowKt {
52-
public static final fun from (Lorg/reactivestreams/Publisher;)Lkotlinx/coroutines/flow/Flow;
53-
public static final fun from (Lorg/reactivestreams/Publisher;I)Lkotlinx/coroutines/flow/Flow;
54-
}
55-

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactor.txt

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ public final class kotlinx/coroutines/reactor/ConvertKt {
55
public static final fun asMono (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lreactor/core/publisher/Mono;
66
}
77

8+
public final class kotlinx/coroutines/reactor/FlowKt {
9+
public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;)Lreactor/core/publisher/Flux;
10+
}
11+
812
public final class kotlinx/coroutines/reactor/FluxKt {
913
public static final fun flux (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lreactor/core/publisher/Flux;
1014
public static final fun flux (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lreactor/core/publisher/Flux;
@@ -28,6 +32,11 @@ public final class kotlinx/coroutines/reactor/ReactorContext : kotlin/coroutines
2832
public final class kotlinx/coroutines/reactor/ReactorContext$Key : kotlin/coroutines/CoroutineContext$Key {
2933
}
3034

35+
public final class kotlinx/coroutines/reactor/ReactorContextInjector : kotlinx/coroutines/reactive/ContextInjector {
36+
public fun <init> ()V
37+
public fun injectCoroutineContext (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher;
38+
}
39+
3140
public final class kotlinx/coroutines/reactor/ReactorContextKt {
3241
public static final fun asCoroutineContext (Lreactor/util/context/Context;)Lkotlinx/coroutines/reactor/ReactorContext;
3342
}

reactive/kotlinx-coroutines-reactive/src/Await.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
1010
import org.reactivestreams.Publisher
1111
import org.reactivestreams.Subscriber
1212
import org.reactivestreams.Subscription
13+
import java.util.*
1314
import kotlin.coroutines.*
1415

1516
/**
@@ -81,6 +82,16 @@ public suspend fun <T> Publisher<T>.awaitSingle(): T = awaitOne(Mode.SINGLE)
8182

8283
// ------------------------ private ------------------------
8384

85+
// ContextInjector service is implemented in `kotlinx-coroutines-reactor` module only.
86+
// If `kotlinx-coroutines-reactor` module is not included, the list is empty.
87+
private val contextInjectors: Array<ContextInjector> =
88+
ServiceLoader.load(ContextInjector::class.java, ContextInjector::class.java.classLoader).toList().toTypedArray()
89+
90+
private fun <T> Publisher<T>.injectCoroutineContext(coroutineContext: CoroutineContext) =
91+
contextInjectors.fold(this) { pub, contextInjector ->
92+
contextInjector.injectCoroutineContext(pub, coroutineContext)
93+
}
94+
8495
private enum class Mode(val s: String) {
8596
FIRST("awaitFirst"),
8697
FIRST_OR_DEFAULT("awaitFirstOrDefault"),
@@ -93,7 +104,7 @@ private suspend fun <T> Publisher<T>.awaitOne(
93104
mode: Mode,
94105
default: T? = null
95106
): T = suspendCancellableCoroutine { cont ->
96-
subscribe(object : Subscriber<T> {
107+
injectCoroutineContext(cont.context).subscribe(object : Subscriber<T> {
97108
private lateinit var subscription: Subscription
98109
private var value: T? = null
99110
private var seenValue = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package kotlinx.coroutines.reactive
2+
3+
import kotlinx.coroutines.InternalCoroutinesApi
4+
import org.reactivestreams.Publisher
5+
import kotlin.coroutines.CoroutineContext
6+
7+
/** @suppress */
8+
@InternalCoroutinesApi
9+
public interface ContextInjector {
10+
/**
11+
* Injects the coroutine context into the context of the publisher.
12+
*/
13+
public fun <T> injectCoroutineContext(publisher: Publisher<T>, coroutineContext: CoroutineContext): Publisher<T>
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:JvmMultifileClass
6+
@file:JvmName("FlowKt")
7+
8+
package kotlinx.coroutines.reactive
9+
10+
import kotlinx.coroutines.*
11+
import kotlinx.coroutines.flow.*
12+
import org.reactivestreams.*
13+
import java.util.concurrent.atomic.*
14+
import kotlin.coroutines.*
15+
16+
/**
17+
* Transforms the given flow to a spec-compliant [Publisher].
18+
*/
19+
@ExperimentalCoroutinesApi
20+
public fun <T : Any> Flow<T>.asPublisher(): Publisher<T> = FlowAsPublisher(this)
21+
22+
/**
23+
* Adapter that transforms [Flow] into TCK-complaint [Publisher].
24+
* [cancel] invocation cancels the original flow.
25+
*/
26+
@Suppress("PublisherImplementation")
27+
private class FlowAsPublisher<T : Any>(private val flow: Flow<T>) : Publisher<T> {
28+
override fun subscribe(subscriber: Subscriber<in T>?) {
29+
if (subscriber == null) throw NullPointerException()
30+
subscriber.onSubscribe(FlowSubscription(flow, subscriber))
31+
}
32+
}
33+
34+
/** @suppress */
35+
@InternalCoroutinesApi
36+
public class FlowSubscription<T>(
37+
@JvmField val flow: Flow<T>,
38+
@JvmField val subscriber: Subscriber<in T>
39+
) : Subscription {
40+
@Volatile
41+
private var canceled: Boolean = false
42+
private val requested = AtomicLong(0L)
43+
private val producer: AtomicReference<CancellableContinuation<Unit>?> = AtomicReference()
44+
45+
// This is actually optimizable
46+
private var job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.LAZY) {
47+
try {
48+
consumeFlow()
49+
subscriber.onComplete()
50+
} catch (e: Throwable) {
51+
// Failed with real exception, not due to cancellation
52+
if (!coroutineContext[Job]!!.isCancelled) {
53+
subscriber.onError(e)
54+
}
55+
}
56+
}
57+
58+
private suspend fun consumeFlow() {
59+
flow.collect { value ->
60+
if (!coroutineContext.isActive) {
61+
subscriber.onComplete()
62+
coroutineContext.ensureActive()
63+
}
64+
65+
if (requested.get() == 0L) {
66+
suspendCancellableCoroutine<Unit> {
67+
producer.set(it)
68+
if (requested.get() != 0L) it.resumeSafely()
69+
}
70+
}
71+
72+
requested.decrementAndGet()
73+
subscriber.onNext(value)
74+
}
75+
}
76+
77+
override fun cancel() {
78+
canceled = true
79+
job.cancel()
80+
}
81+
82+
override fun request(n: Long) {
83+
if (n <= 0) {
84+
return
85+
}
86+
87+
if (canceled) return
88+
89+
job.start()
90+
var snapshot: Long
91+
var newValue: Long
92+
do {
93+
snapshot = requested.get()
94+
newValue = snapshot + n
95+
if (newValue <= 0L) newValue = Long.MAX_VALUE
96+
} while (!requested.compareAndSet(snapshot, newValue))
97+
98+
val prev = producer.get()
99+
if (prev == null || !producer.compareAndSet(prev, null)) return
100+
prev.resumeSafely()
101+
}
102+
103+
private fun CancellableContinuation<Unit>.resumeSafely() {
104+
val token = tryResume(Unit)
105+
if (token != null) {
106+
completeResume(token)
107+
}
108+
}
109+
}

reactive/kotlinx-coroutines-reactive/src/flow/PublisherAsFlow.kt renamed to reactive/kotlinx-coroutines-reactive/src/PublisherAsFlow.kt

+14-5
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5-
package kotlinx.coroutines.reactive.flow
5+
@file:JvmMultifileClass
6+
@file:JvmName("FlowKt")
7+
8+
package kotlinx.coroutines.reactive
69

710
import kotlinx.coroutines.*
811
import kotlinx.coroutines.channels.*
912
import kotlinx.coroutines.flow.*
1013
import kotlinx.coroutines.flow.internal.*
11-
import kotlinx.coroutines.reactive.*
1214
import org.reactivestreams.*
15+
import java.util.*
1316
import kotlin.coroutines.*
1417

1518
/**
@@ -21,13 +24,11 @@ import kotlin.coroutines.*
2124
* If any of the resulting flow transformations fails, subscription is immediately cancelled and all in-flights elements
2225
* are discarded.
2326
*/
24-
@JvmName("from")
2527
@ExperimentalCoroutinesApi
2628
public fun <T : Any> Publisher<T>.asFlow(): Flow<T> =
2729
PublisherAsFlow(this, 1)
2830

2931
@FlowPreview
30-
@JvmName("from")
3132
@Deprecated(
3233
message = "batchSize parameter is deprecated, use .buffer() instead to control the backpressure",
3334
level = DeprecationLevel.ERROR,
@@ -70,7 +71,7 @@ private class PublisherAsFlow<T : Any>(
7071

7172
override suspend fun collect(collector: FlowCollector<T>) {
7273
val subscriber = ReactiveSubscriber<T>(capacity, requestSize)
73-
publisher.subscribe(subscriber)
74+
publisher.injectCoroutineContext(coroutineContext).subscribe(subscriber)
7475
try {
7576
var consumed = 0L
7677
while (true) {
@@ -127,3 +128,11 @@ private class ReactiveSubscriber<T : Any>(
127128
subscription.cancel()
128129
}
129130
}
131+
132+
// ContextInjector service is implemented in `kotlinx-coroutines-reactor` module only.
133+
// If `kotlinx-coroutines-reactor` module is not included, the list is empty.
134+
private val contextInjectors: List<ContextInjector> =
135+
ServiceLoader.load(ContextInjector::class.java, ContextInjector::class.java.classLoader).toList()
136+
137+
private fun <T> Publisher<T>.injectCoroutineContext(coroutineContext: CoroutineContext) =
138+
contextInjectors.fold(this) { pub, contextInjector -> contextInjector.injectCoroutineContext(pub, coroutineContext) }

reactive/kotlinx-coroutines-reactive/src/flow/FlowAsPublisher.kt

-103
This file was deleted.

reactive/kotlinx-coroutines-reactive/test/flow/IterableFlowTckTest.kt renamed to reactive/kotlinx-coroutines-reactive/test/IterableFlowTckTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
@file:Suppress("UNCHECKED_CAST")
66

7-
package kotlinx.coroutines.reactive.flow
7+
package kotlinx.coroutines.reactive
88

99
import kotlinx.coroutines.flow.*
1010
import org.junit.*

0 commit comments

Comments
 (0)