Skip to content

Commit 835ee18

Browse files
authored
Add a way to construct ReactorContext from ContextView (#2622)
Fixes #2575
1 parent 347feed commit 835ee18

File tree

5 files changed

+68
-28
lines changed

5 files changed

+68
-28
lines changed

reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,16 @@ public final class kotlinx/coroutines/reactor/MonoKt {
3333
public final class kotlinx/coroutines/reactor/ReactorContext : kotlin/coroutines/AbstractCoroutineContextElement {
3434
public static final field Key Lkotlinx/coroutines/reactor/ReactorContext$Key;
3535
public fun <init> (Lreactor/util/context/Context;)V
36+
public fun <init> (Lreactor/util/context/ContextView;)V
3637
public final fun getContext ()Lreactor/util/context/Context;
3738
}
3839

3940
public final class kotlinx/coroutines/reactor/ReactorContext$Key : kotlin/coroutines/CoroutineContext$Key {
4041
}
4142

4243
public final class kotlinx/coroutines/reactor/ReactorContextKt {
43-
public static final fun asCoroutineContext (Lreactor/util/context/Context;)Lkotlinx/coroutines/reactor/ReactorContext;
44+
public static final synthetic fun asCoroutineContext (Lreactor/util/context/Context;)Lkotlinx/coroutines/reactor/ReactorContext;
45+
public static final fun asCoroutineContext (Lreactor/util/context/ContextView;)Lkotlinx/coroutines/reactor/ReactorContext;
4446
}
4547

4648
public final class kotlinx/coroutines/reactor/ReactorFlowKt {

reactive/kotlinx-coroutines-reactor/src/Convert.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public fun <T> Deferred<T?>.asMono(context: CoroutineContext): Mono<T> = mono(co
4141
/**
4242
* Converts a stream of elements received from the channel to the hot reactive flux.
4343
*
44-
* Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers,
45-
* they'll receive values in round-robin way.
44+
* Every subscriber receives values from this channel in a **fan-out** fashion. If the are multiple subscribers,
45+
* they'll receive values in a round-robin way.
4646
* @param context -- the coroutine context from which the resulting flux is going to be signalled
4747
*/
4848
@Deprecated(message = "Deprecated in the favour of consumeAsFlow()",

reactive/kotlinx-coroutines-reactor/src/Flux.kt

+32-14
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ import reactor.util.context.*
1414
import kotlin.coroutines.*
1515

1616
/**
17-
* Creates cold reactive [Flux] that runs a given [block] in a coroutine.
17+
* Creates a cold reactive [Flux] that runs the given [block] in a coroutine.
1818
* Every time the returned flux is subscribed, it starts a new coroutine in the specified [context].
19-
* Coroutine emits ([Subscriber.onNext]) values with `send`, completes ([Subscriber.onComplete])
20-
* when the coroutine completes or channel is explicitly closed and emits error ([Subscriber.onError])
21-
* if coroutine throws an exception or closes channel with a cause.
22-
* Unsubscribing cancels running coroutine.
19+
* The coroutine emits ([Subscriber.onNext]) values with [send][ProducerScope.send], completes ([Subscriber.onComplete])
20+
* when the coroutine completes, or, in case the coroutine throws an exception or the channel is closed,
21+
* emits the error ([Subscriber.onError]) and closes the channel with the cause.
22+
* Unsubscribing cancels the running coroutine.
2323
*
24-
* Invocations of `send` are suspended appropriately when subscribers apply back-pressure and to ensure that
25-
* `onNext` is not invoked concurrently.
26-
*
27-
* Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance.
24+
* Invocations of [send][ProducerScope.send] are suspended appropriately when subscribers apply back-pressure and to
25+
* ensure that [onNext][Subscriber.onNext] is not invoked concurrently.
2826
*
2927
* **Note: This is an experimental api.** Behaviour of publishers that work as children in a parent scope with respect
3028
* to cancellation and error handling may change in the future.
29+
*
30+
* @throws IllegalArgumentException if the provided [context] contains a [Job] instance.
3131
*/
3232
@ExperimentalCoroutinesApi
3333
public fun <T> flux(
@@ -43,12 +43,13 @@ private fun <T> reactorPublish(
4343
scope: CoroutineScope,
4444
context: CoroutineContext = EmptyCoroutineContext,
4545
@BuilderInference block: suspend ProducerScope<T>.() -> Unit
46-
): Publisher<T> = Publisher { subscriber ->
47-
// specification requires NPE on null subscriber
48-
if (subscriber == null) throw NullPointerException("Subscriber cannot be null")
49-
require(subscriber is CoreSubscriber) { "Subscriber is not an instance of CoreSubscriber, context can not be extracted." }
46+
): Publisher<T> = Publisher onSubscribe@{ subscriber: Subscriber<in T>? ->
47+
if (subscriber !is CoreSubscriber) {
48+
subscriber.reject(IllegalArgumentException("Subscriber is not an instance of CoreSubscriber, context can not be extracted."))
49+
return@onSubscribe
50+
}
5051
val currentContext = subscriber.currentContext()
51-
val reactorContext = (context[ReactorContext]?.context?.putAll(currentContext) ?: currentContext).asCoroutineContext()
52+
val reactorContext = context.extendReactorContext(currentContext)
5253
val newContext = scope.newCoroutineContext(context + reactorContext)
5354
val coroutine = PublisherCoroutine(newContext, subscriber, REACTOR_HANDLER)
5455
subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions
@@ -66,6 +67,23 @@ private val REACTOR_HANDLER: (Throwable, CoroutineContext) -> Unit = { cause, ct
6667
}
6768
}
6869

70+
/** The proper way to reject the subscriber, according to
71+
* [the reactive spec](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.3/README.md#1.9)
72+
*/
73+
private fun <T> Subscriber<T>?.reject(t: Throwable) {
74+
if (this == null)
75+
throw NullPointerException("The subscriber can not be null")
76+
onSubscribe(object: Subscription {
77+
override fun request(n: Long) {
78+
// intentionally left blank
79+
}
80+
override fun cancel() {
81+
// intentionally left blank
82+
}
83+
})
84+
onError(t)
85+
}
86+
6987
@Deprecated(
7088
message = "CoroutineScope.flux is deprecated in favour of top-level flux",
7189
level = DeprecationLevel.HIDDEN,

reactive/kotlinx-coroutines-reactor/src/Mono.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private fun <T> monoInternal(
8383
context: CoroutineContext,
8484
block: suspend CoroutineScope.() -> T?
8585
): Mono<T> = Mono.create { sink ->
86-
val reactorContext = (context[ReactorContext]?.context?.putAll(sink.currentContext()) ?: sink.currentContext()).asCoroutineContext()
86+
val reactorContext = context.extendReactorContext(sink.currentContext())
8787
val newContext = scope.newCoroutineContext(context + reactorContext)
8888
val coroutine = MonoCoroutine(newContext, sink)
8989
sink.onDispose(coroutine)

reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt

+30-10
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
package kotlinx.coroutines.reactor
66

77
import kotlinx.coroutines.ExperimentalCoroutinesApi
8-
import reactor.util.context.Context
98
import kotlin.coroutines.*
109
import kotlinx.coroutines.reactive.*
10+
import reactor.util.context.*
1111

1212
/**
13-
* Wraps Reactor's [Context] into [CoroutineContext] element for seamless integration Reactor and kotlinx.coroutines.
14-
* [Context.asCoroutineContext] is defined to add Reactor's [Context] elements as part of [CoroutineContext].
15-
* Coroutine context element that propagates information about Reactor's [Context] through coroutines.
13+
* Wraps Reactor's [Context] into a [CoroutineContext] element for seamless integration between
14+
* Reactor and kotlinx.coroutines.
15+
* [Context.asCoroutineContext] puts Reactor's [Context] elements into a [CoroutineContext],
16+
* which can be used to propagate the information about Reactor's [Context] through coroutines.
1617
*
17-
* This context element is implicitly propagated through subscriber's context by all Reactive integrations, such as [mono], [flux],
18-
* [Publisher.asFlow][asFlow], [Flow.asPublisher][asPublisher] and [Flow.asFlux][asFlux].
19-
* Functions that subscribe to the reactive stream (e.g. [Publisher.awaitFirst][kotlinx.coroutines.reactive.awaitFirst])
20-
* also propagate the [ReactorContext] to the subscriber's [Context].
18+
* This context element is implicitly propagated through subscribers' context by all Reactive integrations,
19+
* such as [mono], [flux], [Publisher.asFlow][asFlow], [Flow.asPublisher][asPublisher] and [Flow.asFlux][asFlux].
20+
* Functions that subscribe to a reactive stream
21+
* (e.g. [Publisher.awaitFirst][kotlinx.coroutines.reactive.awaitFirst]), too, propagate [ReactorContext]
22+
* to the subscriber's [Context].
2123
**
2224
* ### Examples of Reactive context integration.
2325
*
@@ -49,12 +51,30 @@ import kotlinx.coroutines.reactive.*
4951
*/
5052
@ExperimentalCoroutinesApi
5153
public class ReactorContext(public val context: Context) : AbstractCoroutineContextElement(ReactorContext) {
54+
55+
// `Context.of` is zero-cost if the argument is a `Context`
56+
public constructor(contextView: ContextView): this(Context.of(contextView))
57+
5258
public companion object Key : CoroutineContext.Key<ReactorContext>
5359
}
5460

5561
/**
56-
* Wraps the given [Context] into [ReactorContext], so it can be added to coroutine's context
62+
* Wraps the given [ContextView] into [ReactorContext], so it can be added to the coroutine's context
63+
* and later used via `coroutineContext[ReactorContext]`.
64+
*/
65+
@ExperimentalCoroutinesApi
66+
public fun ContextView.asCoroutineContext(): ReactorContext = ReactorContext(this)
67+
68+
/**
69+
* Wraps the given [Context] into [ReactorContext], so it can be added to the coroutine's context
5770
* and later used via `coroutineContext[ReactorContext]`.
5871
*/
5972
@ExperimentalCoroutinesApi
60-
public fun Context.asCoroutineContext(): ReactorContext = ReactorContext(this)
73+
@Deprecated("The more general version for ContextView should be used instead", level = DeprecationLevel.HIDDEN)
74+
public fun Context.asCoroutineContext(): ReactorContext = readOnly().asCoroutineContext() // `readOnly()` is zero-cost.
75+
76+
/**
77+
* Updates the Reactor context in this [CoroutineContext], adding (or possibly replacing) some values.
78+
*/
79+
internal fun CoroutineContext.extendReactorContext(extensions: ContextView): CoroutineContext =
80+
(this[ReactorContext]?.context?.putAll(extensions) ?: extensions).asCoroutineContext()

0 commit comments

Comments
 (0)