From 3fff4a7902d7e0b3ccb80f88ec144ed8a5e48700 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 24 Dec 2019 11:53:30 +0300 Subject: [PATCH 1/8] Implement Java9 reactive streams integration This was accomplished by copying `kotlinx-coroutines-reactive`. Judging by the fact that https://github.com/reactive-streams/reactive-streams-jvm/blob/master/api/src/main/java9/org/reactivestreams/FlowAdapters.java defines converters between Java9 Flow and Reactive Streams as thin wrappers that simply redirect methods, this should be enough. Divergences from `kotlinx-coroutines-reactive` are as follows: * The target bytecode is set to JDK9. * JDK11 is required to build the module. * Automated change of all occurrences `org.reactivestreams` to `java.util.concurrent.Flow`. * Removal of `RangePublisherTest`, `RangePublisherBufferdTest`, and `UnboundedIntegerIncrementPublisherTest`. They all are heavily based on the examples provided for the Reactive Streams, and to use them for testing this integration would only be possible with heavy use of `FlowAdapters`, which seems redundant as the integration with the examples themselves is already tested in `kotlinx-coroutines-reactive`, and the correctness of the wrappers is probably a given. * Use of `FlowAdapters` where needed to make everything valid code. --- README.md | 5 +- reactive/kotlinx-coroutines-jdk9/README.md | 9 + .../api/kotlinx-coroutines-jdk9.api | 69 +++++ reactive/kotlinx-coroutines-jdk9/build.gradle | 57 ++++ reactive/kotlinx-coroutines-jdk9/package.list | 1 + reactive/kotlinx-coroutines-jdk9/src/Await.kt | 161 ++++++++++ .../kotlinx-coroutines-jdk9/src/Channel.kt | 121 ++++++++ .../src/ContextInjector.kt | 15 + .../kotlinx-coroutines-jdk9/src/Convert.kt | 24 ++ .../kotlinx-coroutines-jdk9/src/Migration.kt | 36 +++ .../kotlinx-coroutines-jdk9/src/Publish.kt | 291 ++++++++++++++++++ .../src/ReactiveFlow.kt | 237 ++++++++++++++ .../test/FlowAsPublisherTest.kt | 79 +++++ .../test/IntegrationTest.kt | 149 +++++++++ .../test/IterableFlowTckTest.kt | 134 ++++++++ .../test/PublishTest.kt | 185 +++++++++++ .../test/PublisherAsFlowTest.kt | 184 +++++++++++ .../test/PublisherBackpressureTest.kt | 61 ++++ .../test/PublisherCompletionStressTest.kt | 36 +++ .../test/PublisherMultiTest.kt | 31 ++ .../test/PublisherSubscriptionSelectTest.kt | 61 ++++ .../test/ReactiveStreamTckTest.kt | 55 ++++ settings.gradle | 1 + 23 files changed, 2000 insertions(+), 2 deletions(-) create mode 100644 reactive/kotlinx-coroutines-jdk9/README.md create mode 100644 reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api create mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle create mode 100644 reactive/kotlinx-coroutines-jdk9/package.list create mode 100644 reactive/kotlinx-coroutines-jdk9/src/Await.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/src/Channel.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/src/Convert.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/src/Migration.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/src/Publish.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt create mode 100644 reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt diff --git a/README.md b/README.md index 45e8489704..330fced532 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,9 @@ suspend fun main() = coroutineScope { * [CoroutinesTimeout] test rule to automatically dump coroutines on test timeout. * [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries: * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [publish], etc), - RxJava 2.x ([rxFlowable], [rxSingle], etc), and - Project Reactor ([flux], [mono], etc). + * Flow (JDK 9) (the same interface as for Reactive Streams), + * RxJava 2.x ([rxFlowable], [rxSingle], etc), and + * Project Reactor ([flux], [mono], etc). * [ui](ui/README.md) — modules that provide coroutine dispatchers for various single-threaded UI libraries: * Android, JavaFX, and Swing. * [integration](integration/README.md) — modules that provide integration with various asynchronous callback- and future-based libraries: diff --git a/reactive/kotlinx-coroutines-jdk9/README.md b/reactive/kotlinx-coroutines-jdk9/README.md new file mode 100644 index 0000000000..9ee700b1aa --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/README.md @@ -0,0 +1,9 @@ +# Module kotlinx-coroutines-jdk9 + +Utilities for [Java Flow](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html). + +Replicates [kotlinx-coroutines-reactive](../kotlinx-coroutines-reactive), an equivalent package for the Reactive Streams. + +# Package kotlinx.coroutines.jdk9 + +Utilities for [Java Flow](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html). diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api new file mode 100644 index 0000000000..f7b6466be4 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -0,0 +1,69 @@ +public final class kotlinx/coroutines/jdk9/AwaitKt { + public static final fun awaitFirst (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirstOrDefault (Ljava/util/concurrent/Flow$Publisher;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirstOrElse (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirstOrNull (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitLast (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingle (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class kotlinx/coroutines/jdk9/ChannelKt { + public static final fun collect (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun consumeEach (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun openSubscription (Ljava/util/concurrent/Flow$Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel; + public static synthetic fun openSubscription$default (Ljava/util/concurrent/Flow$Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel; +} + +public abstract interface class kotlinx/coroutines/jdk9/ContextInjector { + public abstract fun injectCoroutineContext (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; +} + +public final class kotlinx/coroutines/jdk9/ConvertKt { + public static final fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; + public static synthetic fun asPublisher$default (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; +} + +public final class kotlinx/coroutines/jdk9/FlowKt { + public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;I)Lkotlinx/coroutines/flow/Flow; + public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; +} + +public final class kotlinx/coroutines/jdk9/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, java/util/concurrent/Flow$Subscription { + public final field flow Lkotlinx/coroutines/flow/Flow; + public final field subscriber Ljava/util/concurrent/Flow$Subscriber; + public fun (Lkotlinx/coroutines/flow/Flow;Ljava/util/concurrent/Flow$Subscriber;)V + public fun cancel ()V + public fun request (J)V +} + +public final class kotlinx/coroutines/jdk9/PublishKt { + public static final fun publish (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; + public static final fun publish (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; + public static synthetic fun publish$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; + public static synthetic fun publish$default (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; + public static final fun publishInternal (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; +} + +public final class kotlinx/coroutines/jdk9/PublisherCoroutine : kotlinx/coroutines/AbstractCoroutine, java/util/concurrent/Flow$Subscription, kotlinx/coroutines/channels/ProducerScope, kotlinx/coroutines/selects/SelectClause2 { + public fun (Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/Flow$Subscriber;Lkotlin/jvm/functions/Function2;)V + public fun cancel ()V + public fun close (Ljava/lang/Throwable;)Z + public fun getChannel ()Lkotlinx/coroutines/channels/SendChannel; + public fun getOnSend ()Lkotlinx/coroutines/selects/SelectClause2; + public fun invokeOnClose (Lkotlin/jvm/functions/Function1;)Ljava/lang/Void; + public synthetic fun invokeOnClose (Lkotlin/jvm/functions/Function1;)V + public fun isClosedForSend ()Z + public fun isFull ()Z + public fun offer (Ljava/lang/Object;)Z + public synthetic fun onCompleted (Ljava/lang/Object;)V + public fun registerSelectClause2 (Lkotlinx/coroutines/selects/SelectInstance;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V + public fun request (J)V + public fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { + public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; + public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; +} + diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle b/reactive/kotlinx-coroutines-jdk9/build.gradle new file mode 100644 index 0000000000..95b63ee4e3 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/build.gradle @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +targetCompatibility = 9 + + +dependencies { + testCompile "org.reactivestreams:reactive-streams-tck:$reactive_streams_version" + testCompile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" +} + +task testNG(type: Test) { + useTestNG() + reports.html.destination = file("$buildDir/reports/testng") + include '**/*ReactiveStreamTckTest.*' + // Skip testNG when tests are filtered with --tests, otherwise it simply fails + onlyIf { + filter.includePatterns.isEmpty() + } + doFirst { + // Classic gradle, nothing works without doFirst + println "TestNG tests: ($includes)" + } +} + +task checkJdk11() { + // only fail w/o JDK_11 when actually trying to compile, not during project setup phase + doLast { + if (!System.env.JDK_11) { + throw new GradleException("JDK_11 environment variable is not defined. " + + "Can't build against JDK 11 runtime and run JDK 11 compatibility tests. " + + "Please ensure JDK 11 is installed and that JDK_11 points to it.") + } + } +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "9" +} + +compileKotlin { + kotlinOptions.jvmTarget = "9" + kotlinOptions.jdkHome = System.env.JDK_11 + dependsOn(checkJdk11) +} + +test { + dependsOn(testNG) + reports.html.destination = file("$buildDir/reports/junit") +} + +tasks.withType(dokka.getClass()) { + externalDocumentationLink { + url = new URL("https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/package.list b/reactive/kotlinx-coroutines-jdk9/package.list new file mode 100644 index 0000000000..43e8ff22c7 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/package.list @@ -0,0 +1 @@ +java.util.concurrent.Flow diff --git a/reactive/kotlinx-coroutines-jdk9/src/Await.kt b/reactive/kotlinx-coroutines-jdk9/src/Await.kt new file mode 100644 index 0000000000..ea9cda071a --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/Await.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.suspendCancellableCoroutine +import java.util.concurrent.Flow.Publisher +import java.util.concurrent.Flow.Subscriber +import java.util.concurrent.Flow.Subscription +import java.util.* +import kotlin.coroutines.* + +/** + * Awaits for the first value from the given publisher without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + */ +public suspend fun Publisher.awaitFirst(): T = awaitOne(Mode.FIRST) + +/** + * Awaits for the first value from the given observable or the [default] value if none is emitted without blocking a + * thread and returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun Publisher.awaitFirstOrDefault(default: T): T = awaitOne(Mode.FIRST_OR_DEFAULT, default) + +/** + * Awaits for the first value from the given observable or `null` value if none is emitted without blocking a + * thread and returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun Publisher.awaitFirstOrNull(): T? = awaitOne(Mode.FIRST_OR_DEFAULT) + +/** + * Awaits for the first value from the given observable or call [defaultValue] to get a value if none is emitted without blocking a + * thread and returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun Publisher.awaitFirstOrElse(defaultValue: () -> T): T = awaitOne(Mode.FIRST_OR_DEFAULT) ?: defaultValue() + +/** + * Awaits for the last value from the given publisher without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + */ +public suspend fun Publisher.awaitLast(): T = awaitOne(Mode.LAST) + +/** + * Awaits for the single value from the given publisher without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) + +// ------------------------ private ------------------------ + +// ContextInjector service is implemented in `kotlinx-coroutines-reactor` module only. +// If `kotlinx-coroutines-reactor` module is not included, the list is empty. +private val contextInjectors: Array = + ServiceLoader.load(ContextInjector::class.java, ContextInjector::class.java.classLoader).iterator().asSequence().toList().toTypedArray() // R8 opto + +private fun Publisher.injectCoroutineContext(coroutineContext: CoroutineContext) = + contextInjectors.fold(this) { pub, contextInjector -> + contextInjector.injectCoroutineContext(pub, coroutineContext) + } + +private enum class Mode(val s: String) { + FIRST("awaitFirst"), + FIRST_OR_DEFAULT("awaitFirstOrDefault"), + LAST("awaitLast"), + SINGLE("awaitSingle"); + override fun toString(): String = s +} + +private suspend fun Publisher.awaitOne( + mode: Mode, + default: T? = null +): T = suspendCancellableCoroutine { cont -> + injectCoroutineContext(cont.context).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + private var value: T? = null + private var seenValue = false + + override fun onSubscribe(sub: Subscription) { + subscription = sub + cont.invokeOnCancellation { sub.cancel() } + sub.request(if (mode == Mode.FIRST) 1 else Long.MAX_VALUE) + } + + override fun onNext(t: T) { + when (mode) { + Mode.FIRST, Mode.FIRST_OR_DEFAULT -> { + if (!seenValue) { + seenValue = true + subscription.cancel() + cont.resume(t) + } + } + Mode.LAST, Mode.SINGLE -> { + if (mode == Mode.SINGLE && seenValue) { + subscription.cancel() + if (cont.isActive) + cont.resumeWithException(IllegalArgumentException("More than one onNext value for $mode")) + } else { + value = t + seenValue = true + } + } + } + } + + @Suppress("UNCHECKED_CAST") + override fun onComplete() { + if (seenValue) { + if (cont.isActive) cont.resume(value as T) + return + } + when { + mode == Mode.FIRST_OR_DEFAULT -> { + cont.resume(default as T) + } + cont.isActive -> { + cont.resumeWithException(NoSuchElementException("No value received via onNext for $mode")) + } + } + } + + override fun onError(e: Throwable) { + cont.resumeWithException(e) + } + }) +} + diff --git a/reactive/kotlinx-coroutines-jdk9/src/Channel.kt b/reactive/kotlinx-coroutines-jdk9/src/Channel.kt new file mode 100644 index 0000000000..385eab15f0 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/Channel.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.atomicfu.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* +import java.util.concurrent.Flow.* + +/** + * Subscribes to this [Publisher] and returns a channel to receive elements emitted by it. + * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this publisher. + + * @param request how many items to request from publisher in advance (optional, one by default). + * + * This method is deprecated in the favor of [Flow]. + * Instead of iterating over the resulting channel please use [collect][Flow.collect]: + * ``` + * asFlow().collect { value -> + * // process value + * } + * ``` + */ +@Deprecated( + message = "Transforming publisher to channel is deprecated, use asFlow() instead", + level = DeprecationLevel.WARNING) // Will be error in 1.4 +public fun Publisher.openSubscription(request: Int = 1): ReceiveChannel { + val channel = SubscriptionChannel(request) + subscribe(channel) + return channel +} + +// Will be promoted to error in 1.3.0, removed in 1.4.0 +@Deprecated(message = "Use collect instead", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.collect(action)")) +public suspend inline fun Publisher.consumeEach(action: (T) -> Unit) = + openSubscription().consumeEach(action) + +/** + * Subscribes to this [Publisher] and performs the specified action for each received element. + * Cancels subscription if any exception happens during collect. + */ +public suspend inline fun Publisher.collect(action: (T) -> Unit) = + openSubscription().consumeEach(action) + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") +private class SubscriptionChannel( + private val request: Int +) : LinkedListChannel(), Subscriber { + init { + require(request >= 0) { "Invalid request size: $request" } + } + + private val _subscription = atomic(null) + + // requested from subscription minus number of received minus number of enqueued receivers, + // can be negative if we have receivers, but no subscription yet + private val _requested = atomic(0) + + // AbstractChannel overrides + @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") + override fun onReceiveEnqueued() { + _requested.loop { wasRequested -> + val subscription = _subscription.value + val needRequested = wasRequested - 1 + if (subscription != null && needRequested < 0) { // need to request more from subscription + // try to fixup by making request + if (wasRequested != request && !_requested.compareAndSet(wasRequested, request)) + return@loop // continue looping if failed + subscription.request((request - needRequested).toLong()) + return + } + // just do book-keeping + if (_requested.compareAndSet(wasRequested, needRequested)) return + } + } + + @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") + override fun onReceiveDequeued() { + _requested.incrementAndGet() + } + + @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") + override fun onClosedIdempotent(closed: LockFreeLinkedListNode) { + _subscription.getAndSet(null)?.cancel() // cancel exactly once + } + + // Subscriber overrides + override fun onSubscribe(s: Subscription) { + _subscription.value = s + while (true) { // lock-free loop on _requested + if (isClosedForSend) { + s.cancel() + return + } + val wasRequested = _requested.value + if (wasRequested >= request) return // ok -- normal story + // otherwise, receivers came before we had subscription or need to make initial request + // try to fixup by making request + if (!_requested.compareAndSet(wasRequested, request)) continue + s.request((request - wasRequested).toLong()) + return + } + } + + override fun onNext(t: T) { + _requested.decrementAndGet() + offer(t) + } + + override fun onComplete() { + close(cause = null) + } + + override fun onError(e: Throwable) { + close(cause = e) + } +} + diff --git a/reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt b/reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt new file mode 100644 index 0000000000..7116e27e2e --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt @@ -0,0 +1,15 @@ +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.InternalCoroutinesApi +import java.util.concurrent.Flow.Publisher +import kotlin.coroutines.CoroutineContext + +/** @suppress */ +@InternalCoroutinesApi +public interface ContextInjector { + /** + * Injects `ReactorContext` element from the given context into the `SubscriberContext` of the publisher. + * This API used as an indirection layer between `reactive` and `reactor` modules. + */ + public fun injectCoroutineContext(publisher: Publisher, coroutineContext: CoroutineContext): Publisher +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/src/Convert.kt b/reactive/kotlinx-coroutines-jdk9/src/Convert.kt new file mode 100644 index 0000000000..758bd6f41e --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/Convert.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.channels.* +import java.util.concurrent.Flow.* +import kotlin.coroutines.* + +/** + * Converts a stream of elements received from the channel to the hot reactive publisher. + * + * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers, + * they'll receive values in round-robin way. + * @param context -- the coroutine context from which the resulting observable is going to be signalled + */ +@Deprecated(message = "Deprecated in the favour of consumeAsFlow()", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("this.consumeAsFlow().asPublisher()")) +public fun ReceiveChannel.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher = publish(context) { + for (t in this@asPublisher) + send(t) +} diff --git a/reactive/kotlinx-coroutines-jdk9/src/Migration.kt b/reactive/kotlinx-coroutines-jdk9/src/Migration.kt new file mode 100644 index 0000000000..dd3abbdfee --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/Migration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmMultifileClass +@file:JvmName("FlowKt") + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.util.concurrent.Flow.* + +// Binary compatibility with Spring 5.2 RC +@Deprecated( + message = "Replaced in favor of ReactiveFlow extension, please import kotlinx.coroutines.jdk9.* instead of kotlinx.coroutines.jdk9.FlowKt", + level = DeprecationLevel.ERROR +) +@JvmName("asFlow") +public fun Publisher.asFlowDeprecated(): Flow = asFlow() + +// Binary compatibility with Spring 5.2 RC +@Deprecated( + message = "Replaced in favor of ReactiveFlow extension, please import kotlinx.coroutines.jdk9.* instead of kotlinx.coroutines.jdk9.FlowKt", + level = DeprecationLevel.ERROR +) +@JvmName("asPublisher") +public fun Flow.asPublisherDeprecated(): Publisher = asPublisher() + +@FlowPreview +@Deprecated( + message = "batchSize parameter is deprecated, use .buffer() instead to control the backpressure", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("asFlow().buffer(batchSize)", imports = ["kotlinx.coroutines.flow.*"]) +) +public fun Publisher.asFlow(batchSize: Int): Flow = asFlow().buffer(batchSize) \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt new file mode 100644 index 0000000000..f2dec33428 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt @@ -0,0 +1,291 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package kotlinx.coroutines.jdk9 + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.selects.* +import kotlinx.coroutines.sync.* +import java.util.concurrent.Flow.* +import kotlin.coroutines.* +import kotlin.internal.LowPriorityInOverloadResolution + +/** + * Creates cold reactive [Publisher] that runs a given [block] in a coroutine. + * Every time the returned flux is subscribed, it starts a new coroutine in the specified [context]. + * Coroutine emits ([Subscriber.onNext]) values with `send`, completes ([Subscriber.onComplete]) + * when the coroutine completes or channel is explicitly closed and emits error ([Subscriber.onError]) + * if coroutine throws an exception or closes channel with a cause. + * Unsubscribing cancels running coroutine. + * + * Invocations of `send` are suspended appropriately when subscribers apply back-pressure and to ensure that + * `onNext` is not invoked concurrently. + * + * Coroutine context can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance. + * + * **Note: This is an experimental api.** Behaviour of publishers that work as children in a parent scope with respect + * to cancellation and error handling may change in the future. + */ +@ExperimentalCoroutinesApi +public fun publish( + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference block: suspend ProducerScope.() -> Unit +): Publisher { + require(context[Job] === null) { "Publisher context cannot contain job in it." + + "Its lifecycle should be managed via subscription. Had $context" } + return publishInternal(GlobalScope, context, DEFAULT_HANDLER, block) +} + +@Deprecated( + message = "CoroutineScope.publish is deprecated in favour of top-level publish", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("publish(context, block)") +) // Since 1.3.0, will be error in 1.3.1 and hidden in 1.4.0. Binary compatibility with Spring +@LowPriorityInOverloadResolution +public fun CoroutineScope.publish( + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference block: suspend ProducerScope.() -> Unit +): Publisher = publishInternal(this, context, DEFAULT_HANDLER ,block) + +/** @suppress For internal use from other reactive integration modules only */ +@InternalCoroutinesApi +public fun publishInternal( + scope: CoroutineScope, // support for legacy publish in scope + context: CoroutineContext, + exceptionOnCancelHandler: (Throwable, CoroutineContext) -> Unit, + block: suspend ProducerScope.() -> Unit +): Publisher = Publisher { subscriber -> + // specification requires NPE on null subscriber + if (subscriber == null) throw NullPointerException("Subscriber cannot be null") + val newContext = scope.newCoroutineContext(context) + val coroutine = PublisherCoroutine(newContext, subscriber, exceptionOnCancelHandler) + subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) +} + +private const val CLOSED = -1L // closed, but have not signalled onCompleted/onError yet +private const val SIGNALLED = -2L // already signalled subscriber onCompleted/onError +private val DEFAULT_HANDLER: (Throwable, CoroutineContext) -> Unit = { t, ctx -> if (t !is CancellationException) handleCoroutineException(ctx, t) } + +@Suppress("CONFLICTING_JVM_DECLARATIONS", "RETURN_TYPE_MISMATCH_ON_INHERITANCE") +@InternalCoroutinesApi +public class PublisherCoroutine( + parentContext: CoroutineContext, + private val subscriber: Subscriber, + private val exceptionOnCancelHandler: (Throwable, CoroutineContext) -> Unit +) : AbstractCoroutine(parentContext, true), ProducerScope, Subscription, SelectClause2> { + override val channel: SendChannel get() = this + + // Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked + private val mutex = Mutex(locked = true) + private val _nRequested = atomic(0L) // < 0 when closed (CLOSED or SIGNALLED) + + @Volatile + private var cancelled = false // true when Subscription.cancel() is invoked + + override val isClosedForSend: Boolean get() = isCompleted + override val isFull: Boolean = mutex.isLocked + override fun close(cause: Throwable?): Boolean = cancelCoroutine(cause) + override fun invokeOnClose(handler: (Throwable?) -> Unit) = + throw UnsupportedOperationException("PublisherCoroutine doesn't support invokeOnClose") + + override fun offer(element: T): Boolean { + if (!mutex.tryLock()) return false + doLockedNext(element) + return true + } + + public override suspend fun send(element: T) { + // fast-path -- try send without suspension + if (offer(element)) return + // slow-path does suspend + return sendSuspend(element) + } + + private suspend fun sendSuspend(element: T) { + mutex.lock() + doLockedNext(element) + } + + override val onSend: SelectClause2> + get() = this + + // registerSelectSend + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun registerSelectClause2(select: SelectInstance, element: T, block: suspend (SendChannel) -> R) { + mutex.onLock.registerSelectClause2(select, null) { + doLockedNext(element) + block(this) + } + } + + /* + * This code is not trivial because of the two properties: + * 1. It ensures conformance to the reactive specification that mandates that onXXX invocations should not + * be concurrent. It uses Mutex to protect all onXXX invocation and ensure conformance even when multiple + * coroutines are invoking `send` function. + * 2. Normally, `onComplete/onError` notification is sent only when coroutine and all its children are complete. + * However, nothing prevents `publish` coroutine from leaking reference to it send channel to some + * globally-scoped coroutine that is invoking `send` outside of this context. Without extra precaution this may + * lead to `onNext` that is concurrent with `onComplete/onError`, so that is why signalling for + * `onComplete/onError` is also done under the same mutex. + */ + + // assert: mutex.isLocked() + private fun doLockedNext(elem: T) { + // check if already closed for send, note that isActive becomes false as soon as cancel() is invoked, + // because the job is cancelled, so this check also ensure conformance to the reactive specification's + // requirement that after cancellation requested we don't call onXXX + if (!isActive) { + unlockAndCheckCompleted() + throw getCancellationException() + } + // notify subscriber + try { + subscriber.onNext(elem) + } catch (e: Throwable) { + // If onNext fails with exception, then we cancel coroutine (with this exception) and then rethrow it + // to abort the corresponding send/offer invocation. From the standpoint of coroutines machinery, + // this failure is essentially equivalent to a failure of a child coroutine. + cancelCoroutine(e) + unlockAndCheckCompleted() + throw e + } + // now update nRequested + while (true) { // lock-free loop on nRequested + val current = _nRequested.value + if (current < 0) break // closed from inside onNext => unlock + if (current == Long.MAX_VALUE) break // no back-pressure => unlock + val updated = current - 1 + if (_nRequested.compareAndSet(current, updated)) { + if (updated == 0L) { + // return to keep locked due to back-pressure + return + } + break // unlock if updated > 0 + } + } + unlockAndCheckCompleted() + } + + private fun unlockAndCheckCompleted() { + /* + * There is no sense to check completion before doing `unlock`, because completion might + * happen after this check and before `unlock` (see `signalCompleted` that does not do anything + * if it fails to acquire the lock that we are still holding). + * We have to recheck `isCompleted` after `unlock` anyway. + */ + mutex.unlock() + // check isCompleted and and try to regain lock to signal completion + if (isCompleted && mutex.tryLock()) { + doLockedSignalCompleted(completionCause, completionCauseHandled) + } + } + + // assert: mutex.isLocked() & isCompleted + private fun doLockedSignalCompleted(cause: Throwable?, handled: Boolean) { + try { + if (_nRequested.value >= CLOSED) { + _nRequested.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed) + // Specification requires that after cancellation requested we don't call onXXX + if (cancelled) { + // If the parent had failed to handle our exception, then we must not lose this exception + if (cause != null && !handled) exceptionOnCancelHandler(cause, context) + return + } + + try { + if (cause != null && cause !is CancellationException) { + /* + * Reactive frameworks have two types of exceptions: regular and fatal. + * Regular are passed to onError. + * Fatal can be passed to onError, but even the standard implementations **can just swallow it** (e.g. see #1297). + * Such behaviour is inconsistent, leads to silent failures and we can't possibly know whether + * the cause will be handled by onError (and moreover, it depends on whether a fatal exception was + * thrown by subscriber or upstream). + * To make behaviour consistent and least surprising, we always handle fatal exceptions + * by coroutines machinery, anyway, they should not be present in regular program flow, + * thus our goal here is just to expose it as soon as possible. + */ + subscriber.onError(cause) + if (!handled && cause.isFatal()) { + exceptionOnCancelHandler(cause, context) + } + } else { + subscriber.onComplete() + } + } catch (e: Throwable) { + handleCoroutineException(context, e) + } + } + } finally { + mutex.unlock() + } + } + + override fun request(n: Long) { + if (n <= 0) { + // Specification requires IAE for n <= 0 + cancelCoroutine(IllegalArgumentException("non-positive subscription request $n")) + return + } + while (true) { // lock-free loop for nRequested + val cur = _nRequested.value + if (cur < 0) return // already closed for send, ignore requests + var upd = cur + n + if (upd < 0 || n == Long.MAX_VALUE) + upd = Long.MAX_VALUE + if (cur == upd) return // nothing to do + if (_nRequested.compareAndSet(cur, upd)) { + // unlock the mutex when we don't have back-pressure anymore + if (cur == 0L) { + unlockAndCheckCompleted() + } + return + } + } + } + + // assert: isCompleted + private fun signalCompleted(cause: Throwable?, handled: Boolean) { + while (true) { // lock-free loop for nRequested + val current = _nRequested.value + if (current == SIGNALLED) return // some other thread holding lock already signalled cancellation/completion + check(current >= 0) // no other thread could have marked it as CLOSED, because onCompleted[Exceptionally] is invoked once + if (!_nRequested.compareAndSet(current, CLOSED)) continue // retry on failed CAS + // Ok -- marked as CLOSED, now can unlock the mutex if it was locked due to backpressure + if (current == 0L) { + doLockedSignalCompleted(cause, handled) + } else { + // otherwise mutex was either not locked or locked in concurrent onNext... try lock it to signal completion + if (mutex.tryLock()) doLockedSignalCompleted(cause, handled) + // Note: if failed `tryLock`, then `doLockedNext` will signal after performing `unlock` + } + return // done anyway + } + } + + override fun onCompleted(value: Unit) { + signalCompleted(null, false) + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + signalCompleted(cause, handled) + } + + override fun cancel() { + // Specification requires that after cancellation publisher stops signalling + // This flag distinguishes subscription cancellation request from the job crash + cancelled = true + super.cancel(null) + } + + private fun Throwable.isFatal() = this is VirtualMachineError || this is ThreadDeath || this is LinkageError +} diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt new file mode 100644 index 0000000000..a1469d72af --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.intrinsics.* +import java.util.concurrent.Flow.* +import java.util.* +import kotlin.coroutines.* + +/** + * Transforms the given reactive [Publisher] into [Flow]. + * Use [buffer] operator on the resulting flow to specify the size of the backpressure. + * More precisely, it specifies the value of the subscription's [request][Subscription.request]. + * [buffer] default capacity is used by default. + * + * If any of the resulting flow transformations fails, subscription is immediately cancelled and all in-flight elements + * are discarded. + * + * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, + * see its documentation for additional details. + */ +public fun Publisher.asFlow(): Flow = + PublisherAsFlow(this) + +/** + * Transforms the given flow to a reactive specification compliant [Publisher]. + * + * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, + * see its documentation for additional details. + */ +public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) + +private class PublisherAsFlow( + private val publisher: Publisher, + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = Channel.BUFFERED +) : ChannelFlow(context, capacity) { + override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = + PublisherAsFlow(publisher, context, capacity) + + /* + * Suppress for Channel.CHANNEL_DEFAULT_CAPACITY. + * It's too counter-intuitive to be public and moving it to Flow companion + * will also create undesired effect. + */ + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + private val requestSize: Long + get() = when (capacity) { + Channel.CONFLATED -> Long.MAX_VALUE // request all and conflate incoming + Channel.RENDEZVOUS -> 1L // need to request at least one anyway + Channel.UNLIMITED -> Long.MAX_VALUE // reactive streams way to say "give all" must be Long.MAX_VALUE + Channel.BUFFERED -> Channel.CHANNEL_DEFAULT_CAPACITY.toLong() + else -> capacity.toLong().also { check(it >= 1) } + } + + override suspend fun collect(collector: FlowCollector) { + val collectContext = coroutineContext + val newDispatcher = context[ContinuationInterceptor] + if (newDispatcher == null || newDispatcher == collectContext[ContinuationInterceptor]) { + // fast path -- subscribe directly in this dispatcher + return collectImpl(collectContext + context, collector) + } + // slow path -- produce in a separate dispatcher + collectSlowPath(collector) + } + + private suspend fun collectSlowPath(collector: FlowCollector) { + coroutineScope { + collector.emitAll(produceImpl(this + context)) + } + } + + private suspend fun collectImpl(injectContext: CoroutineContext, collector: FlowCollector) { + val subscriber = ReactiveSubscriber(capacity, requestSize) + // inject subscribe context into publisher + publisher.injectCoroutineContext(injectContext).subscribe(subscriber) + try { + var consumed = 0L + while (true) { + val value = subscriber.takeNextOrNull() ?: break + collector.emit(value) + if (++consumed == requestSize) { + consumed = 0L + subscriber.makeRequest() + } + } + } finally { + subscriber.cancel() + } + } + + // The second channel here is used for produceIn/broadcastIn and slow-path (dispatcher change) + override suspend fun collectTo(scope: ProducerScope) = + collectImpl(scope.coroutineContext, SendingCollector(scope.channel)) +} + +@Suppress("SubscriberImplementation") +private class ReactiveSubscriber( + capacity: Int, + private val requestSize: Long +) : Subscriber { + private lateinit var subscription: Subscription + private val channel = Channel(capacity) + + suspend fun takeNextOrNull(): T? = channel.receiveOrNull() + + override fun onNext(value: T) { + // Controlled by requestSize + require(channel.offer(value)) { "Element $value was not added to channel because it was full, $channel" } + } + + override fun onComplete() { + channel.close() + } + + override fun onError(t: Throwable?) { + channel.close(t) + } + + override fun onSubscribe(s: Subscription) { + subscription = s + makeRequest() + } + + fun makeRequest() { + subscription.request(requestSize) + } + + fun cancel() { + subscription.cancel() + } +} + +// ContextInjector service is implemented in `kotlinx-coroutines-reactor` module only. +// If `kotlinx-coroutines-reactor` module is not included, the list is empty. +private val contextInjectors: List = + ServiceLoader.load(ContextInjector::class.java, ContextInjector::class.java.classLoader).toList() + +private fun Publisher.injectCoroutineContext(coroutineContext: CoroutineContext) = + contextInjectors.fold(this) { pub, contextInjector -> contextInjector.injectCoroutineContext(pub, coroutineContext) } + + +/** + * Adapter that transforms [Flow] into TCK-complaint [Publisher]. + * [cancel] invocation cancels the original flow. + */ +@Suppress("PublisherImplementation") +private class FlowAsPublisher(private val flow: Flow) : Publisher { + override fun subscribe(subscriber: Subscriber?) { + if (subscriber == null) throw NullPointerException() + subscriber.onSubscribe(FlowSubscription(flow, subscriber)) + } +} + +/** @suppress */ +@InternalCoroutinesApi +public class FlowSubscription( + @JvmField val flow: Flow, + @JvmField val subscriber: Subscriber +) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, false) { + private val requested = atomic(0L) + private val producer = atomic?>(null) + + override fun onStart() { + ::flowProcessing.startCoroutineCancellable(this) + } + + private suspend fun flowProcessing() { + try { + consumeFlow() + subscriber.onComplete() + } catch (e: Throwable) { + try { + if (e is CancellationException) { + subscriber.onComplete() + } else { + subscriber.onError(e) + } + } catch (e: Throwable) { + // Last ditch report + handleCoroutineException(coroutineContext, e) + } + } + } + + /* + * This method has at most one caller at any time (triggered from the `request` method) + */ + private suspend fun consumeFlow() { + flow.collect { value -> + /* + * Flow is scopeless, thus if it's not active, its subscription was cancelled. + * No intermediate "child failed, but flow coroutine is not" states are allowed. + */ + coroutineContext.ensureActive() + if (requested.value <= 0L) { + suspendCancellableCoroutine { + producer.value = it + if (requested.value != 0L) it.resumeSafely() + } + } + requested.decrementAndGet() + subscriber.onNext(value) + } + } + + override fun cancel() { + cancel(null) + } + + override fun request(n: Long) { + if (n <= 0) { + return + } + start() + requested.update { value -> + val newValue = value + n + if (newValue <= 0L) Long.MAX_VALUE else newValue + } + val producer = producer.getAndSet(null) ?: return + producer.resumeSafely() + } + + private fun CancellableContinuation.resumeSafely() { + val token = tryResume(Unit) + if (token != null) { + completeResume(token) + } + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt new file mode 100644 index 0000000000..898feaccc5 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import java.util.concurrent.Flow.* +import kotlin.test.* + +class FlowAsPublisherTest : TestBase() { + + @Test + fun testErrorOnCancellationIsReported() { + expect(1) + flow { + emit(2) + try { + hang { expect(3) } + } finally { + throw TestException() + } + }.asPublisher().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onComplete() { + expectUnreached() + } + + override fun onSubscribe(s: Subscription?) { + subscription = s!! + subscription.request(2) + } + + override fun onNext(t: Int) { + expect(t) + subscription.cancel() + } + + override fun onError(t: Throwable?) { + assertTrue(t is TestException) + expect(4) + } + }) + finish(5) + } + + @Test + fun testCancellationIsNotReported() { + expect(1) + flow { + emit(2) + hang { expect(3) } + }.asPublisher().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onComplete() { + expect(4) + } + + override fun onSubscribe(s: Subscription?) { + subscription = s!! + subscription.request(2) + } + + override fun onNext(t: Int) { + expect(t) + subscription.cancel() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt new file mode 100644 index 0000000000..9f9aafa9c5 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import java.util.concurrent.Flow.* +import kotlin.coroutines.* +import kotlin.test.* + +@RunWith(Parameterized::class) +class IntegrationTest( + private val ctx: Ctx, + private val delay: Boolean +) : TestBase() { + + enum class Ctx { + MAIN { override fun invoke(context: CoroutineContext): CoroutineContext = context.minusKey(Job) }, + DEFAULT { override fun invoke(context: CoroutineContext): CoroutineContext = Dispatchers.Default }, + UNCONFINED { override fun invoke(context: CoroutineContext): CoroutineContext = Dispatchers.Unconfined }; + + abstract operator fun invoke(context: CoroutineContext): CoroutineContext + } + + companion object { + @Parameterized.Parameters(name = "ctx={0}, delay={1}") + @JvmStatic + fun params(): Collection> = Ctx.values().flatMap { ctx -> + listOf(false, true).map { delay -> + arrayOf(ctx, delay) + } + } + } + + @Test + fun testEmpty(): Unit = runBlocking { + val pub = publish(ctx(coroutineContext)) { + if (delay) delay(1) + // does not send anything + } + assertNSE { pub.awaitFirst() } + assertEquals("OK", pub.awaitFirstOrDefault("OK")) + assertNull(pub.awaitFirstOrNull()) + assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) + assertNSE { pub.awaitLast() } + assertNSE { pub.awaitSingle() } + var cnt = 0 + pub.collect { cnt++ } + assertEquals(0, cnt) + } + + @Test + fun testSingle() = runBlocking { + val pub = publish(ctx(coroutineContext)) { + if (delay) delay(1) + send("OK") + } + assertEquals("OK", pub.awaitFirst()) + assertEquals("OK", pub.awaitFirstOrDefault("!")) + assertEquals("OK", pub.awaitFirstOrNull()) + assertEquals("OK", pub.awaitFirstOrElse { "ELSE" }) + assertEquals("OK", pub.awaitLast()) + assertEquals("OK", pub.awaitSingle()) + var cnt = 0 + pub.collect { + assertEquals("OK", it) + cnt++ + } + assertEquals(1, cnt) + } + + @Test + fun testNumbers() = runBlocking { + val n = 100 * stressTestMultiplier + val pub = publish(ctx(coroutineContext)) { + for (i in 1..n) { + send(i) + if (delay) delay(1) + } + } + assertEquals(1, pub.awaitFirst()) + assertEquals(1, pub.awaitFirstOrDefault(0)) + assertEquals(n, pub.awaitLast()) + assertEquals(1, pub.awaitFirstOrNull()) + assertEquals(1, pub.awaitFirstOrElse { 0 }) + assertIAE { pub.awaitSingle() } + checkNumbers(n, pub) + val channel = pub.openSubscription() + checkNumbers(n, channel.asPublisher(ctx(coroutineContext))) + channel.cancel() + } + + @Test + fun testCancelWithoutValue() = runTest { + val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { + publish { + hang {} + }.awaitFirst() + } + + job.cancel() + job.join() + } + + @Test + fun testEmptySingle() = runTest(unhandled = listOf({e -> e is NoSuchElementException})) { + expect(1) + val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { + publish { + yield() + expect(2) + // Nothing to emit + }.awaitFirst() + } + + job.join() + finish(3) + } + + private suspend fun checkNumbers(n: Int, pub: Publisher) { + var last = 0 + pub.collect { + assertEquals(++last, it) + } + assertEquals(n, last) + } + + private inline fun assertIAE(block: () -> Unit) { + try { + block() + expectUnreached() + } catch (e: Throwable) { + assertTrue(e is IllegalArgumentException) + } + } + + private inline fun assertNSE(block: () -> Unit) { + try { + block() + expectUnreached() + } catch (e: Throwable) { + assertTrue(e is NoSuchElementException) + } + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt b/reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt new file mode 100644 index 0000000000..cf04857ebc --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UNCHECKED_CAST") + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.flow.* +import org.junit.Test +import java.util.concurrent.Flow +import org.reactivestreams.tck.* +import org.reactivestreams.Publisher +import org.reactivestreams.FlowAdapters + +import java.util.concurrent.Flow.Subscription +import java.util.concurrent.Flow.Subscriber +import java.util.ArrayList +import java.util.concurrent.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ForkJoinPool.commonPool +import kotlin.test.* + +class IterableFlowTckTest : PublisherVerification(TestEnvironment()) { + + private fun generate(num: Long): Array { + return Array(if (num >= Integer.MAX_VALUE) 1000000 else num.toInt()) { it.toLong() } + } + + override fun createPublisher(elements: Long): Publisher { + return FlowAdapters.toPublisher(generate(elements).asIterable().asFlow().asPublisher()) + } + + @Suppress("SubscriberImplementation") + override fun createFailedPublisher(): Publisher? { + /* + * This is a hack for our adapter structure: + * Tests assume that calling "collect" is enough for publisher to fail and it is not + * true for our implementation + */ + val pub = { error(42) }.asFlow().asPublisher() + return FlowAdapters.toPublisher(Flow.Publisher { subscriber -> + pub.subscribe(object : Subscriber by subscriber as Subscriber { + override fun onSubscribe(s: Subscription) { + subscriber.onSubscribe(s) + s.request(1) + } + }) + }) + } + + @Test + fun testStackOverflowTrampoline() { + val latch = CountDownLatch(1) + val collected = ArrayList() + val toRequest = 1000L + val array = generate(toRequest) + val publisher = array.asIterable().asFlow().asPublisher() + + publisher.subscribe(object : Subscriber { + private lateinit var s: Subscription + + override fun onSubscribe(s: Subscription) { + this.s = s + s.request(1) + } + + override fun onNext(aLong: Long) { + collected.add(aLong) + + s.request(1) + } + + override fun onError(t: Throwable) { + + } + + override fun onComplete() { + latch.countDown() + } + }) + + latch.await(5, TimeUnit.SECONDS) + assertEquals(collected, array.toList()) + } + + @Test + fun testConcurrentRequest() { + val latch = CountDownLatch(1) + val collected = ArrayList() + val n = 50000L + val array = generate(n) + val publisher = array.asIterable().asFlow().asPublisher() + + publisher.subscribe(object : Subscriber { + private var s: Subscription? = null + + override fun onSubscribe(s: Subscription) { + this.s = s + for (i in 0 until n) { + commonPool().execute { s.request(1) } + } + } + + override fun onNext(aLong: Long) { + collected.add(aLong) + } + + override fun onError(t: Throwable) { + + } + + override fun onComplete() { + latch.countDown() + } + }) + + latch.await(50, TimeUnit.SECONDS) + assertEquals(array.toList(), collected) + } + + @Ignore + override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() { + // This test has a bug in it + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt new file mode 100644 index 0000000000..4c53389dcd --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import org.junit.Test +import java.util.concurrent.Flow.* +import kotlin.test.* + +class PublishTest : TestBase() { + @Test + fun testBasicEmpty() = runTest { + expect(1) + val publisher = publish(currentDispatcher()) { + expect(5) + } + expect(2) + publisher.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription?) { expect(3) } + override fun onNext(t: Int?) { expectUnreached() } + override fun onComplete() { expect(6) } + override fun onError(t: Throwable?) { expectUnreached() } + }) + expect(4) + yield() // to publish coroutine + finish(7) + } + + @Test + fun testBasicSingle() = runTest { + expect(1) + val publisher = publish(currentDispatcher()) { + expect(5) + send(42) + expect(7) + } + expect(2) + publisher.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription) { + expect(3) + s.request(1) + } + override fun onNext(t: Int) { + expect(6) + assertEquals(42, t) + } + override fun onComplete() { expect(8) } + override fun onError(t: Throwable?) { expectUnreached() } + }) + expect(4) + yield() // to publish coroutine + finish(9) + } + + @Test + fun testBasicError() = runTest { + expect(1) + val publisher = publish(currentDispatcher()) { + expect(5) + throw RuntimeException("OK") + } + expect(2) + publisher.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription) { + expect(3) + s.request(1) + } + override fun onNext(t: Int) { expectUnreached() } + override fun onComplete() { expectUnreached() } + override fun onError(t: Throwable) { + expect(6) + assertTrue(t is RuntimeException) + assertEquals("OK", t.message) + } + }) + expect(4) + yield() // to publish coroutine + finish(7) + } + + @Test + fun testHandleFailureAfterCancel() = runTest { + expect(1) + + val eh = CoroutineExceptionHandler { _, t -> + assertTrue(t is RuntimeException) + expect(6) + } + val publisher = publish(Dispatchers.Unconfined + eh) { + try { + expect(3) + delay(10000) + } finally { + expect(5) + throw RuntimeException("FAILED") // crash after cancel + } + } + var sub: Subscription? = null + publisher.subscribe(object : Subscriber { + override fun onComplete() { + expectUnreached() + } + + override fun onSubscribe(s: Subscription) { + expect(2) + sub = s + } + + override fun onNext(t: Unit?) { + expectUnreached() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + expect(4) + sub!!.cancel() + finish(7) + } + + @Test + fun testOnNextError() = runTest { + expect(1) + val publisher = publish(currentDispatcher()) { + expect(4) + try { + send("OK") + } catch(e: Throwable) { + expect(6) + assert(e is TestException) + } + } + expect(2) + val latch = CompletableDeferred() + publisher.subscribe(object : Subscriber { + override fun onComplete() { + expectUnreached() + } + + override fun onSubscribe(s: Subscription) { + expect(3) + s.request(1) + } + + override fun onNext(t: String) { + expect(5) + assertEquals("OK", t) + throw TestException() + } + + override fun onError(t: Throwable) { + expect(7) + assert(t is TestException) + latch.complete(Unit) + } + }) + latch.await() + finish(8) + } + + @Test + fun testFailingConsumer() = runTest { + val pub = publish(currentDispatcher()) { + repeat(3) { + expect(it + 1) // expect(1), expect(2) *should* be invoked + send(it) + } + } + try { + pub.collect { + throw TestException() + } + } catch (e: TestException) { + finish(3) + } + } + + @Test + fun testIllegalArgumentException() { + assertFailsWith { publish(Job()) { } } + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt new file mode 100644 index 0000000000..b608c6b863 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class PublisherAsFlowTest : TestBase() { + @Test + fun testCancellation() = runTest { + var onNext = 0 + var onCancelled = 0 + var onError = 0 + + val publisher = publish(currentDispatcher()) { + coroutineContext[Job]?.invokeOnCompletion { + if (it is CancellationException) ++onCancelled + } + + repeat(100) { + send(it) + } + } + + publisher.asFlow().launchIn(CoroutineScope(Dispatchers.Unconfined)) { + onEach { + ++onNext + throw RuntimeException() + } + catch { + ++onError + } + }.join() + + + assertEquals(1, onNext) + assertEquals(1, onError) + assertEquals(1, onCancelled) + } + + @Test + fun testBufferSize1() = runTest { + val publisher = publish(currentDispatcher()) { + expect(1) + send(3) + + expect(2) + send(5) + + expect(4) + send(7) + expect(6) + } + + publisher.asFlow().buffer(1).collect { + expect(it) + } + + finish(8) + } + + @Test + fun testBufferSizeDefault() = runTest { + val publisher = publish(currentDispatcher()) { + repeat(64) { + send(it + 1) + expect(it + 1) + } + assertFalse { offer(-1) } + } + + publisher.asFlow().collect { + expect(64 + it) + } + + finish(129) + } + + @Test + fun testDefaultCapacityIsProperlyOverwritten() = runTest { + val publisher = publish(currentDispatcher()) { + expect(1) + send(3) + expect(2) + send(5) + expect(4) + send(7) + expect(6) + } + + publisher.asFlow().flowOn(wrapperDispatcher()).buffer(1).collect { + expect(it) + } + + finish(8) + } + + @Test + fun testBufferSize10() = runTest { + val publisher = publish(currentDispatcher()) { + expect(1) + send(5) + + expect(2) + send(6) + + expect(3) + send(7) + expect(4) + } + + publisher.asFlow().buffer(10).collect { + expect(it) + } + + finish(8) + } + + @Test + fun testConflated() = runTest { + val publisher = publish(currentDispatcher()) { + for (i in 1..5) send(i) + } + val list = publisher.asFlow().conflate().toList() + assertEquals(listOf(1, 5), list) + } + + @Test + fun testProduce() = runTest { + val flow = publish(currentDispatcher()) { repeat(10) { send(it) } }.asFlow() + check((0..9).toList(), flow.produceIn(this)) + check((0..9).toList(), flow.buffer(2).produceIn(this)) + check((0..9).toList(), flow.buffer(Channel.UNLIMITED).produceIn(this)) + check(listOf(0, 9), flow.conflate().produceIn(this)) + } + + private suspend fun check(expected: List, channel: ReceiveChannel) { + val result = ArrayList(10) + channel.consumeEach { result.add(it) } + assertEquals(expected, result) + } + + @Test + fun testProduceCancellation() = runTest { + expect(1) + // publisher is an async coroutine, so it overproduces to the channel, but still gets cancelled + val flow = publish(currentDispatcher()) { + expect(3) + repeat(10) { value -> + when (value) { + in 0..6 -> send(value) + 7 -> try { + send(value) + } catch (e: CancellationException) { + expect(5) + throw e + } + else -> expectUnreached() + } + } + }.asFlow().buffer(1) + assertFailsWith { + coroutineScope { + expect(2) + val channel = flow.produceIn(this) + channel.consumeEach { value -> + when (value) { + in 0..4 -> {} + 5 -> { + expect(4) + throw TestException() + } + else -> expectUnreached() + } + } + } + } + finish(6) + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt new file mode 100644 index 0000000000..fda133a279 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import org.junit.* +import java.util.concurrent.Flow.* + +class PublisherBackpressureTest : TestBase() { + @Test + fun testCancelWhileBPSuspended() = runBlocking { + expect(1) + val observable = publish(currentDispatcher()) { + expect(5) + send("A") // will not suspend, because an item was requested + expect(7) + send("B") // second requested item + expect(9) + try { + send("C") // will suspend (no more requested) + } finally { + expect(12) + } + expectUnreached() + } + expect(2) + var sub: Subscription? = null + observable.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription) { + sub = s + expect(3) + s.request(2) // request two items + } + + override fun onNext(t: String) { + when (t) { + "A" -> expect(6) + "B" -> expect(8) + else -> error("Should not happen") + } + } + + override fun onComplete() { + expectUnreached() + } + + override fun onError(e: Throwable) { + expectUnreached() + } + }) + expect(4) + yield() // yield to observable coroutine + expect(10) + sub!!.cancel() // now unsubscribe -- shall cancel coroutine (& do not signal) + expect(11) + yield() // shall perform finally in coroutine + finish(13) + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt new file mode 100644 index 0000000000..cf89c6a51d --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import org.junit.* +import java.util.* +import kotlin.coroutines.* + +class PublisherCompletionStressTest : TestBase() { + private val N_REPEATS = 10_000 * stressTestMultiplier + + private fun CoroutineScope.range(context: CoroutineContext, start: Int, count: Int) = publish(context) { + for (x in start until start + count) send(x) + } + + @Test + fun testCompletion() { + val rnd = Random() + repeat(N_REPEATS) { + val count = rnd.nextInt(5) + runBlocking { + withTimeout(5000) { + var received = 0 + range(Dispatchers.Default, 1, count).collect { x -> + received++ + if (x != received) error("$x != $received") + } + if (received != count) error("$received != $count") + } + } + } + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt new file mode 100644 index 0000000000..4ea467e695 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.* + +class PublisherMultiTest : TestBase() { + @Test + fun testConcurrentStress() = runBlocking { + val n = 10_000 * stressTestMultiplier + val observable = publish { + // concurrent emitters (many coroutines) + val jobs = List(n) { + // launch + launch { + send(it) + } + } + jobs.forEach { it.join() } + } + val resultSet = mutableSetOf() + observable.collect { + assertTrue(resultSet.add(it)) + } + assertEquals(n, resultSet.size) + } +} diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt new file mode 100644 index 0000000000..fab59c9ec2 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import kotlin.test.* + +@RunWith(Parameterized::class) +class PublisherSubscriptionSelectTest(private val request: Int) : TestBase() { + companion object { + @Parameterized.Parameters(name = "request = {0}") + @JvmStatic + fun params(): Collection> = listOf(0, 1, 10).map { arrayOf(it) } + } + + @Test + fun testSelect() = runTest { + // source with n ints + val n = 1000 * stressTestMultiplier + val source = publish { repeat(n) { send(it) } } + var a = 0 + var b = 0 + // open two subs + val channelA = source.openSubscription(request) + val channelB = source.openSubscription(request) + loop@ while (true) { + val done: Int = select { + channelA.onReceiveOrNull { + if (it != null) assertEquals(a++, it) + if (it == null) 0 else 1 + } + channelB.onReceiveOrNull { + if (it != null) assertEquals(b++, it) + if (it == null) 0 else 2 + } + } + when (done) { + 0 -> break@loop + 1 -> { + val r = channelB.receiveOrNull() + if (r != null) assertEquals(b++, r) + } + 2 -> { + val r = channelA.receiveOrNull() + if (r != null) assertEquals(a++, r) + } + } + } + + channelA.cancel() + channelB.cancel() + // should receive one of them fully + assertTrue(a == n || b == n) + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt b/reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt new file mode 100644 index 0000000000..fa38234e59 --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.jdk9 + +import kotlinx.coroutines.* +import org.reactivestreams.FlowAdapters +import org.reactivestreams.Publisher +import org.reactivestreams.tck.* +import org.testng.* +import org.testng.annotations.* + + +class ReactiveStreamTckTest : TestBase() { + + @Factory(dataProvider = "dispatchers") + fun createTests(dispatcher: Dispatcher): Array { + return arrayOf(ReactiveStreamTckTestSuite(dispatcher)) + } + + @DataProvider(name = "dispatchers") + public fun dispatchers(): Array> = Dispatcher.values().map { arrayOf(it) }.toTypedArray() + + + public class ReactiveStreamTckTestSuite( + private val dispatcher: Dispatcher + ) : PublisherVerification(TestEnvironment(500, 500)) { + + override fun createPublisher(elements: Long): Publisher = + FlowAdapters.toPublisher( + publish(dispatcher.dispatcher) { + for (i in 1..elements) send(i) + }) + + override fun createFailedPublisher(): Publisher = + FlowAdapters.toPublisher( + publish(dispatcher.dispatcher) { + throw TestException() + } + ) + + @Test + public override fun optional_spec105_emptyStreamMustTerminateBySignallingOnComplete() { + throw SkipException("Skipped") + } + + class TestException : Exception() + } +} + +enum class Dispatcher(val dispatcher: CoroutineDispatcher) { + DEFAULT(Dispatchers.Default), + UNCONFINED(Dispatchers.Unconfined) +} diff --git a/settings.gradle b/settings.gradle index 15d377d3f9..27a971f19b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ module('integration/kotlinx-coroutines-play-services') module('reactive/kotlinx-coroutines-reactive') module('reactive/kotlinx-coroutines-reactor') +module('reactive/kotlinx-coroutines-jdk9') module('reactive/kotlinx-coroutines-rx2') module('ui/kotlinx-coroutines-android') module('ui/kotlinx-coroutines-android/android-unit-tests') From f5b488ad5b14b914a813184c49699c49524d11e7 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 24 Dec 2019 17:17:45 +0300 Subject: [PATCH 2/8] Remove deprecated methods from the Java Flow integration module The `kotlinx-coroutines-jdk9` module, being a copy of `kotlinx-coroutines-reactive`, has its share of legacy APIs defined. As this is a new module, there is no reason to preserve them. The changes include: * `Migration.kt`, being a file completely dedicated to warnings about old API usage, has been removed. * `IntegrationTest.kt` changed slightly so that it no longer uses the subscription channel API, which is deprecated. * `Channel.kt` is now just an implementation detail. It does not expose any public methods. * In particular, `Publisher.collect` has been moved to `ReactiveFlow.kt` and is no longer inline, as it would expose `openSubscription`, which is deprecated. * `PublisherSubscriptionSelectTest`, which tests use of `select` with the subscription channel API, is not included anymore. * `Convert.kt` also has been removed altogether, having no non-deprecated methods. --- .../api/kotlinx-coroutines-jdk9.api | 19 +----- .../kotlinx-coroutines-jdk9/src/Channel.kt | 17 +----- .../kotlinx-coroutines-jdk9/src/Convert.kt | 24 -------- .../kotlinx-coroutines-jdk9/src/Migration.kt | 36 ----------- .../src/ReactiveFlow.kt | 7 +++ .../test/IntegrationTest.kt | 8 +-- .../test/PublisherSubscriptionSelectTest.kt | 61 ------------------- 7 files changed, 13 insertions(+), 159 deletions(-) delete mode 100644 reactive/kotlinx-coroutines-jdk9/src/Convert.kt delete mode 100644 reactive/kotlinx-coroutines-jdk9/src/Migration.kt delete mode 100644 reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index f7b6466be4..8fbe6b5e74 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -7,28 +7,10 @@ public final class kotlinx/coroutines/jdk9/AwaitKt { public static final fun awaitSingle (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class kotlinx/coroutines/jdk9/ChannelKt { - public static final fun collect (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun consumeEach (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun openSubscription (Ljava/util/concurrent/Flow$Publisher;I)Lkotlinx/coroutines/channels/ReceiveChannel; - public static synthetic fun openSubscription$default (Ljava/util/concurrent/Flow$Publisher;IILjava/lang/Object;)Lkotlinx/coroutines/channels/ReceiveChannel; -} - public abstract interface class kotlinx/coroutines/jdk9/ContextInjector { public abstract fun injectCoroutineContext (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; } -public final class kotlinx/coroutines/jdk9/ConvertKt { - public static final fun asPublisher (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; - public static synthetic fun asPublisher$default (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; -} - -public final class kotlinx/coroutines/jdk9/FlowKt { - public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; - public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;I)Lkotlinx/coroutines/flow/Flow; - public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; -} - public final class kotlinx/coroutines/jdk9/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, java/util/concurrent/Flow$Subscription { public final field flow Lkotlinx/coroutines/flow/Flow; public final field subscriber Ljava/util/concurrent/Flow$Subscriber; @@ -65,5 +47,6 @@ public final class kotlinx/coroutines/jdk9/PublisherCoroutine : kotlinx/coroutin public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; + public static final fun collect (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/reactive/kotlinx-coroutines-jdk9/src/Channel.kt b/reactive/kotlinx-coroutines-jdk9/src/Channel.kt index 385eab15f0..c74a38bba1 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Channel.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Channel.kt @@ -24,27 +24,12 @@ import java.util.concurrent.Flow.* * } * ``` */ -@Deprecated( - message = "Transforming publisher to channel is deprecated, use asFlow() instead", - level = DeprecationLevel.WARNING) // Will be error in 1.4 -public fun Publisher.openSubscription(request: Int = 1): ReceiveChannel { +internal fun Publisher.openSubscription(request: Int = 1): ReceiveChannel { val channel = SubscriptionChannel(request) subscribe(channel) return channel } -// Will be promoted to error in 1.3.0, removed in 1.4.0 -@Deprecated(message = "Use collect instead", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.collect(action)")) -public suspend inline fun Publisher.consumeEach(action: (T) -> Unit) = - openSubscription().consumeEach(action) - -/** - * Subscribes to this [Publisher] and performs the specified action for each received element. - * Cancels subscription if any exception happens during collect. - */ -public suspend inline fun Publisher.collect(action: (T) -> Unit) = - openSubscription().consumeEach(action) - @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") private class SubscriptionChannel( private val request: Int diff --git a/reactive/kotlinx-coroutines-jdk9/src/Convert.kt b/reactive/kotlinx-coroutines-jdk9/src/Convert.kt deleted file mode 100644 index 758bd6f41e..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/src/Convert.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.jdk9 - -import kotlinx.coroutines.channels.* -import java.util.concurrent.Flow.* -import kotlin.coroutines.* - -/** - * Converts a stream of elements received from the channel to the hot reactive publisher. - * - * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers, - * they'll receive values in round-robin way. - * @param context -- the coroutine context from which the resulting observable is going to be signalled - */ -@Deprecated(message = "Deprecated in the favour of consumeAsFlow()", - level = DeprecationLevel.WARNING, - replaceWith = ReplaceWith("this.consumeAsFlow().asPublisher()")) -public fun ReceiveChannel.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher = publish(context) { - for (t in this@asPublisher) - send(t) -} diff --git a/reactive/kotlinx-coroutines-jdk9/src/Migration.kt b/reactive/kotlinx-coroutines-jdk9/src/Migration.kt deleted file mode 100644 index dd3abbdfee..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/src/Migration.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:JvmMultifileClass -@file:JvmName("FlowKt") - -package kotlinx.coroutines.jdk9 - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import java.util.concurrent.Flow.* - -// Binary compatibility with Spring 5.2 RC -@Deprecated( - message = "Replaced in favor of ReactiveFlow extension, please import kotlinx.coroutines.jdk9.* instead of kotlinx.coroutines.jdk9.FlowKt", - level = DeprecationLevel.ERROR -) -@JvmName("asFlow") -public fun Publisher.asFlowDeprecated(): Flow = asFlow() - -// Binary compatibility with Spring 5.2 RC -@Deprecated( - message = "Replaced in favor of ReactiveFlow extension, please import kotlinx.coroutines.jdk9.* instead of kotlinx.coroutines.jdk9.FlowKt", - level = DeprecationLevel.ERROR -) -@JvmName("asPublisher") -public fun Flow.asPublisherDeprecated(): Publisher = asPublisher() - -@FlowPreview -@Deprecated( - message = "batchSize parameter is deprecated, use .buffer() instead to control the backpressure", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("asFlow().buffer(batchSize)", imports = ["kotlinx.coroutines.flow.*"]) -) -public fun Publisher.asFlow(batchSize: Int): Flow = asFlow().buffer(batchSize) \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index a1469d72af..ea9521352e 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -37,6 +37,13 @@ public fun Publisher.asFlow(): Flow = */ public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) +/** + * Subscribes to this [Publisher] and performs the specified action for each received element. + * Cancels subscription if any exception happens during collect. + */ +public suspend fun Publisher.collect(action: (T) -> Unit) = + openSubscription().consumeEach(action) + private class PublisherAsFlow( private val publisher: Publisher, context: CoroutineContext = EmptyCoroutineContext, diff --git a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt index 9f9aafa9c5..b882b32782 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.jdk9 import kotlinx.coroutines.* import org.junit.Test +import kotlinx.coroutines.flow.flowOn import org.junit.runner.* import org.junit.runners.* import java.util.concurrent.Flow.* @@ -89,9 +90,8 @@ class IntegrationTest( assertEquals(1, pub.awaitFirstOrElse { 0 }) assertIAE { pub.awaitSingle() } checkNumbers(n, pub) - val channel = pub.openSubscription() - checkNumbers(n, channel.asPublisher(ctx(coroutineContext))) - channel.cancel() + val flow = pub.asFlow() + checkNumbers(n, flow.flowOn(ctx(coroutineContext)).asPublisher()) } @Test @@ -107,7 +107,7 @@ class IntegrationTest( } @Test - fun testEmptySingle() = runTest(unhandled = listOf({e -> e is NoSuchElementException})) { + fun testEmptySingle() = runTest(unhandled = listOf { e -> e is NoSuchElementException}) { expect(1) val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { publish { diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt deleted file mode 100644 index fab59c9ec2..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/test/PublisherSubscriptionSelectTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.jdk9 - -import kotlinx.coroutines.* -import kotlinx.coroutines.selects.* -import org.junit.Test -import org.junit.runner.* -import org.junit.runners.* -import kotlin.test.* - -@RunWith(Parameterized::class) -class PublisherSubscriptionSelectTest(private val request: Int) : TestBase() { - companion object { - @Parameterized.Parameters(name = "request = {0}") - @JvmStatic - fun params(): Collection> = listOf(0, 1, 10).map { arrayOf(it) } - } - - @Test - fun testSelect() = runTest { - // source with n ints - val n = 1000 * stressTestMultiplier - val source = publish { repeat(n) { send(it) } } - var a = 0 - var b = 0 - // open two subs - val channelA = source.openSubscription(request) - val channelB = source.openSubscription(request) - loop@ while (true) { - val done: Int = select { - channelA.onReceiveOrNull { - if (it != null) assertEquals(a++, it) - if (it == null) 0 else 1 - } - channelB.onReceiveOrNull { - if (it != null) assertEquals(b++, it) - if (it == null) 0 else 2 - } - } - when (done) { - 0 -> break@loop - 1 -> { - val r = channelB.receiveOrNull() - if (r != null) assertEquals(b++, r) - } - 2 -> { - val r = channelA.receiveOrNull() - if (r != null) assertEquals(a++, r) - } - } - } - - channelA.cancel() - channelB.cancel() - // should receive one of them fully - assertTrue(a == n || b == n) - } -} \ No newline at end of file From dafe32c9d69def02e93a74de43b6c731381aa92e Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 28 Jan 2020 12:52:11 +0300 Subject: [PATCH 3/8] Remove context injection from JDK9 Flow integration --- .../api/kotlinx-coroutines-jdk9.api | 4 ---- reactive/kotlinx-coroutines-jdk9/src/Await.kt | 12 +----------- .../src/ContextInjector.kt | 15 --------------- .../kotlinx-coroutines-jdk9/src/ReactiveFlow.kt | 17 ++++------------- 4 files changed, 5 insertions(+), 43 deletions(-) delete mode 100644 reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index 8fbe6b5e74..85545c0006 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -7,10 +7,6 @@ public final class kotlinx/coroutines/jdk9/AwaitKt { public static final fun awaitSingle (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class kotlinx/coroutines/jdk9/ContextInjector { - public abstract fun injectCoroutineContext (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; -} - public final class kotlinx/coroutines/jdk9/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, java/util/concurrent/Flow$Subscription { public final field flow Lkotlinx/coroutines/flow/Flow; public final field subscriber Ljava/util/concurrent/Flow$Subscriber; diff --git a/reactive/kotlinx-coroutines-jdk9/src/Await.kt b/reactive/kotlinx-coroutines-jdk9/src/Await.kt index ea9cda071a..747e277486 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Await.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Await.kt @@ -82,16 +82,6 @@ public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) // ------------------------ private ------------------------ -// ContextInjector service is implemented in `kotlinx-coroutines-reactor` module only. -// If `kotlinx-coroutines-reactor` module is not included, the list is empty. -private val contextInjectors: Array = - ServiceLoader.load(ContextInjector::class.java, ContextInjector::class.java.classLoader).iterator().asSequence().toList().toTypedArray() // R8 opto - -private fun Publisher.injectCoroutineContext(coroutineContext: CoroutineContext) = - contextInjectors.fold(this) { pub, contextInjector -> - contextInjector.injectCoroutineContext(pub, coroutineContext) - } - private enum class Mode(val s: String) { FIRST("awaitFirst"), FIRST_OR_DEFAULT("awaitFirstOrDefault"), @@ -104,7 +94,7 @@ private suspend fun Publisher.awaitOne( mode: Mode, default: T? = null ): T = suspendCancellableCoroutine { cont -> - injectCoroutineContext(cont.context).subscribe(object : Subscriber { + subscribe(object : Subscriber { private lateinit var subscription: Subscription private var value: T? = null private var seenValue = false diff --git a/reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt b/reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt deleted file mode 100644 index 7116e27e2e..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/src/ContextInjector.kt +++ /dev/null @@ -1,15 +0,0 @@ -package kotlinx.coroutines.jdk9 - -import kotlinx.coroutines.InternalCoroutinesApi -import java.util.concurrent.Flow.Publisher -import kotlin.coroutines.CoroutineContext - -/** @suppress */ -@InternalCoroutinesApi -public interface ContextInjector { - /** - * Injects `ReactorContext` element from the given context into the `SubscriberContext` of the publisher. - * This API used as an indirection layer between `reactive` and `reactor` modules. - */ - public fun injectCoroutineContext(publisher: Publisher, coroutineContext: CoroutineContext): Publisher -} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index ea9521352e..559b91bfeb 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -72,7 +72,7 @@ private class PublisherAsFlow( val newDispatcher = context[ContinuationInterceptor] if (newDispatcher == null || newDispatcher == collectContext[ContinuationInterceptor]) { // fast path -- subscribe directly in this dispatcher - return collectImpl(collectContext + context, collector) + return collectImpl(collector) } // slow path -- produce in a separate dispatcher collectSlowPath(collector) @@ -84,10 +84,10 @@ private class PublisherAsFlow( } } - private suspend fun collectImpl(injectContext: CoroutineContext, collector: FlowCollector) { + private suspend fun collectImpl(collector: FlowCollector) { val subscriber = ReactiveSubscriber(capacity, requestSize) // inject subscribe context into publisher - publisher.injectCoroutineContext(injectContext).subscribe(subscriber) + publisher.subscribe(subscriber) try { var consumed = 0L while (true) { @@ -105,7 +105,7 @@ private class PublisherAsFlow( // The second channel here is used for produceIn/broadcastIn and slow-path (dispatcher change) override suspend fun collectTo(scope: ProducerScope) = - collectImpl(scope.coroutineContext, SendingCollector(scope.channel)) + collectImpl(SendingCollector(scope.channel)) } @Suppress("SubscriberImplementation") @@ -145,15 +145,6 @@ private class ReactiveSubscriber( } } -// ContextInjector service is implemented in `kotlinx-coroutines-reactor` module only. -// If `kotlinx-coroutines-reactor` module is not included, the list is empty. -private val contextInjectors: List = - ServiceLoader.load(ContextInjector::class.java, ContextInjector::class.java.classLoader).toList() - -private fun Publisher.injectCoroutineContext(coroutineContext: CoroutineContext) = - contextInjectors.fold(this) { pub, contextInjector -> contextInjector.injectCoroutineContext(pub, coroutineContext) } - - /** * Adapter that transforms [Flow] into TCK-complaint [Publisher]. * [cancel] invocation cancels the original flow. From abf2090eb9d45393c8c32a9d57e505fbcc0a6ff2 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 28 Jan 2020 15:17:25 +0300 Subject: [PATCH 4/8] Simplify JDK9 Flow integration by calling ReactiveStreams integration --- reactive/kotlinx-coroutines-jdk9/README.md | 3 +- .../api/kotlinx-coroutines-jdk9.api | 28 -- reactive/kotlinx-coroutines-jdk9/build.gradle | 3 +- reactive/kotlinx-coroutines-jdk9/src/Await.kt | 96 +------ .../kotlinx-coroutines-jdk9/src/Channel.kt | 106 ------- .../kotlinx-coroutines-jdk9/src/Publish.kt | 259 +----------------- .../src/ReactiveFlow.kt | 215 +-------------- 7 files changed, 31 insertions(+), 679 deletions(-) delete mode 100644 reactive/kotlinx-coroutines-jdk9/src/Channel.kt diff --git a/reactive/kotlinx-coroutines-jdk9/README.md b/reactive/kotlinx-coroutines-jdk9/README.md index 9ee700b1aa..fcabd7da15 100644 --- a/reactive/kotlinx-coroutines-jdk9/README.md +++ b/reactive/kotlinx-coroutines-jdk9/README.md @@ -2,7 +2,8 @@ Utilities for [Java Flow](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html). -Replicates [kotlinx-coroutines-reactive](../kotlinx-coroutines-reactive), an equivalent package for the Reactive Streams. +Implemented as a collection of thin wrappers over [kotlinx-coroutines-reactive](../kotlinx-coroutines-reactive), +an equivalent package for the Reactive Streams. # Package kotlinx.coroutines.jdk9 diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index 85545c0006..3e58c8ba20 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -7,37 +7,9 @@ public final class kotlinx/coroutines/jdk9/AwaitKt { public static final fun awaitSingle (Ljava/util/concurrent/Flow$Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class kotlinx/coroutines/jdk9/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, java/util/concurrent/Flow$Subscription { - public final field flow Lkotlinx/coroutines/flow/Flow; - public final field subscriber Ljava/util/concurrent/Flow$Subscriber; - public fun (Lkotlinx/coroutines/flow/Flow;Ljava/util/concurrent/Flow$Subscriber;)V - public fun cancel ()V - public fun request (J)V -} - public final class kotlinx/coroutines/jdk9/PublishKt { public static final fun publish (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; - public static final fun publish (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; public static synthetic fun publish$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; - public static synthetic fun publish$default (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; - public static final fun publishInternal (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; -} - -public final class kotlinx/coroutines/jdk9/PublisherCoroutine : kotlinx/coroutines/AbstractCoroutine, java/util/concurrent/Flow$Subscription, kotlinx/coroutines/channels/ProducerScope, kotlinx/coroutines/selects/SelectClause2 { - public fun (Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/Flow$Subscriber;Lkotlin/jvm/functions/Function2;)V - public fun cancel ()V - public fun close (Ljava/lang/Throwable;)Z - public fun getChannel ()Lkotlinx/coroutines/channels/SendChannel; - public fun getOnSend ()Lkotlinx/coroutines/selects/SelectClause2; - public fun invokeOnClose (Lkotlin/jvm/functions/Function1;)Ljava/lang/Void; - public synthetic fun invokeOnClose (Lkotlin/jvm/functions/Function1;)V - public fun isClosedForSend ()Z - public fun isFull ()Z - public fun offer (Ljava/lang/Object;)Z - public synthetic fun onCompleted (Ljava/lang/Object;)V - public fun registerSelectClause2 (Lkotlinx/coroutines/selects/SelectInstance;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V - public fun request (J)V - public fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle b/reactive/kotlinx-coroutines-jdk9/build.gradle index 95b63ee4e3..24e5d7f820 100644 --- a/reactive/kotlinx-coroutines-jdk9/build.gradle +++ b/reactive/kotlinx-coroutines-jdk9/build.gradle @@ -5,8 +5,9 @@ targetCompatibility = 9 dependencies { + compile project(":kotlinx-coroutines-reactive") + compile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" testCompile "org.reactivestreams:reactive-streams-tck:$reactive_streams_version" - testCompile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" } task testNG(type: Test) { diff --git a/reactive/kotlinx-coroutines-jdk9/src/Await.kt b/reactive/kotlinx-coroutines-jdk9/src/Await.kt index 747e277486..e533a86552 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Await.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Await.kt @@ -4,14 +4,9 @@ package kotlinx.coroutines.jdk9 -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Job -import kotlinx.coroutines.suspendCancellableCoroutine import java.util.concurrent.Flow.Publisher -import java.util.concurrent.Flow.Subscriber -import java.util.concurrent.Flow.Subscription -import java.util.* -import kotlin.coroutines.* +import org.reactivestreams.FlowAdapters +import kotlinx.coroutines.reactive.* /** * Awaits for the first value from the given publisher without blocking a thread and @@ -23,7 +18,7 @@ import kotlin.coroutines.* * * @throws NoSuchElementException if publisher does not emit any value */ -public suspend fun Publisher.awaitFirst(): T = awaitOne(Mode.FIRST) +public suspend fun Publisher.awaitFirst(): T = FlowAdapters.toPublisher(this).awaitFirst() /** * Awaits for the first value from the given observable or the [default] value if none is emitted without blocking a @@ -33,7 +28,8 @@ public suspend fun Publisher.awaitFirst(): T = awaitOne(Mode.FIRST) * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ -public suspend fun Publisher.awaitFirstOrDefault(default: T): T = awaitOne(Mode.FIRST_OR_DEFAULT, default) +public suspend fun Publisher.awaitFirstOrDefault(default: T): T = + FlowAdapters.toPublisher(this).awaitFirstOrDefault(default) /** * Awaits for the first value from the given observable or `null` value if none is emitted without blocking a @@ -43,7 +39,8 @@ public suspend fun Publisher.awaitFirstOrDefault(default: T): T = awaitOn * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ -public suspend fun Publisher.awaitFirstOrNull(): T? = awaitOne(Mode.FIRST_OR_DEFAULT) +public suspend fun Publisher.awaitFirstOrNull(): T? = + FlowAdapters.toPublisher(this).awaitFirstOrNull() /** * Awaits for the first value from the given observable or call [defaultValue] to get a value if none is emitted without blocking a @@ -53,7 +50,8 @@ public suspend fun Publisher.awaitFirstOrNull(): T? = awaitOne(Mode.FIRST * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ -public suspend fun Publisher.awaitFirstOrElse(defaultValue: () -> T): T = awaitOne(Mode.FIRST_OR_DEFAULT) ?: defaultValue() +public suspend fun Publisher.awaitFirstOrElse(defaultValue: () -> T): T = + FlowAdapters.toPublisher(this).awaitFirstOrElse(defaultValue) /** * Awaits for the last value from the given publisher without blocking a thread and @@ -65,7 +63,8 @@ public suspend fun Publisher.awaitFirstOrElse(defaultValue: () -> T): T = * * @throws NoSuchElementException if publisher does not emit any value */ -public suspend fun Publisher.awaitLast(): T = awaitOne(Mode.LAST) +public suspend fun Publisher.awaitLast(): T = + FlowAdapters.toPublisher(this).awaitLast() /** * Awaits for the single value from the given publisher without blocking a thread and @@ -78,74 +77,5 @@ public suspend fun Publisher.awaitLast(): T = awaitOne(Mode.LAST) * @throws NoSuchElementException if publisher does not emit any value * @throws IllegalArgumentException if publisher emits more than one value */ -public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) - -// ------------------------ private ------------------------ - -private enum class Mode(val s: String) { - FIRST("awaitFirst"), - FIRST_OR_DEFAULT("awaitFirstOrDefault"), - LAST("awaitLast"), - SINGLE("awaitSingle"); - override fun toString(): String = s -} - -private suspend fun Publisher.awaitOne( - mode: Mode, - default: T? = null -): T = suspendCancellableCoroutine { cont -> - subscribe(object : Subscriber { - private lateinit var subscription: Subscription - private var value: T? = null - private var seenValue = false - - override fun onSubscribe(sub: Subscription) { - subscription = sub - cont.invokeOnCancellation { sub.cancel() } - sub.request(if (mode == Mode.FIRST) 1 else Long.MAX_VALUE) - } - - override fun onNext(t: T) { - when (mode) { - Mode.FIRST, Mode.FIRST_OR_DEFAULT -> { - if (!seenValue) { - seenValue = true - subscription.cancel() - cont.resume(t) - } - } - Mode.LAST, Mode.SINGLE -> { - if (mode == Mode.SINGLE && seenValue) { - subscription.cancel() - if (cont.isActive) - cont.resumeWithException(IllegalArgumentException("More than one onNext value for $mode")) - } else { - value = t - seenValue = true - } - } - } - } - - @Suppress("UNCHECKED_CAST") - override fun onComplete() { - if (seenValue) { - if (cont.isActive) cont.resume(value as T) - return - } - when { - mode == Mode.FIRST_OR_DEFAULT -> { - cont.resume(default as T) - } - cont.isActive -> { - cont.resumeWithException(NoSuchElementException("No value received via onNext for $mode")) - } - } - } - - override fun onError(e: Throwable) { - cont.resumeWithException(e) - } - }) -} - +public suspend fun Publisher.awaitSingle(): T = + FlowAdapters.toPublisher(this).awaitSingle() diff --git a/reactive/kotlinx-coroutines-jdk9/src/Channel.kt b/reactive/kotlinx-coroutines-jdk9/src/Channel.kt deleted file mode 100644 index c74a38bba1..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/src/Channel.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.jdk9 - -import kotlinx.atomicfu.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.internal.* -import java.util.concurrent.Flow.* - -/** - * Subscribes to this [Publisher] and returns a channel to receive elements emitted by it. - * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this publisher. - - * @param request how many items to request from publisher in advance (optional, one by default). - * - * This method is deprecated in the favor of [Flow]. - * Instead of iterating over the resulting channel please use [collect][Flow.collect]: - * ``` - * asFlow().collect { value -> - * // process value - * } - * ``` - */ -internal fun Publisher.openSubscription(request: Int = 1): ReceiveChannel { - val channel = SubscriptionChannel(request) - subscribe(channel) - return channel -} - -@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") -private class SubscriptionChannel( - private val request: Int -) : LinkedListChannel(), Subscriber { - init { - require(request >= 0) { "Invalid request size: $request" } - } - - private val _subscription = atomic(null) - - // requested from subscription minus number of received minus number of enqueued receivers, - // can be negative if we have receivers, but no subscription yet - private val _requested = atomic(0) - - // AbstractChannel overrides - @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") - override fun onReceiveEnqueued() { - _requested.loop { wasRequested -> - val subscription = _subscription.value - val needRequested = wasRequested - 1 - if (subscription != null && needRequested < 0) { // need to request more from subscription - // try to fixup by making request - if (wasRequested != request && !_requested.compareAndSet(wasRequested, request)) - return@loop // continue looping if failed - subscription.request((request - needRequested).toLong()) - return - } - // just do book-keeping - if (_requested.compareAndSet(wasRequested, needRequested)) return - } - } - - @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") - override fun onReceiveDequeued() { - _requested.incrementAndGet() - } - - @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") - override fun onClosedIdempotent(closed: LockFreeLinkedListNode) { - _subscription.getAndSet(null)?.cancel() // cancel exactly once - } - - // Subscriber overrides - override fun onSubscribe(s: Subscription) { - _subscription.value = s - while (true) { // lock-free loop on _requested - if (isClosedForSend) { - s.cancel() - return - } - val wasRequested = _requested.value - if (wasRequested >= request) return // ok -- normal story - // otherwise, receivers came before we had subscription or need to make initial request - // try to fixup by making request - if (!_requested.compareAndSet(wasRequested, request)) continue - s.request((request - wasRequested).toLong()) - return - } - } - - override fun onNext(t: T) { - _requested.decrementAndGet() - offer(t) - } - - override fun onComplete() { - close(cause = null) - } - - override fun onError(e: Throwable) { - close(cause = e) - } -} - diff --git a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt index f2dec33428..d8e99dfaa4 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt @@ -2,18 +2,13 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") - package kotlinx.coroutines.jdk9 -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* -import kotlinx.coroutines.selects.* -import kotlinx.coroutines.sync.* import java.util.concurrent.Flow.* import kotlin.coroutines.* -import kotlin.internal.LowPriorityInOverloadResolution +import org.reactivestreams.FlowAdapters /** * Creates cold reactive [Publisher] that runs a given [block] in a coroutine. @@ -38,254 +33,6 @@ public fun publish( context: CoroutineContext = EmptyCoroutineContext, @BuilderInference block: suspend ProducerScope.() -> Unit ): Publisher { - require(context[Job] === null) { "Publisher context cannot contain job in it." + - "Its lifecycle should be managed via subscription. Had $context" } - return publishInternal(GlobalScope, context, DEFAULT_HANDLER, block) -} - -@Deprecated( - message = "CoroutineScope.publish is deprecated in favour of top-level publish", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("publish(context, block)") -) // Since 1.3.0, will be error in 1.3.1 and hidden in 1.4.0. Binary compatibility with Spring -@LowPriorityInOverloadResolution -public fun CoroutineScope.publish( - context: CoroutineContext = EmptyCoroutineContext, - @BuilderInference block: suspend ProducerScope.() -> Unit -): Publisher = publishInternal(this, context, DEFAULT_HANDLER ,block) - -/** @suppress For internal use from other reactive integration modules only */ -@InternalCoroutinesApi -public fun publishInternal( - scope: CoroutineScope, // support for legacy publish in scope - context: CoroutineContext, - exceptionOnCancelHandler: (Throwable, CoroutineContext) -> Unit, - block: suspend ProducerScope.() -> Unit -): Publisher = Publisher { subscriber -> - // specification requires NPE on null subscriber - if (subscriber == null) throw NullPointerException("Subscriber cannot be null") - val newContext = scope.newCoroutineContext(context) - val coroutine = PublisherCoroutine(newContext, subscriber, exceptionOnCancelHandler) - subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions - coroutine.start(CoroutineStart.DEFAULT, coroutine, block) -} - -private const val CLOSED = -1L // closed, but have not signalled onCompleted/onError yet -private const val SIGNALLED = -2L // already signalled subscriber onCompleted/onError -private val DEFAULT_HANDLER: (Throwable, CoroutineContext) -> Unit = { t, ctx -> if (t !is CancellationException) handleCoroutineException(ctx, t) } - -@Suppress("CONFLICTING_JVM_DECLARATIONS", "RETURN_TYPE_MISMATCH_ON_INHERITANCE") -@InternalCoroutinesApi -public class PublisherCoroutine( - parentContext: CoroutineContext, - private val subscriber: Subscriber, - private val exceptionOnCancelHandler: (Throwable, CoroutineContext) -> Unit -) : AbstractCoroutine(parentContext, true), ProducerScope, Subscription, SelectClause2> { - override val channel: SendChannel get() = this - - // Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked - private val mutex = Mutex(locked = true) - private val _nRequested = atomic(0L) // < 0 when closed (CLOSED or SIGNALLED) - - @Volatile - private var cancelled = false // true when Subscription.cancel() is invoked - - override val isClosedForSend: Boolean get() = isCompleted - override val isFull: Boolean = mutex.isLocked - override fun close(cause: Throwable?): Boolean = cancelCoroutine(cause) - override fun invokeOnClose(handler: (Throwable?) -> Unit) = - throw UnsupportedOperationException("PublisherCoroutine doesn't support invokeOnClose") - - override fun offer(element: T): Boolean { - if (!mutex.tryLock()) return false - doLockedNext(element) - return true - } - - public override suspend fun send(element: T) { - // fast-path -- try send without suspension - if (offer(element)) return - // slow-path does suspend - return sendSuspend(element) - } - - private suspend fun sendSuspend(element: T) { - mutex.lock() - doLockedNext(element) - } - - override val onSend: SelectClause2> - get() = this - - // registerSelectSend - @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") - override fun registerSelectClause2(select: SelectInstance, element: T, block: suspend (SendChannel) -> R) { - mutex.onLock.registerSelectClause2(select, null) { - doLockedNext(element) - block(this) - } - } - - /* - * This code is not trivial because of the two properties: - * 1. It ensures conformance to the reactive specification that mandates that onXXX invocations should not - * be concurrent. It uses Mutex to protect all onXXX invocation and ensure conformance even when multiple - * coroutines are invoking `send` function. - * 2. Normally, `onComplete/onError` notification is sent only when coroutine and all its children are complete. - * However, nothing prevents `publish` coroutine from leaking reference to it send channel to some - * globally-scoped coroutine that is invoking `send` outside of this context. Without extra precaution this may - * lead to `onNext` that is concurrent with `onComplete/onError`, so that is why signalling for - * `onComplete/onError` is also done under the same mutex. - */ - - // assert: mutex.isLocked() - private fun doLockedNext(elem: T) { - // check if already closed for send, note that isActive becomes false as soon as cancel() is invoked, - // because the job is cancelled, so this check also ensure conformance to the reactive specification's - // requirement that after cancellation requested we don't call onXXX - if (!isActive) { - unlockAndCheckCompleted() - throw getCancellationException() - } - // notify subscriber - try { - subscriber.onNext(elem) - } catch (e: Throwable) { - // If onNext fails with exception, then we cancel coroutine (with this exception) and then rethrow it - // to abort the corresponding send/offer invocation. From the standpoint of coroutines machinery, - // this failure is essentially equivalent to a failure of a child coroutine. - cancelCoroutine(e) - unlockAndCheckCompleted() - throw e - } - // now update nRequested - while (true) { // lock-free loop on nRequested - val current = _nRequested.value - if (current < 0) break // closed from inside onNext => unlock - if (current == Long.MAX_VALUE) break // no back-pressure => unlock - val updated = current - 1 - if (_nRequested.compareAndSet(current, updated)) { - if (updated == 0L) { - // return to keep locked due to back-pressure - return - } - break // unlock if updated > 0 - } - } - unlockAndCheckCompleted() - } - - private fun unlockAndCheckCompleted() { - /* - * There is no sense to check completion before doing `unlock`, because completion might - * happen after this check and before `unlock` (see `signalCompleted` that does not do anything - * if it fails to acquire the lock that we are still holding). - * We have to recheck `isCompleted` after `unlock` anyway. - */ - mutex.unlock() - // check isCompleted and and try to regain lock to signal completion - if (isCompleted && mutex.tryLock()) { - doLockedSignalCompleted(completionCause, completionCauseHandled) - } - } - - // assert: mutex.isLocked() & isCompleted - private fun doLockedSignalCompleted(cause: Throwable?, handled: Boolean) { - try { - if (_nRequested.value >= CLOSED) { - _nRequested.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed) - // Specification requires that after cancellation requested we don't call onXXX - if (cancelled) { - // If the parent had failed to handle our exception, then we must not lose this exception - if (cause != null && !handled) exceptionOnCancelHandler(cause, context) - return - } - - try { - if (cause != null && cause !is CancellationException) { - /* - * Reactive frameworks have two types of exceptions: regular and fatal. - * Regular are passed to onError. - * Fatal can be passed to onError, but even the standard implementations **can just swallow it** (e.g. see #1297). - * Such behaviour is inconsistent, leads to silent failures and we can't possibly know whether - * the cause will be handled by onError (and moreover, it depends on whether a fatal exception was - * thrown by subscriber or upstream). - * To make behaviour consistent and least surprising, we always handle fatal exceptions - * by coroutines machinery, anyway, they should not be present in regular program flow, - * thus our goal here is just to expose it as soon as possible. - */ - subscriber.onError(cause) - if (!handled && cause.isFatal()) { - exceptionOnCancelHandler(cause, context) - } - } else { - subscriber.onComplete() - } - } catch (e: Throwable) { - handleCoroutineException(context, e) - } - } - } finally { - mutex.unlock() - } - } - - override fun request(n: Long) { - if (n <= 0) { - // Specification requires IAE for n <= 0 - cancelCoroutine(IllegalArgumentException("non-positive subscription request $n")) - return - } - while (true) { // lock-free loop for nRequested - val cur = _nRequested.value - if (cur < 0) return // already closed for send, ignore requests - var upd = cur + n - if (upd < 0 || n == Long.MAX_VALUE) - upd = Long.MAX_VALUE - if (cur == upd) return // nothing to do - if (_nRequested.compareAndSet(cur, upd)) { - // unlock the mutex when we don't have back-pressure anymore - if (cur == 0L) { - unlockAndCheckCompleted() - } - return - } - } - } - - // assert: isCompleted - private fun signalCompleted(cause: Throwable?, handled: Boolean) { - while (true) { // lock-free loop for nRequested - val current = _nRequested.value - if (current == SIGNALLED) return // some other thread holding lock already signalled cancellation/completion - check(current >= 0) // no other thread could have marked it as CLOSED, because onCompleted[Exceptionally] is invoked once - if (!_nRequested.compareAndSet(current, CLOSED)) continue // retry on failed CAS - // Ok -- marked as CLOSED, now can unlock the mutex if it was locked due to backpressure - if (current == 0L) { - doLockedSignalCompleted(cause, handled) - } else { - // otherwise mutex was either not locked or locked in concurrent onNext... try lock it to signal completion - if (mutex.tryLock()) doLockedSignalCompleted(cause, handled) - // Note: if failed `tryLock`, then `doLockedNext` will signal after performing `unlock` - } - return // done anyway - } - } - - override fun onCompleted(value: Unit) { - signalCompleted(null, false) - } - - override fun onCancelled(cause: Throwable, handled: Boolean) { - signalCompleted(cause, handled) - } - - override fun cancel() { - // Specification requires that after cancellation publisher stops signalling - // This flag distinguishes subscription cancellation request from the job crash - cancelled = true - super.cancel(null) - } - - private fun Throwable.isFatal() = this is VirtualMachineError || this is ThreadDeath || this is LinkageError + val reactivePublisher : org.reactivestreams.Publisher = kotlinx.coroutines.reactive.publish(context, block) + return FlowAdapters.toFlowPublisher(reactivePublisher) } diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index 559b91bfeb..26c6da473b 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -4,15 +4,12 @@ package kotlinx.coroutines.jdk9 -import kotlinx.atomicfu.* -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.flow.internal.* -import kotlinx.coroutines.intrinsics.* +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.asPublisher +import kotlinx.coroutines.reactive.collect import java.util.concurrent.Flow.* -import java.util.* -import kotlin.coroutines.* +import org.reactivestreams.FlowAdapters /** * Transforms the given reactive [Publisher] into [Flow]. @@ -22,12 +19,9 @@ import kotlin.coroutines.* * * If any of the resulting flow transformations fails, subscription is immediately cancelled and all in-flight elements * are discarded. - * - * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, - * see its documentation for additional details. */ public fun Publisher.asFlow(): Flow = - PublisherAsFlow(this) + FlowAdapters.toPublisher(this).asFlow() /** * Transforms the given flow to a reactive specification compliant [Publisher]. @@ -35,201 +29,14 @@ public fun Publisher.asFlow(): Flow = * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, * see its documentation for additional details. */ -public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) +public fun Flow.asPublisher(): Publisher { + val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher() + return FlowAdapters.toFlowPublisher(reactivePublisher) +} /** * Subscribes to this [Publisher] and performs the specified action for each received element. * Cancels subscription if any exception happens during collect. */ -public suspend fun Publisher.collect(action: (T) -> Unit) = - openSubscription().consumeEach(action) - -private class PublisherAsFlow( - private val publisher: Publisher, - context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - PublisherAsFlow(publisher, context, capacity) - - /* - * Suppress for Channel.CHANNEL_DEFAULT_CAPACITY. - * It's too counter-intuitive to be public and moving it to Flow companion - * will also create undesired effect. - */ - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - private val requestSize: Long - get() = when (capacity) { - Channel.CONFLATED -> Long.MAX_VALUE // request all and conflate incoming - Channel.RENDEZVOUS -> 1L // need to request at least one anyway - Channel.UNLIMITED -> Long.MAX_VALUE // reactive streams way to say "give all" must be Long.MAX_VALUE - Channel.BUFFERED -> Channel.CHANNEL_DEFAULT_CAPACITY.toLong() - else -> capacity.toLong().also { check(it >= 1) } - } - - override suspend fun collect(collector: FlowCollector) { - val collectContext = coroutineContext - val newDispatcher = context[ContinuationInterceptor] - if (newDispatcher == null || newDispatcher == collectContext[ContinuationInterceptor]) { - // fast path -- subscribe directly in this dispatcher - return collectImpl(collector) - } - // slow path -- produce in a separate dispatcher - collectSlowPath(collector) - } - - private suspend fun collectSlowPath(collector: FlowCollector) { - coroutineScope { - collector.emitAll(produceImpl(this + context)) - } - } - - private suspend fun collectImpl(collector: FlowCollector) { - val subscriber = ReactiveSubscriber(capacity, requestSize) - // inject subscribe context into publisher - publisher.subscribe(subscriber) - try { - var consumed = 0L - while (true) { - val value = subscriber.takeNextOrNull() ?: break - collector.emit(value) - if (++consumed == requestSize) { - consumed = 0L - subscriber.makeRequest() - } - } - } finally { - subscriber.cancel() - } - } - - // The second channel here is used for produceIn/broadcastIn and slow-path (dispatcher change) - override suspend fun collectTo(scope: ProducerScope) = - collectImpl(SendingCollector(scope.channel)) -} - -@Suppress("SubscriberImplementation") -private class ReactiveSubscriber( - capacity: Int, - private val requestSize: Long -) : Subscriber { - private lateinit var subscription: Subscription - private val channel = Channel(capacity) - - suspend fun takeNextOrNull(): T? = channel.receiveOrNull() - - override fun onNext(value: T) { - // Controlled by requestSize - require(channel.offer(value)) { "Element $value was not added to channel because it was full, $channel" } - } - - override fun onComplete() { - channel.close() - } - - override fun onError(t: Throwable?) { - channel.close(t) - } - - override fun onSubscribe(s: Subscription) { - subscription = s - makeRequest() - } - - fun makeRequest() { - subscription.request(requestSize) - } - - fun cancel() { - subscription.cancel() - } -} - -/** - * Adapter that transforms [Flow] into TCK-complaint [Publisher]. - * [cancel] invocation cancels the original flow. - */ -@Suppress("PublisherImplementation") -private class FlowAsPublisher(private val flow: Flow) : Publisher { - override fun subscribe(subscriber: Subscriber?) { - if (subscriber == null) throw NullPointerException() - subscriber.onSubscribe(FlowSubscription(flow, subscriber)) - } -} - -/** @suppress */ -@InternalCoroutinesApi -public class FlowSubscription( - @JvmField val flow: Flow, - @JvmField val subscriber: Subscriber -) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, false) { - private val requested = atomic(0L) - private val producer = atomic?>(null) - - override fun onStart() { - ::flowProcessing.startCoroutineCancellable(this) - } - - private suspend fun flowProcessing() { - try { - consumeFlow() - subscriber.onComplete() - } catch (e: Throwable) { - try { - if (e is CancellationException) { - subscriber.onComplete() - } else { - subscriber.onError(e) - } - } catch (e: Throwable) { - // Last ditch report - handleCoroutineException(coroutineContext, e) - } - } - } - - /* - * This method has at most one caller at any time (triggered from the `request` method) - */ - private suspend fun consumeFlow() { - flow.collect { value -> - /* - * Flow is scopeless, thus if it's not active, its subscription was cancelled. - * No intermediate "child failed, but flow coroutine is not" states are allowed. - */ - coroutineContext.ensureActive() - if (requested.value <= 0L) { - suspendCancellableCoroutine { - producer.value = it - if (requested.value != 0L) it.resumeSafely() - } - } - requested.decrementAndGet() - subscriber.onNext(value) - } - } - - override fun cancel() { - cancel(null) - } - - override fun request(n: Long) { - if (n <= 0) { - return - } - start() - requested.update { value -> - val newValue = value + n - if (newValue <= 0L) Long.MAX_VALUE else newValue - } - val producer = producer.getAndSet(null) ?: return - producer.resumeSafely() - } - - private fun CancellableContinuation.resumeSafely() { - val token = tryResume(Unit) - if (token != null) { - completeResume(token) - } - } -} +public suspend inline fun Publisher.collect(action: (T) -> Unit) = + FlowAdapters.toPublisher(this).collect(action) From 97f9030a7e2281093b85e8a8347405e2e5e5f5cc Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 4 Feb 2020 15:39:51 +0300 Subject: [PATCH 5/8] Remove TCK tests from JDK9 Flow integration Since the JDK9 Flow integration is now implemented as thin wrappers around the Reactive Streams integration, there is no need for such thorough testing, and additional tests would only cause the overhead of needing to fix two copies at once when changing the Reactive Streams integration. --- reactive/kotlinx-coroutines-jdk9/build.gradle | 34 ----- .../test/IterableFlowTckTest.kt | 134 ------------------ .../test/ReactiveStreamTckTest.kt | 55 ------- 3 files changed, 223 deletions(-) delete mode 100644 reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt delete mode 100644 reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle b/reactive/kotlinx-coroutines-jdk9/build.gradle index 24e5d7f820..8737e8ed6d 100644 --- a/reactive/kotlinx-coroutines-jdk9/build.gradle +++ b/reactive/kotlinx-coroutines-jdk9/build.gradle @@ -3,36 +3,9 @@ */ targetCompatibility = 9 - dependencies { compile project(":kotlinx-coroutines-reactive") compile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" - testCompile "org.reactivestreams:reactive-streams-tck:$reactive_streams_version" -} - -task testNG(type: Test) { - useTestNG() - reports.html.destination = file("$buildDir/reports/testng") - include '**/*ReactiveStreamTckTest.*' - // Skip testNG when tests are filtered with --tests, otherwise it simply fails - onlyIf { - filter.includePatterns.isEmpty() - } - doFirst { - // Classic gradle, nothing works without doFirst - println "TestNG tests: ($includes)" - } -} - -task checkJdk11() { - // only fail w/o JDK_11 when actually trying to compile, not during project setup phase - doLast { - if (!System.env.JDK_11) { - throw new GradleException("JDK_11 environment variable is not defined. " + - "Can't build against JDK 11 runtime and run JDK 11 compatibility tests. " + - "Please ensure JDK 11 is installed and that JDK_11 points to it.") - } - } } compileTestKotlin { @@ -41,13 +14,6 @@ compileTestKotlin { compileKotlin { kotlinOptions.jvmTarget = "9" - kotlinOptions.jdkHome = System.env.JDK_11 - dependsOn(checkJdk11) -} - -test { - dependsOn(testNG) - reports.html.destination = file("$buildDir/reports/junit") } tasks.withType(dokka.getClass()) { diff --git a/reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt b/reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt deleted file mode 100644 index cf04857ebc..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/test/IterableFlowTckTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("UNCHECKED_CAST") - -package kotlinx.coroutines.jdk9 - -import kotlinx.coroutines.flow.* -import org.junit.Test -import java.util.concurrent.Flow -import org.reactivestreams.tck.* -import org.reactivestreams.Publisher -import org.reactivestreams.FlowAdapters - -import java.util.concurrent.Flow.Subscription -import java.util.concurrent.Flow.Subscriber -import java.util.ArrayList -import java.util.concurrent.* -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ForkJoinPool.commonPool -import kotlin.test.* - -class IterableFlowTckTest : PublisherVerification(TestEnvironment()) { - - private fun generate(num: Long): Array { - return Array(if (num >= Integer.MAX_VALUE) 1000000 else num.toInt()) { it.toLong() } - } - - override fun createPublisher(elements: Long): Publisher { - return FlowAdapters.toPublisher(generate(elements).asIterable().asFlow().asPublisher()) - } - - @Suppress("SubscriberImplementation") - override fun createFailedPublisher(): Publisher? { - /* - * This is a hack for our adapter structure: - * Tests assume that calling "collect" is enough for publisher to fail and it is not - * true for our implementation - */ - val pub = { error(42) }.asFlow().asPublisher() - return FlowAdapters.toPublisher(Flow.Publisher { subscriber -> - pub.subscribe(object : Subscriber by subscriber as Subscriber { - override fun onSubscribe(s: Subscription) { - subscriber.onSubscribe(s) - s.request(1) - } - }) - }) - } - - @Test - fun testStackOverflowTrampoline() { - val latch = CountDownLatch(1) - val collected = ArrayList() - val toRequest = 1000L - val array = generate(toRequest) - val publisher = array.asIterable().asFlow().asPublisher() - - publisher.subscribe(object : Subscriber { - private lateinit var s: Subscription - - override fun onSubscribe(s: Subscription) { - this.s = s - s.request(1) - } - - override fun onNext(aLong: Long) { - collected.add(aLong) - - s.request(1) - } - - override fun onError(t: Throwable) { - - } - - override fun onComplete() { - latch.countDown() - } - }) - - latch.await(5, TimeUnit.SECONDS) - assertEquals(collected, array.toList()) - } - - @Test - fun testConcurrentRequest() { - val latch = CountDownLatch(1) - val collected = ArrayList() - val n = 50000L - val array = generate(n) - val publisher = array.asIterable().asFlow().asPublisher() - - publisher.subscribe(object : Subscriber { - private var s: Subscription? = null - - override fun onSubscribe(s: Subscription) { - this.s = s - for (i in 0 until n) { - commonPool().execute { s.request(1) } - } - } - - override fun onNext(aLong: Long) { - collected.add(aLong) - } - - override fun onError(t: Throwable) { - - } - - override fun onComplete() { - latch.countDown() - } - }) - - latch.await(50, TimeUnit.SECONDS) - assertEquals(array.toList(), collected) - } - - @Ignore - override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { - } - - @Ignore - override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { - } - - @Ignore - override fun required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() { - // This test has a bug in it - } -} diff --git a/reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt b/reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt deleted file mode 100644 index fa38234e59..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/test/ReactiveStreamTckTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.jdk9 - -import kotlinx.coroutines.* -import org.reactivestreams.FlowAdapters -import org.reactivestreams.Publisher -import org.reactivestreams.tck.* -import org.testng.* -import org.testng.annotations.* - - -class ReactiveStreamTckTest : TestBase() { - - @Factory(dataProvider = "dispatchers") - fun createTests(dispatcher: Dispatcher): Array { - return arrayOf(ReactiveStreamTckTestSuite(dispatcher)) - } - - @DataProvider(name = "dispatchers") - public fun dispatchers(): Array> = Dispatcher.values().map { arrayOf(it) }.toTypedArray() - - - public class ReactiveStreamTckTestSuite( - private val dispatcher: Dispatcher - ) : PublisherVerification(TestEnvironment(500, 500)) { - - override fun createPublisher(elements: Long): Publisher = - FlowAdapters.toPublisher( - publish(dispatcher.dispatcher) { - for (i in 1..elements) send(i) - }) - - override fun createFailedPublisher(): Publisher = - FlowAdapters.toPublisher( - publish(dispatcher.dispatcher) { - throw TestException() - } - ) - - @Test - public override fun optional_spec105_emptyStreamMustTerminateBySignallingOnComplete() { - throw SkipException("Skipped") - } - - class TestException : Exception() - } -} - -enum class Dispatcher(val dispatcher: CoroutineDispatcher) { - DEFAULT(Dispatchers.Default), - UNCONFINED(Dispatchers.Unconfined) -} From dd4a9dd0bc5b18b6bc323556703b310037afeb5a Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 7 Feb 2020 12:06:13 +0300 Subject: [PATCH 6/8] Remove mentions of ReactorContext from JDK 9 Flow integration doc --- reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index 26c6da473b..ce91164a3b 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -25,9 +25,6 @@ public fun Publisher.asFlow(): Flow = /** * Transforms the given flow to a reactive specification compliant [Publisher]. - * - * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, - * see its documentation for additional details. */ public fun Flow.asPublisher(): Publisher { val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher() From 2a383c8a074b89226fffd8888a9f8ab77f575c89 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 14 Feb 2020 13:10:08 +0300 Subject: [PATCH 7/8] Refactoring: remove assertNSE and assertIAE --- .../test/IntegrationTest.kt | 25 +++--------------- .../test/IntegrationTest.kt | 25 +++--------------- .../test/IntegrationTest.kt | 26 +++---------------- 3 files changed, 12 insertions(+), 64 deletions(-) diff --git a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt index b882b32782..12b819383b 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt @@ -43,12 +43,12 @@ class IntegrationTest( if (delay) delay(1) // does not send anything } - assertNSE { pub.awaitFirst() } + assertFailsWith { pub.awaitFirst() } assertEquals("OK", pub.awaitFirstOrDefault("OK")) assertNull(pub.awaitFirstOrNull()) assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) - assertNSE { pub.awaitLast() } - assertNSE { pub.awaitSingle() } + assertFailsWith { pub.awaitLast() } + assertFailsWith { pub.awaitSingle() } var cnt = 0 pub.collect { cnt++ } assertEquals(0, cnt) @@ -88,7 +88,7 @@ class IntegrationTest( assertEquals(n, pub.awaitLast()) assertEquals(1, pub.awaitFirstOrNull()) assertEquals(1, pub.awaitFirstOrElse { 0 }) - assertIAE { pub.awaitSingle() } + assertFailsWith { pub.awaitSingle() } checkNumbers(n, pub) val flow = pub.asFlow() checkNumbers(n, flow.flowOn(ctx(coroutineContext)).asPublisher()) @@ -129,21 +129,4 @@ class IntegrationTest( assertEquals(n, last) } - private inline fun assertIAE(block: () -> Unit) { - try { - block() - expectUnreached() - } catch (e: Throwable) { - assertTrue(e is IllegalArgumentException) - } - } - - private inline fun assertNSE(block: () -> Unit) { - try { - block() - expectUnreached() - } catch (e: Throwable) { - assertTrue(e is NoSuchElementException) - } - } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt index 3ec0b93481..6f7d98480b 100644 --- a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt @@ -42,12 +42,12 @@ class IntegrationTest( if (delay) delay(1) // does not send anything } - assertNSE { pub.awaitFirst() } + assertFailsWith { pub.awaitFirst() } assertEquals("OK", pub.awaitFirstOrDefault("OK")) assertNull(pub.awaitFirstOrNull()) assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) - assertNSE { pub.awaitLast() } - assertNSE { pub.awaitSingle() } + assertFailsWith { pub.awaitLast() } + assertFailsWith { pub.awaitSingle() } var cnt = 0 pub.collect { cnt++ } assertEquals(0, cnt) @@ -87,7 +87,7 @@ class IntegrationTest( assertEquals(n, pub.awaitLast()) assertEquals(1, pub.awaitFirstOrNull()) assertEquals(1, pub.awaitFirstOrElse { 0 }) - assertIAE { pub.awaitSingle() } + assertFailsWith { pub.awaitSingle() } checkNumbers(n, pub) val channel = pub.openSubscription() checkNumbers(n, channel.asPublisher(ctx(coroutineContext))) @@ -129,21 +129,4 @@ class IntegrationTest( assertEquals(n, last) } - private inline fun assertIAE(block: () -> Unit) { - try { - block() - expectUnreached() - } catch (e: Throwable) { - assertTrue(e is IllegalArgumentException) - } - } - - private inline fun assertNSE(block: () -> Unit) { - try { - block() - expectUnreached() - } catch (e: Throwable) { - assertTrue(e is NoSuchElementException) - } - } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt index 4faebbd251..22e0e72191 100644 --- a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt @@ -42,12 +42,12 @@ class IntegrationTest( if (delay) delay(1) // does not send anything } - assertNSE { observable.awaitFirst() } + assertFailsWith { observable.awaitFirst() } assertEquals("OK", observable.awaitFirstOrDefault("OK")) assertNull(observable.awaitFirstOrNull()) assertEquals("ELSE", observable.awaitFirstOrElse { "ELSE" }) - assertNSE { observable.awaitLast() } - assertNSE { observable.awaitSingle() } + assertFailsWith { observable.awaitLast() } + assertFailsWith { observable.awaitSingle() } var cnt = 0 observable.collect { cnt++ @@ -89,7 +89,7 @@ class IntegrationTest( assertEquals(1, observable.awaitFirstOrNull()) assertEquals(1, observable.awaitFirstOrElse { 0 }) assertEquals(n, observable.awaitLast()) - assertIAE { observable.awaitSingle() } + assertFailsWith { observable.awaitSingle() } checkNumbers(n, observable) val channel = observable.openSubscription() checkNumbers(n, channel.asObservable(ctx(coroutineContext))) @@ -131,22 +131,4 @@ class IntegrationTest( assertEquals(n, last) } - - private inline fun assertIAE(block: () -> Unit) { - try { - block() - expectUnreached() - } catch (e: Throwable) { - assertTrue(e is IllegalArgumentException) - } - } - - private inline fun assertNSE(block: () -> Unit) { - try { - block() - expectUnreached() - } catch (e: Throwable) { - assertTrue(e is NoSuchElementException) - } - } } \ No newline at end of file From 0aa59892931984643b47f8fe4b3208a4d8ee67a1 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 10 Mar 2020 18:34:26 +0300 Subject: [PATCH 8/8] Fix --- .../api/kotlinx-coroutines-jdk9.api | 4 +- reactive/kotlinx-coroutines-jdk9/src/Await.kt | 14 +++---- .../kotlinx-coroutines-jdk9/src/Publish.kt | 8 ++-- .../src/ReactiveFlow.kt | 8 ++-- .../test/FlowAsPublisherTest.kt | 14 +++---- .../test/IntegrationTest.kt | 14 +++---- .../test/PublishTest.kt | 38 +++++++++---------- .../test/PublisherAsFlowTest.kt | 16 ++++---- .../test/PublisherBackpressureTest.kt | 10 ++--- .../test/PublisherCompletionStressTest.kt | 2 +- .../test/PublisherMultiTest.kt | 2 +- 11 files changed, 65 insertions(+), 65 deletions(-) diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index 3e58c8ba20..d4bc1698ef 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -8,8 +8,8 @@ public final class kotlinx/coroutines/jdk9/AwaitKt { } public final class kotlinx/coroutines/jdk9/PublishKt { - public static final fun publish (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; - public static synthetic fun publish$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; + public static final fun flowPublish (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/util/concurrent/Flow$Publisher; + public static synthetic fun flowPublish$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; } public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { diff --git a/reactive/kotlinx-coroutines-jdk9/src/Await.kt b/reactive/kotlinx-coroutines-jdk9/src/Await.kt index e533a86552..88268890e2 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Await.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Await.kt @@ -4,7 +4,7 @@ package kotlinx.coroutines.jdk9 -import java.util.concurrent.Flow.Publisher +import java.util.concurrent.* import org.reactivestreams.FlowAdapters import kotlinx.coroutines.reactive.* @@ -18,7 +18,7 @@ import kotlinx.coroutines.reactive.* * * @throws NoSuchElementException if publisher does not emit any value */ -public suspend fun Publisher.awaitFirst(): T = FlowAdapters.toPublisher(this).awaitFirst() +public suspend fun Flow.Publisher.awaitFirst(): T = FlowAdapters.toPublisher(this).awaitFirst() /** * Awaits for the first value from the given observable or the [default] value if none is emitted without blocking a @@ -28,7 +28,7 @@ public suspend fun Publisher.awaitFirst(): T = FlowAdapters.toPublisher(t * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ -public suspend fun Publisher.awaitFirstOrDefault(default: T): T = +public suspend fun Flow.Publisher.awaitFirstOrDefault(default: T): T = FlowAdapters.toPublisher(this).awaitFirstOrDefault(default) /** @@ -39,7 +39,7 @@ public suspend fun Publisher.awaitFirstOrDefault(default: T): T = * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ -public suspend fun Publisher.awaitFirstOrNull(): T? = +public suspend fun Flow.Publisher.awaitFirstOrNull(): T? = FlowAdapters.toPublisher(this).awaitFirstOrNull() /** @@ -50,7 +50,7 @@ public suspend fun Publisher.awaitFirstOrNull(): T? = * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ -public suspend fun Publisher.awaitFirstOrElse(defaultValue: () -> T): T = +public suspend fun Flow.Publisher.awaitFirstOrElse(defaultValue: () -> T): T = FlowAdapters.toPublisher(this).awaitFirstOrElse(defaultValue) /** @@ -63,7 +63,7 @@ public suspend fun Publisher.awaitFirstOrElse(defaultValue: () -> T): T = * * @throws NoSuchElementException if publisher does not emit any value */ -public suspend fun Publisher.awaitLast(): T = +public suspend fun Flow.Publisher.awaitLast(): T = FlowAdapters.toPublisher(this).awaitLast() /** @@ -77,5 +77,5 @@ public suspend fun Publisher.awaitLast(): T = * @throws NoSuchElementException if publisher does not emit any value * @throws IllegalArgumentException if publisher emits more than one value */ -public suspend fun Publisher.awaitSingle(): T = +public suspend fun Flow.Publisher.awaitSingle(): T = FlowAdapters.toPublisher(this).awaitSingle() diff --git a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt index d8e99dfaa4..d274083668 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt @@ -6,12 +6,12 @@ package kotlinx.coroutines.jdk9 import kotlinx.coroutines.* import kotlinx.coroutines.channels.* -import java.util.concurrent.Flow.* +import java.util.concurrent.* import kotlin.coroutines.* import org.reactivestreams.FlowAdapters /** - * Creates cold reactive [Publisher] that runs a given [block] in a coroutine. + * Creates cold reactive [Flow.Publisher] that runs a given [block] in a coroutine. * Every time the returned flux is subscribed, it starts a new coroutine in the specified [context]. * Coroutine emits ([Subscriber.onNext]) values with `send`, completes ([Subscriber.onComplete]) * when the coroutine completes or channel is explicitly closed and emits error ([Subscriber.onError]) @@ -29,10 +29,10 @@ import org.reactivestreams.FlowAdapters * to cancellation and error handling may change in the future. */ @ExperimentalCoroutinesApi -public fun publish( +public fun flowPublish( context: CoroutineContext = EmptyCoroutineContext, @BuilderInference block: suspend ProducerScope.() -> Unit -): Publisher { +): Flow.Publisher { val reactivePublisher : org.reactivestreams.Publisher = kotlinx.coroutines.reactive.publish(context, block) return FlowAdapters.toFlowPublisher(reactivePublisher) } diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index ce91164a3b..6568c73a4a 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asPublisher import kotlinx.coroutines.reactive.collect -import java.util.concurrent.Flow.* +import java.util.concurrent.Flow as JFlow import org.reactivestreams.FlowAdapters /** @@ -20,13 +20,13 @@ import org.reactivestreams.FlowAdapters * If any of the resulting flow transformations fails, subscription is immediately cancelled and all in-flight elements * are discarded. */ -public fun Publisher.asFlow(): Flow = +public fun JFlow.Publisher.asFlow(): Flow = FlowAdapters.toPublisher(this).asFlow() /** * Transforms the given flow to a reactive specification compliant [Publisher]. */ -public fun Flow.asPublisher(): Publisher { +public fun Flow.asPublisher(): JFlow.Publisher { val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher() return FlowAdapters.toFlowPublisher(reactivePublisher) } @@ -35,5 +35,5 @@ public fun Flow.asPublisher(): Publisher { * Subscribes to this [Publisher] and performs the specified action for each received element. * Cancels subscription if any exception happens during collect. */ -public suspend inline fun Publisher.collect(action: (T) -> Unit) = +public suspend inline fun JFlow.Publisher.collect(action: (T) -> Unit) = FlowAdapters.toPublisher(this).collect(action) diff --git a/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt index 898feaccc5..8017ee5b4f 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt @@ -7,7 +7,7 @@ package kotlinx.coroutines.jdk9 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test -import java.util.concurrent.Flow.* +import java.util.concurrent.Flow as JFlow import kotlin.test.* class FlowAsPublisherTest : TestBase() { @@ -22,14 +22,14 @@ class FlowAsPublisherTest : TestBase() { } finally { throw TestException() } - }.asPublisher().subscribe(object : Subscriber { - private lateinit var subscription: Subscription + }.asPublisher().subscribe(object : JFlow.Subscriber { + private lateinit var subscription: JFlow.Subscription override fun onComplete() { expectUnreached() } - override fun onSubscribe(s: Subscription?) { + override fun onSubscribe(s: JFlow.Subscription?) { subscription = s!! subscription.request(2) } @@ -53,14 +53,14 @@ class FlowAsPublisherTest : TestBase() { flow { emit(2) hang { expect(3) } - }.asPublisher().subscribe(object : Subscriber { - private lateinit var subscription: Subscription + }.asPublisher().subscribe(object : JFlow.Subscriber { + private lateinit var subscription: JFlow.Subscription override fun onComplete() { expect(4) } - override fun onSubscribe(s: Subscription?) { + override fun onSubscribe(s: JFlow.Subscription?) { subscription = s!! subscription.request(2) } diff --git a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt index 12b819383b..5bfddfee17 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/IntegrationTest.kt @@ -9,7 +9,7 @@ import org.junit.Test import kotlinx.coroutines.flow.flowOn import org.junit.runner.* import org.junit.runners.* -import java.util.concurrent.Flow.* +import java.util.concurrent.Flow as JFlow import kotlin.coroutines.* import kotlin.test.* @@ -39,7 +39,7 @@ class IntegrationTest( @Test fun testEmpty(): Unit = runBlocking { - val pub = publish(ctx(coroutineContext)) { + val pub = flowPublish(ctx(coroutineContext)) { if (delay) delay(1) // does not send anything } @@ -56,7 +56,7 @@ class IntegrationTest( @Test fun testSingle() = runBlocking { - val pub = publish(ctx(coroutineContext)) { + val pub = flowPublish(ctx(coroutineContext)) { if (delay) delay(1) send("OK") } @@ -77,7 +77,7 @@ class IntegrationTest( @Test fun testNumbers() = runBlocking { val n = 100 * stressTestMultiplier - val pub = publish(ctx(coroutineContext)) { + val pub = flowPublish(ctx(coroutineContext)) { for (i in 1..n) { send(i) if (delay) delay(1) @@ -97,7 +97,7 @@ class IntegrationTest( @Test fun testCancelWithoutValue() = runTest { val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { - publish { + flowPublish { hang {} }.awaitFirst() } @@ -110,7 +110,7 @@ class IntegrationTest( fun testEmptySingle() = runTest(unhandled = listOf { e -> e is NoSuchElementException}) { expect(1) val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { - publish { + flowPublish { yield() expect(2) // Nothing to emit @@ -121,7 +121,7 @@ class IntegrationTest( finish(3) } - private suspend fun checkNumbers(n: Int, pub: Publisher) { + private suspend fun checkNumbers(n: Int, pub: JFlow.Publisher) { var last = 0 pub.collect { assertEquals(++last, it) diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt index 4c53389dcd..1a36a389fa 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/PublishTest.kt @@ -6,19 +6,19 @@ package kotlinx.coroutines.jdk9 import kotlinx.coroutines.* import org.junit.Test -import java.util.concurrent.Flow.* +import java.util.concurrent.Flow as JFlow import kotlin.test.* class PublishTest : TestBase() { @Test fun testBasicEmpty() = runTest { expect(1) - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(5) } expect(2) - publisher.subscribe(object : Subscriber { - override fun onSubscribe(s: Subscription?) { expect(3) } + publisher.subscribe(object : JFlow.Subscriber { + override fun onSubscribe(s: JFlow.Subscription?) { expect(3) } override fun onNext(t: Int?) { expectUnreached() } override fun onComplete() { expect(6) } override fun onError(t: Throwable?) { expectUnreached() } @@ -31,14 +31,14 @@ class PublishTest : TestBase() { @Test fun testBasicSingle() = runTest { expect(1) - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(5) send(42) expect(7) } expect(2) - publisher.subscribe(object : Subscriber { - override fun onSubscribe(s: Subscription) { + publisher.subscribe(object : JFlow.Subscriber { + override fun onSubscribe(s: JFlow.Subscription) { expect(3) s.request(1) } @@ -57,13 +57,13 @@ class PublishTest : TestBase() { @Test fun testBasicError() = runTest { expect(1) - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(5) throw RuntimeException("OK") } expect(2) - publisher.subscribe(object : Subscriber { - override fun onSubscribe(s: Subscription) { + publisher.subscribe(object : JFlow.Subscriber { + override fun onSubscribe(s: JFlow.Subscription) { expect(3) s.request(1) } @@ -88,7 +88,7 @@ class PublishTest : TestBase() { assertTrue(t is RuntimeException) expect(6) } - val publisher = publish(Dispatchers.Unconfined + eh) { + val publisher = flowPublish(Dispatchers.Unconfined + eh) { try { expect(3) delay(10000) @@ -97,13 +97,13 @@ class PublishTest : TestBase() { throw RuntimeException("FAILED") // crash after cancel } } - var sub: Subscription? = null - publisher.subscribe(object : Subscriber { + var sub: JFlow.Subscription? = null + publisher.subscribe(object : JFlow.Subscriber { override fun onComplete() { expectUnreached() } - override fun onSubscribe(s: Subscription) { + override fun onSubscribe(s: JFlow.Subscription) { expect(2) sub = s } @@ -124,7 +124,7 @@ class PublishTest : TestBase() { @Test fun testOnNextError() = runTest { expect(1) - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(4) try { send("OK") @@ -135,12 +135,12 @@ class PublishTest : TestBase() { } expect(2) val latch = CompletableDeferred() - publisher.subscribe(object : Subscriber { + publisher.subscribe(object : JFlow.Subscriber { override fun onComplete() { expectUnreached() } - override fun onSubscribe(s: Subscription) { + override fun onSubscribe(s: JFlow.Subscription) { expect(3) s.request(1) } @@ -163,7 +163,7 @@ class PublishTest : TestBase() { @Test fun testFailingConsumer() = runTest { - val pub = publish(currentDispatcher()) { + val pub = flowPublish(currentDispatcher()) { repeat(3) { expect(it + 1) // expect(1), expect(2) *should* be invoked send(it) @@ -180,6 +180,6 @@ class PublishTest : TestBase() { @Test fun testIllegalArgumentException() { - assertFailsWith { publish(Job()) { } } + assertFailsWith { flowPublish(Job()) { } } } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt index b608c6b863..97f106b3c5 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherAsFlowTest.kt @@ -16,7 +16,7 @@ class PublisherAsFlowTest : TestBase() { var onCancelled = 0 var onError = 0 - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { coroutineContext[Job]?.invokeOnCompletion { if (it is CancellationException) ++onCancelled } @@ -44,7 +44,7 @@ class PublisherAsFlowTest : TestBase() { @Test fun testBufferSize1() = runTest { - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(1) send(3) @@ -65,7 +65,7 @@ class PublisherAsFlowTest : TestBase() { @Test fun testBufferSizeDefault() = runTest { - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { repeat(64) { send(it + 1) expect(it + 1) @@ -82,7 +82,7 @@ class PublisherAsFlowTest : TestBase() { @Test fun testDefaultCapacityIsProperlyOverwritten() = runTest { - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(1) send(3) expect(2) @@ -101,7 +101,7 @@ class PublisherAsFlowTest : TestBase() { @Test fun testBufferSize10() = runTest { - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { expect(1) send(5) @@ -122,7 +122,7 @@ class PublisherAsFlowTest : TestBase() { @Test fun testConflated() = runTest { - val publisher = publish(currentDispatcher()) { + val publisher = flowPublish(currentDispatcher()) { for (i in 1..5) send(i) } val list = publisher.asFlow().conflate().toList() @@ -131,7 +131,7 @@ class PublisherAsFlowTest : TestBase() { @Test fun testProduce() = runTest { - val flow = publish(currentDispatcher()) { repeat(10) { send(it) } }.asFlow() + val flow = flowPublish(currentDispatcher()) { repeat(10) { send(it) } }.asFlow() check((0..9).toList(), flow.produceIn(this)) check((0..9).toList(), flow.buffer(2).produceIn(this)) check((0..9).toList(), flow.buffer(Channel.UNLIMITED).produceIn(this)) @@ -148,7 +148,7 @@ class PublisherAsFlowTest : TestBase() { fun testProduceCancellation() = runTest { expect(1) // publisher is an async coroutine, so it overproduces to the channel, but still gets cancelled - val flow = publish(currentDispatcher()) { + val flow = flowPublish(currentDispatcher()) { expect(3) repeat(10) { value -> when (value) { diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt index fda133a279..bc9d58ebcf 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherBackpressureTest.kt @@ -6,13 +6,13 @@ package kotlinx.coroutines.jdk9 import kotlinx.coroutines.* import org.junit.* -import java.util.concurrent.Flow.* +import java.util.concurrent.Flow as JFlow class PublisherBackpressureTest : TestBase() { @Test fun testCancelWhileBPSuspended() = runBlocking { expect(1) - val observable = publish(currentDispatcher()) { + val observable = flowPublish(currentDispatcher()) { expect(5) send("A") // will not suspend, because an item was requested expect(7) @@ -26,9 +26,9 @@ class PublisherBackpressureTest : TestBase() { expectUnreached() } expect(2) - var sub: Subscription? = null - observable.subscribe(object : Subscriber { - override fun onSubscribe(s: Subscription) { + var sub: JFlow.Subscription? = null + observable.subscribe(object : JFlow.Subscriber { + override fun onSubscribe(s: JFlow.Subscription) { sub = s expect(3) s.request(2) // request two items diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt index cf89c6a51d..8462df2c6f 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherCompletionStressTest.kt @@ -12,7 +12,7 @@ import kotlin.coroutines.* class PublisherCompletionStressTest : TestBase() { private val N_REPEATS = 10_000 * stressTestMultiplier - private fun CoroutineScope.range(context: CoroutineContext, start: Int, count: Int) = publish(context) { + private fun CoroutineScope.range(context: CoroutineContext, start: Int, count: Int) = flowPublish(context) { for (x in start until start + count) send(x) } diff --git a/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt b/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt index 4ea467e695..a44850b52d 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/PublisherMultiTest.kt @@ -12,7 +12,7 @@ class PublisherMultiTest : TestBase() { @Test fun testConcurrentStress() = runBlocking { val n = 10_000 * stressTestMultiplier - val observable = publish { + val observable = flowPublish { // concurrent emitters (many coroutines) val jobs = List(n) { // launch