From 6e7aa7be2439776d6bb9664d2ab0fe56cce59cde Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 8 May 2020 18:11:26 +0300 Subject: [PATCH 01/30] Introduce SharedFlow and sharing operators Summary of changes: * SharedFlow, MutableSharedFlow and its constructor. * StateFlow implements SharedFlow. * SharedFlow.onSubscription operator, clarified docs in other onXxx operators. * BufferOverflow strategy in kotlinx.coroutines.channels package. * shareIn and stateIn operators and SharingStarted strategies for them. * SharedFlow.flowOn error lint (up from StateFlow). * Precise cancellable() operator fusion. * Precise distinctUntilChanged() operator fusion. * StateFlow.compareAndSet function. * asStateFlow and asSharedFlow read-only view functions. * Consistently clarified docs on cold vs hot flows. * Future deprecation notice for BroadcastChannel, ConflatedBroadcastChannel, broadcast, and broadcastIn. * Channel(...) constructor function has onBufferOverflow parameter. * buffer(...) operator has onBufferOverflow parameter. * shareIn/stateIn buffer and overflow strategy are configured via upstream buffer operators. * shareIn/stateIn fuse with upstream flowOn for more efficient execution. * conflate() is implemented as buffer(onBufferOverflow=KEEP_LATEST), non-suspending strategies are reasonably supported with 0 and default capacities. * Added reactive operator migration hints. * WhileSubscribed with kotlin.time.Duration params Fixes #2034 Fixes #2047 Co-authored-by: Ibraheem Zaman <1zaman@users.noreply.github.com> Co-authored-by: Thomas Vos Co-authored-by: Travis Wyatt --- README.md | 4 +- .../api/kotlinx-coroutines-core.api | 97 ++- .../common/src/CompletableDeferred.kt | 2 +- .../common/src/CompletableJob.kt | 2 +- .../common/src/channels/AbstractChannel.kt | 10 +- .../common/src/channels/ArrayChannel.kt | 131 +-- .../common/src/channels/Broadcast.kt | 7 +- .../common/src/channels/BroadcastChannel.kt | 7 +- .../common/src/channels/BufferOverflow.kt | 36 + .../common/src/channels/Channel.kt | 76 +- .../src/channels/ConflatedBroadcastChannel.kt | 6 +- .../common/src/channels/Produce.kt | 22 +- .../common/src/flow/AbstractSharedFlow.kt | 93 ++ .../common/src/flow/Builders.kt | 52 +- .../common/src/flow/Channels.kt | 17 +- .../common/src/flow/Flow.kt | 22 +- .../common/src/flow/Migration.kt | 59 +- .../common/src/flow/SharedFlow.kt | 651 ++++++++++++++ .../common/src/flow/SharingStarted.kt | 213 +++++ .../common/src/flow/StateFlow.kt | 261 +++--- .../common/src/flow/internal/ChannelFlow.kt | 122 ++- .../common/src/flow/internal/DistinctFlow.kt | 32 + .../common/src/flow/internal/Merge.kt | 30 +- .../common/src/flow/operators/Context.kt | 85 +- .../common/src/flow/operators/Distinct.kt | 27 +- .../common/src/flow/operators/Emitters.kt | 11 +- .../common/src/flow/operators/Lint.kt | 37 +- .../common/src/flow/operators/Share.kt | 403 +++++++++ .../common/src/flow/operators/Transform.kt | 2 +- .../channels/ChannelBufferOverflowTest.kt | 40 + .../test/channels/ChannelFactoryTest.kt | 17 +- .../ConflatedChannelArrayModelTest.kt | 11 + .../test/channels/ConflatedChannelTest.kt | 19 +- .../common/test/flow/StateFlowTest.kt | 113 --- .../common/test/flow/VirtualTime.kt | 14 +- .../flow/operators/BufferConflationTest.kt | 146 ++++ .../common/test/flow/operators/BufferTest.kt | 33 +- .../test/flow/sharing/ShareInBufferTest.kt | 98 +++ .../flow/sharing/ShareInConflationTest.kt | 162 ++++ .../test/flow/sharing/ShareInFusionTest.kt | 56 ++ .../common/test/flow/sharing/ShareInTest.kt | 214 +++++ .../flow/sharing/SharedFlowScenarioTest.kt | 331 ++++++++ .../test/flow/sharing/SharedFlowTest.kt | 798 ++++++++++++++++++ .../test/flow/sharing/SharingStartedTest.kt | 183 ++++ .../SharingStartedWhileSubscribedTest.kt | 42 + .../common/test/flow/sharing/StateFlowTest.kt | 207 +++++ .../common/test/flow/sharing/StateInTest.kt | 78 ++ kotlinx-coroutines-core/jvm/test/TestBase.kt | 2 +- .../jvm/test/flow/SharingStressTest.kt | 193 +++++ .../kotlinx-coroutines-reactive/README.md | 4 +- .../src/ReactiveFlow.kt | 30 +- ui/coroutines-guide-ui.md | 2 +- 52 files changed, 4789 insertions(+), 521 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/SharedFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/SharingStarted.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt create mode 100644 kotlinx-coroutines-core/common/src/flow/operators/Share.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt delete mode 100644 kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt diff --git a/README.md b/README.md index 4b6f99be0b..362e80f57e 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ suspend fun main() = coroutineScope { * [DebugProbes] API to probe, keep track of, print and dump active coroutines; * [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), + * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [kotlinx.coroutines.reactive.publish], etc), * Flow (JDK 9) (the same interface as for Reactive Streams), * RxJava 2.x ([rxFlowable], [rxSingle], etc), and * RxJava 3.x ([rxFlowable], [rxSingle], etc), and @@ -291,7 +291,7 @@ The `develop` branch is pushed to `master` during release. [Publisher.collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/collect.html [Publisher.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-single.html -[publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html +[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html [rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 36cbdb6960..b3fd743ed0 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -580,6 +580,14 @@ public final class kotlinx/coroutines/channels/BroadcastKt { public static synthetic fun broadcast$default (Lkotlinx/coroutines/channels/ReceiveChannel;ILkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; } +public final class kotlinx/coroutines/channels/BufferOverflow : java/lang/Enum { + public static final field DROP_LATEST Lkotlinx/coroutines/channels/BufferOverflow; + public static final field DROP_OLDEST Lkotlinx/coroutines/channels/BufferOverflow; + public static final field SUSPEND Lkotlinx/coroutines/channels/BufferOverflow; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/channels/BufferOverflow; + public static fun values ()[Lkotlinx/coroutines/channels/BufferOverflow; +} + public abstract interface class kotlinx/coroutines/channels/Channel : kotlinx/coroutines/channels/ReceiveChannel, kotlinx/coroutines/channels/SendChannel { public static final field BUFFERED I public static final field CONFLATED I @@ -612,8 +620,10 @@ public final class kotlinx/coroutines/channels/ChannelIterator$DefaultImpls { } public final class kotlinx/coroutines/channels/ChannelKt { - public static final fun Channel (I)Lkotlinx/coroutines/channels/Channel; + public static final synthetic fun Channel (I)Lkotlinx/coroutines/channels/Channel; + public static final fun Channel (ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/channels/Channel; public static synthetic fun Channel$default (IILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; + public static synthetic fun Channel$default (ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; } public final class kotlinx/coroutines/channels/ChannelsKt { @@ -868,7 +878,7 @@ public final class kotlinx/coroutines/debug/internal/DebuggerInfo : java/io/Seri public final fun getState ()Ljava/lang/String; } -public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { +public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/CancellableFlow, kotlinx/coroutines/flow/Flow { public fun ()V public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun collectSafely (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -895,10 +905,15 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun asFlow ([I)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([J)Lkotlinx/coroutines/flow/Flow; public static final fun asFlow ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun asSharedFlow (Lkotlinx/coroutines/flow/MutableSharedFlow;)Lkotlinx/coroutines/flow/SharedFlow; + public static final fun asStateFlow (Lkotlinx/coroutines/flow/MutableStateFlow;)Lkotlinx/coroutines/flow/StateFlow; public static final fun broadcastIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public static synthetic fun broadcastIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;ILjava/lang/Object;)Lkotlinx/coroutines/channels/BroadcastChannel; - public static final fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static final synthetic fun buffer (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; + public static final fun buffer (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;IILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun buffer$default (Lkotlinx/coroutines/flow/Flow;ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public static final fun cache (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun callbackFlow (Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun cancellable (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun catch (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; @@ -988,10 +1003,15 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun onErrorReturn (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun onErrorReturn$default (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun onStart (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun onSubscription (Lkotlinx/coroutines/flow/SharedFlow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun produceIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final fun publish (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun publish (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun publishOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static final fun receiveAsFlow (Lkotlinx/coroutines/channels/ReceiveChannel;)Lkotlinx/coroutines/flow/Flow; public static final fun reduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun replay (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun replay (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final synthetic fun retry (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun retry (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; @@ -1003,11 +1023,16 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; + public static final fun shareIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/flow/SharingStarted;)Lkotlinx/coroutines/flow/SharedFlow; + public static synthetic fun shareIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/flow/SharingStarted;ILjava/lang/Object;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun skip (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; + public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + public static synthetic fun stateIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V @@ -1029,17 +1054,59 @@ public final class kotlinx/coroutines/flow/FlowKt { } public final class kotlinx/coroutines/flow/LintKt { + public static final fun cancellable (Lkotlinx/coroutines/flow/SharedFlow;)Lkotlinx/coroutines/flow/Flow; public static final fun conflate (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; - public static final fun flowOn (Lkotlinx/coroutines/flow/StateFlow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; + public static final fun flowOn (Lkotlinx/coroutines/flow/SharedFlow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; } -public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/StateFlow { +public abstract interface class kotlinx/coroutines/flow/MutableSharedFlow : kotlinx/coroutines/flow/FlowCollector, kotlinx/coroutines/flow/SharedFlow { + public abstract fun getSubscriptionCount ()Lkotlinx/coroutines/flow/StateFlow; + public abstract fun resetReplayCache ()V + public abstract fun tryEmit (Ljava/lang/Object;)Z +} + +public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/MutableSharedFlow, kotlinx/coroutines/flow/StateFlow { + public abstract fun compareAndSet (Ljava/lang/Object;Ljava/lang/Object;)Z public abstract fun getValue ()Ljava/lang/Object; public abstract fun setValue (Ljava/lang/Object;)V } -public abstract interface class kotlinx/coroutines/flow/StateFlow : kotlinx/coroutines/flow/Flow { +public abstract interface class kotlinx/coroutines/flow/SharedFlow : kotlinx/coroutines/flow/Flow { + public abstract fun getReplayCache ()Ljava/util/List; +} + +public final class kotlinx/coroutines/flow/SharedFlowKt { + public static final fun MutableSharedFlow (IILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/MutableSharedFlow; + public static synthetic fun MutableSharedFlow$default (IILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/MutableSharedFlow; +} + +public final class kotlinx/coroutines/flow/SharingCommand : java/lang/Enum { + public static final field START Lkotlinx/coroutines/flow/SharingCommand; + public static final field STOP Lkotlinx/coroutines/flow/SharingCommand; + public static final field STOP_AND_RESET_REPLAY_CACHE Lkotlinx/coroutines/flow/SharingCommand; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/flow/SharingCommand; + public static fun values ()[Lkotlinx/coroutines/flow/SharingCommand; +} + +public abstract interface class kotlinx/coroutines/flow/SharingStarted { + public static final field Companion Lkotlinx/coroutines/flow/SharingStarted$Companion; + public abstract fun command (Lkotlinx/coroutines/flow/StateFlow;)Lkotlinx/coroutines/flow/Flow; +} + +public final class kotlinx/coroutines/flow/SharingStarted$Companion { + public final fun WhileSubscribed (JJ)Lkotlinx/coroutines/flow/SharingStarted; + public static synthetic fun WhileSubscribed$default (Lkotlinx/coroutines/flow/SharingStarted$Companion;JJILjava/lang/Object;)Lkotlinx/coroutines/flow/SharingStarted; + public final fun getEagerly ()Lkotlinx/coroutines/flow/SharingStarted; + public final fun getLazily ()Lkotlinx/coroutines/flow/SharingStarted; +} + +public final class kotlinx/coroutines/flow/SharingStartedKt { + public static final fun WhileSubscribed-9tZugJw (Lkotlinx/coroutines/flow/SharingStarted$Companion;DD)Lkotlinx/coroutines/flow/SharingStarted; + public static synthetic fun WhileSubscribed-9tZugJw$default (Lkotlinx/coroutines/flow/SharingStarted$Companion;DDILjava/lang/Object;)Lkotlinx/coroutines/flow/SharingStarted; +} + +public abstract interface class kotlinx/coroutines/flow/StateFlow : kotlinx/coroutines/flow/SharedFlow { public abstract fun getValue ()Ljava/lang/Object; } @@ -1050,13 +1117,15 @@ public final class kotlinx/coroutines/flow/StateFlowKt { public abstract class kotlinx/coroutines/flow/internal/ChannelFlow : kotlinx/coroutines/flow/internal/FusibleFlow { public final field capacity I public final field context Lkotlin/coroutines/CoroutineContext; - public fun (Lkotlin/coroutines/CoroutineContext;I)V - public fun additionalToStringProps ()Ljava/lang/String; + public final field onBufferOverflow Lkotlinx/coroutines/channels/BufferOverflow; + public fun (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)V + protected fun additionalToStringProps ()Ljava/lang/String; public fun broadcastImpl (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/CoroutineStart;)Lkotlinx/coroutines/channels/BroadcastChannel; public fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected abstract fun collectTo (Lkotlinx/coroutines/channels/ProducerScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - protected abstract fun create (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/ChannelFlow; - public fun fuse (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/FusibleFlow; + protected abstract fun create (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/internal/ChannelFlow; + public fun dropChannelOperators ()Lkotlinx/coroutines/flow/Flow; + public fun fuse (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; public fun produceImpl (Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/channels/ReceiveChannel; public fun toString ()Ljava/lang/String; } @@ -1065,16 +1134,20 @@ public final class kotlinx/coroutines/flow/internal/CombineKt { public static final fun combineInternal (Lkotlinx/coroutines/flow/FlowCollector;[Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class kotlinx/coroutines/flow/internal/DistinctFlowKt { + public static final fun unsafeDistinctFlow (ZLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; +} + public final class kotlinx/coroutines/flow/internal/FlowExceptions_commonKt { public static final fun checkIndexOverflow (I)I } public abstract interface class kotlinx/coroutines/flow/internal/FusibleFlow : kotlinx/coroutines/flow/Flow { - public abstract fun fuse (Lkotlin/coroutines/CoroutineContext;I)Lkotlinx/coroutines/flow/internal/FusibleFlow; + public abstract fun fuse (Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;)Lkotlinx/coroutines/flow/Flow; } public final class kotlinx/coroutines/flow/internal/FusibleFlow$DefaultImpls { - public static synthetic fun fuse$default (Lkotlinx/coroutines/flow/internal/FusibleFlow;Lkotlin/coroutines/CoroutineContext;IILjava/lang/Object;)Lkotlinx/coroutines/flow/internal/FusibleFlow; + public static synthetic fun fuse$default (Lkotlinx/coroutines/flow/internal/FusibleFlow;Lkotlin/coroutines/CoroutineContext;ILkotlinx/coroutines/channels/BufferOverflow;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; } public final class kotlinx/coroutines/flow/internal/SafeCollector_commonKt { diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index d24f1837cd..0605817afa 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.selects.* * All functions on this interface are **thread-safe** and can * be safely invoked from concurrent coroutines without external synchronization. * - * **`CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**, + * **The `CompletableDeferred` interface is not stable for inheritance in 3rd party libraries**, * as new methods might be added to this interface in the future, but is stable for use. */ public interface CompletableDeferred : Deferred { diff --git a/kotlinx-coroutines-core/common/src/CompletableJob.kt b/kotlinx-coroutines-core/common/src/CompletableJob.kt index 8e6b1ab02f..74a92e36e5 100644 --- a/kotlinx-coroutines-core/common/src/CompletableJob.kt +++ b/kotlinx-coroutines-core/common/src/CompletableJob.kt @@ -11,7 +11,7 @@ package kotlinx.coroutines * All functions on this interface are **thread-safe** and can * be safely invoked from concurrent coroutines without external synchronization. * - * **`CompletableJob` interface is not stable for inheritance in 3rd party libraries**, + * **The `CompletableJob` interface is not stable for inheritance in 3rd party libraries**, * as new methods might be added to this interface in the future, but is stable for use. */ public interface CompletableJob : Job { diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 28c7ceabe1..b53e025021 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -959,23 +959,23 @@ internal const val RECEIVE_RESULT = 2 @JvmField @SharedImmutable -internal val OFFER_SUCCESS: Any = Symbol("OFFER_SUCCESS") +internal val OFFER_SUCCESS = Symbol("OFFER_SUCCESS") @JvmField @SharedImmutable -internal val OFFER_FAILED: Any = Symbol("OFFER_FAILED") +internal val OFFER_FAILED = Symbol("OFFER_FAILED") @JvmField @SharedImmutable -internal val POLL_FAILED: Any = Symbol("POLL_FAILED") +internal val POLL_FAILED = Symbol("POLL_FAILED") @JvmField @SharedImmutable -internal val ENQUEUE_FAILED: Any = Symbol("ENQUEUE_FAILED") +internal val ENQUEUE_FAILED = Symbol("ENQUEUE_FAILED") @JvmField @SharedImmutable -internal val HANDLER_INVOKED: Any = Symbol("ON_CLOSE_HANDLER_INVOKED") +internal val HANDLER_INVOKED = Symbol("ON_CLOSE_HANDLER_INVOKED") internal typealias Handler = (Throwable?) -> Unit diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index e26579eff7..7b49be70cd 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -23,13 +23,15 @@ internal open class ArrayChannel( /** * Buffer capacity. */ - val capacity: Int + private val capacity: Int, + private val onBufferOverflow: BufferOverflow ) : AbstractChannel() { init { require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } } private val lock = ReentrantLock() + /* * Guarded by lock. * Allocate minimum of capacity and 16 to avoid excess memory pressure for large channels when it's not necessary. @@ -41,7 +43,7 @@ internal open class ArrayChannel( protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = size.value == 0 protected final override val isBufferAlwaysFull: Boolean get() = false - protected final override val isBufferFull: Boolean get() = size.value == capacity + protected final override val isBufferFull: Boolean get() = size.value == capacity && onBufferOverflow == BufferOverflow.SUSPEND override val isFull: Boolean get() = lock.withLock { isFullImpl } override val isEmpty: Boolean get() = lock.withLock { isEmptyImpl } @@ -53,31 +55,26 @@ internal open class ArrayChannel( lock.withLock { val size = this.size.value closedForSend?.let { return it } - if (size < capacity) { - // tentatively put element to buffer - this.size.value = size + 1 // update size before checking queue (!!!) - // check for receivers that were waiting on empty queue - if (size == 0) { - loop@ while (true) { - receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued - if (receive is Closed) { - this.size.value = size // restore size - return receive!! - } - val token = receive!!.tryResumeReceive(element, null) - if (token != null) { - assert { token === RESUME_TOKEN } - this.size.value = size // restore size - return@withLock - } + // update size before checking queue (!!!) + updateBufferSize(size)?.let { return it } + // check for receivers that were waiting on empty queue + if (size == 0) { + loop@ while (true) { + receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued + if (receive is Closed) { + this.size.value = size // restore size + return receive!! + } + val token = receive!!.tryResumeReceive(element, null) + if (token != null) { + assert { token === RESUME_TOKEN } + this.size.value = size // restore size + return@withLock } } - ensureCapacity(size) - buffer[(head + size) % buffer.size] = element // actually queue element - return OFFER_SUCCESS } - // size == capacity: full - return OFFER_FAILED + enqueueElement(size, element) + return OFFER_SUCCESS } // breaks here if offer meets receiver receive!!.completeResumeReceive(element) @@ -90,41 +87,36 @@ internal open class ArrayChannel( lock.withLock { val size = this.size.value closedForSend?.let { return it } - if (size < capacity) { - // tentatively put element to buffer - this.size.value = size + 1 // update size before checking queue (!!!) - // check for receivers that were waiting on empty queue - if (size == 0) { - loop@ while (true) { - val offerOp = describeTryOffer(element) - val failure = select.performAtomicTrySelect(offerOp) - when { - failure == null -> { // offered successfully - this.size.value = size // restore size - receive = offerOp.result - return@withLock - } - failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer - failure === RETRY_ATOMIC -> {} // retry - failure === ALREADY_SELECTED || failure is Closed<*> -> { - this.size.value = size // restore size - return failure - } - else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") + // update size before checking queue (!!!) + updateBufferSize(size)?.let { return it } + // check for receivers that were waiting on empty queue + if (size == 0) { + loop@ while (true) { + val offerOp = describeTryOffer(element) + val failure = select.performAtomicTrySelect(offerOp) + when { + failure == null -> { // offered successfully + this.size.value = size // restore size + receive = offerOp.result + return@withLock + } + failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer + failure === RETRY_ATOMIC -> {} // retry + failure === ALREADY_SELECTED || failure is Closed<*> -> { + this.size.value = size // restore size + return failure } + else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") } } - // let's try to select sending this element to buffer - if (!select.trySelect()) { // :todo: move trySelect completion outside of lock - this.size.value = size // restore size - return ALREADY_SELECTED - } - ensureCapacity(size) - buffer[(head + size) % buffer.size] = element // actually queue element - return OFFER_SUCCESS } - // size == capacity: full - return OFFER_FAILED + // let's try to select sending this element to buffer + if (!select.trySelect()) { // :todo: move trySelect completion outside of lock + this.size.value = size // restore size + return ALREADY_SELECTED + } + enqueueElement(size, element) + return OFFER_SUCCESS } // breaks here if offer meets receiver receive!!.completeResumeReceive(element) @@ -135,6 +127,35 @@ internal open class ArrayChannel( super.enqueueSend(send) } + // Guarded by lock + // Result is `OFFER_SUCCESS | OFFER_FAILED | null` + private fun updateBufferSize(currentSize: Int): Symbol? { + if (currentSize < capacity) { + size.value = currentSize + 1 // tentatively put it into the buffer + return null // proceed + } + // buffer is full + return when (onBufferOverflow) { + BufferOverflow.SUSPEND -> OFFER_FAILED + BufferOverflow.DROP_LATEST -> OFFER_SUCCESS + BufferOverflow.DROP_OLDEST -> null // proceed, will drop oldest in enqueueElement + } + } + + // Guarded by lock + private fun enqueueElement(currentSize: Int, element: E) { + if (currentSize < capacity) { + ensureCapacity(currentSize) + buffer[(head + currentSize) % buffer.size] = element // actually queue element + } else { + // buffer is full + assert { onBufferOverflow == BufferOverflow.DROP_OLDEST } // the only way we can get here + buffer[head % buffer.size] = null // drop oldest element + buffer[(head + currentSize) % buffer.size] = element // actually queue element + head = (head + 1) % buffer.size + } + } + // Guarded by lock private fun ensureCapacity(currentSize: Int) { if (currentSize >= buffer.size) { diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index 863d1387fc..790580e0a3 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* -import kotlin.native.concurrent.* /** * Broadcasts all elements of the channel. @@ -34,8 +33,10 @@ import kotlin.native.concurrent.* * * This function has an inappropriate result type of [BroadcastChannel] which provides * [send][BroadcastChannel.send] and [close][BroadcastChannel.close] operations that interfere with - * the broadcasting coroutine in hard-to-specify ways. It will be replaced with - * sharing operators on [Flow][kotlinx.coroutines.flow.Flow] in the future. + * the broadcasting coroutine in hard-to-specify ways. + * + * **Note: This API is obsolete.** It will be deprecated and replaced with the + * [Flow.shareIn][kotlinx.coroutines.flow.shareIn] operator when it becomes stable. * * @param start coroutine start option. The default value is [CoroutineStart.LAZY]. */ diff --git a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt index 312480f943..d356566f17 100644 --- a/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/BroadcastChannel.kt @@ -7,9 +7,9 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.channels.Channel.Factory.CHANNEL_DEFAULT_CAPACITY +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED /** @@ -20,9 +20,10 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED * See `BroadcastChannel()` factory function for the description of available * broadcast channel implementations. * - * **Note: This is an experimental api.** It may be changed in the future updates. + * **Note: This API is obsolete.** It will be deprecated and replaced by [SharedFlow][kotlinx.coroutines.flow.SharedFlow] + * when it becomes stable. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // not @ObsoleteCoroutinesApi to reduce burden for people who are still using it public interface BroadcastChannel : SendChannel { /** * Subscribes to this [BroadcastChannel] and returns a channel to receive elements from it. diff --git a/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt new file mode 100644 index 0000000000..99994ea81b --- /dev/null +++ b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.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.channels + +import kotlinx.coroutines.* + +/** + * A strategy for buffer overflow handling in [channels][Channel] and [flows][kotlinx.coroutines.flow.Flow] that + * controls what is going to be sacrificed on buffer overflow: + * + * * [SUSPEND] — the upstream that is [sending][SendChannel.send] or + * is [emitting][kotlinx.coroutines.flow.FlowCollector.emit] a value is **suspended** while the buffer is full. + * * [DROP_OLDEST] — drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. + * * [DROP_LATEST] — drop **the latest** value that is being added to the buffer right now on buffer overflow + * (so that buffer contents stay the same), do not suspend. + */ +@ExperimentalCoroutinesApi +public enum class BufferOverflow { + /** + * Suspend on buffer overflow. + */ + SUSPEND, + + /** + * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend. + */ + DROP_OLDEST, + + /** + * Drop **the latest** value that is being added to the buffer right now on buffer overflow + * (so that buffer contents stay the same), do not suspend. + */ + DROP_LATEST +} diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index c4b4a9b25e..54fa2da9b6 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -486,28 +486,45 @@ public interface ChannelIterator { * Conceptually, a channel is similar to Java's [BlockingQueue][java.util.concurrent.BlockingQueue], * but it has suspending operations instead of blocking ones and can be [closed][SendChannel.close]. * + * ### Creating channels + * * The `Channel(capacity)` factory function is used to create channels of different kinds depending on * the value of the `capacity` integer: * - * * When `capacity` is 0 — it creates a `RendezvousChannel`. + * * When `capacity` is 0 — it creates a _rendezvous_ channel. * This channel does not have any buffer at all. An element is transferred from the sender * to the receiver only when [send] and [receive] invocations meet in time (rendezvous), so [send] suspends * until another coroutine invokes [receive], and [receive] suspends until another coroutine invokes [send]. * - * * When `capacity` is [Channel.UNLIMITED] — it creates a `LinkedListChannel`. + * * When `capacity` is [Channel.UNLIMITED] — it creates a channel with effectively unlimited buffer. * This channel has a linked-list buffer of unlimited capacity (limited only by available memory). * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * - * * When `capacity` is [Channel.CONFLATED] — it creates a `ConflatedChannel`. + * * When `capacity` is [Channel.CONFLATED] — it creates a _conflated_ channel * This channel buffers at most one element and conflates all subsequent `send` and `offer` invocations, * so that the receiver always gets the last element sent. - * Back-to-send sent elements are _conflated_ — only the last sent element is received, + * Back-to-send sent elements are conflated — only the last sent element is received, * while previously sent elements **are lost**. * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * * * When `capacity` is positive but less than [UNLIMITED] — it creates an array-based channel with the specified capacity. * This channel has an array buffer of a fixed `capacity`. * [Sending][send] suspends only when the buffer is full, and [receiving][receive] suspends only when the buffer is empty. + * + * Buffered channels can be configured with an additional [`onBufferOverflow`][BufferOverflow] parameter. It controls the behaviour + * of the channel's [send][Channel.send] function on buffer overflow: + * + * * [SUSPEND][BufferOverflow.SUSPEND] — the default, suspend `send` on buffer overflow until there is + * free space in the buffer. + * * [DROP_OLDEST][BufferOverflow.DROP_OLDEST] — do not suspend the `send`, add the latest value to the buffer, + * drop the oldest one from the buffer. + * A channel with `capacity = 1` and `onBufferOverflow = DROP_OLDEST` is a _conflated_ channel. + * * [DROP_LATEST][BufferOverflow.DROP_LATEST] — do not suspend the `send`, drop the value that is being sent, + * keep the buffer contents intact. + * + * A non-default `onBufferOverflow` implicitly creates a channel with at least one buffered element and + * is ignored for a channel with unlimited buffer. It cannot be specified for `capacity = CONFLATED`, which + * is a shortcut by itself. */ public interface Channel : SendChannel, ReceiveChannel { /** @@ -515,25 +532,26 @@ public interface Channel : SendChannel, ReceiveChannel { */ public companion object Factory { /** - * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function + * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function. */ public const val UNLIMITED: Int = Int.MAX_VALUE /** - * Requests a rendezvous channel in the `Channel(...)` factory function — a `RendezvousChannel` gets created. + * Requests a rendezvous channel in the `Channel(...)` factory function — a channel that does not have a buffer. */ public const val RENDEZVOUS: Int = 0 /** - * Requests a conflated channel in the `Channel(...)` factory function — a `ConflatedChannel` gets created. + * Requests a conflated channel in the `Channel(...)` factory function. This is a shortcut to creating + * a channel with [`onBufferOverflow = DROP_OLDEST`][BufferOverflow.DROP_OLDEST]. */ public const val CONFLATED: Int = -1 /** - * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function — - * an `ArrayChannel` gets created with the default capacity. - * The default capacity is 64 and can be overridden by setting - * [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. + * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function. + * The default capacity for a channel that [suspends][BufferOverflow.SUSPEND] on overflow + * is 64 and can be overridden by setting [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. + * For non-suspending channels, a buffer of capacity 1 is used. */ public const val BUFFERED: Int = -2 @@ -557,17 +575,41 @@ public interface Channel : SendChannel, ReceiveChannel { * See [Channel] interface documentation for details. * * @param capacity either a positive channel capacity or one of the constants defined in [Channel.Factory]. + * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to + * a [suspending][BufferOverflow.SUSPEND] attempt to [send][Channel.send] a value, + * supported only when `capacity >= 0` or `capacity == Channel.BUFFERED`, + * implicitly creates a channel with at least one buffered element). * @throws IllegalArgumentException when [capacity] < -2 */ -public fun Channel(capacity: Int = RENDEZVOUS): Channel = +public fun Channel(capacity: Int = RENDEZVOUS, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Channel = when (capacity) { - RENDEZVOUS -> RendezvousChannel() - UNLIMITED -> LinkedListChannel() - CONFLATED -> ConflatedChannel() - BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) - else -> ArrayChannel(capacity) + RENDEZVOUS -> { + if (onBufferOverflow == BufferOverflow.SUSPEND) + RendezvousChannel() // an efficient implementation of rendezvous channel + else + ArrayChannel(1, onBufferOverflow) // support buffer overflow with buffered channel + } + CONFLATED -> { + require(onBufferOverflow == BufferOverflow.SUSPEND) { + "CONFLATED capacity cannot be used with non-default onBufferOverflow" + } + ConflatedChannel() + } + UNLIMITED -> LinkedListChannel() // ignores onBufferOverflow: it has buffer, but it never overflows + BUFFERED -> ArrayChannel( // uses default capacity with SUSPEND + if (onBufferOverflow == BufferOverflow.SUSPEND) CHANNEL_DEFAULT_CAPACITY else 1, onBufferOverflow + ) + else -> { + if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST) + ConflatedChannel() // conflated implementation is more efficient but appears to work in the same way + else + ArrayChannel(capacity, onBufferOverflow) + } } +@Deprecated(level = DeprecationLevel.HIDDEN, message = "For binary compatibility") +public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity) + /** * Indicates an attempt to [send][SendChannel.send] to a [isClosedForSend][SendChannel.isClosedForSend] channel * that was closed without a cause. A _failed_ channel rethrows the original [close][SendChannel.close] cause diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 2b9375ddec..dc19ee379c 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlin.jvm.* -import kotlin.native.concurrent.* /** * Broadcasts the most recently sent element (aka [value]) to all [openSubscription] subscribers. @@ -27,9 +26,10 @@ import kotlin.native.concurrent.* * [opening][openSubscription] and [closing][ReceiveChannel.cancel] subscription takes O(N) time, where N is the * number of subscribers. * - * **Note: This API is experimental.** It may be changed in the future updates. + * **Note: This API is obsolete.** It will be deprecated and replaced by [StateFlow][kotlinx.coroutines.flow.StateFlow] + * when it becomes stable. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // not @ObsoleteCoroutinesApi to reduce burden for people who are still using it public class ConflatedBroadcastChannel() : BroadcastChannel { /** * Creates an instance of this class that already holds a value. diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 1b1581a99e..a8e272c11d 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -91,13 +91,8 @@ public fun CoroutineScope.produce( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, @BuilderInference block: suspend ProducerScope.() -> Unit -): ReceiveChannel { - val channel = Channel(capacity) - val newContext = newCoroutineContext(context) - val coroutine = ProducerCoroutine(newContext, channel) - coroutine.start(CoroutineStart.DEFAULT, coroutine, block) - return coroutine -} +): ReceiveChannel = + produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block) /** * **This is an internal API and should not be used from general code.** @@ -118,8 +113,19 @@ public fun CoroutineScope.produce( start: CoroutineStart = CoroutineStart.DEFAULT, onCompletion: CompletionHandler? = null, @BuilderInference block: suspend ProducerScope.() -> Unit +): ReceiveChannel = + produce(context, capacity, BufferOverflow.SUSPEND, start, onCompletion, block) + +// Internal version of produce that is maximally flexible, but is not exposed through public API (too many params) +internal fun CoroutineScope.produce( + context: CoroutineContext = EmptyCoroutineContext, + capacity: Int = 0, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + start: CoroutineStart = CoroutineStart.DEFAULT, + onCompletion: CompletionHandler? = null, + @BuilderInference block: suspend ProducerScope.() -> Unit ): ReceiveChannel { - val channel = Channel(capacity) + val channel = Channel(capacity, onBufferOverflow) val newContext = newCoroutineContext(context) val coroutine = ProducerCoroutine(newContext, channel) if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion) diff --git a/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt new file mode 100644 index 0000000000..765a4f1559 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.assert +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* + +internal abstract class AbstractSharedFlowSlot { + abstract fun allocateLocked(flow: F): Boolean + abstract fun freeLocked(flow: F): List>? // returns a list of continuation to resume after lock +} + +internal abstract class AbstractSharedFlow> : SynchronizedObject() { + @Suppress("UNCHECKED_CAST") + protected var slots: Array? = null // allocated when needed + private set + protected var nCollectors = 0 // number of allocated (!free) slots + private set + private var nextIndex = 0 // oracle for the next free slot index + private var _subscriptionCount: MutableStateFlow? = null // init on first need + + val subscriptionCount: StateFlow + get() = synchronized(this) { + // allocate under lock in sync with nCollectors variable + _subscriptionCount ?: MutableStateFlow(nCollectors).also { + _subscriptionCount = it + } + } + + protected abstract fun createSlot(): S + + protected abstract fun createSlotArray(size: Int): Array + + @Suppress("UNCHECKED_CAST") + protected fun allocateSlot(): S { + // Actually create slot under lock + var subscriptionCount: MutableStateFlow? = null + val slot = synchronized(this) { + val slots = when(val curSlots = slots) { + null -> createSlotArray(2).also { slots = it } + else -> if (nCollectors >= curSlots.size) { + curSlots.copyOf(2 * curSlots.size).also { slots = it } + } else { + curSlots + } + } + var index = nextIndex + var slot: S + while (true) { + slot = slots[index] ?: createSlot().also { slots[index] = it } + index++ + if (index >= slots.size) index = 0 + if ((slot as AbstractSharedFlowSlot).allocateLocked(this)) break // break when found and allocated free slot + } + nextIndex = index + nCollectors++ + subscriptionCount = _subscriptionCount // retrieve under lock if initialized + slot + } + // increments subscription count + subscriptionCount?.increment(1) + return slot + } + + @Suppress("UNCHECKED_CAST") + protected fun freeSlot(slot: S) { + // Release slot under lock + var subscriptionCount: MutableStateFlow? = null + val resumeList = synchronized(this) { + nCollectors-- + subscriptionCount = _subscriptionCount // retrieve under lock if initialized + // Reset next index oracle if we have no more active collectors for more predictable behavior next time + if (nCollectors == 0) nextIndex = 0 + (slot as AbstractSharedFlowSlot).freeLocked(this) + } + // Resume suspended coroutines + resumeList?.forEach { it.resume(Unit) } + // decrement subscription count + subscriptionCount?.increment(-1) + } + + protected inline fun forEachSlotLocked(block: (S) -> Unit) { + if (nCollectors == 0) return + slots?.forEach { slot -> + if (slot != null) block(slot) + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 8fd9ae76a4..5e8b5512a4 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -16,7 +16,8 @@ import kotlin.jvm.* import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** - * Creates a flow from the given suspendable [block]. + * Creates a _cold_ flow from the given suspendable [block]. + * The flow being _cold_ means that the [block] is called every time a terminal operator is applied to the resulting flow. * * Example of usage: * @@ -62,7 +63,7 @@ private class SafeFlow(private val block: suspend FlowCollector.() -> Unit } /** - * Creates a flow that produces a single value from the given functional type. + * Creates a _cold_ flow that produces a single value from the given functional type. */ @FlowPreview public fun (() -> T).asFlow(): Flow = flow { @@ -70,8 +71,10 @@ public fun (() -> T).asFlow(): Flow = flow { } /** - * Creates a flow that produces a single value from the given functional type. + * Creates a _cold_ flow that produces a single value from the given functional type. + * * Example of usage: + * * ``` * suspend fun remoteCall(): R = ... * fun remoteCallFlow(): Flow = ::remoteCall.asFlow() @@ -83,7 +86,7 @@ public fun (suspend () -> T).asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given iterable. + * Creates a _cold_ flow that produces values from the given iterable. */ public fun Iterable.asFlow(): Flow = flow { forEach { value -> @@ -92,7 +95,7 @@ public fun Iterable.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given iterator. + * Creates a _cold_ flow that produces values from the given iterator. */ public fun Iterator.asFlow(): Flow = flow { forEach { value -> @@ -101,7 +104,7 @@ public fun Iterator.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given sequence. + * Creates a _cold_ flow that produces values from the given sequence. */ public fun Sequence.asFlow(): Flow = flow { forEach { value -> @@ -113,6 +116,7 @@ public fun Sequence.asFlow(): Flow = flow { * Creates a flow that produces values from the specified `vararg`-arguments. * * Example of usage: + * * ``` * flowOf(1, 2, 3) * ``` @@ -124,7 +128,7 @@ public fun flowOf(vararg elements: T): Flow = flow { } /** - * Creates flow that produces the given [value]. + * Creates a flow that produces the given [value]. */ public fun flowOf(value: T): Flow = flow { /* @@ -144,7 +148,9 @@ private object EmptyFlow : Flow { } /** - * Creates a flow that produces values from the given array. + * Creates a _cold_ flow that produces values from the given array. + * The flow being _cold_ means that the array components are read every time a terminal operator is applied + * to the resulting flow. */ public fun Array.asFlow(): Flow = flow { forEach { value -> @@ -153,7 +159,9 @@ public fun Array.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the array. + * Creates a _cold_ flow that produces values from the array. + * The flow being _cold_ means that the array components are read every time a terminal operator is applied + * to the resulting flow. */ public fun IntArray.asFlow(): Flow = flow { forEach { value -> @@ -162,7 +170,9 @@ public fun IntArray.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the array. + * Creates a _cold_ flow that produces values from the given array. + * The flow being _cold_ means that the array components are read every time a terminal operator is applied + * to the resulting flow. */ public fun LongArray.asFlow(): Flow = flow { forEach { value -> @@ -208,7 +218,7 @@ public fun flowViaChannel( } /** - * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] + * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be * produced by code that is running in a different context or concurrently. * The resulting flow is _cold_, which means that [block] is called every time a terminal operator @@ -256,7 +266,7 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() ChannelFlowBuilder(block) /** - * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] + * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] * provided to the builder's [block] of code via [ProducerScope]. It allows elements to be * produced by code that is running in a different context or concurrently. * @@ -319,10 +329,11 @@ public fun callbackFlow(@BuilderInference block: suspend ProducerScope.() private open class ChannelFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowBuilder(block, context, capacity) + capacity: Int = BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowBuilder(block, context, capacity, onBufferOverflow) override suspend fun collectTo(scope: ProducerScope) = block(scope) @@ -334,8 +345,9 @@ private open class ChannelFlowBuilder( private class CallbackFlowBuilder( private val block: suspend ProducerScope.() -> Unit, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = BUFFERED -) : ChannelFlowBuilder(block, context, capacity) { + capacity: Int = BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlowBuilder(block, context, capacity, onBufferOverflow) { override suspend fun collectTo(scope: ProducerScope) { super.collectTo(scope) @@ -355,6 +367,6 @@ private class CallbackFlowBuilder( } } - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - CallbackFlowBuilder(block, context, capacity) + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + CallbackFlowBuilder(block, context, capacity, onBufferOverflow) } diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 2d3ef95aa1..d51791963e 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -116,8 +116,9 @@ private class ChannelAsFlow( private val channel: ReceiveChannel, private val consume: Boolean, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL -) : ChannelFlow(context, capacity) { + capacity: Int = Channel.OPTIONAL_CHANNEL, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { private val consumed = atomic(false) private fun markConsumed() { @@ -126,8 +127,11 @@ private class ChannelAsFlow( } } - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelAsFlow(channel, consume, context, capacity) + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelAsFlow(channel, consume, context, capacity, onBufferOverflow) + + override fun dropChannelOperators(): Flow? = + ChannelAsFlow(channel, consume) override suspend fun collectTo(scope: ProducerScope) = SendingCollector(scope).emitAllImpl(channel, consume) // use efficient channel receiving code from emitAll @@ -154,7 +158,7 @@ private class ChannelAsFlow( } } - override fun additionalToStringProps(): String = "channel=$channel, " + override fun additionalToStringProps(): String = "channel=$channel" } /** @@ -181,6 +185,9 @@ public fun BroadcastChannel.asFlow(): Flow = flow { * Use [buffer] operator on the flow before calling `broadcastIn` to specify a value other than * default and to control what happens when data is produced faster than it is consumed, * that is to control backpressure behavior. + * + * **Note: This API is obsolete.** It will be deprecated and replaced with + * the [Flow.shareIn] operator when it becomes stable. */ @FlowPreview public fun Flow.broadcastIn( diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index b7e2518694..19a5b43f31 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -9,8 +9,7 @@ import kotlinx.coroutines.flow.internal.* import kotlin.coroutines.* /** - * A cold asynchronous data stream that sequentially emits values - * and completes normally or with an exception. + * An asynchronous data stream that sequentially emits values and completes normally or with an exception. * * _Intermediate operators_ on the flow such as [map], [filter], [take], [zip], etc are functions that are * applied to the _upstream_ flow or flows and return a _downstream_ flow where further operators can be applied to. @@ -39,11 +38,12 @@ import kotlin.coroutines.* * with an exception for a few operations specifically designed to introduce concurrency into flow * execution such as [buffer] and [flatMapMerge]. See their documentation for details. * - * The `Flow` interface does not carry information whether a flow truly is a cold stream that can be collected repeatedly and - * triggers execution of the same code every time it is collected, or if it is a hot stream that emits different - * values from the same running source on each collection. However, conventionally flows represent cold streams. - * Transitions between hot and cold streams are supported via channels and the corresponding API: - * [channelFlow], [produceIn], [broadcastIn]. + * The `Flow` interface does not carry information whether a flow is a _cold_ stream that can be collected repeatedly and + * triggers execution of the same code every time it is collected, or if it is a _hot_ stream that emits different + * values from the same running source on each collection. Usually flows represent _cold_ streams, but + * there is a [SharedFlow] subtype that represents _hot_ streams. In addition to that, any flow can be turned + * into a _hot_ one by the [stateIn] and [shareIn] operators, or by converting the flow into a hot channel + * via the [produceIn] operator. * * ### Flow builders * @@ -55,6 +55,8 @@ import kotlin.coroutines.* * sequential calls to [emit][FlowCollector.emit] function. * * [channelFlow { ... }][channelFlow] builder function to construct arbitrary flows from * potentially concurrent calls to the [send][kotlinx.coroutines.channels.SendChannel.send] function. + * * [MutableStateFlow] and [MutableSharedFlow] define the corresponding constructor functions to create + * a _hot_ flow that can be directly updated. * * ### Flow constraints * @@ -159,9 +161,9 @@ import kotlin.coroutines.* * * ### Not stable for inheritance * - * **`Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **The `Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use `flow { ... }` builder function to create an implementation. + * Use the `flow { ... }` builder function to create an implementation. */ public interface Flow { /** @@ -201,7 +203,7 @@ public interface Flow { * ``` */ @FlowPreview -public abstract class AbstractFlow : Flow { +public abstract class AbstractFlow : Flow, CancellableFlow { @InternalCoroutinesApi public final override suspend fun collect(collector: FlowCollector) { diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index bb2f584474..59873eba5f 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -9,8 +9,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* -import kotlinx.coroutines.flow.internal.* -import kotlinx.coroutines.flow.internal.unsafeFlow import kotlin.coroutines.* import kotlin.jvm.* @@ -99,7 +97,7 @@ public fun Flow.publishOn(context: CoroutineContext): Flow = noImpl() * Opposed to subscribeOn, it it **possible** to use multiple `flowOn` operators in the one flow * @suppress */ -@Deprecated(message = "Use flowOn instead", level = DeprecationLevel.ERROR) +@Deprecated(message = "Use 'flowOn' instead", level = DeprecationLevel.ERROR) public fun Flow.subscribeOn(context: CoroutineContext): Flow = noImpl() /** @@ -151,7 +149,7 @@ public fun Flow.onErrorResumeNext(fallback: Flow): Flow = noImpl() * @suppress */ @Deprecated( - message = "Use launchIn with onEach, onCompletion and catch operators instead", + message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", level = DeprecationLevel.ERROR ) public fun Flow.subscribe(): Unit = noImpl() @@ -161,7 +159,7 @@ public fun Flow.subscribe(): Unit = noImpl() * @suppress */ @Deprecated( - message = "Use launchIn with onEach, onCompletion and catch operators instead", + message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", level = DeprecationLevel.ERROR )public fun Flow.subscribe(onEach: suspend (T) -> Unit): Unit = noImpl() @@ -170,7 +168,7 @@ public fun Flow.subscribe(): Unit = noImpl() * @suppress */ @Deprecated( - message = "Use launchIn with onEach, onCompletion and catch operators instead", + message = "Use 'launchIn' with 'onEach', 'onCompletion' and 'catch' instead", level = DeprecationLevel.ERROR )public fun Flow.subscribe(onEach: suspend (T) -> Unit, onError: suspend (Throwable) -> Unit): Unit = noImpl() @@ -181,7 +179,7 @@ public fun Flow.subscribe(): Unit = noImpl() */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Flow analogue is named flatMapConcat", + message = "Flow analogue is 'flatMapConcat'", replaceWith = ReplaceWith("flatMapConcat(mapper)") ) public fun Flow.flatMap(mapper: suspend (T) -> Flow): Flow = noImpl() @@ -438,3 +436,50 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F ) @ExperimentalCoroutinesApi public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = runningReduce(operation) + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'publish()' is 'shareIn'. \n" + + "publish().connect() is the default strategy (no extra call is needed), \n" + + "publish().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "publish().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.shareIn(scope, 0)") +) +public fun Flow.publish(): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'publish(bufferSize)' is 'buffer' followed by 'shareIn'. \n" + + "publish().connect() is the default strategy (no extra call is needed), \n" + + "publish().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "publish().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.buffer(bufferSize).shareIn(scope, 0)") +) +public fun Flow.publish(bufferSize: Int): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'replay()' is 'shareIn' with unlimited replay. \n" + + "replay().connect() is the default strategy (no extra call is needed), \n" + + "replay().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "replay().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.shareIn(scope, Int.MAX_VALUE)") +) +public fun Flow.replay(): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'replay(bufferSize)' is 'shareIn' with the specified replay parameter. \n" + + "replay().connect() is the default strategy (no extra call is needed), \n" + + "replay().autoConnect() translates to 'started = SharingStared.Lazily' argument, \n" + + "replay().refCount() translates to 'started = SharingStared.WhileSubscribed()' argument.", + replaceWith = ReplaceWith("this.shareIn(scope, bufferSize)") +) +public fun Flow.replay(bufferSize: Int): Flow = noImpl() + +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Flow analogue of 'cache()' is 'shareIn' with unlimited replay and 'started = SharingStared.Lazily' argument'", + replaceWith = ReplaceWith("this.shareIn(scope, Int.MAX_VALUE, started = SharingStared.Lazily)") +) +public fun Flow.cache(): Flow = noImpl() \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt new file mode 100644 index 0000000000..21f00e4138 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -0,0 +1,651 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.native.concurrent.* + +/** + * A _hot_ [Flow] that shares emitted values among all its collectors in a broadcast fashion, so that all collectors + * get all emitted values. A shared flow is called _hot_ because its active instance exists independently of the + * presence of collectors. This is opposed to a regular [Flow], such as defined by the [`flow { ... }`][flow] function, + * which is _cold_ and is started separately for each collector. + * + * **Shared flow never completes**. A call to [Flow.collect] on a shared flow never completes normally, and + * neither does a coroutine started by the [Flow.launchIn] function. An active collector of a shared flow is called a _subscriber_. + * + * A subscriber of a shared flow can be cancelled. This usually happens when the scope in which the coroutine is running + * is cancelled. A subscriber to a shared flow is always [cancellable][Flow.cancellable], and checks for + * cancellation before each emission. Note that most terminal operators like [Flow.toList] would also not complete, + * when applied to a shared flow, but flow-truncating operators like [Flow.take] and [Flow.takeWhile] can be used on a + * shared flow to turn it into a completing one. + * + * A [mutable shared flow][MutableSharedFlow] is created using the [MutableSharedFlow(...)] constructor function. + * Its state can be updated by [emitting][MutableSharedFlow.emit] values to it and performing other operations. + * See the [MutableSharedFlow] documentation for details. + * + * [SharedFlow] is useful for broadcasting events that happen inside an application to subscribers that can come and go. + * For example, the following class encapsulates an event bus that distributes events to all subscribers + * in a _rendezvous_ manner, suspending until all subscribers process each event: + * + * ``` + * class EventBus { + * private val _events = MutableSharedFlow(0) // private mutable shared flow + * val events get() = _events.asSharedFlow() // publicly exposed as read-only shared flow + * + * suspend fun produceEvent(event: Event) { + * _events.emit(event) // suspends until all subscribers receive it + * } + * } + * ``` + * + * As an alternative to the above usage with the `MutableSharedFlow(...)` constructor function, + * any _cold_ [Flow] can be converted to a shared flow using the [shareIn] operator. + * + * There is a specialized implementation of shared flow for the case where the most recent state value needs + * to be shared. See [StateFlow] for details. + * + * ### Replay cache and buffer + * + * A shared flow keeps a specific number of the most recent values in its _replay cache_. Every new subscriber first + * gets the values from the replay cache and then gets new emitted values. The maximum size of the replay cache is + * specified when the shared flow is created by the `replay` parameter. A snapshot of the current replay cache + * is available via the [replayCache] property and it can be reset with the [MutableSharedFlow.resetReplayCache] function. + * + * A replay cache also provides buffer for emissions to the shared flow, allowing slow subscribers to + * get values from the buffer without suspending emitters. The buffer space determines how much slow subscribers + * can lag from the fast ones. When creating a shared flow, additional buffer capacity beyond replay can be reserved + * using the `extraBufferCapacity` parameter. + * + * A shared flow with a buffer can be configured to avoid suspension of emitters on buffer overflow using + * the `onBufferOverflow` parameter, which is equal to one of the entries of the [BufferOverflow] enum. When a strategy other + * than [SUSPENDED][BufferOverflow.SUSPEND] is configured, emissions to the shared flow never suspend. + * + * ### SharedFlow vs BroadcastChannel + * + * Conceptually shared flow is similar to [BroadcastChannel][BroadcastChannel] + * and is designed to completely replace `BroadcastChannel` in the future. + * It has the following important differences: + * + * * `SharedFlow` is simpler, because it does not have to implement all the [Channel] APIs, which allows + * for faster and simpler implementation. + * * `SharedFlow` supports configurable replay and buffer overflow strategy. + * * `SharedFlow` has a clear separation into a read-only `SharedFlow` interface and a [MutableSharedFlow]. + * * `SharedFlow` cannot be closed like `BroadcastChannel` and can never represent a failure. + * All errors and completion signals should be explicitly _materialized_ if needed. + * + * To migrate [BroadcastChannel] usage to [SharedFlow], start by replacing usages of the `BroadcastChannel(capacity)` + * constructor with `MutableSharedFlow(0, extraBufferCapacity=capacity)` (broadcast channel does not replay + * values to new subscribers). Replace [send][BroadcastChannel.send] and [offer][BroadcastChannel.offer] calls + * with [emit][MutableStateFlow.emit] and [tryEmit][MutableStateFlow.tryEmit], and convert subscribers' code to flow operators. + * + * ### Concurrency + * + * All methods of shared flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. + * + * ### Operator fusion + * + * Application of [flowOn][Flow.flowOn], [buffer] with [RENDEZVOUS][Channel.RENDEZVOUS] capacity, + * or [cancellable] operators to a shared flow has no effect. + * + * ### Implementation notes + * + * Shared flow implementation uses a lock to ensure thread-safety, but suspending collector and emitter coroutines are + * resumed outside of this lock to avoid dead-locks when using unconfined coroutines. Adding new subscribers + * has `O(1)` amortized cost, but emitting has `O(N)` cost, where `N` is the number of subscribers. + * + * ### Not stable for inheritance + * + * **The `SharedFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * might be added to this interface in the future, but is stable for use. + * Use the `MutableSharedFlow(replay, ...)` constructor function to create an implementation. + */ +@ExperimentalCoroutinesApi +public interface SharedFlow : Flow { + /** + * A snapshot of the replay cache. + */ + public val replayCache: List +} + +/** + * A mutable [SharedFlow] that provides functions to [emit] values to the flow. + * An instance of `MutableSharedFlow` with the given configuration parameters can be created using `MutableSharedFlow(...)` + * constructor function. + * + * See the [SharedFlow] documentation for details on shared flows. + * + * `MutableSharedFlow` is a [SharedFlow] that also provides the abilities to [emit] a value, + * to [tryEmit] without suspension if possible, to track the [subscriptionCount], + * and to [resetReplayCache]. + * + * ### Concurrency + * + * All methods of shared flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. + * + * ### Not stable for inheritance + * + * **The `MutableSharedFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * might be added to this interface in the future, but is stable for use. + * Use the `MutableSharedFlow(...)` constructor function to create an implementation. + */ +@ExperimentalCoroutinesApi +public interface MutableSharedFlow : SharedFlow, FlowCollector { + /** + * Tries to emit a [value] to this shared flow without suspending. It returns `true` if the value was + * emitted successfully. When this function returns `false`, it means that the call to a plain [emit] + * function will suspend until there is a buffer space available. + * + * A shared flow configured with a [BufferOverflow] strategy other than [SUSPEND][BufferOverflow.SUSPEND] + * (either [DROP_OLDEST][BufferOverflow.DROP_OLDEST] or [DROP_LATEST][BufferOverflow.DROP_LATEST]) never + * suspends on [emit], and thus `tryEmit` to such a shared flow always returns `true`. + */ + public fun tryEmit(value: T): Boolean + + /** + * The number of subscribers (active collectors) to this shared flow. + * + * This state can be used to react to changes in the number of subscriptions to this shared flow. + * For example, if you need to call `onActive` when the first subscriber appears and `onInactive` + * when the last one disappears, you can set it up like this: + * + * ``` + * sharedFlow.subscriptionCount + * .map { count -> count > 0 } // map count into active/inactive flag + * .distinctUntilChanged() // only react to true<->false changes + * .onEach { isActive -> // configure an action + * if (isActive) onActive() else onInactive() + * } + * .launchIn(scope) // launch it + * ``` + */ + public val subscriptionCount: StateFlow + + /** + * Resets the [replayCache] of this shared flow to an empty state. + * New subscribers will be receiving only the values that were emitted after this call, + * while old subscribers will still be receiving previously buffered values. + * To reset a shared flow to an initial value, emit the value after this call. + * + * On a [MutableStateFlow], which always contains a single value, this function is not + * supported, and throws an [UnsupportedOperationException]. To reset a [MutableStateFlow] + * to an initial value, just update its [value][MutableStateFlow.value]. + */ + public fun resetReplayCache() +} + +/** + * Creates a [MutableSharedFlow] with the given configuration parameters. + * + * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. + * + * @param replay the number of values replayed to new subscribers (cannot be negative). + * @param extraBufferCapacity the number of values buffered in addition to `replay`. + * [emit][MutableSharedFlow.emit] does not suspend while there is a buffer space remaining (optional, cannot be negative, defaults to zero). + * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to + * [suspending][BufferOverflow.SUSPEND] attempts to [emit][MutableSharedFlow.emit] a value, + * supported only when `replay > 0` or `extraBufferCapacity > 0`). + */ +@Suppress("FunctionName", "UNCHECKED_CAST") +@ExperimentalCoroutinesApi +public fun MutableSharedFlow( + replay: Int, + extraBufferCapacity: Int = 0, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +): MutableSharedFlow { + require(replay >= 0) { "replay cannot be negative" } + require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative" } + require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) { + "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy" + } + val bufferCapacity0 = replay + extraBufferCapacity + val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow + return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow) +} + +// ------------------------------------ Implementation ------------------------------------ + +private class SharedFlowSlot : AbstractSharedFlowSlot>() { + @JvmField + var index = -1L // current "to-be-emitted" index, -1 means the slot is free now + + @JvmField + var cont: Continuation? = null // collector waiting for new value + + override fun allocateLocked(flow: SharedFlowImpl<*>): Boolean { + if (index >= 0) return false // not free + index = flow.updateNewCollectorIndexLocked() + return true + } + + override fun freeLocked(flow: SharedFlowImpl<*>): List>? { + assert { index >= 0 } + val oldIndex = index + index = -1L + cont = null // cleanup continuation reference + return flow.updateCollectorIndexLocked(oldIndex) + } +} + +private class SharedFlowImpl( + private val replay: Int, + private val bufferCapacity: Int, + private val onBufferOverflow: BufferOverflow +) : AbstractSharedFlow(), MutableSharedFlow, CancellableFlow, FusibleFlow { + /* + Logical structure of the buffer + + buffered values + /-----------------------\ + replayCache queued emitters + /----------\/----------------------\ + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + | | 1 | 2 | 3 | 4 | 5 | 6 | E | E | E | E | E | E | | | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + ^ ^ ^ ^ + | | | | + head | head + bufferSize head + totalSize + | | | + index of the slowest | index of the fastest + possible collector | possible collector + | | + | replayIndex == new collector's index + \---------------------- / + range of possible minCollectorIndex + + head == minOf(minCollectorIndex, replayIndex) // by definition + totalSize == bufferSize + queueSize // by definition + + INVARIANTS: + minCollectorIndex = activeSlots.minOf { it.index } ?: (head + bufferSize) + replayIndex <= head + bufferSize + */ + + // Stored state + private var buffer: Array? = null // allocated when needed, allocated size always power of two + private var replayIndex = 0L // minimal index from which new collector gets values + private var minCollectorIndex = 0L // minimal index of active collectors, equal to replayIndex if there are none + private var bufferSize = 0 // number of buffered values + private var queueSize = 0 // number of queued emitters + + // Computed state + private val head: Long get() = minOf(minCollectorIndex, replayIndex) + private val replaySize: Int get() = (head + bufferSize - replayIndex).toInt() + private val totalSize: Int get() = bufferSize + queueSize + private val bufferEndIndex: Long get() = head + bufferSize + private val queueEndIndex: Long get() = head + bufferSize + queueSize + + override val replayCache: List + get() = synchronized(this) { + val replaySize = this.replaySize + if (replaySize == 0) return emptyList() + val result = ArrayList(replaySize) + val buffer = buffer!! // must be allocated, because replaySize > 0 + @Suppress("UNCHECKED_CAST") + for (i in 0 until replaySize) result += buffer.getBufferAt(replayIndex + i) as T + result + } + + @Suppress("UNCHECKED_CAST") + override suspend fun collect(collector: FlowCollector) { + val slot = allocateSlot() + try { + if (collector is SubscribedFlowCollector) collector.onSubscription() + val collectorJob = currentCoroutineContext()[Job] + while (true) { + var newValue: Any? + while (true) { + newValue = tryTakeValue(slot) // attempt no-suspend fast path first + if (newValue !== NO_VALUE) break + awaitValue(slot) // await signal that the new value is available + } + collectorJob?.ensureActive() + collector.emit(newValue as T) + } + } finally { + freeSlot(slot) + } + } + + override fun tryEmit(value: T): Boolean { + var resumeList: List>? = null + val emitted = synchronized(this) { + if (tryEmitLocked(value)) { + resumeList = findSlotsToResumeLocked() + true + } else { + false + } + } + resumeList?.forEach { it.resume(Unit) } + return emitted + } + + override suspend fun emit(value: T) { + if (tryEmit(value)) return // fast-path + emitSuspend(value) + } + + @Suppress("UNCHECKED_CAST") + private fun tryEmitLocked(value: T): Boolean { + // Fast path without collectors -> no buffering + if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true + // With collectors we'll have to buffer + // cannot emit now if buffer is full & blocked by slow collectors + if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) { + when (onBufferOverflow) { + BufferOverflow.SUSPEND -> return false // will suspend + BufferOverflow.DROP_LATEST -> return true // just drop incoming + BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead + } + } + enqueueLocked(value) + bufferSize++ // value was added to buffer + // drop oldest from the buffer if it became more than bufferCapacity + if (bufferSize > bufferCapacity) dropOldestLocked() + // keep replaySize not larger that needed + if (replaySize > replay) { // increment replayIndex by one + updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex) + } + return true + } + + private fun tryEmitNoCollectorsLocked(value: T): Boolean { + assert { nCollectors == 0 } + if (replay == 0) return true // no need to replay, just forget it now + enqueueLocked(value) // enqueue to replayCache + bufferSize++ // value was added to buffer + // drop oldest from the buffer if it became more than replay + if (bufferSize > replay) dropOldestLocked() + minCollectorIndex = head + bufferSize // a default value (max allowed) + return true + } + + private fun dropOldestLocked() { + buffer!!.setBufferAt(head, null) + bufferSize-- + val newHead = head + 1 + if (replayIndex < newHead) replayIndex = newHead + if (minCollectorIndex < newHead) correctCollectorIndexesOnDropOldest(newHead) + assert { head == newHead } // since head = minOf(minCollectorIndex, replayIndex) it should have updated + } + + private fun correctCollectorIndexesOnDropOldest(newHead: Long) { + forEachSlotLocked { slot -> + @Suppress("ConvertTwoComparisonsToRangeCheck") // Bug in JS backend + if (slot.index >= 0 && slot.index < newHead) { + slot.index = newHead // force move it up (this collector was too slow and missed the value at its index) + } + } + minCollectorIndex = newHead + } + + // enqueues item to buffer array, caller shall increment either bufferSize or queueSize + private fun enqueueLocked(item: Any?) { + val curSize = totalSize + val buffer = when (val curBuffer = buffer) { + null -> growBuffer(null, 0, 2) + else -> if (curSize >= curBuffer.size) growBuffer(curBuffer, curSize,curBuffer.size * 2) else curBuffer + } + buffer.setBufferAt(head + curSize, item) + } + + private fun growBuffer(curBuffer: Array?, curSize: Int, newSize: Int): Array { + check(newSize > 0) { "Buffer size overflow" } + val newBuffer = arrayOfNulls(newSize).also { buffer = it } + if (curBuffer == null) return newBuffer + val head = head + for (i in 0 until curSize) { + newBuffer.setBufferAt(head + i, curBuffer.getBufferAt(head + i)) + } + return newBuffer + } + + private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine sc@{ cont -> + var resumeList: List>? = null + val emitter = synchronized(this) lock@{ + // recheck buffer under lock again (make sure it is really full) + if (tryEmitLocked(value)) { + cont.resume(Unit) + resumeList = findSlotsToResumeLocked() + return@lock null + } + // add suspended emitter to the buffer + Emitter(this, head + totalSize, value, cont).also { + enqueueLocked(it) + queueSize++ // added to queue of waiting emitters + // synchronous shared flow might rendezvous with waiting emitter + if (bufferCapacity == 0) resumeList = findSlotsToResumeLocked() + } + } + // outside of the lock: register dispose on cancellation + emitter?.let { cont.disposeOnCancellation(it) } + // outside of the lock: resume slots if needed + resumeList?.forEach { it.resume(Unit) } + } + + private fun cancelEmitter(emitter: Emitter) = synchronized(this) { + if (emitter.index < head) return // already skipped past this index + val buffer = buffer!! + if (buffer.getBufferAt(emitter.index) !== emitter) return // already resumed + buffer.setBufferAt(emitter.index, NO_VALUE) + cleanupTailLocked() + } + + internal fun updateNewCollectorIndexLocked(): Long { + val index = replayIndex + if (index < minCollectorIndex) minCollectorIndex = index + return index + } + + // Is called when a collector disappears or changes index, returns a list of continuations to resume after lock + internal fun updateCollectorIndexLocked(oldIndex: Long): List>? { + assert { oldIndex >= minCollectorIndex } + if (oldIndex > minCollectorIndex) return null // nothing changes, it was not min + // start computing new minimal index of active collectors + val head = head + var newMinCollectorIndex = head + bufferSize + // take into account a special case of sync shared flow that can go past 1st queued emitter + if (bufferCapacity == 0 && queueSize > 0) newMinCollectorIndex++ + forEachSlotLocked { slot -> + @Suppress("ConvertTwoComparisonsToRangeCheck") // Bug in JS backend + if (slot.index >= 0 && slot.index < newMinCollectorIndex) newMinCollectorIndex = slot.index + } + assert { newMinCollectorIndex >= minCollectorIndex } // can only grow + if (newMinCollectorIndex <= minCollectorIndex) return null // nothing changes + // Compute new buffer size if we drop items we no longer need and no emitter is resumed: + // We must keep all the items from newMinIndex to the end of buffer + var newBufferEndIndex = bufferEndIndex // var to grow when waiters are resumed + val maxResumeCount = if (nCollectors > 0) { + // If we have collectors we can resume up to maxResumeCount waiting emitters + // a) queueSize -> that's how many waiting emitters we have + // b) bufferCapacity - newBufferSize0 -> that's how many we can afford to resume to add w/o exceeding bufferCapacity + val newBufferSize0 = (newBufferEndIndex - newMinCollectorIndex).toInt() + minOf(queueSize, bufferCapacity - newBufferSize0) + } else { + // If we don't have collectors anymore we must resume all waiting emitters + queueSize // that's how many waiting emitters we have (at most) + } + var resumeList: ArrayList>? = null + val newQueueEndIndex = newBufferEndIndex + queueSize + if (maxResumeCount > 0) { // collect emitters to resume if we have them + resumeList = ArrayList(maxResumeCount) + val buffer = buffer!! + for (curEmitterIndex in newBufferEndIndex until newQueueEndIndex) { + val emitter = buffer.getBufferAt(curEmitterIndex) + if (emitter !== NO_VALUE) { + emitter as Emitter // must have Emitter class + resumeList.add(emitter.cont) + buffer.setBufferAt(curEmitterIndex, NO_VALUE) // make as canceled if we moved ahead + buffer.setBufferAt(newBufferEndIndex, emitter.value) + newBufferEndIndex++ + if (resumeList.size >= maxResumeCount) break // enough resumed, done + } + } + } + // Compute new buffer size -> how many values we now actually have after resume + val newBufferSize1 = (newBufferEndIndex - head).toInt() + // Compute new replay size -> limit to replay the number of items we need, take into account that it can only grow + var newReplayIndex = maxOf(replayIndex, newBufferEndIndex - minOf(replay, newBufferSize1)) + // adjustment for synchronous case with cancelled emitter (NO_VALUE) + if (bufferCapacity == 0 && newReplayIndex < newQueueEndIndex && buffer!!.getBufferAt(newReplayIndex) == NO_VALUE) { + newBufferEndIndex++ + newReplayIndex++ + } + // Update buffer state + updateBufferLocked(newReplayIndex, newMinCollectorIndex, newBufferEndIndex, newQueueEndIndex) + // just in case we've moved all buffered emitters and have NO_VALUE's at the tail now + cleanupTailLocked() + return resumeList + } + + private fun updateBufferLocked( + newReplayIndex: Long, + newMinCollectorIndex: Long, + newBufferEndIndex: Long, + newQueueEndIndex: Long + ) { + // Compute new head value + val newHead = minOf(newMinCollectorIndex, newReplayIndex) + assert { newHead >= head } + // cleanup items we don't have to buffer anymore (because head is about to move) + for (index in head until newHead) buffer!!.setBufferAt(index, null) + // update all state variables to newly computed values + replayIndex = newReplayIndex + minCollectorIndex = newMinCollectorIndex + bufferSize = (newBufferEndIndex - newHead).toInt() + queueSize = (newQueueEndIndex - newBufferEndIndex).toInt() + // check our key invariants (just in case) + assert { bufferSize >= 0 } + assert { queueSize >= 0 } + assert { replayIndex <= this.head + bufferSize } + } + + // Removes all the NO_VALUE items from the end of the queue and reduces its size + private fun cleanupTailLocked() { + // If we have synchronous case, then keep one emitter queued + if (bufferCapacity == 0 && queueSize <= 1) return // return, don't clear it + val buffer = buffer!! + while (queueSize > 0 && buffer.getBufferAt(head + totalSize - 1) === NO_VALUE) { + queueSize-- + buffer.setBufferAt(head + totalSize, null) + } + } + + // returns NO_VALUE if cannot take value without suspension + private fun tryTakeValue(slot: SharedFlowSlot): Any? { + var resumeList: List>? = null + val value = synchronized(this) { + val index = tryPeekLocked(slot) + if (index < 0) { + NO_VALUE + } else { + val oldIndex = slot.index + val newValue = getPeekedValueLockedAt(index) + slot.index = index + 1 // points to the next index after peeked one + resumeList = updateCollectorIndexLocked(oldIndex) + newValue + } + } + resumeList?.forEach { it.resume(Unit) } + return value + } + + // returns -1 if cannot peek value without suspension + private fun tryPeekLocked(slot: SharedFlowSlot): Long { + // return buffered value if possible + val index = slot.index + if (index < bufferEndIndex) return index + if (bufferCapacity > 0) return -1L // if there's a buffer, never try to rendezvous with emitters + // Synchronous shared flow (bufferCapacity == 0) tries to rendezvous + if (index > head) return -1L // ... but only with the first emitter (never look forward) + if (queueSize == 0) return -1L // nothing there to rendezvous with + return index // rendezvous with the first emitter + } + + private fun getPeekedValueLockedAt(index: Long): Any? = + when (val item = buffer!!.getBufferAt(index)) { + is Emitter -> item.value + else -> item + } + + private suspend fun awaitValue(slot: SharedFlowSlot): Unit = suspendCancellableCoroutine { cont -> + synchronized(this) lock@{ + val index = tryPeekLocked(slot) // recheck under this lock + if (index < 0) { + slot.cont = cont // Ok -- suspending + } else { + cont.resume(Unit) // has value, no need to suspend + return@lock + } + slot.cont = cont // suspend, waiting + } + } + + private fun findSlotsToResumeLocked(): List>? { + var result: ArrayList>? = null + forEachSlotLocked loop@{ slot -> + val cont = slot.cont ?: return@loop // only waiting slots + if (tryPeekLocked(slot) < 0) return@loop // only slots that can peek a value + val a = result ?: ArrayList>(2).also { result = it } + a.add(cont) + slot.cont = null // not waiting anymore + } + return result + } + + override fun createSlot() = SharedFlowSlot() + override fun createSlotArray(size: Int): Array = arrayOfNulls(size) + + override fun resetReplayCache() = synchronized(this) { + // Update buffer state + updateBufferLocked( + newReplayIndex = bufferEndIndex, + newMinCollectorIndex = minCollectorIndex, + newBufferEndIndex = bufferEndIndex, + newQueueEndIndex = queueEndIndex + ) + } + + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseSharedFlow(context, capacity, onBufferOverflow) + + private class Emitter( + @JvmField val flow: SharedFlowImpl<*>, + @JvmField var index: Long, + @JvmField val value: Any?, + @JvmField val cont: Continuation + ) : DisposableHandle { + override fun dispose() = flow.cancelEmitter(this) + } +} + +@SharedImmutable +@JvmField +internal val NO_VALUE = Symbol("NO_VALUE") + +private fun Array.getBufferAt(index: Long) = get(index.toInt() and (size - 1)) +private fun Array.setBufferAt(index: Long, item: Any?) = set(index.toInt() and (size - 1), item) + +internal fun SharedFlow.fuseSharedFlow( + context: CoroutineContext, + capacity: Int, + onBufferOverflow: BufferOverflow +): Flow { + // context is irrelevant for shared flow and making additional rendezvous is meaningless + // however, additional non-trivial buffering after shared flow could make sense for very slow subscribers + if ((capacity == Channel.RENDEZVOUS || capacity == Channel.OPTIONAL_CHANNEL) && onBufferOverflow == BufferOverflow.SUSPEND) { + return this + } + // Apply channel flow operator as usual + return ChannelFlowOperatorImpl(this, context, capacity, onBufferOverflow) +} diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt new file mode 100644 index 0000000000..247ed6d8c4 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* +import kotlin.time.* + +/** + * A command emitted by [SharingStarted] implementations to control the sharing coroutine in + * the [shareIn] and [stateIn] operators. + */ +@ExperimentalCoroutinesApi +public enum class SharingCommand { + /** + * Starts the sharing coroutine. + * + * Emitting this command again does not do anything. Emit [STOP] and then [START] to restart an + * upstream flow. + */ + START, + + /** + * Stops the sharing coroutine. + */ + STOP, + + /** + * Stops the sharing coroutine and resets the [SharedFlow.replayCache] to its initial state. + * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; + * the [stateIn] operator resets the value to its original `initialValue`. + */ + STOP_AND_RESET_REPLAY_CACHE +} + +/** + * A strategy for starting and stopping the sharing coroutine in [shareIn] and [stateIn] operators. + * + * This interface provides a set of built-in strategies: [Eagerly], [Lazily], [WhileSubscribed], and + * supports custom strategies by implementing this interface's [command] function. + * + * For example, it is possible to define a custom strategy that starts the upstream only when the number + * of subscribers exceeds the given `threshold` and make it an extension on [SharingStarted.Companion] so + * that it looks like a built-in strategy on the use-site: + * + * ``` + * fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = + * object : SharingStarted { + * override fun command(subscriptionCount: StateFlow): Flow = + * subscriptionCount + * .map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } + * } + * ``` + * + * ### Commands + * + * The `SharingStarted` strategy works by emitting [commands][SharingCommand] that control upstream flow from its + * [`command`][command] flow implementation function. Back-to-back emissions of the same command have no effect. + * Only emission of a different command has effect: + * + * * [START][SharingCommand.START] — the upstream flow is stared. + * * [STOP][SharingCommand.STOP] — the upstream flow is stopped. + * * [STOP_AND_RESET_REPLAY_CACHE][SharingCommand.STOP_AND_RESET_REPLAY_CACHE] — + * the upstream flow is stopped and the [SharedFlow.replayCache] is reset to its initial state. + * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; + * the [stateIn] operator resets the value to its original `initialValue`. + * + * Initially, the upstream flow is stopped and is in the initial state, so the emission of additional + * [STOP][SharingCommand.STOP] and [STOP_AND_RESET_REPLAY_CACHE][SharingCommand.STOP_AND_RESET_REPLAY_CACHE] commands will + * have no effect. + * + * The completion of the `command` flow normally has no effect (the upstream flow keeps running if it was running). + * The failure of the `command` flow cancels the sharing coroutine and the upstream flow. + */ +@ExperimentalCoroutinesApi +public interface SharingStarted { + public companion object { + /** + * Sharing is started immediately and never stops. + */ + @ExperimentalCoroutinesApi + public val Eagerly: SharingStarted = StartedEagerly() + + /** + * Sharing is started when the first subscriber appears and never stops. + */ + @ExperimentalCoroutinesApi + public val Lazily: SharingStarted = StartedLazily() + + /** + * Sharing is started when the first subscriber appears, immediately stops when the last + * subscriber disappears (by default), keeping the replay cache forever (by default). + * + * It has the following optional parameters: + * + * * [stopTimeoutMillis] — configures a delay (in milliseconds) between the disappearance of the last + * subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately). + * * [replayExpirationMillis] — configures a delay (in milliseconds) between the stopping of + * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator + * and resets the cached value to the original `initialValue` for the [stateIn] operator). + * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer) + * + * This function throws [IllegalArgumentException] when either [stopTimeoutMillis] or [replayExpirationMillis] + * are negative. + */ + @Suppress("FunctionName") + @ExperimentalCoroutinesApi + public fun WhileSubscribed( + stopTimeoutMillis: Long = 0, + replayExpirationMillis: Long = Long.MAX_VALUE + ): SharingStarted = + StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis) + } + + /** + * Transforms the [subscriptionCount][MutableSharedFlow.subscriptionCount] state of the shared flow into the + * flow of [commands][SharingCommand] that control the sharing coroutine. See the [SharingStarted] interface + * documentation for details. + */ + public fun command(subscriptionCount: StateFlow): Flow +} + +/** + * Sharing is started when the first subscriber appears, immediately stops when the last + * subscriber disappears (by default), keeping the replay cache forever (by default). + * + * It has the following optional parameters: + * + * * [stopTimeout] — configures a delay between the disappearance of the last + * subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately). + * * [replayExpiration] — configures a delay between the stopping of + * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator + * and resets the cached value to the original `initialValue` for the [stateIn] operator). + * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer) + * + * This function throws [IllegalArgumentException] when either [stopTimeout] or [replayExpiration] + * are negative. + */ +@Suppress("FunctionName") +@ExperimentalTime +@ExperimentalCoroutinesApi +public fun SharingStarted.Companion.WhileSubscribed( + stopTimeout: Duration = Duration.ZERO, + replayExpiration: Duration = Duration.INFINITE +): SharingStarted = + StartedWhileSubscribed(stopTimeout.toLongMilliseconds(), replayExpiration.toLongMilliseconds()) + +// -------------------------------- implementation -------------------------------- + +private class StartedEagerly : SharingStarted { + private val alwaysStarted = unsafeDistinctFlow { emit(SharingCommand.START) } + override fun command(subscriptionCount: StateFlow): Flow = alwaysStarted + override fun toString(): String = "SharingStarted.Eagerly" +} + +private class StartedLazily : SharingStarted { + override fun command(subscriptionCount: StateFlow): Flow = unsafeDistinctFlow { + var started = false + subscriptionCount.collect { count -> + if (count > 0 && !started) { + started = true + emit(SharingCommand.START) + } + } + } + + override fun toString(): String = "SharingStarted.Lazily" +} + +private class StartedWhileSubscribed( + private val stopTimeout: Long, + private val replayExpiration: Long +) : SharingStarted { + init { + require(stopTimeout >= 0) { "stopTimeout($stopTimeout ms) cannot be negative" } + require(replayExpiration >= 0) { "replayExpiration($replayExpiration ms) cannot be negative" } + } + + override fun command(subscriptionCount: StateFlow): Flow = subscriptionCount + .transformLatest { count -> + if (count > 0) { + emit(SharingCommand.START) + } else { + delay(stopTimeout) + if (replayExpiration > 0) { + emit(SharingCommand.STOP) + delay(replayExpiration) + } + emit(SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + } + } + .dropWhile { it != SharingCommand.START } // don't emit any STOP/RESET_BUFFER to start with, only START + .distinctUntilChanged() // just in case somebody forgets it, don't leak our multiple sending of START + + @OptIn(ExperimentalStdlibApi::class) + override fun toString(): String { + val params = buildList(2) { + if (stopTimeout > 0) add("stopTimeout=${stopTimeout}ms") + if (replayExpiration < Long.MAX_VALUE) add("replayExpiration=${replayExpiration}ms") + } + return "SharingStarted.WhileSubscribed(${params.joinToString()})" + } + + // equals & hashcode to facilitate testing, not documented in public contract + override fun equals(other: Any?): Boolean = + other is StartedWhileSubscribed && + stopTimeout == other.stopTimeout && + replayExpiration == other.replayExpiration + + override fun hashCode(): Int = stopTimeout.hashCode() * 13 + replayExpiration.hashCode() +} diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index b2bbb6d3ae..4b88ce1fc4 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -13,9 +13,12 @@ import kotlin.coroutines.* import kotlin.native.concurrent.* /** - * A [Flow] that represents a read-only state with a single updatable data [value] that emits updates - * to the value to its collectors. The current value can be retrieved via [value] property. - * The flow of future updates to the value can be observed by collecting values from this flow. + * A [SharedFlow] that represents a read-only state with a single updatable data [value] that emits updates + * to the value to its collectors. A state flow is a _hot_ flow because its active instance exists independently + * of the presence of collectors. Its current value can be retrieved via the [value] property. + * + * **State flow never completes**. A call to [Flow.collect] on a state flow never completes normally, and + * neither does a coroutine started by the [Flow.launchIn] function. An active collector of a state flow is called a _subscriber_. * * A [mutable state flow][MutableStateFlow] is created using `MutableStateFlow(value)` constructor function with * the initial value. The value of mutable state flow can be updated by setting its [value] property. @@ -31,7 +34,7 @@ import kotlin.native.concurrent.* * ``` * class CounterModel { * private val _counter = MutableStateFlow(0) // private mutable state flow - * val counter: StateFlow get() = _counter // publicly exposed as read-only state flow + * val counter get() = _counter.asStateFlow() // publicly exposed as read-only state flow * * fun inc() { * _counter.value++ @@ -47,6 +50,9 @@ import kotlin.native.concurrent.* * val sumFlow: Flow = aModel.counter.combine(bModel.counter) { a, b -> a + b } * ``` * + * As an alternative to the above usage with the `MutableStateFlow(...)` constructor function, + * any _cold_ [Flow] can be converted to a state flow using the [stateIn] operator. + * * ### Strong equality-based conflation * * Values in state flow are conflated using [Any.equals] comparison in a similar way to @@ -55,12 +61,35 @@ import kotlin.native.concurrent.* * when new value is equal to the previously emitted one. State flow behavior with classes that violate * the contract for [Any.equals] is unspecified. * + * ### State flow is a shared flow + * + * State flow is a special-purpose, high-performance, and efficient implementation of [SharedFlow] for the narrow, + * but widely used case of sharing a state. See the [SharedFlow] documentation for the basic rules, + * constraints, and operators that are applicable to all shared flows. + * + * State flow always has an initial value, replays one most recent value to new subscribers, does not buffer any + * more values, but keeps the last emitted one, and does not support [resetReplayCache][MutableSharedFlow.resetReplayCache]. + * A state flow behaves identically to a shared flow when it is created + * with the following parameters and the [distinctUntilChanged] operator is applied to it: + * + * ``` + * // MutableStateFlow(initialValue) is a shared flow with the following parameters: + * val shared = MutableSharedFlow( + * replay = 1, + * onBufferOverflow = BufferOverflow.DROP_OLDEST, + * ) + * shared.tryEmit(initialValue) // emit the initial value + * val state = shared.distinctUntilChanged() // get StateFlow-like behavior + * ``` + * + * Use [SharedFlow] when you need a [StateFlow] with tweaks in its behavior such as extra buffering, replaying more + * values, or omitting the initial value. + * * ### StateFlow vs ConflatedBroadcastChannel * - * Conceptually state flow is similar to - * [ConflatedBroadcastChannel][kotlinx.coroutines.channels.ConflatedBroadcastChannel] + * Conceptually, state flow is similar to [ConflatedBroadcastChannel] * and is designed to completely replace `ConflatedBroadcastChannel` in the future. - * It has the following important difference: + * It has the following important differences: * * * `StateFlow` is simpler, because it does not have to implement all the [Channel] APIs, which allows * for faster, garbage-free implementation, unlike `ConflatedBroadcastChannel` implementation that @@ -70,38 +99,44 @@ import kotlin.native.concurrent.* * * `StateFlow` has a clear separation into a read-only `StateFlow` interface and a [MutableStateFlow]. * * `StateFlow` conflation is based on equality like [distinctUntilChanged] operator, * unlike conflation in `ConflatedBroadcastChannel` that is based on reference identity. - * * `StateFlow` cannot be currently closed like `ConflatedBroadcastChannel` and can never represent a failure. - * This feature might be added in the future if enough compelling use-cases are found. + * * `StateFlow` cannot be closed like `ConflatedBroadcastChannel` and can never represent a failure. + * All errors and completion signals should be explicitly _materialized_ if needed. * * `StateFlow` is designed to better cover typical use-cases of keeping track of state changes in time, taking * more pragmatic design choices for the sake of convenience. * + * To migrate [ConflatedBroadcastChannel] usage to [StateFlow], start by replacing usages of the `ConflatedBroadcastChannel()` + * constructor with `MutableStateFlow(initialValue)`, using `null` as an initial value if you don't have one. + * Replace [send][ConflatedBroadcastChannel.send] and [offer][ConflatedBroadcastChannel.offer] calls + * with updates to the state flow's [MutableStateFlow.value], and convert subscribers' code to flow operators. + * You can use the [filterNotNull] operator to mimic behavior of a `ConflatedBroadcastChannel` without initial value. + * * ### Concurrency * - * All methods of data flow are **thread-safe** and can be safely invoked from concurrent coroutines without + * All methods of state flow are **thread-safe** and can be safely invoked from concurrent coroutines without * external synchronization. * * ### Operator fusion * * Application of [flowOn][Flow.flowOn], [conflate][Flow.conflate], * [buffer] with [CONFLATED][Channel.CONFLATED] or [RENDEZVOUS][Channel.RENDEZVOUS] capacity, - * or a [distinctUntilChanged][Flow.distinctUntilChanged] operator has no effect on the state flow. + * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. * * ### Implementation notes * * State flow implementation is optimized for memory consumption and allocation-freedom. It uses a lock to ensure * thread-safety, but suspending collector coroutines are resumed outside of this lock to avoid dead-locks when - * using unconfined coroutines. Adding new collectors has `O(1)` amortized cost, but updating a [value] has `O(N)` - * cost, where `N` is the number of active collectors. + * using unconfined coroutines. Adding new subscribers has `O(1)` amortized cost, but updating a [value] has `O(N)` + * cost, where `N` is the number of active subscribers. * * ### Not stable for inheritance * - * **`StateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **`The StateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use `MutableStateFlow()` constructor function to create an implementation. + * Use the `MutableStateFlow(value)` constructor function to create an implementation. */ @ExperimentalCoroutinesApi -public interface StateFlow : Flow { +public interface StateFlow : SharedFlow { /** * The current value of this state flow. */ @@ -110,23 +145,31 @@ public interface StateFlow : Flow { /** * A mutable [StateFlow] that provides a setter for [value]. + * An instance of `MutableStateFlow` with the given initial `value` can be created using + * `MutableStateFlow(value)` constructor function. * - * See [StateFlow] documentation for details. + * See the [StateFlow] documentation for details on state flows. * * ### Not stable for inheritance * - * **`MutableStateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods + * **The `MutableStateFlow` interface is not stable for inheritance in 3rd party libraries**, as new methods * might be added to this interface in the future, but is stable for use. - * Use `MutableStateFlow()` constructor function to create an implementation. + * Use the `MutableStateFlow()` constructor function to create an implementation. */ @ExperimentalCoroutinesApi -public interface MutableStateFlow : StateFlow { +public interface MutableStateFlow : StateFlow, MutableSharedFlow { /** * The current value of this state flow. * * Setting a value that is [equal][Any.equals] to the previous one does nothing. */ public override var value: T + + /** + * Atomically compares the current [value] with [expect] and sets it to [update] if it is equal to [expect]. + * The result is `true` if the [value] was set to [update] and `false` otherwise. + */ + public fun compareAndSet(expect: T, update: T): Boolean } /** @@ -144,14 +187,12 @@ private val NONE = Symbol("NONE") @SharedImmutable private val PENDING = Symbol("PENDING") -private const val INITIAL_SIZE = 2 // optimized for just a few collectors - // StateFlow slots are allocated for its collectors -private class StateFlowSlot { +private class StateFlowSlot : AbstractSharedFlowSlot>() { /** * Each slot can have one of the following states: * - * * `null` -- it is not used right now. Can [allocate] to new collector. + * * `null` -- it is not used right now. Can [allocateLocked] to new collector. * * `NONE` -- used by a collector, but neither suspended nor has pending value. * * `PENDING` -- pending to process new value. * * `CancellableContinuationImpl` -- suspended waiting for new value. @@ -161,15 +202,16 @@ private class StateFlowSlot { */ private val _state = atomic(null) - fun allocate(): Boolean { + override fun allocateLocked(flow: StateFlowImpl<*>): Boolean { // No need for atomic check & update here, since allocated happens under StateFlow lock if (_state.value != null) return false // not free _state.value = NONE // allocated return true } - fun free() { + override fun freeLocked(flow: StateFlowImpl<*>): List>? { _state.value = null // free now + return null // nothing more to do } @Suppress("UNCHECKED_CAST") @@ -207,72 +249,99 @@ private class StateFlowSlot { } } -private class StateFlowImpl(initialValue: Any) : SynchronizedObject(), MutableStateFlow, FusibleFlow { - private val _state = atomic(initialValue) // T | NULL +private class StateFlowImpl( + initialState: Any // T | NULL +) : AbstractSharedFlow(), MutableStateFlow, DistinctFlow, CancellableFlow, FusibleFlow { + private val _state = atomic(initialState) // T | NULL private var sequence = 0 // serializes updates, value update is in process when sequence is odd - private var slots = arrayOfNulls(INITIAL_SIZE) - private var nSlots = 0 // number of allocated (!free) slots - private var nextIndex = 0 // oracle for the next free slot index + + override val isDefaultEquivalence: Boolean + get() = true // it is a DistinctFlow with default equivalence, so distinctUntilChanged is NOP on it @Suppress("UNCHECKED_CAST") public override var value: T get() = NULL.unbox(_state.value) - set(value) { - var curSequence = 0 - var curSlots: Array = this.slots // benign race, we will not use it - val newState = value ?: NULL - synchronized(this) { - val oldState = _state.value - if (oldState == newState) return // Don't do anything if value is not changing - _state.value = newState - curSequence = sequence - if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update) - curSequence++ // make it odd - sequence = curSequence - } else { - // update is already in process, notify it, and return - sequence = curSequence + 2 // change sequence to notify, keep it odd - return - } - curSlots = slots // read current reference to collectors under lock + set(value) { updateState(null, value ?: NULL) } + + override fun compareAndSet(expect: T, update: T): Boolean = + updateState(expect ?: NULL, update ?: NULL) + + private fun updateState(expectedState: Any?, newState: Any): Boolean { + var curSequence = 0 + var curSlots: Array? = this.slots // benign race, we will not use it + synchronized(this) { + val oldState = _state.value + if (expectedState != null && oldState != expectedState) return false // CAS support + if (oldState == newState) return true // Don't do anything if value is not changing, but CAS -> true + _state.value = newState + curSequence = sequence + if (curSequence and 1 == 0) { // even sequence means quiescent state flow (no ongoing update) + curSequence++ // make it odd + sequence = curSequence + } else { + // update is already in process, notify it, and return + sequence = curSequence + 2 // change sequence to notify, keep it odd + return true // updated } - /* - Fire value updates outside of the lock to avoid deadlocks with unconfined coroutines - Loop until we're done firing all the changes. This is sort of simple flat combining that - ensures sequential firing of concurrent updates and avoids the storm of collector resumes - when updates happen concurrently from many threads. - */ - while (true) { - // Benign race on element read from array - for (col in curSlots) { - col?.makePending() - } - // check if the value was updated again while we were updating the old one - synchronized(this) { - if (sequence == curSequence) { // nothing changed, we are done - sequence = curSequence + 1 // make sequence even again - return // done - } - // reread everything for the next loop under the lock - curSequence = sequence - curSlots = slots + curSlots = slots // read current reference to collectors under lock + } + /* + Fire value updates outside of the lock to avoid deadlocks with unconfined coroutines. + Loop until we're done firing all the changes. This is a sort of simple flat combining that + ensures sequential firing of concurrent updates and avoids the storm of collector resumes + when updates happen concurrently from many threads. + */ + while (true) { + // Benign race on element read from array + curSlots?.forEach { + it?.makePending() + } + // check if the value was updated again while we were updating the old one + synchronized(this) { + if (sequence == curSequence) { // nothing changed, we are done + sequence = curSequence + 1 // make sequence even again + return true // done, updated } + // reread everything for the next loop under the lock + curSequence = sequence + curSlots = slots } } + } + + override val replayCache: List + get() = listOf(value) + + override fun tryEmit(value: T): Boolean { + this.value = value + return true + } + + override suspend fun emit(value: T) { + this.value = value + } + + @Suppress("UNCHECKED_CAST") + override fun resetReplayCache() { + throw UnsupportedOperationException("MutableStateFlow.resetReplayCache is not supported") + } override suspend fun collect(collector: FlowCollector) { val slot = allocateSlot() - var prevState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet) try { + if (collector is SubscribedFlowCollector) collector.onSubscription() + val collectorJob = currentCoroutineContext()[Job] + var oldState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet) // The loop is arranged so that it starts delivering current value without waiting first while (true) { // Here the coroutine could have waited for a while to be dispatched, // so we use the most recent state here to ensure the best possible conflation of stale values val newState = _state.value // Conflate value emissions using equality - if (prevState == null || newState != prevState) { + if (oldState == null || oldState != newState) { + collectorJob?.ensureActive() collector.emit(NULL.unbox(newState)) - prevState = newState + oldState = newState } // Note: if awaitPending is cancelled, then it bails out of this loop and calls freeSlot if (!slot.takePending()) { // try fast-path without suspending first @@ -284,33 +353,29 @@ private class StateFlowImpl(initialValue: Any) : SynchronizedObject(), Mutabl } } - private fun allocateSlot(): StateFlowSlot = synchronized(this) { - val size = slots.size - if (nSlots >= size) slots = slots.copyOf(2 * size) - var index = nextIndex - var slot: StateFlowSlot - while (true) { - slot = slots[index] ?: StateFlowSlot().also { slots[index] = it } - index++ - if (index >= slots.size) index = 0 - if (slot.allocate()) break // break when found and allocated free slot - } - nextIndex = index - nSlots++ - slot - } + override fun createSlot() = StateFlowSlot() + override fun createSlotArray(size: Int): Array = arrayOfNulls(size) - private fun freeSlot(slot: StateFlowSlot): Unit = synchronized(this) { - slot.free() - nSlots-- - } + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseStateFlow(context, capacity, onBufferOverflow) +} - override fun fuse(context: CoroutineContext, capacity: Int): FusibleFlow { - // context is irrelevant for state flow and it is always conflated - // so it should not do anything unless buffering is requested - return when (capacity) { - Channel.CONFLATED, Channel.RENDEZVOUS -> this - else -> ChannelFlowOperatorImpl(this, context, capacity) - } +internal fun MutableStateFlow.increment(delta: Int) { + while (true) { // CAS loop + val current = value + if (compareAndSet(current, current + delta)) return } } + +internal fun StateFlow.fuseStateFlow( + context: CoroutineContext, + capacity: Int, + onBufferOverflow: BufferOverflow +): Flow { + // state flow is always conflated so additional conflation does not have any effect + assert { capacity != Channel.CONFLATED } // should be desugared by callers + if ((capacity in 0..1 || capacity == Channel.BUFFERED) && onBufferOverflow == BufferOverflow.DROP_OLDEST) { + return this + } + return fuseSharedFlow(context, capacity, onBufferOverflow) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt index 994d38074e..e53ef35c45 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -16,7 +16,7 @@ internal fun Flow.asChannelFlow(): ChannelFlow = this as? ChannelFlow ?: ChannelFlowOperatorImpl(this) /** - * Operators that can fuse with [buffer] and [flowOn] operators implement this interface. + * Operators that can fuse with **downstream** [buffer] and [flowOn] operators implement this interface. * * @suppress **This an internal API and should not be used from general code.** */ @@ -24,16 +24,18 @@ internal fun Flow.asChannelFlow(): ChannelFlow = public interface FusibleFlow : Flow { /** * This function is called by [flowOn] (with context) and [buffer] (with capacity) operators - * that are applied to this flow. + * that are applied to this flow. Should not be used with [capacity] of [Channel.CONFLATED] + * (it shall be desugared to `capacity = 0, onBufferOverflow = DROP_OLDEST`). */ public fun fuse( context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL - ): FusibleFlow + capacity: Int = Channel.OPTIONAL_CHANNEL, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND + ): Flow } /** - * Operators that use channels extend this `ChannelFlow` and are always fused with each other. + * Operators that use channels as their "output" extend this `ChannelFlow` and are always fused with each other. * This class servers as a skeleton implementation of [FusibleFlow] and provides other cross-cutting * methods like ability to [produceIn] and [broadcastIn] the corresponding flow, thus making it * possible to directly use the backing channel if it exists (hence the `ChannelFlow` name). @@ -45,8 +47,13 @@ public abstract class ChannelFlow( // upstream context @JvmField public val context: CoroutineContext, // buffer capacity between upstream and downstream context - @JvmField public val capacity: Int + @JvmField public val capacity: Int, + // buffer overflow strategy + @JvmField public val onBufferOverflow: BufferOverflow ) : FusibleFlow { + init { + assert { capacity != Channel.CONFLATED } // CONFLATED must be desugared to 0, DROP_OLDEST by callers + } // shared code to create a suspend lambda from collectTo function in one place internal val collectToFun: suspend (ProducerScope) -> Unit @@ -55,35 +62,62 @@ public abstract class ChannelFlow( private val produceCapacity: Int get() = if (capacity == Channel.OPTIONAL_CHANNEL) Channel.BUFFERED else capacity - public override fun fuse(context: CoroutineContext, capacity: Int): FusibleFlow { + /** + * When this [ChannelFlow] implementation can work without a channel (supports [Channel.OPTIONAL_CHANNEL]), + * then it should return a non-null value from this function, so that a caller can use it without the effect of + * additional [flowOn] and [buffer] operators, by incorporating its + * [context], [capacity], and [onBufferOverflow] into its own implementation. + */ + public open fun dropChannelOperators(): Flow? = null + + public override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): Flow { + assert { capacity != Channel.CONFLATED } // CONFLATED must be desugared to (0, DROP_OLDEST) by callers // note: previous upstream context (specified before) takes precedence val newContext = context + this.context - val newCapacity = when { - this.capacity == Channel.OPTIONAL_CHANNEL -> capacity - capacity == Channel.OPTIONAL_CHANNEL -> this.capacity - this.capacity == Channel.BUFFERED -> capacity - capacity == Channel.BUFFERED -> this.capacity - this.capacity == Channel.CONFLATED -> Channel.CONFLATED - capacity == Channel.CONFLATED -> Channel.CONFLATED - else -> { - // sanity checks - assert { this.capacity >= 0 } - assert { capacity >= 0 } - // combine capacities clamping to UNLIMITED on overflow - val sum = this.capacity + capacity - if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow + val newCapacity: Int + val newOverflow: BufferOverflow + if (onBufferOverflow != BufferOverflow.SUSPEND) { + // this additional buffer never suspends => overwrite preceding buffering configuration + newCapacity = capacity + newOverflow = onBufferOverflow + } else { + // combine capacities, keep previous overflow strategy + newCapacity = when { + this.capacity == Channel.OPTIONAL_CHANNEL -> capacity + capacity == Channel.OPTIONAL_CHANNEL -> this.capacity + this.capacity == Channel.BUFFERED -> capacity + capacity == Channel.BUFFERED -> this.capacity + else -> { + // sanity checks + assert { this.capacity >= 0 } + assert { capacity >= 0 } + // combine capacities clamping to UNLIMITED on overflow + val sum = this.capacity + capacity + if (sum >= 0) sum else Channel.UNLIMITED // unlimited on int overflow + } } + newOverflow = this.onBufferOverflow } - if (newContext == this.context && newCapacity == this.capacity) return this - return create(newContext, newCapacity) + if (newContext == this.context && newCapacity == this.capacity && newOverflow == this.onBufferOverflow) + return this + return create(newContext, newCapacity, newOverflow) } - protected abstract fun create(context: CoroutineContext, capacity: Int): ChannelFlow + protected abstract fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow protected abstract suspend fun collectTo(scope: ProducerScope) - public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel = - scope.broadcast(context, produceCapacity, start, block = collectToFun) + // broadcastImpl is used in broadcastIn operator which is obsolete and replaced by SharedFlow. + // BroadcastChannel does not support onBufferOverflow beyond simple conflation + public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel { + val broadcastCapacity = when (onBufferOverflow) { + BufferOverflow.SUSPEND -> produceCapacity + BufferOverflow.DROP_OLDEST -> Channel.CONFLATED + BufferOverflow.DROP_LATEST -> + throw IllegalArgumentException("Broadcast channel does not support BufferOverflow.DROP_LATEST") + } + return scope.broadcast(context, broadcastCapacity, start, block = collectToFun) + } /** * Here we use ATOMIC start for a reason (#1825). @@ -94,26 +128,33 @@ public abstract class ChannelFlow( * Thus `onCompletion` and `finally` blocks won't be executed and it may lead to a different kinds of memory leaks. */ public open fun produceImpl(scope: CoroutineScope): ReceiveChannel = - scope.produce(context, produceCapacity, start = CoroutineStart.ATOMIC, block = collectToFun) + scope.produce(context, produceCapacity, onBufferOverflow, start = CoroutineStart.ATOMIC, block = collectToFun) override suspend fun collect(collector: FlowCollector): Unit = coroutineScope { collector.emitAll(produceImpl(this)) } - public open fun additionalToStringProps(): String = "" + protected open fun additionalToStringProps(): String? = null // debug toString - override fun toString(): String = - "$classSimpleName[${additionalToStringProps()}context=$context, capacity=$capacity]" + override fun toString(): String { + val props = ArrayList(4) + additionalToStringProps()?.let { props.add(it) } + if (context !== EmptyCoroutineContext) props.add("context=$context") + if (capacity != Channel.OPTIONAL_CHANNEL) props.add("capacity=$capacity") + if (onBufferOverflow != BufferOverflow.SUSPEND) props.add("onBufferOverflow=$onBufferOverflow") + return "$classSimpleName[${props.joinToString(", ")}]" + } } // ChannelFlow implementation that operates on another flow before it internal abstract class ChannelFlowOperator( - @JvmField val flow: Flow, + @JvmField protected val flow: Flow, context: CoroutineContext, - capacity: Int -) : ChannelFlow(context, capacity) { + capacity: Int, + onBufferOverflow: BufferOverflow +) : ChannelFlow(context, capacity, onBufferOverflow) { protected abstract suspend fun flowCollect(collector: FlowCollector) // Changes collecting context upstream to the specified newContext, while collecting in the original context @@ -148,14 +189,19 @@ internal abstract class ChannelFlowOperator( override fun toString(): String = "$flow -> ${super.toString()}" } -// Simple channel flow operator: flowOn, buffer, or their fused combination +/** + * Simple channel flow operator: [flowOn], [buffer], or their fused combination. + */ internal class ChannelFlowOperatorImpl( flow: Flow, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.OPTIONAL_CHANNEL -) : ChannelFlowOperator(flow, context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowOperatorImpl(flow, context, capacity) + capacity: Int = Channel.OPTIONAL_CHANNEL, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlowOperator(flow, context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowOperatorImpl(flow, context, capacity, onBufferOverflow) + + override fun dropChannelOperators(): Flow? = flow override suspend fun flowCollect(collector: FlowCollector) = flow.collect(collector) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt new file mode 100644 index 0000000000..d141f7d32e --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.internal + +import kotlinx.coroutines.flow.* + +/** + * An internal interface that marks the flow impls with distinct values, so that + * application of [distinctUntilChanged] can be optimized away. A class implementing + * this interface can be conditionally distinct via [isDefaultEquivalence] without + * having to have multiple copies of the same code. + */ +internal interface DistinctFlow : Flow { + val isDefaultEquivalence: Boolean // true when using default equivalence +} + +/** + * An analogue of the [flow] builder that does not check the context of execution of the resulting flow, + * and the implementation must also guarantee that all emitted values are distinct, so that [distinctUntilChanged] + * can be optimized away. Used in our own operators where we trust these contracts to be met. + */ +@PublishedApi +internal inline fun unsafeDistinctFlow( + isDefaultEquivalence: Boolean = true, + @BuilderInference crossinline block: suspend FlowCollector.() -> Unit +): Flow = + object : DistinctFlow { + override val isDefaultEquivalence = isDefaultEquivalence + override suspend fun collect(collector: FlowCollector) = collector.block() + } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt index 798f38b8bd..530bcc1e5a 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt @@ -14,10 +14,11 @@ internal class ChannelFlowTransformLatest( private val transform: suspend FlowCollector.(value: T) -> Unit, flow: Flow, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlowOperator(flow, context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowTransformLatest(transform, flow, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlowOperator(flow, context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowTransformLatest(transform, flow, context, capacity, onBufferOverflow) override suspend fun flowCollect(collector: FlowCollector) { assert { collector is SendingCollector } // So cancellation behaviour is not leaking into the downstream @@ -41,10 +42,11 @@ internal class ChannelFlowMerge( private val flow: Flow>, private val concurrency: Int, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelFlowMerge(flow, concurrency, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelFlowMerge(flow, concurrency, context, capacity, onBufferOverflow) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { return scope.flowProduce(context, capacity, block = collectToFun) @@ -72,17 +74,17 @@ internal class ChannelFlowMerge( } } - override fun additionalToStringProps(): String = - "concurrency=$concurrency, " + override fun additionalToStringProps(): String = "concurrency=$concurrency" } internal class ChannelLimitedFlowMerge( private val flows: Iterable>, context: CoroutineContext = EmptyCoroutineContext, - capacity: Int = Channel.BUFFERED -) : ChannelFlow(context, capacity) { - override fun create(context: CoroutineContext, capacity: Int): ChannelFlow = - ChannelLimitedFlowMerge(flows, context, capacity) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + ChannelLimitedFlowMerge(flows, context, capacity, onBufferOverflow) override fun produceImpl(scope: CoroutineScope): ReceiveChannel { return scope.flowProduce(context, capacity, block = collectToFun) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 010d781c02..5c50ec7990 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -60,13 +60,23 @@ import kotlin.jvm.* * Q : -->---------- [2A] -- [2B] -- [2C] -->-- // collect * ``` * - * When operator's code takes time to execute this decreases the total execution time of the flow. + * When the operator's code takes some time to execute, this decreases the total execution time of the flow. * A [channel][Channel] is used between the coroutines to send elements emitted by the coroutine `P` to * the coroutine `Q`. If the code before `buffer` operator (in the coroutine `P`) is faster than the code after * `buffer` operator (in the coroutine `Q`), then this channel will become full at some point and will suspend * the producer coroutine `P` until the consumer coroutine `Q` catches up. * The [capacity] parameter defines the size of this buffer. * + * ### Buffer overflow + * + * By default, the emitter is suspended when the buffer overflows, to let collector catch up. This strategy can be + * overridden with an optional [onBufferOverflow] parameter so that the emitter is never suspended. In this + * case, on buffer overflow either the oldest value in the buffer is dropped with the [DROP_OLDEST][BufferOverflow.DROP_OLDEST] + * strategy and the latest emitted value is added to the buffer, + * or the latest value that is being emitted is dropped with the [DROP_LATEST][BufferOverflow.DROP_LATEST] strategy, + * keeping the buffer intact. + * To implement either of the custom strategies, a buffer of at least one element is used. + * * ### Operator fusion * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are @@ -76,9 +86,12 @@ import kotlin.jvm.* * which effectively requests a buffer of any size. Multiple requests with a specified buffer * size produce a buffer with the sum of the requested buffer sizes. * + * A `buffer` call with a non-default value of the [onBufferOverflow] parameter overrides all immediately preceding + * buffering operators, because it never suspends its upstream, and thus no upstream buffer would ever be used. + * * ### Conceptual implementation * - * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its + * The actual implementation of `buffer` is not trivial due to the fusing, but conceptually its basic * implementation is equivalent to the following code that can be written using [produce] * coroutine builder to produce a channel and [consumeEach][ReceiveChannel.consumeEach] extension to consume it: * @@ -96,24 +109,43 @@ import kotlin.jvm.* * * ### Conflation * - * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is provided as a shortcut via - * [conflate] operator. See its documentation for details. + * Usage of this function with [capacity] of [Channel.CONFLATED][Channel.CONFLATED] is a shortcut to + * `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`, and is available via + * a separate [conflate] operator. See its documentation for details. * * @param capacity type/capacity of the buffer between coroutines. Allowed values are the same as in `Channel(...)` - * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], - * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating - * an explicitly requested size. + * factory function: [BUFFERED][Channel.BUFFERED] (by default), [CONFLATED][Channel.CONFLATED], + * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating + * an explicitly requested size. + * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to + * [SUSPEND][BufferOverflow.SUSPEND], supported only when `capacity >= 0` or `capacity == Channel.BUFFERED`, + * implicitly creates a channel with at least one buffered element). */ -public fun Flow.buffer(capacity: Int = BUFFERED): Flow { +@Suppress("NAME_SHADOWING") +public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Flow { require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) { "Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity" } + require(capacity != CONFLATED || onBufferOverflow == BufferOverflow.SUSPEND) { + "CONFLATED capacity cannot be used with non-default onBufferOverflow" + } + // desugar CONFLATED capacity to (0, DROP_OLDEST) + var capacity = capacity + var onBufferOverflow = onBufferOverflow + if (capacity == CONFLATED) { + capacity = 0 + onBufferOverflow = BufferOverflow.DROP_OLDEST + } + // create a flow return when (this) { - is FusibleFlow -> fuse(capacity = capacity) - else -> ChannelFlowOperatorImpl(this, capacity = capacity) + is FusibleFlow -> fuse(capacity = capacity, onBufferOverflow = onBufferOverflow) + else -> ChannelFlowOperatorImpl(this, capacity = capacity, onBufferOverflow = onBufferOverflow) } } +@Deprecated(level = DeprecationLevel.HIDDEN, message = "For binary compatibility") +public fun Flow.buffer(capacity: Int = BUFFERED): Flow = buffer(capacity) + /** * Conflates flow emissions via conflated channel and runs collector in a separate coroutine. * The effect of this is that emitter is never suspended due to a slow collector, but collector @@ -138,7 +170,9 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow { * assertEquals(listOf(1, 10, 20, 30), result) * ``` * - * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED]. + * Note that `conflate` operator is a shortcut for [buffer] with `capacity` of [Channel.CONFLATED][Channel.CONFLATED], + * with is, in turn, a shortcut to a buffer that only keeps the latest element as + * created by `buffer(onBufferOverflow = `[`BufferOverflow.DROP_OLDEST`][BufferOverflow.DROP_OLDEST]`)`. * * ### Operator fusion * @@ -194,8 +228,8 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * .flowOn(Dispatchers.Default) * ``` * - * Note that an instance of [StateFlow] does not have an execution context by itself, - * so applying `flowOn` to a `StateFlow` has not effect. See [StateFlow] documentation on Operator Fusion. + * Note that an instance of [SharedFlow] does not have an execution context by itself, + * so applying `flowOn` to a `SharedFlow` has not effect. See the [SharedFlow] documentation on Operator Fusion. * * @throws [IllegalArgumentException] if provided context contains [Job] instance. */ @@ -211,17 +245,30 @@ public fun Flow.flowOn(context: CoroutineContext): Flow { /** * Returns a flow which checks cancellation status on each emission and throws * the corresponding cancellation cause if flow collector was cancelled. - * Note that [flow] builder is [cancellable] by default. + * Note that [flow] builder and all implementations of [SharedFlow] are [cancellable] by default. * * This operator provides a shortcut for `.onEach { currentCoroutineContext().ensureActive() }`. * See [ensureActive][CoroutineContext.ensureActive] for details. */ -public fun Flow.cancellable(): Flow { - if (this is AbstractFlow<*>) return this // Fast-path, already cancellable - return unsafeFlow { - collect { +public fun Flow.cancellable(): Flow = + when (this) { + is CancellableFlow<*> -> this // Fast-path, already cancellable + else -> CancellableFlowImpl(this) + } + +/** + * Internal marker for flows that are [cancellable]. + */ +internal interface CancellableFlow : Flow + +/** + * Named implementation class for a flow that is defined by the [cancellable] function. + */ +private class CancellableFlowImpl(val flow: Flow) : CancellableFlow { + override suspend fun collect(collector: FlowCollector) { + flow.collect { currentCoroutineContext().ensureActive() - emit(it) + collector.emit(it) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index 35048484d2..d6f26b46fd 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -9,7 +9,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* -import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** * Returns flow where all subsequent repetitions of the same value are filtered out. @@ -19,40 +18,46 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * See [StateFlow] documentation on Operator Fusion. */ public fun Flow.distinctUntilChanged(): Flow = - when (this) { - is StateFlow<*> -> this - else -> distinctUntilChangedBy { it } + when { + this is DistinctFlow<*> && isDefaultEquivalence -> this // some other internal impls beyond StateFlow are distinct too + else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent) } /** * Returns flow where all subsequent repetitions of the same value are filtered out, when compared * with each other via the provided [areEquivalent] function. */ +@Suppress("UNCHECKED_CAST") public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> Boolean): Flow = - distinctUntilChangedBy(keySelector = { it }, areEquivalent = areEquivalent) + distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = areEquivalent as (Any?, Any?) -> Boolean) /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * key is extracted with [keySelector] function. */ public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = - distinctUntilChangedBy(keySelector = keySelector, areEquivalent = { old, new -> old == new }) + distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent) + +private val defaultKeySelector: (Any?) -> Any? = { it } +private val defaultAreEquivalent: (Any?, Any?) -> Boolean = { old, new -> old == new } /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * keys are extracted with [keySelector] function and compared with each other via the * provided [areEquivalent] function. + * + * NOTE: It is non-inline to share a single implementing class. */ -private inline fun Flow.distinctUntilChangedBy( - crossinline keySelector: (T) -> K, - crossinline areEquivalent: (old: K, new: K) -> Boolean +private fun Flow.distinctUntilChangedBy( + keySelector: (T) -> Any?, + areEquivalent: (old: Any?, new: Any?) -> Boolean ): Flow = - flow { + unsafeDistinctFlow(keySelector === defaultKeySelector && areEquivalent === defaultAreEquivalent) { var previousKey: Any? = NULL collect { value -> val key = keySelector(value) @Suppress("UNCHECKED_CAST") - if (previousKey === NULL || !areEquivalent(previousKey as K, key)) { + if (previousKey === NULL || !areEquivalent(previousKey, key)) { previousKey = key emit(value) } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index fb37da3a83..8e802c753a 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -55,7 +55,12 @@ internal inline fun Flow.unsafeTransform( } /** - * Invokes the given [action] when this flow starts to be collected. + * Returns a flow that invokes the given [action] **before** this flow starts to be collected. + * + * The [action] is called before the upstream flow is started, so if it is used with a [SharedFlow] + * there is **no guarantee** that emissions to the upstream flow that happen inside or immediately + * after this `onStart` action will be collected + * (see [onSubscription] for an alternative operator on shared flows). * * The receiver of the [action] is [FlowCollector], so `onStart` can emit additional elements. * For example: @@ -80,7 +85,7 @@ public fun Flow.onStart( } /** - * Invokes the given [action] when the given flow is completed or cancelled, passing + * Returns a flow that invokes the given [action] **after** the flow is completed or cancelled, passing * the cancellation exception or failure as cause parameter of [action]. * * Conceptually, `onCompletion` is similar to wrapping the flow collection into a `finally` block, @@ -126,7 +131,7 @@ public fun Flow.onStart( * ``` * * The receiver of the [action] is [FlowCollector] and this operator can be used to emit additional - * elements at the end if it completed successfully. For example: + * elements at the end **if it completed successfully**. For example: * * ``` * flowOf("a", "b", "c") diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index 5500034e9f..db4542ffce 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -4,41 +4,48 @@ package kotlinx.coroutines.flow -import kotlinx.coroutines.* import kotlin.coroutines.* /** - * Returns this. - * Applying [flowOn][Flow.flowOn] operator to [StateFlow] has no effect. - * See [StateFlow] documentation on Operator Fusion. + * Applying [cancellable][Flow.cancellable] to a [SharedFlow] has no effect. + * See the [SharedFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying flowOn operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", + message = "Applying 'cancellable' to a SharedFlow has no effect. See the SharedFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) -public fun StateFlow.flowOn(context: CoroutineContext): Flow = noImpl() +public fun SharedFlow.cancellable(): Flow = noImpl() /** - * Returns this. - * Applying [conflate][Flow.conflate] operator to [StateFlow] has no effect. - * See [StateFlow] documentation on Operator Fusion. + * Applying [flowOn][Flow.flowOn] to [SharedFlow] has no effect. + * See the [SharedFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying conflate operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", + message = "Applying 'flowOn' to SharedFlow has no effect. See the SharedFlow documentation on Operator Fusion.", + replaceWith = ReplaceWith("this") +) +public fun SharedFlow.flowOn(context: CoroutineContext): Flow = noImpl() + +/** + * Applying [conflate][Flow.conflate] to [StateFlow] has no effect. + * See the [StateFlow] documentation on Operator Fusion. + */ +@Deprecated( + level = DeprecationLevel.ERROR, + message = "Applying 'conflate' to StateFlow has no effect. See the StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) public fun StateFlow.conflate(): Flow = noImpl() /** - * Returns this. - * Applying [distinctUntilChanged][Flow.distinctUntilChanged] operator to [StateFlow] has no effect. - * See [StateFlow] documentation on Operator Fusion. + * Applying [distinctUntilChanged][Flow.distinctUntilChanged] to [StateFlow] has no effect. + * See the [StateFlow] documentation on Operator Fusion. */ @Deprecated( level = DeprecationLevel.ERROR, - message = "Applying distinctUntilChanged operator to StateFlow has no effect. See StateFlow documentation on Operator Fusion.", + message = "Applying 'distinctUntilChanged' to StateFlow has no effect. See the StateFlow documentation on Operator Fusion.", replaceWith = ReplaceWith("this") ) public fun StateFlow.distinctUntilChanged(): Flow = noImpl() @@ -69,4 +76,4 @@ public fun StateFlow.distinctUntilChanged(): Flow = noImpl() // replaceWith = ReplaceWith("currentCoroutineContext()") //) //public val FlowCollector<*>.coroutineContext: CoroutineContext -// get() = noImpl() \ No newline at end of file +// get() = noImpl() diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt new file mode 100644 index 0000000000..4e518327df --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -0,0 +1,403 @@ +/* + * 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.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* + +// -------------------------------- shareIn -------------------------------- + +/** + * Converts a _cold_ [Flow] into a _hot_ [SharedFlow] that is started in the given coroutine [scope], + * sharing emissions from a single running instance of the upstream flow with multiple downstream subscribers, + * and replaying a specified number of [replay] values to new subscribers. See the [SharedFlow] documentation + * for the general concepts of shared flows. + * + * The starting of the sharing coroutine is controlled by the [started] parameter. By default, the sharing coroutine is started + * [Eagerly][SharingStarted.Eagerly], so the upstream flow is started even before the first subscribers appear. Note + * that in this case all values emitted by the upstream beyond the most recent values as specified by + * [replay] parameter **will be immediately discarded**. + * + * Additional options for the [started] parameter are: + * + * * [Lazily][SharingStarted.Lazily] — starts the upstream flow after the first subscriber appears, which guarantees + * that this first subscriber gets all the emitted values, while subsequent subscribers are only guaranteed to + * get the most recent [replay] values. The upstream flow continues to be active even when all subscribers + * disappear, but only the most recent [replay] values are cached without subscribers. + * * [WhileSubscribed()][SharingStarted.WhileSubscribed] — starts the upstream flow when the first subscriber + * appears, immediately stops when the last subscriber disappears, keeping the replay cache forever. + * It has additional optional configuration parameters as explained in its documentation. + * * A custom strategy can be supplied by implementing the [SharingStarted] interface. + * + * The `shareIn` operator is useful in situations when there is a _cold_ flow that is expensive to create and/or + * to maintain, but there are multiple subscribers that need to collect its values. For example, consider a + * flow of messages coming from a backend over the expensive network connection, taking a lot of + * time to establish. Conceptually, it might be implemented like this: + * + * ``` + * val backendMessages: Flow = flow { + * connectToBackend() // takes a lot of time + * try { + * while (true) { + * emit(receiveMessageFromBackend()) + * } + * } finally { + * disconnectFromBackend() + * } + * } + * ``` + * + * If this flow is directly used in the application, then every time it is collected a fresh connection is + * established, and it will take a while before messages start flowing. However, we can share a single connection + * and establish it eagerly like this: + * + * ``` + * val messages: SharedFlow = backendMessages.shareIn(scope, 0) + * ``` + * + * Now a single connection is shared between all collectors from `messages`, and there is a chance that the connection + * is already established by the time it is needed. + * + * ### Upstream completion and error handling + * + * **Normal completion of the upstream flow has no effect on subscribers**, and the sharing coroutine continues to run. If a + * a strategy like [SharingStarted.WhileSubscribed] is used, then the upstream can get restarted again. If a special + * action on upstream completion is needed, then an [onCompletion] operator can be used before the + * `shareIn` operator to emit a special value in this case, like this: + * + * ``` + * backendMessages + * .onCompletion { cause -> if (cause == null) emit(UpstreamHasCompletedMessage) } + * .shareIn(scope, 0) + * ``` + * + * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, + * and will be handled by the [scope] in which the sharing coroutine is launched. Custom exception handling + * can be configured by using the [catch] or [retry] operators before the `shareIn` operator. + * For example, to retry connection on any `IOException` with 1 second delay between attempts, use: + * + * ``` + * val messages = backendMessages + * .retry { e -> + * val shallRetry = e is IOException // other exception are bugs - handle them + * if (shallRetry) delay(1000) + * shallRetry + * } + * .shareIn(scope, 0) + * ``` + * + * ### Initial value + * + * When a special initial value is needed to signal to subscribers that the upstream is still loading the data, + * use the [onStart] operator on the upstream flow. For example: + * + * ``` + * backendMessages + * .onStart { emit(UpstreamIsStartingMessage) } + * .shareIn(scope, 1) // replay one most recent message + * ``` + * + * ### Buffering and conflation + * + * The `shareIn` operator runs the upstream flow in a separate coroutine, and buffers emissions from upstream as explained + * in the [buffer] operator's description, using a buffer of [replay] size or the default (whichever is larger). + * This default buffering can be overridden with an explicit buffer configuration by preceding the `shareIn` call + * with [buffer] or [conflate], for example: + * + * * `buffer(0).shareIn(scope, 0)` — overrides the default buffer size and creates a [SharedFlow] without a buffer. + * Effectively, it configures sequential processing between the upstream emitter and subscribers, + * as the emitter is suspended until all subscribers process the value. Note, that the value is still immediately + * discarded when there are no subscribers. + * * `buffer(b).shareIn(scope, r)` — creates a [SharedFlow] with `replay = r` and `extraBufferCapacity = b`. + * * `conflate().shareIn(scope, r)` — creates a [SharedFlow] with `replay = r`, `onBufferOverflow = DROP_OLDEST`, + * and `extraBufferCapacity = 1` when `replay == 0` to support this strategy. + * + * ### Operator fusion + * + * Application of [flowOn][Flow.flowOn], [buffer] with [RENDEZVOUS][Channel.RENDEZVOUS] capacity, + * or [cancellable] operators to the resulting shared flow has no effect. + * + * ### Exceptions + * + * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. + * + * @param scope the coroutine scope in which sharing is started. + * @param replay the number of values replayed to new subscribers (cannot be negative). + * @param started the strategy that controls when sharing is started and stopped + * (optional, default to [Eagerly][SharingStarted.Eagerly] starting the sharing without waiting for subscribers). + */ +@ExperimentalCoroutinesApi +public fun Flow.shareIn( + scope: CoroutineScope, + replay: Int, + started: SharingStarted = SharingStarted.Eagerly +): SharedFlow { + val config = configureSharing(replay) + val shared = MutableSharedFlow( + replay = replay, + extraBufferCapacity = config.extraBufferCapacity, + onBufferOverflow = config.onBufferOverflow + ) + @Suppress("UNCHECKED_CAST") + scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T) + return shared.asSharedFlow() +} + +private class SharingConfig( + @JvmField val upstream: Flow, + @JvmField val extraBufferCapacity: Int, + @JvmField val onBufferOverflow: BufferOverflow, + @JvmField val context: CoroutineContext +) + +// Decomposes upstream flow to fuse with it when possible +private fun Flow.configureSharing(replay: Int): SharingConfig { + assert { replay >= 0 } + val defaultExtraCapacity = replay.coerceAtLeast(Channel.CHANNEL_DEFAULT_CAPACITY) - replay + // Combine with preceding buffer/flowOn and channel-using operators + if (this is ChannelFlow) { + // Check if this ChannelFlow can operate without a channel + val upstream = dropChannelOperators() + if (upstream != null) { // Yes, it can => eliminate the intermediate channel + return SharingConfig( + upstream = upstream, + extraBufferCapacity = when (capacity) { + Channel.OPTIONAL_CHANNEL, Channel.BUFFERED, 0 -> // handle special capacities + when { + onBufferOverflow == BufferOverflow.SUSPEND -> // buffer was configured with suspension + if (capacity == 0) 0 else defaultExtraCapacity // keep explicitly configured 0 or use default + replay == 0 -> 1 // no suspension => need at least buffer of one + else -> 0 // replay > 0 => no need for extra buffer beyond replay because we don't suspend + } + else -> capacity // otherwise just use the specified capacity as extra capacity + }, + onBufferOverflow = onBufferOverflow, + context = context + ) + } + } + // Add sharing operator on top with a default buffer + return SharingConfig( + upstream = this, + extraBufferCapacity = defaultExtraCapacity, + onBufferOverflow = BufferOverflow.SUSPEND, + context = EmptyCoroutineContext + ) +} + +// Launches sharing coroutine +private fun CoroutineScope.launchSharing( + context: CoroutineContext, + upstream: Flow, + shared: MutableSharedFlow, + started: SharingStarted, + initialValue: T +) { + launch(context) { // the single coroutine to rule the sharing + started.command(shared.subscriptionCount) + .distinctUntilChanged() // only changes in command have effect + .collectLatest { // cancels block on new emission + when (it) { + SharingCommand.START -> upstream.collect(shared) // can be cancelled + SharingCommand.STOP -> { /* just cancel and do nothing else */ } + SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> { + if (initialValue === NO_VALUE) { + shared.resetReplayCache() // regular shared flow -> reset cache + } else { + shared.tryEmit(initialValue) // state flow -> reset to initial value + } + } + } + } + } +} + +// -------------------------------- stateIn -------------------------------- + +/** + * Converts a _cold_ [Flow] into a _hot_ [StateFlow] that is started in the given coroutine [scope], + * sharing the most recently emitted value from a single running instance of the upstream flow with multiple + * downstream subscribers. See the [StateFlow] documentation for the general concepts of state flows. + * + * The starting of the sharing coroutine is controlled by the [started] parameter, as explained in the + * documentation for [shareIn] operator. + * + * The `stateIn` operator is useful in situations when there is a _cold_ flow that provides updates to the + * value of some state and is expensive to create and/or to maintain, but there are multiple subscribers + * that need to collect the most recent state value. For example, consider a + * flow of state updates coming from a backend over the expensive network connection, taking a lot of + * time to establish. Conceptually it might be implemented like this: + * + * ``` + * val backendState: Flow = flow { + * connectToBackend() // takes a lot of time + * try { + * while (true) { + * emit(receiveStateUpdateFromBackend()) + * } + * } finally { + * disconnectFromBackend() + * } + * } + * ``` + * + * If this flow is directly used in the application, then every time it is collected a fresh connection is + * established, and it will take a while before state updates start flowing. However, we can share a single connection + * and establish it eagerly like this: + * + * ``` + * val state: StateFlow = backendMessages.stateIn(scope, initialValue = State.LOADING) + * ``` + * + * Now, a single connection is shared between all collectors from `state`, and there is a chance that the connection + * is already established by the time it is needed. + * + * ### Upstream completion and error handling + * + * **Normal completion of the upstream flow has no effect on subscribers**, and the sharing coroutine continues to run. If a + * a strategy like [SharingStarted.WhileSubscribed] is used, then the upstream can get restarted again. If a special + * action on upstream completion is needed, then an [onCompletion] operator can be used before + * the `stateIn` operator to emit a special value in this case. See the [shareIn] operator's documentation for an example. + * + * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, + * and will be handled by the [scope] in which the sharing coroutine is launched. Custom exception handling + * can be configured by using the [catch] or [retry] operators before the `stateIn` operator, similarly to + * the [shareIn] operator. + * + * ### Operator fusion + * + * Application of [flowOn][Flow.flowOn], [conflate][Flow.conflate], + * [buffer] with [CONFLATED][Channel.CONFLATED] or [RENDEZVOUS][Channel.RENDEZVOUS] capacity, + * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. + * + * @param scope the coroutine scope in which sharing is started. + * @param started the strategy that controls when sharing is started and stopped + * (optional, default to [Eagerly][SharingStarted.Eagerly] starting the sharing without waiting for subscribers). + * @param initialValue the initial value of the state flow. + * This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy + * with the `replayExpirationMillis` parameter. + */ +@ExperimentalCoroutinesApi +public fun Flow.stateIn( + scope: CoroutineScope, + started: SharingStarted = SharingStarted.Eagerly, + initialValue: T +): StateFlow { + val config = configureSharing(1) + val state = MutableStateFlow(initialValue) + scope.launchSharing(config.context, config.upstream, state, started, initialValue) + return state.asStateFlow() +} + +/** + * Starts the upstream flow in a given [scope], suspends until the first value is emitted, and returns a _hot_ + * [StateFlow] of future emissions, sharing the most recently emitted value from this running instance of the upstream flow + * with multiple downstream subscribers. See the [StateFlow] documentation for the general concepts of state flows. + * + * @param scope the coroutine scope in which sharing is started. + */ +@ExperimentalCoroutinesApi +public suspend fun Flow.stateIn(scope: CoroutineScope): StateFlow { + val config = configureSharing(1) + val result = CompletableDeferred>() + scope.launchSharingDeferred(config.context, config.upstream, result) + return result.await() +} + +private fun CoroutineScope.launchSharingDeferred( + context: CoroutineContext, + upstream: Flow, + result: CompletableDeferred> +) { + launch(context) { + var state: MutableStateFlow? = null + upstream.collect { value -> + state?.let { it.value = value } ?: run { + state = MutableStateFlow(value).also { + result.complete(it.asStateFlow()) + } + } + } + } +} + +// -------------------------------- asSharedFlow/asStateFlow -------------------------------- + +/** + * Represents this mutable shared flow as a read-only shared flow. + */ +@ExperimentalCoroutinesApi +public fun MutableSharedFlow.asSharedFlow(): SharedFlow = + ReadonlySharedFlow(this) + +/** + * Represents this mutable state flow as a read-only state flow. + */ +@ExperimentalCoroutinesApi +public fun MutableStateFlow.asStateFlow(): StateFlow = + ReadonlyStateFlow(this) + +private class ReadonlySharedFlow( + flow: SharedFlow +) : SharedFlow by flow, CancellableFlow, FusibleFlow { + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseSharedFlow(context, capacity, onBufferOverflow) +} + +private class ReadonlyStateFlow( + flow: StateFlow +) : StateFlow by flow, CancellableFlow, FusibleFlow, DistinctFlow { + override val isDefaultEquivalence: Boolean + get() = true + + override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = + fuseStateFlow(context, capacity, onBufferOverflow) +} + +// -------------------------------- onSubscription -------------------------------- + +/** + * Returns a flow that invokes the given [action] **after** this shared flow starts to be collected + * (after the subscription is registered). + * + * The [action] is called before any value is emitted from the upstream + * flow to this subscription but after the subscription is established. It is guaranteed that all emissions to + * the upstream flow that happen inside or immediately after this `onSubscription` action will be + * collected by this subscription. + * + * The receiver of the [action] is [FlowCollector], so `onSubscription` can emit additional elements. + */ +@ExperimentalCoroutinesApi +public fun SharedFlow.onSubscription(action: suspend FlowCollector.() -> Unit): SharedFlow = + SubscribedSharedFlow(this, action) + +private class SubscribedSharedFlow( + private val sharedFlow: SharedFlow, + private val action: suspend FlowCollector.() -> Unit +) : SharedFlow by sharedFlow { + override suspend fun collect(collector: FlowCollector) = + sharedFlow.collect(SubscribedFlowCollector(collector, action)) +} + +internal class SubscribedFlowCollector( + private val collector: FlowCollector, + private val action: suspend FlowCollector.() -> Unit +) : FlowCollector by collector { + suspend fun onSubscription() { + val safeCollector = SafeCollector(collector, coroutineContext) + try { + safeCollector.action() + } finally { + safeCollector.releaseIntercepted() + } + if (collector is SubscribedFlowCollector) collector.onSubscription() + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index 520311ee5d..e3552d2893 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -67,7 +67,7 @@ public fun Flow.withIndex(): Flow> = flow { } /** - * Returns a flow which performs the given [action] on each value of the original flow. + * Returns a flow that invokes the given [action] **before** each value of the upstream flow is emitted downstream. */ public fun Flow.onEach(action: suspend (T) -> Unit): Flow = transform { value -> action(value) diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt new file mode 100644 index 0000000000..41f60479f2 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ChannelBufferOverflowTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* +import kotlin.test.* + +class ChannelBufferOverflowTest : TestBase() { + @Test + fun testDropLatest() = runTest { + val c = Channel(2, BufferOverflow.DROP_LATEST) + assertTrue(c.offer(1)) + assertTrue(c.offer(2)) + assertTrue(c.offer(3)) // overflows, dropped + c.send(4) // overflows dropped + assertEquals(1, c.receive()) + assertTrue(c.offer(5)) + assertTrue(c.offer(6)) // overflows, dropped + assertEquals(2, c.receive()) + assertEquals(5, c.receive()) + assertEquals(null, c.poll()) + } + + @Test + fun testDropOldest() = runTest { + val c = Channel(2, BufferOverflow.DROP_OLDEST) + assertTrue(c.offer(1)) + assertTrue(c.offer(2)) + assertTrue(c.offer(3)) // overflows, keeps 2, 3 + c.send(4) // overflows, keeps 3, 4 + assertEquals(3, c.receive()) + assertTrue(c.offer(5)) + assertTrue(c.offer(6)) // overflows, keeps 5, 6 + assertEquals(5, c.receive()) + assertEquals(6, c.receive()) + assertEquals(null, c.poll()) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt index 72ba315450..413c91f5a7 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelFactoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -9,7 +9,6 @@ import kotlin.test.* class ChannelFactoryTest : TestBase() { - @Test fun testRendezvousChannel() { assertTrue(Channel() is RendezvousChannel) @@ -19,21 +18,31 @@ class ChannelFactoryTest : TestBase() { @Test fun testLinkedListChannel() { assertTrue(Channel(Channel.UNLIMITED) is LinkedListChannel) + assertTrue(Channel(Channel.UNLIMITED, BufferOverflow.DROP_OLDEST) is LinkedListChannel) + assertTrue(Channel(Channel.UNLIMITED, BufferOverflow.DROP_LATEST) is LinkedListChannel) } @Test fun testConflatedChannel() { assertTrue(Channel(Channel.CONFLATED) is ConflatedChannel) + assertTrue(Channel(1, BufferOverflow.DROP_OLDEST) is ConflatedChannel) } @Test fun testArrayChannel() { assertTrue(Channel(1) is ArrayChannel) + assertTrue(Channel(1, BufferOverflow.DROP_LATEST) is ArrayChannel) assertTrue(Channel(10) is ArrayChannel) } @Test - fun testInvalidCapacityNotSupported() = runTest({ it is IllegalArgumentException }) { - Channel(-3) + fun testInvalidCapacityNotSupported() { + assertFailsWith { Channel(-3) } + } + + @Test + fun testUnsupportedBufferOverflow() { + assertFailsWith { Channel(Channel.CONFLATED, BufferOverflow.DROP_OLDEST) } + assertFailsWith { Channel(Channel.CONFLATED, BufferOverflow.DROP_LATEST) } } } diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt new file mode 100644 index 0000000000..bd588c726b --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelArrayModelTest.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +// Test that ArrayChannel(1, DROP_OLDEST) works just like ConflatedChannel() +class ConflatedChannelArrayModelTest : ConflatedChannelTest() { + override fun createConflatedChannel(): Channel = + ArrayChannel(1, BufferOverflow.DROP_OLDEST) +} diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 4deb3858f0..18f2843868 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -7,10 +7,13 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlin.test.* -class ConflatedChannelTest : TestBase() { +open class ConflatedChannelTest : TestBase() { + protected open fun createConflatedChannel() = + Channel(Channel.CONFLATED) + @Test fun testBasicConflationOfferPoll() { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() assertNull(q.poll()) assertTrue(q.offer(1)) assertTrue(q.offer(2)) @@ -21,7 +24,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflatedSend() = runTest { - val q = ConflatedChannel() + val q = createConflatedChannel() q.send(1) q.send(2) // shall conflated previously sent assertEquals(2, q.receiveOrNull()) @@ -29,7 +32,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflatedClose() = runTest { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() q.send(1) q.close() // shall become closed but do not conflate last sent item yet assertTrue(q.isClosedForSend) @@ -43,7 +46,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflationSendReceive() = runTest { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() expect(1) launch { // receiver coroutine expect(4) @@ -71,7 +74,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConsumeAll() = runTest { - val q = Channel(Channel.CONFLATED) + val q = createConflatedChannel() expect(1) for (i in 1..10) { q.send(i) // stores as last @@ -85,7 +88,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testCancelWithCause() = runTest({ it is TestCancellationException }) { - val channel = Channel(Channel.CONFLATED) + val channel = createConflatedChannel() channel.cancel(TestCancellationException()) channel.receiveOrNull() } diff --git a/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt deleted file mode 100644 index a6be97eb97..0000000000 --- a/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt +++ /dev/null @@ -1,113 +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.flow - -import kotlinx.coroutines.* -import kotlin.test.* - -class StateFlowTest : TestBase() { - @Test - fun testNormalAndNull() = runTest { - expect(1) - val state = MutableStateFlow(0) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - assertFailsWith { - state.collect { value -> - when (value) { - 0 -> expect(3) - 1 -> expect(5) - null -> expect(8) - 2 -> expect(10) - else -> expectUnreached() - } - } - } - expect(12) - } - expect(4) // collector is waiting - state.value = 1 // fire in the hole! - assertEquals(1, state.value) - yield() - expect(6) - state.value = 1 // same value, nothing happens - yield() - expect(7) - state.value = null // null value - assertNull(state.value) - yield() - expect(9) - state.value = 2 // another value - assertEquals(2, state.value) - yield() - expect(11) - job.cancel() - yield() - finish(13) - } - - @Test - fun testEqualsConflation() = runTest { - expect(1) - val state = MutableStateFlow(Data(0)) - val job = launch(start = CoroutineStart.UNDISPATCHED) { - expect(2) - assertFailsWith { - state.collect { value -> - when(value.i) { - 0 -> expect(3) // initial value - 2 -> expect(5) - 4 -> expect(7) - else -> error("Unexpected $value") - } - } - } - expect(9) - } - state.value = Data(1) // conflated - state.value = Data(0) // equals to last emitted - yield() // no repeat zero - state.value = Data(3) // conflated - state.value = Data(2) // delivered - expect(4) - yield() - state.value = Data(2) // equals to last one, dropped - yield() - state.value = Data(5) // conflated - state.value = Data(4) // delivered - expect(6) - yield() - expect(8) - job.cancel() - yield() - finish(10) - } - - data class Data(val i: Int) - - @Test - fun testDataModel() = runTest { - val s = CounterModel() - launch { - val sum = s.counter.take(11).sum() - assertEquals(55, sum) - } - repeat(10) { - yield() - s.inc() - } - } - - class CounterModel { - // private data flow - private val _counter = MutableStateFlow(0) - // publicly exposed as a flow - val counter: StateFlow get() = _counter - - fun inc() { - _counter.value++ - } - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt index 9b257d933e..9132633c59 100644 --- a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt +++ b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt @@ -7,11 +7,12 @@ package kotlinx.coroutines import kotlin.coroutines.* import kotlin.jvm.* -private class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineDispatcher(), Delay { - +internal class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineDispatcher(), Delay { private val originalDispatcher = enclosingScope.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher private val heap = ArrayList() // TODO use MPP heap/ordered set implementation (commonize ThreadSafeHeap) - private var currentTime = 0L + + var currentTime = 0L + private set init { /* @@ -51,16 +52,19 @@ private class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineD override fun isDispatchNeeded(context: CoroutineContext): Boolean = originalDispatcher.isDispatchNeeded(context) override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - val task = TimedTask(block, currentTime + timeMillis) + val task = TimedTask(block, deadline(timeMillis)) heap += task return task } override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - val task = TimedTask(Runnable { with(continuation) { resumeUndispatched(Unit) } }, currentTime + timeMillis) + val task = TimedTask(Runnable { with(continuation) { resumeUndispatched(Unit) } }, deadline(timeMillis)) heap += task continuation.invokeOnCancellation { task.dispose() } } + + private fun deadline(timeMillis: Long) = + if (timeMillis == Long.MAX_VALUE) Long.MAX_VALUE else currentTime + timeMillis } /** diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt new file mode 100644 index 0000000000..7b66977226 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferConflationTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +/** + * A _behavioral_ test for conflation options that can be configured by the [buffer] operator to test that it is + * implemented properly and that adjacent [buffer] calls are fused properly. +*/ +class BufferConflationTest : TestBase() { + private val n = 100 // number of elements to emit for test + + private fun checkConflate( + capacity: Int, + onBufferOverflow: BufferOverflow = BufferOverflow.DROP_OLDEST, + op: suspend Flow.() -> Flow + ) = runTest { + expect(1) + // emit all and conflate, then collect first & last + val expectedList = when (onBufferOverflow) { + BufferOverflow.DROP_OLDEST -> listOf(0) + (n - capacity until n).toList() // first item & capacity last ones + BufferOverflow.DROP_LATEST -> (0..capacity).toList() // first & capacity following ones + else -> error("cannot happen") + } + flow { + repeat(n) { i -> + expect(i + 2) + emit(i) + } + } + .op() + .collect { i -> + val j = expectedList.indexOf(i) + expect(n + 2 + j) + } + finish(n + 2 + expectedList.size) + } + + @Test + fun testConflate() = + checkConflate(1) { + conflate() + } + + @Test + fun testBufferConflated() = + checkConflate(1) { + buffer(Channel.CONFLATED) + } + + @Test + fun testBufferDropOldest() = + checkConflate(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testBuffer0DropOldest() = + checkConflate(1) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testBuffer1DropOldest() = + checkConflate(1) { + buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testBuffer10DropOldest() = + checkConflate(10) { + buffer(10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + } + + @Test + fun testConflateOverridesBuffer() = + checkConflate(1) { + buffer(42).conflate() + } + + @Test // conflate().conflate() should work like a single conflate + fun testDoubleConflate() = + checkConflate(1) { + conflate().conflate() + } + + @Test + fun testConflateBuffer10Combine() = + checkConflate(10) { + conflate().buffer(10) + } + + @Test + fun testBufferDropLatest() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test + fun testBuffer0DropLatest() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test + fun testBuffer1DropLatest() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(1, onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test // overrides previous buffer + fun testBufferDropLatestOverrideBuffer() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + buffer(42).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test // overrides previous conflate + fun testBufferDropLatestOverrideConflate() = + checkConflate(1, BufferOverflow.DROP_LATEST) { + conflate().buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + } + + @Test + fun testBufferDropLatestBuffer7Combine() = + checkConflate(7, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).buffer(7) + } + + @Test + fun testConflateOverrideBufferDropLatest() = + checkConflate(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).conflate() + } + + @Test + fun testBuffer3DropOldestOverrideBuffer8DropLatest() = + checkConflate(3, BufferOverflow.DROP_OLDEST) { + buffer(8, onBufferOverflow = BufferOverflow.DROP_LATEST) + .buffer(3, BufferOverflow.DROP_OLDEST) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt index b68e115637..6352aacf41 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/BufferTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.flow @@ -9,13 +9,21 @@ import kotlinx.coroutines.channels.* import kotlin.math.* import kotlin.test.* +/** + * A _behavioral_ test for buffering that is introduced by the [buffer] operator to test that it is + * implemented properly and that adjacent [buffer] calls are fused properly. + */ class BufferTest : TestBase() { - private val n = 50 // number of elements to emit for test + private val n = 200 // number of elements to emit for test private val defaultBufferSize = 64 // expected default buffer size (per docs) // Use capacity == -1 to check case of "no buffer" private fun checkBuffer(capacity: Int, op: suspend Flow.() -> Flow) = runTest { expect(1) + /* + Channels perform full rendezvous. Sender does not suspend when there is a suspended receiver and vice-versa. + Thus, perceived batch size is +2 from capacity. + */ val batchSize = capacity + 2 flow { repeat(n) { i -> @@ -163,27 +171,6 @@ class BufferTest : TestBase() { .flowOn(wrapperDispatcher()).buffer(5) } - @Test - fun testConflate() = runTest { - expect(1) - // emit all and conflate / then collect first & last - flow { - repeat(n) { i -> - expect(i + 2) - emit(i) - } - } - .buffer(Channel.CONFLATED) - .collect { i -> - when (i) { - 0 -> expect(n + 2) // first value - n - 1 -> expect(n + 3) // last value - else -> error("Unexpected $i") - } - } - finish(n + 4) - } - @Test fun testCancellation() = runTest { val result = flow { diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt new file mode 100644 index 0000000000..2272918af0 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.math.* +import kotlin.test.* + +/** + * Similar to [BufferTest], but tests [shareIn] buffering and its fusion with [buffer] operators. + */ +class ShareInBufferTest : TestBase() { + private val n = 200 // number of elements to emit for test + private val defaultBufferSize = 64 // expected default buffer size (per docs) + + // Use capacity == -1 to check case of "no buffer" + private fun checkBuffer(capacity: Int, op: suspend Flow.(CoroutineScope) -> Flow) = runTest { + expect(1) + /* + Shared flows do not perform full rendezvous. On buffer overflow emitter always suspends until all + subscribers get the value and then resumes. Thus, perceived batch size is +1 from buffer capacity. + */ + val batchSize = capacity + 1 + val upstream = flow { + repeat(n) { i -> + val batchNo = i / batchSize + val batchIdx = i % batchSize + expect(batchNo * batchSize * 2 + batchIdx + 2) + emit(i) + } + emit(-1) // done + } + coroutineScope { + upstream + .op(this) + .takeWhile { i -> i >= 0 } // until done + .collect { i -> + val batchNo = i / batchSize + val batchIdx = i % batchSize + // last batch might have smaller size + val k = min((batchNo + 1) * batchSize, n) - batchNo * batchSize + expect(batchNo * batchSize * 2 + k + batchIdx + 2) + } + coroutineContext.cancelChildren() // cancels sharing + } + finish(2 * n + 2) + } + + @Test + fun testReplay0DefaultBuffer() = + checkBuffer(defaultBufferSize) { + shareIn(it, 0) + } + + @Test + fun testReplay1DefaultBuffer() = + checkBuffer(defaultBufferSize) { + shareIn(it, 1) + } + + @Test // buffer is padded to default size as needed + fun testReplay10DefaultBuffer() = + checkBuffer(maxOf(10, defaultBufferSize)) { + shareIn(it, 10) + } + + @Test // buffer is padded to default size as needed + fun testReplay100DefaultBuffer() = + checkBuffer( maxOf(100, defaultBufferSize)) { + shareIn(it, 100) + } + + @Test + fun testDefaultBufferKeepsDefault() = + checkBuffer(defaultBufferSize) { + buffer().shareIn(it, 0) + } + + @Test + fun testOverrideDefaultBuffer0() = + checkBuffer(0) { + buffer(0).shareIn(it, 0) + } + + @Test + fun testOverrideDefaultBuffer10() = + checkBuffer(10) { + buffer(10).shareIn(it, 0) + } + + @Test // buffer and replay sizes add up + fun testBufferReplaySum() = + checkBuffer(41) { + buffer(10).buffer(20).shareIn(it, 11) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt new file mode 100644 index 0000000000..7e866fa664 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +/** + * Similar to [ShareInBufferTest] and [BufferConflationTest], + * but tests [shareIn] and its fusion with [conflate] operator. + */ +class ShareInConflationTest : TestBase() { + private val n = 100 + + private fun checkConflation( + bufferCapacity: Int, + onBufferOverflow: BufferOverflow = BufferOverflow.DROP_OLDEST, + op: suspend Flow.(CoroutineScope) -> Flow + ) = runTest { + expect(1) + // emit all and conflate, then should collect bufferCapacity latest ones + val done = Job() + flow { + repeat(n) { i -> + expect(i + 2) + emit(i) + } + done.join() // wait until done collection + emit(-1) // signal flow completion + } + .op(this) + .takeWhile { i -> i >= 0 } + .collect { i -> + val first = if (onBufferOverflow == BufferOverflow.DROP_LATEST) 0 else n - bufferCapacity + val last = first + bufferCapacity - 1 + if (i in first..last) { + expect(n + i - first + 2) + if (i == last) done.complete() // received the last one + } else { + error("Unexpected $i") + } + } + finish(n + bufferCapacity + 2) + } + + @Test + fun testConflateReplay1() = + checkConflation(1) { + conflate().shareIn(it, 1) + } + + @Test // still looks like conflating the last value for the first subscriber (will not replay to others though) + fun testConflateReplay0() = + checkConflation(1) { + conflate().shareIn(it, 0) + } + + @Test + fun testConflateReplay5() = + checkConflation(5) { + conflate().shareIn(it, 5) + } + + @Test + fun testBufferDropOldestReplay1() = + checkConflation(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1) + } + + @Test + fun testBufferDropOldestReplay0() = + checkConflation(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0) + } + + @Test + fun testBufferDropOldestReplay10() = + checkConflation(10) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 10) + } + + @Test + fun testBuffer20DropOldestReplay0() = + checkConflation(20) { + buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0) + } + + @Test + fun testBuffer7DropOldestReplay11() = + checkConflation(18) { + buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 11) + } + + @Test // a preceding buffer() gets overridden by conflate() + fun testBufferConflateOverride() = + checkConflation(1) { + buffer(23).conflate().shareIn(it, 1) + } + + @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) + fun testBufferDropOldestOverride() = + checkConflation(1) { + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1) + } + + @Test + fun testBufferDropLatestReplay0() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + } + + @Test + fun testBufferDropLatestReplay1() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1) + } + + @Test + fun testBufferDropLatestReplay10() = + checkConflation(10, BufferOverflow.DROP_LATEST) { + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10) + } + + @Test + fun testBuffer0DropLatestReplay0() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + } + + @Test + fun testBuffer0DropLatestReplay1() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1) + } + + @Test + fun testBuffer0DropLatestReplay10() = + checkConflation(10, BufferOverflow.DROP_LATEST) { + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10) + } + + @Test + fun testBuffer5DropLatestReplay0() = + checkConflation(5, BufferOverflow.DROP_LATEST) { + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + } + + @Test + fun testBuffer5DropLatestReplay10() = + checkConflation(15, BufferOverflow.DROP_LATEST) { + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10) + } + + @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) + fun testBufferDropLatestOverride() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt new file mode 100644 index 0000000000..d691f6f432 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class ShareInFusionTest : TestBase() { + /** + * Test perfect fusion for operators **after** [shareIn]. + */ + @Test + fun testOperatorFusion() = runTest { + val sh = emptyFlow().shareIn(this, 0) + assertTrue(sh !is MutableSharedFlow<*>) // cannot be cast to mutable shared flow!!! + assertSame(sh, (sh as Flow<*>).cancellable()) + assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) + coroutineContext.cancelChildren() + } + + @Test + fun testFlowOnContextFusion() = runTest { + val flow = flow { + assertEquals("FlowCtx", currentCoroutineContext()[CoroutineName]?.name) + emit("OK") + }.flowOn(CoroutineName("FlowCtx")) + assertEquals("OK", flow.shareIn(this, 1).first()) + coroutineContext.cancelChildren() + } + + /** + * Tests that `channelFlow { ... }.buffer(x)` works according to the [channelFlow] docs, and subsequent + * application of [shareIn] does not leak upstream. + */ + @Test + fun testChannelFlowBufferShareIn() = runTest { + expect(1) + val flow = channelFlow { + // send a batch of 10 elements using [offer] + for (i in 1..10) { + assertTrue(offer(i)) // offer must succeed, because buffer + } + send(0) // done + }.buffer(10) // request a buffer of 10 + // ^^^^^^^^^ buffer stays here + val shared = flow.shareIn(this, 0) + shared + .takeWhile { it > 0 } + .collect { i -> expect(i + 1) } + finish(12) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt new file mode 100644 index 0000000000..f0673038a8 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class ShareInTest : TestBase() { + @Test + fun testReplay0Eager() = runTest { + expect(1) + val flow = flowOf("OK") + val shared = flow.shareIn(this, 0) + yield() // actually start sharing + // all subscribers miss "OK" + val jobs = List(10) { + shared.onEach { expectUnreached() }.launchIn(this) + } + yield() // ensure nothing is collected + jobs.forEach { it.cancel() } + finish(2) + } + + @Test + fun testReplay0Lazy() = testReplayZeroOrOne(0) + + @Test + fun testReplay1Lazy() = testReplayZeroOrOne(1) + + private fun testReplayZeroOrOne(replay: Int) = runTest { + expect(1) + val doneBarrier = Job() + val flow = flow { + expect(2) + emit("OK") + doneBarrier.join() + emit("DONE") + } + val sharingJob = Job() + val shared = flow.shareIn(this + sharingJob, replay, started = SharingStarted.Lazily) + yield() // should not start sharing + // first subscriber gets "OK", other subscribers miss "OK" + val n = 10 + val replayOfs = replay * (n - 1) + val subscriberJobs = List(n) { index -> + val subscribedBarrier = Job() + val job = shared + .onSubscription { + subscribedBarrier.complete() + } + .onEach { value -> + when (value) { + "OK" -> { + expect(3 + index) + if (replay == 0) { // only the first subscriber collects "OK" without replay + assertEquals(0, index) + } + } + "DONE" -> { + expect(4 + index + replayOfs) + } + else -> expectUnreached() + } + } + .takeWhile { it != "DONE" } + .launchIn(this) + subscribedBarrier.join() // wait until the launched job subscribed before launching the next one + job + } + doneBarrier.complete() + subscriberJobs.joinAll() + expect(4 + n + replayOfs) + sharingJob.cancel() + finish(5 + n + replayOfs) + } + + @Test + fun testUpstreamCompleted() = + testUpstreamCompletedOrFailed(failed = false) + + @Test + fun testUpstreamFailed() = + testUpstreamCompletedOrFailed(failed = true) + + private fun testUpstreamCompletedOrFailed(failed: Boolean) = runTest { + val emitted = Job() + val terminate = Job() + val sharingJob = CompletableDeferred() + val upstream = flow { + emit("OK") + emitted.complete() + terminate.join() + if (failed) throw TestException() + } + val shared = upstream.shareIn(this + sharingJob, 1) + assertEquals(emptyList(), shared.replayCache) + emitted.join() // should start sharing, emit & cache + assertEquals(listOf("OK"), shared.replayCache) + terminate.complete() + sharingJob.complete(Unit) + sharingJob.join() // should complete sharing + assertEquals(listOf("OK"), shared.replayCache) // cache is still there + if (failed) { + assertTrue(sharingJob.getCompletionExceptionOrNull() is TestException) + } else { + assertNull(sharingJob.getCompletionExceptionOrNull()) + } + } + + @Test + fun testWhileSubscribedBasic() = + testWhileSubscribed(1, SharingStarted.WhileSubscribed()) + + @Test + fun testWhileSubscribedCustomAtLeast1() = + testWhileSubscribed(1, SharingStarted.WhileSubscribedAtLeast(1)) + + @Test + fun testWhileSubscribedCustomAtLeast2() = + testWhileSubscribed(2, SharingStarted.WhileSubscribedAtLeast(2)) + + @OptIn(ExperimentalStdlibApi::class) + private fun testWhileSubscribed(threshold: Int, started: SharingStarted) = runTest { + expect(1) + val flowState = FlowState() + val n = 3 // max number of subscribers + val log = Channel(2 * n) + + suspend fun checkStartTransition(subscribers: Int) { + when (subscribers) { + in 0 until threshold -> assertFalse(flowState.started) + threshold -> { + flowState.awaitStart() // must eventually start the flow + for (i in 1..threshold) { + assertEquals("sub$i: OK", log.receive()) // threshold subs must receive the values + } + } + in threshold + 1..n -> assertTrue(flowState.started) + } + } + + suspend fun checkStopTransition(subscribers: Int) { + when (subscribers) { + in threshold + 1..n -> assertTrue(flowState.started) + threshold - 1 -> flowState.awaitStop() // upstream flow must be eventually stopped + in 0..threshold - 2 -> assertFalse(flowState.started) // should have stopped already + } + } + + val flow = flow { + flowState.track { + emit("OK") + delay(Long.MAX_VALUE) // await forever, will get cancelled + } + } + + val shared = flow.shareIn(this, 0, started = started) + repeat(5) { // repeat scenario a few times + yield() + assertFalse(flowState.started) // flow is not running even if we yield + // start 3 subscribers + val subs = ArrayList() + for (i in 1..n) { + subs += shared + .onEach { value -> // only the first threshold subscribers get the value + when (i) { + in 1..threshold -> log.offer("sub$i: $value") + else -> expectUnreached() + } + } + .onCompletion { log.offer("sub$i: completion") } + .launchIn(this) + checkStartTransition(i) + } + // now cancel all subscribers + for (i in 1..n) { + subs.removeFirst().cancel() // cancel subscriber + assertEquals("sub$i: completion", log.receive()) // subscriber shall shutdown + checkStopTransition(n - i) + } + } + coroutineContext.cancelChildren() // cancel sharing job + finish(2) + } + + private fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = + object : SharingStarted { + override fun command(subscriptionCount: StateFlow): Flow = + subscriptionCount + .map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } + } + + private class FlowState { + private val timeLimit = 10000L + private val _started = MutableStateFlow(false) + val started: Boolean get() = _started.value + fun start() = check(_started.compareAndSet(expect = false, update = true)) + fun stop() = check(_started.compareAndSet(expect = true, update = false)) + suspend fun awaitStart() = withTimeout(timeLimit) { _started.first { it } } + suspend fun awaitStop() = withTimeout(timeLimit) { _started.first { !it } } + } + + private suspend fun FlowState.track(block: suspend () -> Unit) { + start() + try { + block() + } finally { + stop() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt new file mode 100644 index 0000000000..f716389fb7 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt @@ -0,0 +1,331 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.coroutines.* +import kotlin.test.* + +/** + * This test suit for [SharedFlow] has a dense framework that allows to test complex + * suspend/resume scenarios while keeping the code readable. Each test here is for + * one specific [SharedFlow] configuration, testing all the various corner cases in its + * behavior. + */ +class SharedFlowScenarioTest : TestBase() { + @Test + fun testReplay1Extra2() = + testSharedFlow(MutableSharedFlow(1, 2)) { + // total buffer size == 3 + expectReplayOf() + emitRightNow(1); expectReplayOf(1) + emitRightNow(2); expectReplayOf(2) + emitRightNow(3); expectReplayOf(3) + emitRightNow(4); expectReplayOf(4) // no prob - no subscribers + val a = subscribe("a"); collect(a, 4) + emitRightNow(5); expectReplayOf(5) + emitRightNow(6); expectReplayOf(6) + emitRightNow(7); expectReplayOf(7) + // suspend/collect sequentially + val e8 = emitSuspends(8); collect(a, 5); emitResumes(e8); expectReplayOf(8) + val e9 = emitSuspends(9); collect(a, 6); emitResumes(e9); expectReplayOf(9) + // buffer full, but parallel emitters can still suspend (queue up) + val e10 = emitSuspends(10) + val e11 = emitSuspends(11) + val e12 = emitSuspends(12) + collect(a, 7); emitResumes(e10); expectReplayOf(10) // buffer 8, 9 | 10 + collect(a, 8); emitResumes(e11); expectReplayOf(11) // buffer 9, 10 | 11 + sharedFlow.resetReplayCache(); expectReplayOf() // 9, 10 11 | no replay + collect(a, 9); emitResumes(e12); expectReplayOf(12) + collect(a, 10, 11, 12); expectReplayOf(12) // buffer empty | 12 + emitRightNow(13); expectReplayOf(13) + emitRightNow(14); expectReplayOf(14) + emitRightNow(15); expectReplayOf(15) // buffer 13, 14 | 15 + val e16 = emitSuspends(16) + val e17 = emitSuspends(17) + val e18 = emitSuspends(18) + cancel(e17); expectReplayOf(15) // cancel in the middle of three emits; buffer 13, 14 | 15 + collect(a, 13); emitResumes(e16); expectReplayOf(16) // buffer 14, 15, | 16 + collect(a, 14); emitResumes(e18); expectReplayOf(18) // buffer 15, 16 | 18 + val e19 = emitSuspends(19) + val e20 = emitSuspends(20) + val e21 = emitSuspends(21) + cancel(e21); expectReplayOf(18) // cancel last emit; buffer 15, 16, 18 + collect(a, 15); emitResumes(e19); expectReplayOf(19) // buffer 16, 18 | 19 + collect(a, 16); emitResumes(e20); expectReplayOf(20) // buffer 18, 19 | 20 + collect(a, 18, 19, 20); expectReplayOf(20) // buffer empty | 20 + emitRightNow(22); expectReplayOf(22) + emitRightNow(23); expectReplayOf(23) + emitRightNow(24); expectReplayOf(24) // buffer 22, 23 | 24 + val e25 = emitSuspends(25) + val e26 = emitSuspends(26) + val e27 = emitSuspends(27) + cancel(e25); expectReplayOf(24) // cancel first emit, buffer 22, 23 | 24 + sharedFlow.resetReplayCache(); expectReplayOf() // buffer 22, 23, 24 | no replay + val b = subscribe("b") // new subscriber + collect(a, 22); emitResumes(e26); expectReplayOf(26) // buffer 23, 24 | 26 + collect(b, 26) + collect(a, 23); emitResumes(e27); expectReplayOf(27) // buffer 24, 26 | 27 + collect(a, 24, 26, 27) // buffer empty | 27 + emitRightNow(28); expectReplayOf(28) + emitRightNow(29); expectReplayOf(29) // buffer 27, 28 | 29 + collect(a, 28, 29) // but b is slow + val e30 = emitSuspends(30) + val e31 = emitSuspends(31) + val e32 = emitSuspends(32) + val e33 = emitSuspends(33) + val e34 = emitSuspends(34) + val e35 = emitSuspends(35) + val e36 = emitSuspends(36) + val e37 = emitSuspends(37) + val e38 = emitSuspends(38) + val e39 = emitSuspends(39) + cancel(e31) // cancel emitter in queue + cancel(b) // cancel slow subscriber -> 3 emitters resume + emitResumes(e30); emitResumes(e32); emitResumes(e33); expectReplayOf(33) // buffer 30, 32 | 33 + val c = subscribe("c"); collect(c, 33) // replays + cancel(e34) + collect(a, 30); emitResumes(e35); expectReplayOf(35) // buffer 32, 33 | 35 + cancel(e37) + cancel(a); emitResumes(e36); emitResumes(e38); expectReplayOf(38) // buffer 35, 36 | 38 + collect(c, 35); emitResumes(e39); expectReplayOf(39) // buffer 36, 38 | 39 + collect(c, 36, 38, 39); expectReplayOf(39) + cancel(c); expectReplayOf(39) // replay stays + } + + @Test + fun testReplay1() = + testSharedFlow(MutableSharedFlow(1)) { + emitRightNow(0); expectReplayOf(0) + emitRightNow(1); expectReplayOf(1) + emitRightNow(2); expectReplayOf(2) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() + emitRightNow(3); expectReplayOf(3) + emitRightNow(4); expectReplayOf(4) + val a = subscribe("a"); collect(a, 4) + emitRightNow(5); expectReplayOf(5); collect(a, 5) + emitRightNow(6) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() + val e7 = emitSuspends(7) + val e8 = emitSuspends(8) + val e9 = emitSuspends(9) + collect(a, 6); emitResumes(e7); expectReplayOf(7) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() // buffer 7 | -- no replay, but still buffered + val b = subscribe("b") + collect(a, 7); emitResumes(e8); expectReplayOf(8) + collect(b, 8) // buffer | 8 -- a is slow + val e10 = emitSuspends(10) + val e11 = emitSuspends(11) + val e12 = emitSuspends(12) + cancel(e9) + collect(a, 8); emitResumes(e10); expectReplayOf(10) + collect(a, 10) // now b's slow + cancel(e11) + collect(b, 10); emitResumes(e12); expectReplayOf(12) + collect(a, 12) + collect(b, 12) + sharedFlow.resetReplayCache(); expectReplayOf() + sharedFlow.resetReplayCache(); expectReplayOf() // nothing is buffered -- both collectors up to date + emitRightNow(13); expectReplayOf(13) + collect(b, 13) // a is slow + val e14 = emitSuspends(14) + val e15 = emitSuspends(15) + val e16 = emitSuspends(16) + cancel(e14) + cancel(a); emitResumes(e15); expectReplayOf(15) // cancelling slow subscriber + collect(b, 15); emitResumes(e16); expectReplayOf(16) + collect(b, 16) + } + + @Test + fun testReplay2Extra2DropOldest() = + testSharedFlow(MutableSharedFlow(2, 2, BufferOverflow.DROP_OLDEST)) { + emitRightNow(0); expectReplayOf(0) + emitRightNow(1); expectReplayOf(0, 1) + emitRightNow(2); expectReplayOf(1, 2) + emitRightNow(3); expectReplayOf(2, 3) + emitRightNow(4); expectReplayOf(3, 4) + val a = subscribe("a") + collect(a, 3) + emitRightNow(5); expectReplayOf(4, 5) + emitRightNow(6); expectReplayOf(5, 6) + emitRightNow(7); expectReplayOf(6, 7) // buffer 4, 5 | 6, 7 + emitRightNow(8); expectReplayOf(7, 8) // buffer 5, 6 | 7, 8 + emitRightNow(9); expectReplayOf(8, 9) // buffer 6, 7 | 8, 9 + collect(a, 6, 7) + val b = subscribe("b") + collect(b, 8, 9) // buffer | 8, 9 + emitRightNow(10); expectReplayOf(9, 10) // buffer 8 | 9, 10 + collect(a, 8, 9, 10) // buffer | 9, 10, note "b" had not collected 10 yet + emitRightNow(11); expectReplayOf(10, 11) // buffer | 10, 11 + emitRightNow(12); expectReplayOf(11, 12) // buffer 10 | 11, 12 + emitRightNow(13); expectReplayOf(12, 13) // buffer 10, 11 | 12, 13 + emitRightNow(14); expectReplayOf(13, 14) // buffer 11, 12 | 13, 14, "b" missed 10 + collect(b, 11, 12, 13, 14) + sharedFlow.resetReplayCache(); expectReplayOf() // buffer 11, 12, 13, 14 | + sharedFlow.resetReplayCache(); expectReplayOf() + collect(a, 11, 12, 13, 14) + emitRightNow(15); expectReplayOf(15) + collect(a, 15) + collect(b, 15) + } + + private fun testSharedFlow( + sharedFlow: MutableSharedFlow, + scenario: suspend ScenarioDsl.() -> Unit + ) = runTest { + var dsl: ScenarioDsl? = null + try { + coroutineScope { + dsl = ScenarioDsl(sharedFlow, coroutineContext) + dsl!!.scenario() + dsl!!.stop() + } + } catch (e: Throwable) { + dsl?.printLog() + throw e + } + } + + private data class TestJob(val job: Job, val name: String) { + override fun toString(): String = name + } + + private open class Action + private data class EmitResumes(val job: TestJob) : Action() + private data class Collected(val job: TestJob, val value: Any?) : Action() + private data class ResumeCollecting(val job: TestJob) : Action() + private data class Cancelled(val job: TestJob) : Action() + + @OptIn(ExperimentalStdlibApi::class) + private class ScenarioDsl( + val sharedFlow: MutableSharedFlow, + coroutineContext: CoroutineContext + ) { + private val log = ArrayList() + private val timeout = 10000L + private val scope = CoroutineScope(coroutineContext + Job()) + private val actions = HashSet() + private val actionWaiters = ArrayDeque>() + private var expectedReplay = emptyList() + + private fun checkReplay() { + assertEquals(expectedReplay, sharedFlow.replayCache) + } + + private fun wakeupWaiters() { + repeat(actionWaiters.size) { + actionWaiters.removeFirst().resume(Unit) + } + } + + private fun addAction(action: Action) { + actions.add(action) + wakeupWaiters() + } + + private suspend fun awaitAction(action: Action) { + withTimeoutOrNull(timeout) { + while (!actions.remove(action)) { + suspendCancellableCoroutine { actionWaiters.add(it) } + } + } ?: error("Timed out waiting for action: $action") + wakeupWaiters() + } + + private fun launchEmit(a: T): TestJob { + val name = "emit($a)" + val job = scope.launch(start = CoroutineStart.UNDISPATCHED) { + val job = TestJob(coroutineContext[Job]!!, name) + try { + log(name) + sharedFlow.emit(a) + log("$name resumes") + addAction(EmitResumes(job)) + } catch(e: CancellationException) { + log("$name cancelled") + addAction(Cancelled(job)) + } + } + return TestJob(job, name) + } + + fun expectReplayOf(vararg a: T) { + expectedReplay = a.toList() + checkReplay() + } + + fun emitRightNow(a: T) { + val job = launchEmit(a) + assertTrue(actions.remove(EmitResumes(job))) + } + + fun emitSuspends(a: T): TestJob { + val job = launchEmit(a) + assertFalse(EmitResumes(job) in actions) + checkReplay() + return job + } + + suspend fun emitResumes(job: TestJob) { + awaitAction(EmitResumes(job)) + } + + suspend fun cancel(job: TestJob) { + log("cancel(${job.name})") + job.job.cancel() + awaitAction(Cancelled(job)) + } + + fun subscribe(id: String): TestJob { + val name = "collect($id)" + val job = scope.launch(start = CoroutineStart.UNDISPATCHED) { + val job = TestJob(coroutineContext[Job]!!, name) + try { + awaitAction(ResumeCollecting(job)) + log("$name start") + sharedFlow.collect { value -> + log("$name -> $value") + addAction(Collected(job, value)) + awaitAction(ResumeCollecting(job)) + log("$name -> $value resumes") + } + error("$name completed") + } catch(e: CancellationException) { + log("$name cancelled") + addAction(Cancelled(job)) + } + } + return TestJob(job, name) + } + + suspend fun collect(job: TestJob, vararg a: T) { + for (value in a) { + checkReplay() // should not have changed + addAction(ResumeCollecting(job)) + awaitAction(Collected(job, value)) + } + } + + fun stop() { + log("--- stop") + scope.cancel() + } + + private fun log(text: String) { + log.add(text) + } + + fun printLog() { + println("--- The most recent log entries ---") + log.takeLast(30).forEach(::println) + println("--- That's it ---") + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt new file mode 100644 index 0000000000..69b8df66f4 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt @@ -0,0 +1,798 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.random.* +import kotlin.test.* + +/** + * This test suite contains some basic tests for [SharedFlow]. There are some scenarios here written + * using [expect] and they are not very readable. See [SharedFlowScenarioTest] for a better + * behavioral test-suit. + */ +class SharedFlowTest : TestBase() { + @Test + fun testRendezvousSharedFlowBasic() = runTest { + expect(1) + val sh = MutableSharedFlow(0) + assertTrue(sh.replayCache.isEmpty()) + assertEquals(0, sh.subscriptionCount.value) + sh.emit(1) // no suspend + assertTrue(sh.replayCache.isEmpty()) + assertEquals(0, sh.subscriptionCount.value) + expect(2) + // one collector + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(3) + sh.collect { + when(it) { + 4 -> expect(5) + 6 -> expect(7) + 10 -> expect(11) + 13 -> expect(14) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(4) + assertEquals(1, sh.subscriptionCount.value) + sh.emit(4) + assertTrue(sh.replayCache.isEmpty()) + expect(6) + sh.emit(6) + expect(8) + // one more collector + val job2 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(9) + sh.collect { + when(it) { + 10 -> expect(12) + 13 -> expect(15) + 17 -> expect(18) + null -> expect(20) + 21 -> expect(22) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(10) + assertEquals(2, sh.subscriptionCount.value) + sh.emit(10) // to both collectors now! + assertTrue(sh.replayCache.isEmpty()) + expect(13) + sh.emit(13) + expect(16) + job1.cancel() // cancel the first collector + yield() + assertEquals(1, sh.subscriptionCount.value) + expect(17) + sh.emit(17) // only to second collector + expect(19) + sh.emit(null) // emit null to the second collector + expect(21) + sh.emit(21) // non-null again + expect(23) + job2.cancel() // cancel the second collector + yield() + assertEquals(0, sh.subscriptionCount.value) + expect(24) + sh.emit(24) // does not go anywhere + assertEquals(0, sh.subscriptionCount.value) + assertTrue(sh.replayCache.isEmpty()) + finish(25) + } + + @Test + fun testRendezvousSharedFlowReset() = runTest { + expect(1) + val sh = MutableSharedFlow(0) + val barrier = Channel(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.collect { + when (it) { + 3 -> { + expect(4) + barrier.receive() // hold on before collecting next one + } + 6 -> expect(10) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(3) + sh.emit(3) // rendezvous + expect(5) + assertFalse(sh.tryEmit(5)) // collector is not ready now + launch(start = CoroutineStart.UNDISPATCHED) { + expect(6) + sh.emit(6) // suspends + expect(12) + } + expect(7) + yield() // no wakeup -> all suspended + expect(8) + // now reset cache -> nothing happens, there is no cache + sh.resetReplayCache() + yield() + expect(9) + // now resume collector + barrier.send(Unit) + yield() // to collector + expect(11) + yield() // to emitter + expect(13) + assertFalse(sh.tryEmit(13)) // rendezvous does not work this way + job.cancel() + finish(14) + } + + @Test + fun testReplay1SharedFlowBasic() = runTest { + expect(1) + val sh = MutableSharedFlow(1) + assertTrue(sh.replayCache.isEmpty()) + assertEquals(0, sh.subscriptionCount.value) + sh.emit(1) // no suspend + assertEquals(listOf(1), sh.replayCache) + assertEquals(0, sh.subscriptionCount.value) + expect(2) + sh.emit(2) // no suspend + assertEquals(listOf(2), sh.replayCache) + expect(3) + // one collector + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(4) + sh.collect { + when(it) { + 2 -> expect(5) // got it immediately from replay cache + 6 -> expect(8) + null -> expect(14) + 17 -> expect(18) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(6) + assertEquals(1, sh.subscriptionCount.value) + sh.emit(6) // does not suspend, but buffers + assertEquals(listOf(6), sh.replayCache) + expect(7) + yield() + expect(9) + // one more collector + val job2 = launch(start = CoroutineStart.UNDISPATCHED) { + expect(10) + sh.collect { + when(it) { + 6 -> expect(11) // from replay cache + null -> expect(15) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(12) + assertEquals(2, sh.subscriptionCount.value) + sh.emit(null) + expect(13) + assertEquals(listOf(null), sh.replayCache) + yield() + assertEquals(listOf(null), sh.replayCache) + expect(16) + job2.cancel() + yield() + assertEquals(1, sh.subscriptionCount.value) + expect(17) + sh.emit(17) + assertEquals(listOf(17), sh.replayCache) + yield() + expect(19) + job1.cancel() + yield() + assertEquals(0, sh.subscriptionCount.value) + assertEquals(listOf(17), sh.replayCache) + finish(20) + } + + @Test + fun testReplay1() = runTest { + expect(1) + val sh = MutableSharedFlow(1) + assertEquals(listOf(), sh.replayCache) + val barrier = Channel(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.collect { + when (it) { + 3 -> { + expect(4) + barrier.receive() // collector waits + } + 5 -> expect(10) + 6 -> expect(11) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(3) + assertTrue(sh.tryEmit(3)) // buffered + assertEquals(listOf(3), sh.replayCache) + yield() // to collector + expect(5) + assertTrue(sh.tryEmit(5)) // buffered + assertEquals(listOf(5), sh.replayCache) + launch(start = CoroutineStart.UNDISPATCHED) { + expect(6) + sh.emit(6) // buffer full, suspended + expect(13) + } + expect(7) + assertEquals(listOf(5), sh.replayCache) + sh.resetReplayCache() // clear cache + assertEquals(listOf(), sh.replayCache) + expect(8) + yield() // emitter still suspended + expect(9) + assertEquals(listOf(), sh.replayCache) + assertFalse(sh.tryEmit(10)) // still no buffer space + assertEquals(listOf(), sh.replayCache) + barrier.send(Unit) // resume collector + yield() // to collector + expect(12) + yield() // to emitter, that should have resumed + expect(14) + job.cancel() + assertEquals(listOf(6), sh.replayCache) + finish(15) + } + + @Test + fun testReplay2Extra1() = runTest { + expect(1) + val sh = MutableSharedFlow( + replay = 2, + extraBufferCapacity = 1 + ) + assertEquals(listOf(), sh.replayCache) + assertTrue(sh.tryEmit(0)) + assertEquals(listOf(0), sh.replayCache) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + var cnt = 0 + sh.collect { + when (it) { + 0 -> when (cnt++) { + 0 -> expect(3) + 1 -> expect(14) + else -> expectUnreached() + } + 1 -> expect(6) + 2 -> expect(7) + 3 -> expect(8) + 4 -> expect(12) + 5 -> expect(13) + 16 -> expect(17) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(4) + assertTrue(sh.tryEmit(1)) // buffered + assertEquals(listOf(0, 1), sh.replayCache) + assertTrue(sh.tryEmit(2)) // buffered + assertEquals(listOf(1, 2), sh.replayCache) + assertTrue(sh.tryEmit(3)) // buffered (buffer size is 3) + assertEquals(listOf(2, 3), sh.replayCache) + expect(5) + yield() // to collector + expect(9) + assertEquals(listOf(2, 3), sh.replayCache) + assertTrue(sh.tryEmit(4)) // can buffer now + assertEquals(listOf(3, 4), sh.replayCache) + assertTrue(sh.tryEmit(5)) // can buffer now + assertEquals(listOf(4, 5), sh.replayCache) + assertTrue(sh.tryEmit(0)) // can buffer one more, let it be zero again + assertEquals(listOf(5, 0), sh.replayCache) + expect(10) + assertFalse(sh.tryEmit(10)) // cannot buffer anymore! + sh.resetReplayCache() // replay cache + assertEquals(listOf(), sh.replayCache) // empty + assertFalse(sh.tryEmit(0)) // still cannot buffer anymore (reset does not help) + assertEquals(listOf(), sh.replayCache) // empty + expect(11) + yield() // resume collector, will get next values + expect(15) + sh.resetReplayCache() // reset again, nothing happens + assertEquals(listOf(), sh.replayCache) // empty + yield() // collector gets nothing -- no change + expect(16) + assertTrue(sh.tryEmit(16)) + assertEquals(listOf(16), sh.replayCache) + yield() // gets it + expect(18) + job.cancel() + finish(19) + } + + @Test + fun testBufferNoReplayCancelWhileBuffering() = runTest { + val n = 123 + val sh = MutableSharedFlow(replay = 0, extraBufferCapacity = n) + repeat(3) { + val m = n / 2 // collect half, then suspend + val barrier = Channel(1) + val collectorJob = sh + .onSubscription { + barrier.send(1) + } + .onEach { value -> + if (value == m) { + barrier.send(2) + delay(Long.MAX_VALUE) + } + } + .launchIn(this) + assertEquals(1, barrier.receive()) // make sure it subscribes + launch(start = CoroutineStart.UNDISPATCHED) { + for (i in 0 until n + m) sh.emit(i) // these emits should go Ok + barrier.send(3) + sh.emit(n + 4) // this emit will suspend on buffer overflow + barrier.send(4) + } + assertEquals(2, barrier.receive()) // wait until m collected + assertEquals(3, barrier.receive()) // wait until all are emitted + collectorJob.cancel() // cancelling collector job must clear buffer and resume emitter + assertEquals(4, barrier.receive()) // verify that emitter resumes + } + } + + @Test + fun testRepeatedResetWithReplay() = runTest { + val n = 10 + val sh = MutableSharedFlow(n) + var i = 0 + repeat(3) { + // collector is slow + val collector = sh.onEach { delay(Long.MAX_VALUE) }.launchIn(this) + val emitter = launch { + repeat(3 * n) { sh.emit(i); i++ } + } + repeat(3) { yield() } // enough to run it to suspension + assertEquals((i - n until i).toList(), sh.replayCache) + sh.resetReplayCache() + assertEquals(emptyList(), sh.replayCache) + repeat(3) { yield() } // enough to run it to suspension + assertEquals(emptyList(), sh.replayCache) // still blocked + collector.cancel() + emitter.cancel() + repeat(3) { yield() } // enough to run it to suspension + } + } + + @Test + fun testSynchronousSharedFlowEmitterCancel() = runTest { + expect(1) + val sh = MutableSharedFlow(0) + val barrier1 = Job() + val barrier2 = Job() + val barrier3 = Job() + val collector1 = sh.onEach { + when (it) { + 1 -> expect(3) + 2 -> { + expect(6) + barrier2.complete() + } + 3 -> { + expect(9) + barrier3.complete() + } + else -> expectUnreached() + } + }.launchIn(this) + val collector2 = sh.onEach { + when (it) { + 1 -> { + expect(4) + barrier1.complete() + delay(Long.MAX_VALUE) + } + else -> expectUnreached() + } + }.launchIn(this) + repeat(2) { yield() } // launch both subscribers + val emitter = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.emit(1) + barrier1.join() + expect(5) + sh.emit(2) // suspends because of slow collector2 + expectUnreached() // will be cancelled + } + barrier2.join() // wait + expect(7) + // Now cancel the emitter! + emitter.cancel() + yield() + // Cancel slow collector + collector2.cancel() + yield() + // emit to fast collector1 + expect(8) + sh.emit(3) + barrier3.join() + expect(10) + // cancel it, too + collector1.cancel() + finish(11) + } + + @Test + fun testDifferentBufferedFlowCapacities() { + for (replay in 0..10) { + for (extraBufferCapacity in 0..5) { + if (replay == 0 && extraBufferCapacity == 0) continue // test only buffered shared flows + try { + val sh = MutableSharedFlow(replay, extraBufferCapacity) + // repeat the whole test a few times to make sure it works correctly when slots are reused + repeat(3) { + testBufferedFlow(sh, replay) + } + } catch (e: Throwable) { + error("Failed for replay=$replay, extraBufferCapacity=$extraBufferCapacity", e) + } + } + } + } + + private fun testBufferedFlow(sh: MutableSharedFlow, replay: Int) = runTest { + reset() + expect(1) + val n = 100 // initially emitted to fill buffer + for (i in 1..n) assertTrue(sh.tryEmit(i)) + // initial expected replayCache + val rcStart = n - replay + 1 + val rcRange = rcStart..n + val rcSize = n - rcStart + 1 + assertEquals(rcRange.toList(), sh.replayCache) + // create collectors + val m = 10 // collectors created + var ofs = 0 + val k = 42 // emissions to collectors + val ecRange = n + 1..n + k + val jobs = List(m) { jobIndex -> + launch(start = CoroutineStart.UNDISPATCHED) { + sh.collect { i -> + when (i) { + in rcRange -> expect(2 + i - rcStart + jobIndex * rcSize) + in ecRange -> expect(2 + ofs + jobIndex) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + } + ofs = rcSize * m + 2 + expect(ofs) + // emit to all k times + for (p in ecRange) { + sh.emit(p) + expect(1 + ofs) // buffered, no suspend + yield() + ofs += 2 + m + expect(ofs) + } + assertEquals(ecRange.toList().takeLast(replay), sh.replayCache) + // cancel all collectors + jobs.forEach { it.cancel() } + yield() + // replay cache is still there + assertEquals(ecRange.toList().takeLast(replay), sh.replayCache) + finish(1 + ofs) + } + + @Test + fun testDropLatest() = testDropLatestOrOldest(BufferOverflow.DROP_LATEST) + + @Test + fun testDropOldest() = testDropLatestOrOldest(BufferOverflow.DROP_OLDEST) + + private fun testDropLatestOrOldest(bufferOverflow: BufferOverflow) = runTest { + reset() + expect(1) + val sh = MutableSharedFlow(1, onBufferOverflow = bufferOverflow) + sh.emit(1) + sh.emit(2) + // always keeps last w/o collectors + assertEquals(listOf(2), sh.replayCache) + assertEquals(0, sh.subscriptionCount.value) + // one collector + val valueAfterOverflow = when (bufferOverflow) { + BufferOverflow.DROP_OLDEST -> 5 + BufferOverflow.DROP_LATEST -> 4 + else -> error("not supported in this test: $bufferOverflow") + } + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + sh.collect { + when(it) { + 2 -> { // replayed + expect(3) + yield() // and suspends, busy waiting + } + valueAfterOverflow -> expect(7) + 8 -> expect(9) + else -> expectUnreached() + } + } + expectUnreached() // does not complete normally + } + expect(4) + assertEquals(1, sh.subscriptionCount.value) + assertEquals(listOf(2), sh.replayCache) + sh.emit(4) // buffering, collector is busy + assertEquals(listOf(4), sh.replayCache) + expect(5) + sh.emit(5) // Buffer overflow here, will not suspend + assertEquals(listOf(valueAfterOverflow), sh.replayCache) + expect(6) + yield() // to the job + expect(8) + sh.emit(8) // not busy now + assertEquals(listOf(8), sh.replayCache) // buffered + yield() // to process + expect(10) + job.cancel() // cancel the job + yield() + assertEquals(0, sh.subscriptionCount.value) + finish(11) + } + + @Test + public fun testOnSubscription() = runTest { + expect(1) + val sh = MutableSharedFlow(0) + fun share(s: String) { launch(start = CoroutineStart.UNDISPATCHED) { sh.emit(s) } } + sh + .onSubscription { + emit("collector->A") + share("share->A") + } + .onSubscription { + emit("collector->B") + share("share->B") + } + .onStart { + emit("collector->C") + share("share->C") // get's lost, no subscribers yet + } + .onStart { + emit("collector->D") + share("share->D") // get's lost, no subscribers yet + } + .onEach { + when (it) { + "collector->D" -> expect(2) + "collector->C" -> expect(3) + "collector->A" -> expect(4) + "collector->B" -> expect(5) + "share->A" -> expect(6) + "share->B" -> { + expect(7) + currentCoroutineContext().cancel() + } + else -> expectUnreached() + } + } + .launchIn(this) + .join() + finish(8) + } + + @Test + fun onSubscriptionThrows() = runTest { + expect(1) + val sh = MutableSharedFlow(1) + sh.tryEmit("OK") // buffer a string + assertEquals(listOf("OK"), sh.replayCache) + sh + .onSubscription { + expect(2) + throw TestException() + } + .catch { e -> + assertTrue(e is TestException) + expect(3) + } + .collect { + // onSubscription throw before replay is emitted, so no value is collected if it throws + expectUnreached() + } + assertEquals(0, sh.subscriptionCount.value) + finish(4) + } + + @Test + fun testBigReplayManySubscribers() = testManySubscribers(true) + + @Test + fun testBigBufferManySubscribers() = testManySubscribers(false) + + private fun testManySubscribers(replay: Boolean) = runTest { + val n = 100 + val rnd = Random(replay.hashCode()) + val sh = MutableSharedFlow( + replay = if (replay) n else 0, + extraBufferCapacity = if (replay) 0 else n + ) + val subs = ArrayList() + for (i in 1..n) { + sh.emit(i) + val subBarrier = Channel() + val subJob = SubJob() + subs += subJob + // will receive all starting from replay or from new emissions only + subJob.lastReceived = if (replay) 0 else i + subJob.job = sh + .onSubscription { + subBarrier.send(Unit) // signal subscribed + } + .onEach { value -> + assertEquals(subJob.lastReceived + 1, value) + subJob.lastReceived = value + } + .launchIn(this) + subBarrier.receive() // wait until subscribed + // must have also receive all from the replay buffer directly after being subscribed + assertEquals(subJob.lastReceived, i) + // 50% of time cancel one subscriber + if (i % 2 == 0) { + val victim = subs.removeAt(rnd.nextInt(subs.size)) + yield() // make sure victim processed all emissions + assertEquals(victim.lastReceived, i) + victim.job.cancel() + } + } + yield() // make sure the last emission is processed + for (subJob in subs) { + assertEquals(subJob.lastReceived, n) + subJob.job.cancel() + } + } + + private class SubJob { + lateinit var job: Job + var lastReceived = 0 + } + + @Test + fun testStateFlowModel() = runTest { + val stateFlow = MutableStateFlow(null) + val expect = modelLog(stateFlow) + val sharedFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + sharedFlow.tryEmit(null) // initial value + val actual = modelLog(sharedFlow) { distinctUntilChanged() } + for (i in 0 until minOf(expect.size, actual.size)) { + if (actual[i] != expect[i]) { + for (j in maxOf(0, i - 10)..i) println("Actual log item #$j: ${actual[j]}") + assertEquals(expect[i], actual[i], "Log item #$i") + } + } + assertEquals(expect.size, actual.size) + } + + private suspend fun modelLog( + sh: MutableSharedFlow, + op: Flow.() -> Flow = { this } + ): List = coroutineScope { + val rnd = Random(1) + val result = ArrayList() + val job = launch { + sh.op().collect { value -> + result.add("Collect: $value") + repeat(rnd.nextInt(0..2)) { + result.add("Collect: yield") + yield() + } + } + } + repeat(1000) { index -> + val value = if (rnd.nextBoolean()) null else rnd.nextData() + if (rnd.nextInt(20) == 0) { + result.add("resetReplayCache & emit: $value") + if (sh !is StateFlow<*>) sh.resetReplayCache() + assertTrue(sh.tryEmit(value)) + } else { + result.add("Emit: $value") + sh.emit(value) + } + repeat(rnd.nextInt(0..2)) { + result.add("Emit: yield") + yield() + } + } + result.add("main: cancel") + job.cancel() + result.add("main: yield") + yield() + result.add("main: join") + job.join() + result + } + + data class Data(val x: Int) + private val dataCache = (1..5).associateWith { Data(it) } + + // Note that we test proper null support here, too + private fun Random.nextData(): Data? { + val x = nextInt(0..5) + if (x == 0) return null + // randomly reuse ref or create a new instance + return if(nextBoolean()) dataCache[x] else Data(x) + } + + @Test + fun testOperatorFusion() { + val sh = MutableSharedFlow(0) + assertSame(sh, (sh as Flow<*>).cancellable()) + assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) + } + + @Test + fun testIllegalArgumentException() { + assertFailsWith { MutableSharedFlow(-1) } + assertFailsWith { MutableSharedFlow(0, extraBufferCapacity = -1) } + assertFailsWith { MutableSharedFlow(0, onBufferOverflow = BufferOverflow.DROP_LATEST) } + assertFailsWith { MutableSharedFlow(0, onBufferOverflow = BufferOverflow.DROP_OLDEST) } + } + + @Test + public fun testReplayCancellability() = testCancellability(fromReplay = true) + + @Test + public fun testEmitCancellability() = testCancellability(fromReplay = false) + + private fun testCancellability(fromReplay: Boolean) = runTest { + expect(1) + val sh = MutableSharedFlow(5) + fun emitTestData() { + for (i in 1..5) assertTrue(sh.tryEmit(i)) + } + if (fromReplay) emitTestData() // fill in replay first + var subscribed = true + val job = sh + .onSubscription { subscribed = true } + .onEach { i -> + when (i) { + 1 -> expect(2) + 2 -> expect(3) + 3 -> { + expect(4) + currentCoroutineContext().cancel() + } + else -> expectUnreached() // shall check for cancellation + } + } + .launchIn(this) + yield() + assertTrue(subscribed) // yielding in enough + if (!fromReplay) emitTestData() // emit after subscription + job.join() + finish(5) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt new file mode 100644 index 0000000000..496fb7f8ff --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.test.* + +/** + * Functional tests for [SharingStarted] using [withVirtualTime] and a DSL to describe + * testing scenarios and expected behavior for different implementations. + */ +class SharingStartedTest : TestBase() { + @Test + fun testEagerly() = + testSharingStarted(SharingStarted.Eagerly, SharingCommand.START) { + subscriptions(1) + rampUpAndDown() + subscriptions(0) + delay(100) + } + + @Test + fun testLazily() = + testSharingStarted(SharingStarted.Lazily) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + } + + @Test + fun testWhileSubscribed() = + testSharingStarted(SharingStarted.WhileSubscribed()) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP) + delay(100) + } + + @Test + fun testWhileSubscribedExpireImmediately() = + testSharingStarted(SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + delay(100) + } + + @Test + fun testWhileSubscribedWithTimeout() = + testSharingStarted(SharingStarted.WhileSubscribed(stopTimeoutMillis = 100)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + delay(50) // don't give it time to stop + subscriptions(1) // resubscribe again + rampUpAndDown() + subscriptions(0) + afterTime(100, SharingCommand.STOP) + delay(100) + } + + @Test + fun testWhileSubscribedExpiration() = + testSharingStarted(SharingStarted.WhileSubscribed(replayExpirationMillis = 200)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP) + delay(150) // don't give it time to reset cache + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0, SharingCommand.STOP) + afterTime(200, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + } + + @Test + fun testWhileSubscribedStopAndExpiration() = + testSharingStarted(SharingStarted.WhileSubscribed(stopTimeoutMillis = 400, replayExpirationMillis = 300)) { + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + delay(350) // don't give it time to stop + subscriptions(1) + rampUpAndDown() + subscriptions(0) + afterTime(400, SharingCommand.STOP) + delay(250) // don't give it time to reset cache + subscriptions(1, SharingCommand.START) + rampUpAndDown() + subscriptions(0) + afterTime(400, SharingCommand.STOP) + afterTime(300, SharingCommand.STOP_AND_RESET_REPLAY_CACHE) + delay(100) + } + + private suspend fun SharingStartedDsl.rampUpAndDown() { + for (i in 2..10) { + delay(100) + subscriptions(i) + } + delay(1000) + for (i in 9 downTo 1) { + subscriptions(i) + delay(100) + } + } + + private fun testSharingStarted( + started: SharingStarted, + initialCommand: SharingCommand? = null, + scenario: suspend SharingStartedDsl.() -> Unit + ) = withVirtualTime { + expect(1) + val dsl = SharingStartedDsl(started, initialCommand, coroutineContext) + dsl.launch() + // repeat every scenario 3 times + repeat(3) { + dsl.scenario() + delay(1000) + } + dsl.stop() + finish(2) + } + + private class SharingStartedDsl( + val started: SharingStarted, + initialCommand: SharingCommand?, + coroutineContext: CoroutineContext + ) { + val subscriptionCount = MutableStateFlow(0) + var previousCommand: SharingCommand? = null + var expectedCommand: SharingCommand? = initialCommand + var expectedTime = 0L + + val dispatcher = coroutineContext[ContinuationInterceptor] as VirtualTimeDispatcher + val scope = CoroutineScope(coroutineContext + Job()) + + suspend fun launch() { + started + .command(subscriptionCount.asStateFlow()) + .onEach { checkCommand(it) } + .launchIn(scope) + letItRun() + } + + fun checkCommand(command: SharingCommand) { + assertTrue(command != previousCommand) + previousCommand = command + assertEquals(expectedCommand, command) + assertEquals(expectedTime, dispatcher.currentTime) + } + + suspend fun subscriptions(count: Int, command: SharingCommand? = null) { + expectedTime = dispatcher.currentTime + subscriptionCount.value = count + if (command != null) { + afterTime(0, command) + } else { + letItRun() + } + } + + suspend fun afterTime(time: Long = 0, command: SharingCommand) { + expectedCommand = command + val remaining = (time - 1).coerceAtLeast(0) // previous letItRun delayed 1ms + expectedTime += remaining + delay(remaining) + letItRun() + } + + private suspend fun letItRun() { + delay(1) + assertEquals(expectedCommand, previousCommand) // make sure expected command was emitted + expectedTime++ // make one more time tick we've delayed + } + + fun stop() { + scope.cancel() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt new file mode 100644 index 0000000000..bcf626e3e3 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharingStartedWhileSubscribedTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlin.test.* +import kotlin.time.* + +class SharingStartedWhileSubscribedTest : TestBase() { + @Test // make sure equals works properly, or otherwise other tests don't make sense + fun testEqualsAndHashcode() { + val params = listOf(0L, 1L, 10L, 100L, 213L, Long.MAX_VALUE) + // HashMap will simultaneously test equals, hashcode and their consistency + val map = HashMap>() + for (i in params) { + for (j in params) { + map[SharingStarted.WhileSubscribed(i, j)] = i to j + } + } + for (i in params) { + for (j in params) { + assertEquals(i to j, map[SharingStarted.WhileSubscribed(i, j)]) + } + } + } + + @OptIn(ExperimentalTime::class) + @Test + fun testDurationParams() { + assertEquals(SharingStarted.WhileSubscribed(0), SharingStarted.WhileSubscribed(Duration.ZERO)) + assertEquals(SharingStarted.WhileSubscribed(10), SharingStarted.WhileSubscribed(10.milliseconds)) + assertEquals(SharingStarted.WhileSubscribed(1000), SharingStarted.WhileSubscribed(1.seconds)) + assertEquals(SharingStarted.WhileSubscribed(Long.MAX_VALUE), SharingStarted.WhileSubscribed(Duration.INFINITE)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 0), SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 3), SharingStarted.WhileSubscribed(replayExpiration = 3.milliseconds)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = 7000), SharingStarted.WhileSubscribed(replayExpiration = 7.seconds)) + assertEquals(SharingStarted.WhileSubscribed(replayExpirationMillis = Long.MAX_VALUE), SharingStarted.WhileSubscribed(replayExpiration = Duration.INFINITE)) + } +} + diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt new file mode 100644 index 0000000000..c61b7a0289 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +class StateFlowTest : TestBase() { + @Test + fun testNormalAndNull() = runTest { + expect(1) + val state = MutableStateFlow(0) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + assertFailsWith { + state.collect { value -> + when (value) { + 0 -> expect(3) + 1 -> expect(5) + null -> expect(8) + 2 -> expect(10) + else -> expectUnreached() + } + } + } + expect(12) + } + expect(4) // collector is waiting + state.value = 1 // fire in the hole! + assertEquals(1, state.value) + yield() + expect(6) + state.value = 1 // same value, nothing happens + yield() + expect(7) + state.value = null // null value + assertNull(state.value) + yield() + expect(9) + state.value = 2 // another value + assertEquals(2, state.value) + yield() + expect(11) + job.cancel() + yield() + finish(13) + } + + @Test + fun testEqualsConflation() = runTest { + expect(1) + val state = MutableStateFlow(Data(0)) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + assertFailsWith { + state.collect { value -> + when (value.i) { + 0 -> expect(3) // initial value + 2 -> expect(5) + 4 -> expect(7) + else -> error("Unexpected $value") + } + } + } + expect(9) + } + state.value = Data(1) // conflated + state.value = Data(0) // equals to last emitted + yield() // no repeat zero + state.value = Data(3) // conflated + state.value = Data(2) // delivered + expect(4) + yield() + state.value = Data(2) // equals to last one, dropped + yield() + state.value = Data(5) // conflated + state.value = Data(4) // delivered + expect(6) + yield() + expect(8) + job.cancel() + yield() + finish(10) + } + + data class Data(val i: Int) + + @Test + fun testDataModel() = runTest { + val s = CounterModel() + launch { + val sum = s.counter.take(11).sum() + assertEquals(55, sum) + } + repeat(10) { + yield() + s.inc() + } + } + + class CounterModel { + // private data flow + private val _counter = MutableStateFlow(0) + + // publicly exposed as a flow + val counter: StateFlow get() = _counter + + fun inc() { + _counter.value++ + } + } + + @Test + public fun testOnSubscriptionWithException() = runTest { + expect(1) + val state = MutableStateFlow("A") + state + .onSubscription { + emit("collector->A") + state.value = "A" + } + .onSubscription { + emit("collector->B") + state.value = "B" + throw TestException() + } + .onStart { + emit("collector->C") + state.value = "C" + } + .onStart { + emit("collector->D") + state.value = "D" + } + .onEach { + when (it) { + "collector->D" -> expect(2) + "collector->C" -> expect(3) + "collector->A" -> expect(4) + "collector->B" -> expect(5) + else -> expectUnreached() + } + } + .catch { e -> + assertTrue(e is TestException) + expect(6) + } + .launchIn(this) + .join() + assertEquals(0, state.subscriptionCount.value) + finish(7) + } + + @Test + fun testOperatorFusion() { + val state = MutableStateFlow(String) + assertSame(state, (state as Flow<*>).cancellable()) + assertSame(state, (state as Flow<*>).distinctUntilChanged()) + assertSame(state, (state as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(state, (state as Flow<*>).conflate()) + assertSame(state, state.buffer(Channel.CONFLATED)) + assertSame(state, state.buffer(Channel.RENDEZVOUS)) + } + + @Test + fun testCancellability() = runTest { + expect(1) + val state = MutableStateFlow(0) + var subscribed = true + val barrier = Channel() + val job = state + .onSubscription { subscribed = true } + .onEach { i -> + when (i) { + 0 -> expect(2) // initial value + 1 -> expect(3) + 2 -> { + expect(4) + currentCoroutineContext().cancel() + } + else -> expectUnreached() // shall check for cancellation + } + barrier.send(i) + } + .launchIn(this) + yield() + assertTrue(subscribed) // yielding in enough + assertEquals(0, barrier.receive()) // should get initial value, too + for (i in 1..3) { // emit after subscription + state.value = i + if (i < 3) assertEquals(i, barrier.receive()) // shall receive it + } + job.join() + finish(5) + } + + @Test + fun testResetUnsupported() { + val state = MutableStateFlow(42) + assertFailsWith { state.resetReplayCache() } + assertEquals(42, state.value) + assertEquals(listOf(42), state.replayCache) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt new file mode 100644 index 0000000000..10bf5feef3 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlin.test.* + +/** + * It is mostly covered by [ShareInTest], this just add state-specific checks. + */ +class StateInTest : TestBase() { + @Test + fun testOperatorFusion() = runTest { + val state = flowOf("OK").stateIn(this) + assertTrue(state !is MutableStateFlow<*>) // cannot be cast to mutable state flow + assertSame(state, (state as Flow<*>).cancellable()) + assertSame(state, (state as Flow<*>).distinctUntilChanged()) + assertSame(state, (state as Flow<*>).flowOn(Dispatchers.Default)) + assertSame(state, (state as Flow<*>).conflate()) + assertSame(state, state.buffer(Channel.CONFLATED)) + assertSame(state, state.buffer(Channel.RENDEZVOUS)) + assertSame(state, state.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)) + assertSame(state, state.buffer(0, onBufferOverflow = BufferOverflow.DROP_OLDEST)) + assertSame(state, state.buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST)) + coroutineContext.cancelChildren() + } + + @Test + fun testUpstreamCompletedNoInitialValue() = + testUpstreamCompletedOrFailedReset(failed = false, iv = false) + + @Test + fun testUpstreamFailedNoInitialValue() = + testUpstreamCompletedOrFailedReset(failed = true, iv = false) + + @Test + fun testUpstreamCompletedWithInitialValue() = + testUpstreamCompletedOrFailedReset(failed = false, iv = true) + + @Test + fun testUpstreamFailedWithInitialValue() = + testUpstreamCompletedOrFailedReset(failed = true, iv = true) + + private fun testUpstreamCompletedOrFailedReset(failed: Boolean, iv: Boolean) = runTest { + val emitted = Job() + val terminate = Job() + val sharingJob = CompletableDeferred() + val upstream = flow { + emit("OK") + emitted.complete() + terminate.join() + if (failed) throw TestException() + } + val scope = this + sharingJob + val shared: StateFlow + if (iv) { + shared = upstream.stateIn(scope, initialValue = null) + assertEquals(null, shared.value) + } else { + shared = upstream.stateIn(scope) + assertEquals("OK", shared.value) // waited until upstream emitted + } + emitted.join() // should start sharing, emit & cache + assertEquals("OK", shared.value) + terminate.complete() + sharingJob.complete(Unit) + sharingJob.join() // should complete sharing + assertEquals("OK", shared.value) // value is still there + if (failed) { + assertTrue(sharingJob.getCompletionExceptionOrNull() is TestException) + } else { + assertNull(sharingJob.getCompletionExceptionOrNull()) + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index bf462cc78f..c3cf7fa61f 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -107,7 +107,7 @@ public actual open class TestBase actual constructor() { * Asserts that this line is never executed. */ public actual fun expectUnreached() { - error("Should not be reached") + error("Should not be reached, current action index is ${actionIndex.get()}") } /** diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt new file mode 100644 index 0000000000..dc1cfa9c67 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import java.util.* +import java.util.concurrent.atomic.* +import kotlin.random.* +import kotlin.test.* +import kotlin.time.* +import kotlin.time.TimeSource + +@OptIn(ExperimentalTime::class) +class SharingStressTest : TestBase() { + private val testDuration = 1000L * stressTestMultiplier + private val nSubscribers = 5 + private val testStarted = TimeSource.Monotonic.markNow() + + @get:Rule + val emitterDispatcher = ExecutorRule(1) + + @get:Rule + val subscriberDispatcher = ExecutorRule(nSubscribers) + + @Test + public fun testNoReplayLazy() = + testStress(0, started = SharingStarted.Lazily) + + @Test + public fun testNoReplayWhileSubscribed() = + testStress(0, started = SharingStarted.WhileSubscribed()) + + @Test + public fun testNoReplayWhileSubscribedTimeout() = + testStress(0, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 50L)) + + @Test + public fun testReplay100WhileSubscribed() = + testStress(100, started = SharingStarted.WhileSubscribed()) + + @Test + public fun testReplay100WhileSubscribedReset() = + testStress(100, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0L)) + + @Test + public fun testReplay100WhileSubscribedTimeout() = + testStress(100, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 50L)) + + @Test + public fun testStateLazy() = + testStress(1, started = SharingStarted.Lazily) + + @Test + public fun testStateWhileSubscribed() = + testStress(1, started = SharingStarted.WhileSubscribed()) + + @Test + public fun testStateWhileSubscribedReset() = + testStress(1, started = SharingStarted.WhileSubscribed(replayExpirationMillis = 0L)) + + private fun testStress(replay: Int, started: SharingStarted) = runTest { + log("-- Stress with replay=$replay, started=$started") + val random = Random(1) + val emitIndex = AtomicLong() + val cancelledEmits = HashSet() + val missingCollects = Collections.synchronizedSet(LinkedHashSet()) + // at most one copy of upstream can be running at any time + val isRunning = AtomicInteger(0) + val upstream = flow { + assertEquals(0, isRunning.getAndIncrement()) + try { + while (true) { + val value = emitIndex.getAndIncrement() + try { + emit(value) + } catch (e: CancellationException) { + // emission was cancelled -> could be missing + cancelledEmits.add(value) + throw e + } + } + } finally { + assertEquals(1, isRunning.getAndDecrement()) + } + } + val subCount = MutableStateFlow(0) + val sharingJob = Job() + val sharingScope = this + emitterDispatcher + sharingJob + val usingStateFlow = replay == 1 + val sharedFlow = if (usingStateFlow) + upstream.stateIn(sharingScope, started, 0L) + else + upstream.shareIn(sharingScope, replay, started) + try { + val subscribers = ArrayList() + withTimeoutOrNull(testDuration) { + // start and stop subscribers + while (true) { + log("Staring $nSubscribers subscribers") + repeat(nSubscribers) { + subscribers += launchSubscriber(sharedFlow, usingStateFlow, subCount, missingCollects) + } + // wait until they all subscribed + subCount.first { it == nSubscribers } + // let them work a bit more & make sure emitter did not hang + val fromEmitIndex = emitIndex.get() + val waitEmitIndex = fromEmitIndex + 100 // wait until 100 emitted + withTimeout(10000) { // wait for at most 10s for something to be emitted + do { + delay(random.nextLong(50L..100L)) + } while (emitIndex.get() < waitEmitIndex) // Ok, enough was emitted, wait more if not + } + // Stop all subscribers and ensure they collected something + log("Stopping subscribers (emitted = ${emitIndex.get() - fromEmitIndex})") + subscribers.forEach { + it.job.cancelAndJoin() + assertTrue { it.count > 0 } // something must be collected too + } + subscribers.clear() + log("Intermission") + delay(random.nextLong(10L..100L)) // wait a bit before starting them again + } + } + if (!subscribers.isEmpty()) { + log("Stopping subscribers") + subscribers.forEach { it.job.cancelAndJoin() } + } + } finally { + log("--- Finally: Cancelling sharing job") + sharingJob.cancel() + } + sharingJob.join() // make sure sharing job did not hang + log("Emitter was cancelled ${cancelledEmits.size} times") + log("Collectors missed ${missingCollects.size} values") + for (value in missingCollects) { + assertTrue(value in cancelledEmits, "Value $value is missing for no apparent reason") + } + } + + private fun CoroutineScope.launchSubscriber( + sharedFlow: SharedFlow, + usingStateFlow: Boolean, + subCount: MutableStateFlow, + missingCollects: MutableSet + ): SubJob { + val subJob = SubJob() + subJob.job = launch(subscriberDispatcher) { + var last = -1L + sharedFlow + .onSubscription { + subCount.increment(1) + } + .onCompletion { + subCount.increment(-1) + } + .collect { j -> + subJob.count++ + // last must grow sequentially, no jumping or losses + if (last == -1L) { + last = j + } else { + val expected = last + 1 + if (usingStateFlow) + assertTrue(expected <= j) + else { + if (expected != j) { + if (j == expected + 1) { + // if missing just one -- could be race with cancelled emit + missingCollects.add(expected) + } else { + // broken otherwise + assertEquals(expected, j) + } + } + } + last = j + } + } + } + return subJob + } + + private class SubJob { + lateinit var job: Job + var count = 0L + } + + private fun log(msg: String) = println("${testStarted.elapsedNow().toLongMilliseconds()} ms: $msg") +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactive/README.md b/reactive/kotlinx-coroutines-reactive/README.md index 0a59b2c251..aed262263d 100644 --- a/reactive/kotlinx-coroutines-reactive/README.md +++ b/reactive/kotlinx-coroutines-reactive/README.md @@ -6,7 +6,7 @@ Coroutine builders: | **Name** | **Result** | **Scope** | **Description** | --------------- | ----------------------------- | ---------------- | --------------- -| [publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe +| [kotlinx.coroutines.reactive.publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe Integration with [Flow]: @@ -37,7 +37,7 @@ Suspending extension functions and suspending iteration: [ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html -[publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html +[kotlinx.coroutines.reactive.publish]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/publish.html [Publisher.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/as-flow.html [Flow.asPublisher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/kotlinx.coroutines.flow.-flow/as-publisher.html [org.reactivestreams.Publisher.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-first.html diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index a51f583b77..fcf524d672 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -47,10 +47,11 @@ public fun Flow.asPublisher(context: CoroutineContext = EmptyCorout 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) + capacity: Int = Channel.BUFFERED, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +) : ChannelFlow(context, capacity, onBufferOverflow) { + override fun create(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow): ChannelFlow = + PublisherAsFlow(publisher, context, capacity, onBufferOverflow) /* * Suppress for Channel.CHANNEL_DEFAULT_CAPACITY. @@ -59,13 +60,15 @@ private class PublisherAsFlow( */ @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) } - } + get() = + if (onBufferOverflow != BufferOverflow.SUSPEND) { + Long.MAX_VALUE // request all, since buffering strategy is to never suspend + } else when (capacity) { + 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 @@ -85,7 +88,7 @@ private class PublisherAsFlow( } private suspend fun collectImpl(injectContext: CoroutineContext, collector: FlowCollector) { - val subscriber = ReactiveSubscriber(capacity, requestSize) + val subscriber = ReactiveSubscriber(capacity, onBufferOverflow, requestSize) // inject subscribe context into publisher publisher.injectCoroutineContext(injectContext).subscribe(subscriber) try { @@ -112,10 +115,11 @@ private class PublisherAsFlow( @Suppress("SubscriberImplementation") private class ReactiveSubscriber( capacity: Int, + onBufferOverflow: BufferOverflow, private val requestSize: Long ) : Subscriber { private lateinit var subscription: Subscription - private val channel = Channel(capacity) + private val channel = Channel(capacity, onBufferOverflow) suspend fun takeNextOrNull(): T? = channel.receiveOrNull() diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index fb73cd26b9..ede5e118a8 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -310,7 +310,7 @@ processing the previous one. The [actor] coroutine builder accepts an optional controls the implementation of the channel that this actor is using for its mailbox. The description of all the available choices is given in documentation of the [`Channel()`][Channel] factory function. -Let us change the code to use `ConflatedChannel` by passing [Channel.CONFLATED] capacity value. The +Let us change the code to use a conflated channel by passing [Channel.CONFLATED] capacity value. The change is only to the line that creates an actor: ```kotlin From 3028679b1426512ca8f50d745b8cd3aa4ed5f213 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 9 Oct 2020 17:34:41 +0300 Subject: [PATCH 02/30] ~ Experimental resetReplayCache --- kotlinx-coroutines-core/common/src/flow/SharedFlow.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 21f00e4138..7ab0da70d4 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -179,7 +179,10 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { * On a [MutableStateFlow], which always contains a single value, this function is not * supported, and throws an [UnsupportedOperationException]. To reset a [MutableStateFlow] * to an initial value, just update its [value][MutableStateFlow.value]. + * + * **Note: This is an experimental api.** This function may be removed or renamed in the future. */ + @ExperimentalCoroutinesApi public fun resetReplayCache() } From 8c83d344e18dc203d22ca2f6bd2848ff9b7e8004 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 9 Oct 2020 17:48:02 +0300 Subject: [PATCH 03/30] ~ No default for shareIn/stateIn started parameter --- .../api/kotlinx-coroutines-core.api | 2 - .../common/src/flow/operators/Share.kt | 21 +++++----- .../test/flow/sharing/ShareInBufferTest.kt | 16 ++++---- .../flow/sharing/ShareInConflationTest.kt | 38 +++++++++---------- .../test/flow/sharing/ShareInFusionTest.kt | 10 ++--- .../common/test/flow/sharing/ShareInTest.kt | 5 ++- .../test/flow/sharing/SharedFlowTest.kt | 2 +- .../common/test/flow/sharing/StateInTest.kt | 2 +- 8 files changed, 46 insertions(+), 50 deletions(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 14c9b9a21a..f4bfbcbd55 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -1025,7 +1025,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun shareIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/flow/SharingStarted;)Lkotlinx/coroutines/flow/SharedFlow; - public static synthetic fun shareIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/flow/SharingStarted;ILjava/lang/Object;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun skip (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; @@ -1033,7 +1032,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun startWith (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun stateIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; - public static synthetic fun stateIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V public static final fun subscribe (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt index 0d41169344..d9b96a0198 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -21,13 +21,12 @@ import kotlin.jvm.* * and replaying a specified number of [replay] values to new subscribers. See the [SharedFlow] documentation * for the general concepts of shared flows. * - * The starting of the sharing coroutine is controlled by the [started] parameter. By default, the sharing coroutine is started - * [Eagerly][SharingStarted.Eagerly], so the upstream flow is started even before the first subscribers appear. Note - * that in this case all values emitted by the upstream beyond the most recent values as specified by - * [replay] parameter **will be immediately discarded**. - * - * Additional options for the [started] parameter are: + * The starting of the sharing coroutine is controlled by the [started] parameter. The following options + * are supported. * + * * [Eagerly][SharingStarted.Eagerly] — the upstream flow is started even before the first subscriber appears. Note + * that in this case all values emitted by the upstream beyond the most recent values as specified by + * [replay] parameter **will be immediately discarded**. * * [Lazily][SharingStarted.Lazily] — starts the upstream flow after the first subscriber appears, which guarantees * that this first subscriber gets all the emitted values, while subsequent subscribers are only guaranteed to * get the most recent [replay] values. The upstream flow continues to be active even when all subscribers @@ -131,14 +130,13 @@ import kotlin.jvm.* * * @param scope the coroutine scope in which sharing is started. * @param replay the number of values replayed to new subscribers (cannot be negative). - * @param started the strategy that controls when sharing is started and stopped - * (optional, default to [Eagerly][SharingStarted.Eagerly] starting the sharing without waiting for subscribers). + * @param started the strategy that controls when sharing is started and stopped. */ @ExperimentalCoroutinesApi public fun Flow.shareIn( scope: CoroutineScope, replay: Int, - started: SharingStarted = SharingStarted.Eagerly + started: SharingStarted ): SharedFlow { val config = configureSharing(replay) val shared = MutableSharedFlow( @@ -279,8 +277,7 @@ private fun CoroutineScope.launchSharing( * [distinctUntilChanged][Flow.distinctUntilChanged], or [cancellable] operators to a state flow has no effect. * * @param scope the coroutine scope in which sharing is started. - * @param started the strategy that controls when sharing is started and stopped - * (optional, default to [Eagerly][SharingStarted.Eagerly] starting the sharing without waiting for subscribers). + * @param started the strategy that controls when sharing is started and stopped. * @param initialValue the initial value of the state flow. * This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy * with the `replayExpirationMillis` parameter. @@ -288,7 +285,7 @@ private fun CoroutineScope.launchSharing( @ExperimentalCoroutinesApi public fun Flow.stateIn( scope: CoroutineScope, - started: SharingStarted = SharingStarted.Eagerly, + started: SharingStarted, initialValue: T ): StateFlow { val config = configureSharing(1) diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt index 2272918af0..426d0ba5ce 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt @@ -51,48 +51,48 @@ class ShareInBufferTest : TestBase() { @Test fun testReplay0DefaultBuffer() = checkBuffer(defaultBufferSize) { - shareIn(it, 0) + shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testReplay1DefaultBuffer() = checkBuffer(defaultBufferSize) { - shareIn(it, 1) + shareIn(it, 1, SharingStarted.Eagerly) } @Test // buffer is padded to default size as needed fun testReplay10DefaultBuffer() = checkBuffer(maxOf(10, defaultBufferSize)) { - shareIn(it, 10) + shareIn(it, 10, SharingStarted.Eagerly) } @Test // buffer is padded to default size as needed fun testReplay100DefaultBuffer() = checkBuffer( maxOf(100, defaultBufferSize)) { - shareIn(it, 100) + shareIn(it, 100, SharingStarted.Eagerly) } @Test fun testDefaultBufferKeepsDefault() = checkBuffer(defaultBufferSize) { - buffer().shareIn(it, 0) + buffer().shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testOverrideDefaultBuffer0() = checkBuffer(0) { - buffer(0).shareIn(it, 0) + buffer(0).shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testOverrideDefaultBuffer10() = checkBuffer(10) { - buffer(10).shareIn(it, 0) + buffer(10).shareIn(it, 0, SharingStarted.Eagerly) } @Test // buffer and replay sizes add up fun testBufferReplaySum() = checkBuffer(41) { - buffer(10).buffer(20).shareIn(it, 11) + buffer(10).buffer(20).shareIn(it, 11, SharingStarted.Eagerly) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt index 7e866fa664..612c8cded7 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt @@ -49,114 +49,114 @@ class ShareInConflationTest : TestBase() { @Test fun testConflateReplay1() = checkConflation(1) { - conflate().shareIn(it, 1) + conflate().shareIn(it, 1, SharingStarted.Eagerly) } @Test // still looks like conflating the last value for the first subscriber (will not replay to others though) fun testConflateReplay0() = checkConflation(1) { - conflate().shareIn(it, 0) + conflate().shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testConflateReplay5() = checkConflation(5) { - conflate().shareIn(it, 5) + conflate().shareIn(it, 5, SharingStarted.Eagerly) } @Test fun testBufferDropOldestReplay1() = checkConflation(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1) + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1, SharingStarted.Eagerly) } @Test fun testBufferDropOldestReplay0() = checkConflation(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0) + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testBufferDropOldestReplay10() = checkConflation(10) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 10) + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 10, SharingStarted.Eagerly) } @Test fun testBuffer20DropOldestReplay0() = checkConflation(20) { - buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0) + buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testBuffer7DropOldestReplay11() = checkConflation(18) { - buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 11) + buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 11, SharingStarted.Eagerly) } @Test // a preceding buffer() gets overridden by conflate() fun testBufferConflateOverride() = checkConflation(1) { - buffer(23).conflate().shareIn(it, 1) + buffer(23).conflate().shareIn(it, 1, SharingStarted.Eagerly) } @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) fun testBufferDropOldestOverride() = checkConflation(1) { - buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1) + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1, SharingStarted.Eagerly) } @Test fun testBufferDropLatestReplay0() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testBufferDropLatestReplay1() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1) + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1, SharingStarted.Eagerly) } @Test fun testBufferDropLatestReplay10() = checkConflation(10, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10) + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10, SharingStarted.Eagerly) } @Test fun testBuffer0DropLatestReplay0() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testBuffer0DropLatestReplay1() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1) + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1, SharingStarted.Eagerly) } @Test fun testBuffer0DropLatestReplay10() = checkConflation(10, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10) + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10, SharingStarted.Eagerly) } @Test fun testBuffer5DropLatestReplay0() = checkConflation(5, BufferOverflow.DROP_LATEST) { - buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) } @Test fun testBuffer5DropLatestReplay10() = checkConflation(15, BufferOverflow.DROP_LATEST) { - buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10) + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10, SharingStarted.Eagerly) } @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) fun testBufferDropLatestOverride() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0) + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt index d691f6f432..a1f40cb6c6 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt @@ -14,7 +14,7 @@ class ShareInFusionTest : TestBase() { */ @Test fun testOperatorFusion() = runTest { - val sh = emptyFlow().shareIn(this, 0) + val sh = emptyFlow().shareIn(this, 0, SharingStarted.Eagerly) assertTrue(sh !is MutableSharedFlow<*>) // cannot be cast to mutable shared flow!!! assertSame(sh, (sh as Flow<*>).cancellable()) assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) @@ -24,11 +24,11 @@ class ShareInFusionTest : TestBase() { @Test fun testFlowOnContextFusion() = runTest { - val flow = flow { + val flow = flow { assertEquals("FlowCtx", currentCoroutineContext()[CoroutineName]?.name) emit("OK") }.flowOn(CoroutineName("FlowCtx")) - assertEquals("OK", flow.shareIn(this, 1).first()) + assertEquals("OK", flow.shareIn(this, 1, SharingStarted.Eagerly).first()) coroutineContext.cancelChildren() } @@ -39,7 +39,7 @@ class ShareInFusionTest : TestBase() { @Test fun testChannelFlowBufferShareIn() = runTest { expect(1) - val flow = channelFlow { + val flow = channelFlow { // send a batch of 10 elements using [offer] for (i in 1..10) { assertTrue(offer(i)) // offer must succeed, because buffer @@ -47,7 +47,7 @@ class ShareInFusionTest : TestBase() { send(0) // done }.buffer(10) // request a buffer of 10 // ^^^^^^^^^ buffer stays here - val shared = flow.shareIn(this, 0) + val shared = flow.shareIn(this, 0, SharingStarted.Eagerly) shared .takeWhile { it > 0 } .collect { i -> expect(i + 1) } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt index f0673038a8..1aaeea3508 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -13,7 +13,7 @@ class ShareInTest : TestBase() { fun testReplay0Eager() = runTest { expect(1) val flow = flowOf("OK") - val shared = flow.shareIn(this, 0) + val shared = flow.shareIn(this, 0, SharingStarted.Eagerly) yield() // actually start sharing // all subscribers miss "OK" val jobs = List(10) { @@ -95,7 +95,7 @@ class ShareInTest : TestBase() { terminate.join() if (failed) throw TestException() } - val shared = upstream.shareIn(this + sharingJob, 1) + val shared = upstream.shareIn(this + sharingJob, 1, SharingStarted.Eagerly) assertEquals(emptyList(), shared.replayCache) emitted.join() // should start sharing, emit & cache assertEquals(listOf("OK"), shared.replayCache) @@ -186,6 +186,7 @@ class ShareInTest : TestBase() { finish(2) } + @Suppress("TestFunctionName") private fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int): SharingStarted = object : SharingStarted { override fun command(subscriptionCount: StateFlow): Flow = diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt index 69b8df66f4..ec8e3eb790 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt @@ -710,7 +710,7 @@ class SharedFlowTest : TestBase() { } } } - repeat(1000) { index -> + repeat(1000) { val value = if (rnd.nextBoolean()) null else rnd.nextData() if (rnd.nextInt(20) == 0) { result.add("resetReplayCache & emit: $value") diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt index 10bf5feef3..2a613afaf7 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt @@ -57,7 +57,7 @@ class StateInTest : TestBase() { val scope = this + sharingJob val shared: StateFlow if (iv) { - shared = upstream.stateIn(scope, initialValue = null) + shared = upstream.stateIn(scope, SharingStarted.Eagerly, null) assertEquals(null, shared.value) } else { shared = upstream.stateIn(scope) From 4196a1ad962f6e9cfa6b91cee048a15427ce9e97 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 12:22:18 +0300 Subject: [PATCH 04/30] ~ Deprecate Flow.broadcastIn --- .../common/src/flow/Channels.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index d51791963e..87a452f726 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -186,10 +186,21 @@ public fun BroadcastChannel.asFlow(): Flow = flow { * default and to control what happens when data is produced faster than it is consumed, * that is to control backpressure behavior. * - * **Note: This API is obsolete.** It will be deprecated and replaced with - * the [Flow.shareIn] operator when it becomes stable. + * ### Deprecated + * + * **This API is deprecated.** The [BroadcastChannel] provides a complex channel-like API for hot flows. + * [SharedFlow] is a easier-to-use and more flow-centric API for the same purposes, so using + * [shareIn] operator is preferred. It is not a direct replacement, so please + * study [shareIn] documentation to see what kind of shared flow fits your use-case. As a rule of thumb: + * + * * Replace `broadcastIn(scope)` and `broadcastIn(scope, CoroutineStart.LAZY)` with `shareIn(scope, 0, SharingStarted.Lazily)`. + * * Replace `broadcastIn(scope, CoroutineStart.DEFAULT)` with `shareIn(scope, 0, SharingStarted.Eagerly)`. */ -@FlowPreview +@Deprecated( + message = "Use shareIn operator and the resulting SharedFlow as a replacement for BroadcastChannel", + replaceWith = ReplaceWith("shareIn(scope, 0, SharingStarted.Lazily)"), + level = DeprecationLevel.WARNING +) public fun Flow.broadcastIn( scope: CoroutineScope, start: CoroutineStart = CoroutineStart.LAZY From f67bbb77fc6e75f1c03c56d6e44b79b4d1c74234 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 12:26:25 +0300 Subject: [PATCH 05/30] ~ Rephrased SharingStarted docs --- kotlinx-coroutines-core/common/src/flow/SharingStarted.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index 247ed6d8c4..978dda6e10 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -15,7 +15,7 @@ import kotlin.time.* @ExperimentalCoroutinesApi public enum class SharingCommand { /** - * Starts the sharing coroutine. + * Starts sharing, launching collection of the upstream flow. * * Emitting this command again does not do anything. Emit [STOP] and then [START] to restart an * upstream flow. @@ -23,12 +23,13 @@ public enum class SharingCommand { START, /** - * Stops the sharing coroutine. + * Stops sharing, cancelling collection of the upstream flow. */ STOP, /** - * Stops the sharing coroutine and resets the [SharedFlow.replayCache] to its initial state. + * Stops sharing, cancelling collection of the upstream flow, and resets the [SharedFlow.replayCache] + * to its initial state. * The [shareIn] operator calls [MutableSharedFlow.resetReplayCache]; * the [stateIn] operator resets the value to its original `initialValue`. */ From 6567aee3f618bf4b99d3c944452fb848581c5561 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 13:18:02 +0300 Subject: [PATCH 06/30] ~ Added version to hidden declarations --- kotlinx-coroutines-core/common/src/channels/Channel.kt | 2 +- kotlinx-coroutines-core/common/src/flow/operators/Context.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index 54fa2da9b6..7660856716 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -607,7 +607,7 @@ public fun Channel(capacity: Int = RENDEZVOUS, onBufferOverflow: BufferOverf } } -@Deprecated(level = DeprecationLevel.HIDDEN, message = "For binary compatibility") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity) /** diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 5c50ec7990..fe6c8124f9 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -143,7 +143,7 @@ public fun Flow.buffer(capacity: Int = BUFFERED, onBufferOverflow: Buffer } } -@Deprecated(level = DeprecationLevel.HIDDEN, message = "For binary compatibility") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") public fun Flow.buffer(capacity: Int = BUFFERED): Flow = buffer(capacity) /** From c349fdef65242992f56f397163fff1fba84c5c72 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 13:18:49 +0300 Subject: [PATCH 07/30] ~ Private val flow in CancellableFlowImpl --- kotlinx-coroutines-core/common/src/flow/operators/Context.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index fe6c8124f9..6e82252540 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -264,7 +264,7 @@ internal interface CancellableFlow : Flow /** * Named implementation class for a flow that is defined by the [cancellable] function. */ -private class CancellableFlowImpl(val flow: Flow) : CancellableFlow { +private class CancellableFlowImpl(private val flow: Flow) : CancellableFlow { override suspend fun collect(collector: FlowCollector) { flow.collect { currentCoroutineContext().ensureActive() From 1658064386c0d79e2b2322466fd95400e2f12e7a Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 13:37:07 +0300 Subject: [PATCH 08/30] ~ Build: testNG task should be triggered by check, not test --- reactive/kotlinx-coroutines-reactive/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts index d21b28f8ce..2ace4f9fcc 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -25,7 +25,9 @@ val testNG by tasks.registering(Test::class) { tasks.test { reports.html.destination = file("$buildDir/reports/junit") +} +tasks.check { dependsOn(testNG) } From cdb31623f6b2ff790021eda27ef5b98b2c429cca Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 14:05:51 +0300 Subject: [PATCH 09/30] ~ Additional PublisherAsFlow tests for asFlow().buffer(...) --- .../src/ReactiveFlow.kt | 5 +- .../test/PublisherAsFlowTest.kt | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index fcf524d672..5834220c40 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -119,7 +119,10 @@ private class ReactiveSubscriber( private val requestSize: Long ) : Subscriber { private lateinit var subscription: Subscription - private val channel = Channel(capacity, onBufferOverflow) + + // This implementation of ReactiveSubscriber always uses "offer" in its onNext implementation and it cannot + // be reliable with rendezvous channel, so a rendezvous channel is replaced with buffer=1 channel + private val channel = Channel(if (capacity == Channel.RENDEZVOUS) 1 else capacity, onBufferOverflow) suspend fun takeNextOrNull(): T? = channel.receiveOrNull() diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt index 61f88f6af3..e8e39d0c7c 100644 --- a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.reactive import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* +import org.reactivestreams.* import kotlin.test.* class PublisherAsFlowTest : TestBase() { @@ -181,4 +182,86 @@ class PublisherAsFlowTest : TestBase() { } finish(6) } + + @Test + fun testRequestRendezvous() = + testRequestSizeWithBuffer(Channel.RENDEZVOUS, BufferOverflow.SUSPEND, 1) + + @Test + fun testRequestBuffer1() = + testRequestSizeWithBuffer(1, BufferOverflow.SUSPEND, 1) + + @Test + fun testRequestBuffer10() = + testRequestSizeWithBuffer(10, BufferOverflow.SUSPEND, 10) + + @Test + fun testRequestBufferUnlimited() = + testRequestSizeWithBuffer(Channel.UNLIMITED, BufferOverflow.SUSPEND, Long.MAX_VALUE) + + @Test + fun testRequestBufferOverflowSuspend() = + testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.SUSPEND, 64) + + @Test + fun testRequestBufferOverflowDropOldest() = + testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.DROP_OLDEST, Long.MAX_VALUE) + + @Test + fun testRequestBufferOverflowDropLatest() = + testRequestSizeWithBuffer(Channel.BUFFERED, BufferOverflow.DROP_LATEST, Long.MAX_VALUE) + + @Test + fun testRequestBuffer10OverflowDropOldest() = + testRequestSizeWithBuffer(10, BufferOverflow.DROP_OLDEST, Long.MAX_VALUE) + + @Test + fun testRequestBuffer10OverflowDropLatest() = + testRequestSizeWithBuffer(10, BufferOverflow.DROP_LATEST, Long.MAX_VALUE) + + /** + * Tests `publisher.asFlow.buffer(...)` chain, verifying expected requests size and that only expected + * values are delivered. + */ + private fun testRequestSizeWithBuffer( + capacity: Int, + onBufferOverflow: BufferOverflow, + expectedRequestSize: Long + ) = runTest { + val m = 50 + // publishers numbers from 1 to m + val publisher = Publisher { s -> + s.onSubscribe(object : Subscription { + var lastSent = 0 + var remaining = 0L + override fun request(n: Long) { + assertEquals(expectedRequestSize, n) + remaining += n + check(remaining >= 0) + while (lastSent < m && remaining > 0) { + s.onNext(++lastSent) + remaining-- + } + if (lastSent == m) s.onComplete() + } + + override fun cancel() {} + }) + } + val flow = publisher + .asFlow() + .buffer(capacity, onBufferOverflow) + val list = flow.toList() + val runSize = if (capacity == Channel.BUFFERED) 1 else capacity + val expected = when(onBufferOverflow) { + // Everything is expected to be delivered + BufferOverflow.SUSPEND -> (1..m).toList() + // Only the last one (by default) or the last "capacity" items delivered + BufferOverflow.DROP_OLDEST -> (m - runSize + 1..m).toList() + // Only the first one (by default) or the first "capacity" items delivered + BufferOverflow.DROP_LATEST -> (1..runSize).toList() + } + assertEquals(expected, list) + } + } From f0e7c6a2cfc3acc96dcd633647a7bdcc90c53b7a Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 14:07:58 +0300 Subject: [PATCH 10/30] ~ Fixed StartedWhileSubscribed.hashCode --- kotlinx-coroutines-core/common/src/flow/SharingStarted.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index 978dda6e10..468bff9246 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -210,5 +210,5 @@ private class StartedWhileSubscribed( stopTimeout == other.stopTimeout && replayExpiration == other.replayExpiration - override fun hashCode(): Int = stopTimeout.hashCode() * 13 + replayExpiration.hashCode() + override fun hashCode(): Int = stopTimeout.hashCode() * 31 + replayExpiration.hashCode() } From 3e9b12f2aeb09bb37791085a12e7986e97bc012d Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 14:39:02 +0300 Subject: [PATCH 11/30] ~ Optimized sharedIn(Lazily/Eagerly), removed DistinctFlow abstraction --- .../common/src/flow/SharingStarted.kt | 6 +-- .../common/src/flow/StateFlow.kt | 5 +-- .../common/src/flow/internal/DistinctFlow.kt | 32 -------------- .../common/src/flow/operators/Distinct.kt | 29 +++++++++--- .../common/src/flow/operators/Share.kt | 44 ++++++++++++------- .../operators/DistinctUntilChangedTest.kt | 23 ++++++++++ 6 files changed, 78 insertions(+), 61 deletions(-) delete mode 100644 kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index 468bff9246..66e43e1f79 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -151,13 +151,13 @@ public fun SharingStarted.Companion.WhileSubscribed( // -------------------------------- implementation -------------------------------- private class StartedEagerly : SharingStarted { - private val alwaysStarted = unsafeDistinctFlow { emit(SharingCommand.START) } - override fun command(subscriptionCount: StateFlow): Flow = alwaysStarted + override fun command(subscriptionCount: StateFlow): Flow = + flowOf(SharingCommand.START) override fun toString(): String = "SharingStarted.Eagerly" } private class StartedLazily : SharingStarted { - override fun command(subscriptionCount: StateFlow): Flow = unsafeDistinctFlow { + override fun command(subscriptionCount: StateFlow): Flow = flow { var started = false subscriptionCount.collect { count -> if (count > 0 && !started) { diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 4b88ce1fc4..e9f4a77346 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -251,13 +251,10 @@ private class StateFlowSlot : AbstractSharedFlowSlot>() { private class StateFlowImpl( initialState: Any // T | NULL -) : AbstractSharedFlow(), MutableStateFlow, DistinctFlow, CancellableFlow, FusibleFlow { +) : AbstractSharedFlow(), MutableStateFlow, CancellableFlow, FusibleFlow { private val _state = atomic(initialState) // T | NULL private var sequence = 0 // serializes updates, value update is in process when sequence is odd - override val isDefaultEquivalence: Boolean - get() = true // it is a DistinctFlow with default equivalence, so distinctUntilChanged is NOP on it - @Suppress("UNCHECKED_CAST") public override var value: T get() = NULL.unbox(_state.value) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt deleted file mode 100644 index d141f7d32e..0000000000 --- a/kotlinx-coroutines-core/common/src/flow/internal/DistinctFlow.kt +++ /dev/null @@ -1,32 +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.flow.internal - -import kotlinx.coroutines.flow.* - -/** - * An internal interface that marks the flow impls with distinct values, so that - * application of [distinctUntilChanged] can be optimized away. A class implementing - * this interface can be conditionally distinct via [isDefaultEquivalence] without - * having to have multiple copies of the same code. - */ -internal interface DistinctFlow : Flow { - val isDefaultEquivalence: Boolean // true when using default equivalence -} - -/** - * An analogue of the [flow] builder that does not check the context of execution of the resulting flow, - * and the implementation must also guarantee that all emitted values are distinct, so that [distinctUntilChanged] - * can be optimized away. Used in our own operators where we trust these contracts to be met. - */ -@PublishedApi -internal inline fun unsafeDistinctFlow( - isDefaultEquivalence: Boolean = true, - @BuilderInference crossinline block: suspend FlowCollector.() -> Unit -): Flow = - object : DistinctFlow { - override val isDefaultEquivalence = isDefaultEquivalence - override suspend fun collect(collector: FlowCollector) = collector.block() - } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index d6f26b46fd..d22445c28e 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.flow +import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* @@ -16,16 +17,19 @@ import kotlin.jvm.* * Note that any instance of [StateFlow] already behaves as if `distinctUtilChanged` operator is * applied to it, so applying `distinctUntilChanged` to a `StateFlow` has no effect. * See [StateFlow] documentation on Operator Fusion. + * Also, repeated application of `distinctUntilChanged` operator on any flow has no effect. */ public fun Flow.distinctUntilChanged(): Flow = - when { - this is DistinctFlow<*> && isDefaultEquivalence -> this // some other internal impls beyond StateFlow are distinct too + when (this) { + is StateFlow<*> -> this // state flows are always distinct else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent) } /** * Returns flow where all subsequent repetitions of the same value are filtered out, when compared * with each other via the provided [areEquivalent] function. + * + * Note that repeated application of `distinctUntilChanged` operator with the same parameter has no effect. */ @Suppress("UNCHECKED_CAST") public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> Boolean): Flow = @@ -34,6 +38,8 @@ public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> B /** * Returns flow where all subsequent repetitions of the same key are filtered out, where * key is extracted with [keySelector] function. + * + * Note that repeated application of `distinctUntilChanged` operator with the same parameter has no effect. */ public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent) @@ -51,15 +57,26 @@ private val defaultAreEquivalent: (Any?, Any?) -> Boolean = { old, new -> old == private fun Flow.distinctUntilChangedBy( keySelector: (T) -> Any?, areEquivalent: (old: Any?, new: Any?) -> Boolean -): Flow = - unsafeDistinctFlow(keySelector === defaultKeySelector && areEquivalent === defaultAreEquivalent) { +): Flow = when { + this is DistinctFlowImpl<*> && this.keySelector === keySelector && this.areEquivalent === areEquivalent -> this // same + else -> DistinctFlowImpl(this, keySelector, areEquivalent) +} + +private class DistinctFlowImpl( + private val upstream: Flow, + @JvmField val keySelector: (T) -> Any?, + @JvmField val areEquivalent: (old: Any?, new: Any?) -> Boolean +): Flow { + @InternalCoroutinesApi + override suspend fun collect(collector: FlowCollector) { var previousKey: Any? = NULL - collect { value -> + upstream.collect { value -> val key = keySelector(value) @Suppress("UNCHECKED_CAST") if (previousKey === NULL || !areEquivalent(previousKey, key)) { previousKey = key - emit(value) + collector.emit(value) } } } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt index d9b96a0198..9db242f50c 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -200,21 +200,36 @@ private fun CoroutineScope.launchSharing( initialValue: T ) { launch(context) { // the single coroutine to rule the sharing - started.command(shared.subscriptionCount) - .distinctUntilChanged() // only changes in command have effect - .collectLatest { // cancels block on new emission - when (it) { - SharingCommand.START -> upstream.collect(shared) // can be cancelled - SharingCommand.STOP -> { /* just cancel and do nothing else */ } - SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> { - if (initialValue === NO_VALUE) { - shared.resetReplayCache() // regular shared flow -> reset cache - } else { - shared.tryEmit(initialValue) // state flow -> reset to initial value + // Optimize common built-in started strategies + when { + started === SharingStarted.Eagerly -> { + // collect immediately & forever + upstream.collect(shared) + } + started === SharingStarted.Lazily -> { + // start collecting on the first subscriber - wait for it first + shared.subscriptionCount.first { it > 0 } + upstream.collect(shared) + } + else -> { + // other & custom strategies + started.command(shared.subscriptionCount) + .distinctUntilChanged() // only changes in command have effect + .collectLatest { // cancels block on new emission + when (it) { + SharingCommand.START -> upstream.collect(shared) // can be cancelled + SharingCommand.STOP -> { /* just cancel and do nothing else */ } + SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> { + if (initialValue === NO_VALUE) { + shared.resetReplayCache() // regular shared flow -> reset cache + } else { + shared.tryEmit(initialValue) // state flow -> reset to initial value + } + } } } - } } + } } } @@ -351,10 +366,7 @@ private class ReadonlySharedFlow( private class ReadonlyStateFlow( flow: StateFlow -) : StateFlow by flow, CancellableFlow, FusibleFlow, DistinctFlow { - override val isDefaultEquivalence: Boolean - get() = true - +) : StateFlow by flow, CancellableFlow, FusibleFlow { override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) = fuseStateFlow(context, capacity, onBufferOverflow) } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt index fc03d367c6..19f83cd941 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt @@ -94,4 +94,27 @@ class DistinctUntilChangedTest : TestBase() { val flow = flowOf(null, 1, null, null).distinctUntilChanged() assertEquals(listOf(null, 1, null), flow.toList()) } + + @Test + fun testRepeatedDistinctFusionDefault() = testRepeatedDistinctFusion { + distinctUntilChanged() + } + + @Test + fun testRepeatedDistinctFusionAreEquivalent() = testRepeatedDistinctFusion { + distinctUntilChanged { old, new -> old == new } + } + + @Test + fun testRepeatedDistinctFusionByKey() = testRepeatedDistinctFusion { + distinctUntilChangedBy { it % 2 } + } + + private fun testRepeatedDistinctFusion(op: Flow.() -> Flow) = runTest { + val flow = (1..10).asFlow() + val d1 = flow.op() + assertNotSame(flow, d1) + val d2 = d1.op() + assertSame(d1, d2) + } } From de979285e66cd4fb6d4c1b6f4946ffc2bf9c9884 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 15:29:58 +0300 Subject: [PATCH 12/30] ~ Fix new DistinctUntilChanged tests on K/N --- .../test/flow/operators/DistinctUntilChangedTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt index 19f83cd941..68e7f66b9d 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt @@ -100,14 +100,20 @@ class DistinctUntilChangedTest : TestBase() { distinctUntilChanged() } + // A separate variable is needed for K/N that does not optimize non-captured lambdas (yet) + private val areEquivalentTestFun: (old: Int, new: Int) -> Boolean = { old, new -> old == new } + @Test fun testRepeatedDistinctFusionAreEquivalent() = testRepeatedDistinctFusion { - distinctUntilChanged { old, new -> old == new } + distinctUntilChanged(areEquivalentTestFun) } + // A separate variable is needed for K/N that does not optimize non-captured lambdas (yet) + private val keySelectorTestFun: (Int) -> Int = { it % 2 } + @Test fun testRepeatedDistinctFusionByKey() = testRepeatedDistinctFusion { - distinctUntilChangedBy { it % 2 } + distinctUntilChangedBy(keySelectorTestFun) } private fun testRepeatedDistinctFusion(op: Flow.() -> Flow) = runTest { From 06cfa80e412954de0138380105ed4151202400d3 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 15:35:22 +0300 Subject: [PATCH 13/30] ~ Bit more details in subscriptionCount docs --- kotlinx-coroutines-core/common/src/flow/SharedFlow.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 7ab0da70d4..da6fc732ed 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -154,6 +154,9 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { /** * The number of subscribers (active collectors) to this shared flow. * + * The integer in the resulting [StateFlow] is not negative and starts with zero for a freshly created + * shared flow. + * * This state can be used to react to changes in the number of subscriptions to this shared flow. * For example, if you need to call `onActive` when the first subscriber appears and `onInactive` * when the last one disappears, you can set it up like this: From 4735501ab228569fd6a51314be5eb64b7f6a1e43 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 15:37:42 +0300 Subject: [PATCH 14/30] ~ ArrayChannel params checks with assert --- kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index 7b49be70cd..d47a998ecb 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -27,7 +27,7 @@ internal open class ArrayChannel( private val onBufferOverflow: BufferOverflow ) : AbstractChannel() { init { - require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } + assert { capacity >= 1 } // ArrayChannel capacity must be at least 1 } private val lock = ReentrantLock() From e86a11101079b9fa77e04262b05f6dd9fa550c7a Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 16:08:52 +0300 Subject: [PATCH 15/30] ~ Revert ArrayChannel params checks with assert, added comment --- kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index d47a998ecb..377d22a0dc 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -27,7 +27,9 @@ internal open class ArrayChannel( private val onBufferOverflow: BufferOverflow ) : AbstractChannel() { init { - assert { capacity >= 1 } // ArrayChannel capacity must be at least 1 + // This check is actually used by the Channel(...) constructor function which checks only for known + // capacities and calls ArrayChannel constructor for everything else. + require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } } private val lock = ReentrantLock() From 9120f28b93916b6e49d949d6b78612c5bb0d3981 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 16:12:27 +0300 Subject: [PATCH 16/30] ~ Improve SharedFlow & StateFlow example code --- kotlinx-coroutines-core/common/src/flow/SharedFlow.kt | 2 +- kotlinx-coroutines-core/common/src/flow/StateFlow.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index da6fc732ed..3ef89cdace 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -38,7 +38,7 @@ import kotlin.native.concurrent.* * ``` * class EventBus { * private val _events = MutableSharedFlow(0) // private mutable shared flow - * val events get() = _events.asSharedFlow() // publicly exposed as read-only shared flow + * val events = _events.asSharedFlow() // publicly exposed as read-only shared flow * * suspend fun produceEvent(event: Event) { * _events.emit(event) // suspends until all subscribers receive it diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index e9f4a77346..0ad8cbf457 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -34,7 +34,7 @@ import kotlin.native.concurrent.* * ``` * class CounterModel { * private val _counter = MutableStateFlow(0) // private mutable state flow - * val counter get() = _counter.asStateFlow() // publicly exposed as read-only state flow + * val counter = _counter.asStateFlow() // publicly exposed as read-only state flow * * fun inc() { * _counter.value++ From cc03f3aacf054546710440db8a65c24a255793fb Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 16:49:04 +0300 Subject: [PATCH 17/30] ~ More eager cancellation check in StateFlow, better test --- .../common/src/flow/StateFlow.kt | 3 +- .../common/test/flow/sharing/StateFlowTest.kt | 32 ----------- .../test/flow/StateFlowCancellabilityTest.kt | 56 +++++++++++++++++++ 3 files changed, 58 insertions(+), 33 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 0ad8cbf457..2badc8b453 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -334,9 +334,10 @@ private class StateFlowImpl( // Here the coroutine could have waited for a while to be dispatched, // so we use the most recent state here to ensure the best possible conflation of stale values val newState = _state.value + // always check for cancellation + collectorJob?.ensureActive() // Conflate value emissions using equality if (oldState == null || oldState != newState) { - collectorJob?.ensureActive() collector.emit(NULL.unbox(newState)) oldState = newState } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt index c61b7a0289..47bee4f665 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt @@ -165,38 +165,6 @@ class StateFlowTest : TestBase() { assertSame(state, state.buffer(Channel.RENDEZVOUS)) } - @Test - fun testCancellability() = runTest { - expect(1) - val state = MutableStateFlow(0) - var subscribed = true - val barrier = Channel() - val job = state - .onSubscription { subscribed = true } - .onEach { i -> - when (i) { - 0 -> expect(2) // initial value - 1 -> expect(3) - 2 -> { - expect(4) - currentCoroutineContext().cancel() - } - else -> expectUnreached() // shall check for cancellation - } - barrier.send(i) - } - .launchIn(this) - yield() - assertTrue(subscribed) // yielding in enough - assertEquals(0, barrier.receive()) // should get initial value, too - for (i in 1..3) { // emit after subscription - state.value = i - if (i < 3) assertEquals(i, barrier.receive()) // shall receive it - } - job.join() - finish(5) - } - @Test fun testResetUnsupported() { val state = MutableStateFlow(42) diff --git a/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt b/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt new file mode 100644 index 0000000000..fc4996c7c0 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import java.util.concurrent.* +import kotlin.test.* + +@Suppress("BlockingMethodInNonBlockingContext") +class StateFlowCancellabilityTest : TestBase() { + @Test + fun testCancellabilityNoConflation() = runTest { + expect(1) + val state = MutableStateFlow(0) + var subscribed = true + var lastReceived = -1 + val barrier = CyclicBarrier(2) + val job = state + .onSubscription { + subscribed = true + barrier.await() + } + .onEach { i -> + when (i) { + 0 -> expect(2) // initial value + 1 -> expect(3) + 2 -> { + expect(4) + currentCoroutineContext().cancel() + } + else -> expectUnreached() // shall check for cancellation + } + lastReceived = i + barrier.await() + barrier.await() + } + .launchIn(this + Dispatchers.Default) + barrier.await() + assertTrue(subscribed) // should have subscribed in the first barrier + barrier.await() + assertEquals(0, lastReceived) // should get initial value, too + for (i in 1..3) { // emit after subscription + state.value = i + barrier.await() // let it go + if (i < 3) { + barrier.await() // wait for receive + assertEquals(i, lastReceived) // shall receive it + } + } + job.join() + finish(5) + } +} + From 322f615b5f24261a5826e2b0cd5841e0cfcebe9e Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 17:31:47 +0300 Subject: [PATCH 18/30] ~ Updated API dump --- kotlinx-coroutines-core/api/kotlinx-coroutines-core.api | 4 ---- 1 file changed, 4 deletions(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index f4bfbcbd55..2a072e2f8a 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -1137,10 +1137,6 @@ public final class kotlinx/coroutines/flow/internal/CombineKt { public static final fun combineInternal (Lkotlinx/coroutines/flow/FlowCollector;[Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class kotlinx/coroutines/flow/internal/DistinctFlowKt { - public static final fun unsafeDistinctFlow (ZLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; -} - public final class kotlinx/coroutines/flow/internal/FlowExceptions_commonKt { public static final fun checkIndexOverflow (I)I } From 22e4800d8736d45096bdef86387a4d2d5fa06233 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 18:03:52 +0300 Subject: [PATCH 19/30] ~ Optimized resume list to array --- .../common/src/flow/AbstractSharedFlow.kt | 15 +++--- .../common/src/flow/SharedFlow.kt | 50 ++++++++++--------- .../common/src/flow/StateFlow.kt | 4 +- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt index 765a4f1559..92055fcf53 100644 --- a/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt @@ -4,15 +4,18 @@ package kotlinx.coroutines.flow -import kotlinx.coroutines.assert -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.internal.* import kotlinx.coroutines.internal.* import kotlin.coroutines.* +import kotlin.jvm.* +import kotlin.native.concurrent.* + +@JvmField +@SharedImmutable +internal val EMPTY_RESUMES = arrayOfNulls?>(0) internal abstract class AbstractSharedFlowSlot { abstract fun allocateLocked(flow: F): Boolean - abstract fun freeLocked(flow: F): List>? // returns a list of continuation to resume after lock + abstract fun freeLocked(flow: F): Array?> // returns continuations to resume after lock } internal abstract class AbstractSharedFlow> : SynchronizedObject() { @@ -71,7 +74,7 @@ internal abstract class AbstractSharedFlow> : Sync protected fun freeSlot(slot: S) { // Release slot under lock var subscriptionCount: MutableStateFlow? = null - val resumeList = synchronized(this) { + val resumes = synchronized(this) { nCollectors-- subscriptionCount = _subscriptionCount // retrieve under lock if initialized // Reset next index oracle if we have no more active collectors for more predictable behavior next time @@ -79,7 +82,7 @@ internal abstract class AbstractSharedFlow> : Sync (slot as AbstractSharedFlowSlot).freeLocked(this) } // Resume suspended coroutines - resumeList?.forEach { it.resume(Unit) } + for (cont in resumes) cont?.resume(Unit) // decrement subscription count subscriptionCount?.increment(-1) } diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 3ef89cdace..4eda99bcb1 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -233,7 +233,7 @@ private class SharedFlowSlot : AbstractSharedFlowSlot>() { return true } - override fun freeLocked(flow: SharedFlowImpl<*>): List>? { + override fun freeLocked(flow: SharedFlowImpl<*>): Array?> { assert { index >= 0 } val oldIndex = index index = -1L @@ -323,16 +323,16 @@ private class SharedFlowImpl( } override fun tryEmit(value: T): Boolean { - var resumeList: List>? = null + var resumes: Array?> = EMPTY_RESUMES val emitted = synchronized(this) { if (tryEmitLocked(value)) { - resumeList = findSlotsToResumeLocked() + resumes = findSlotsToResumeLocked() true } else { false } } - resumeList?.forEach { it.resume(Unit) } + for (cont in resumes) cont?.resume(Unit) return emitted } @@ -417,12 +417,12 @@ private class SharedFlowImpl( } private suspend fun emitSuspend(value: T) = suspendCancellableCoroutine sc@{ cont -> - var resumeList: List>? = null + var resumes: Array?> = EMPTY_RESUMES val emitter = synchronized(this) lock@{ // recheck buffer under lock again (make sure it is really full) if (tryEmitLocked(value)) { cont.resume(Unit) - resumeList = findSlotsToResumeLocked() + resumes = findSlotsToResumeLocked() return@lock null } // add suspended emitter to the buffer @@ -430,13 +430,13 @@ private class SharedFlowImpl( enqueueLocked(it) queueSize++ // added to queue of waiting emitters // synchronous shared flow might rendezvous with waiting emitter - if (bufferCapacity == 0) resumeList = findSlotsToResumeLocked() + if (bufferCapacity == 0) resumes = findSlotsToResumeLocked() } } // outside of the lock: register dispose on cancellation emitter?.let { cont.disposeOnCancellation(it) } // outside of the lock: resume slots if needed - resumeList?.forEach { it.resume(Unit) } + for (cont in resumes) cont?.resume(Unit) } private fun cancelEmitter(emitter: Emitter) = synchronized(this) { @@ -454,9 +454,9 @@ private class SharedFlowImpl( } // Is called when a collector disappears or changes index, returns a list of continuations to resume after lock - internal fun updateCollectorIndexLocked(oldIndex: Long): List>? { + internal fun updateCollectorIndexLocked(oldIndex: Long): Array?> { assert { oldIndex >= minCollectorIndex } - if (oldIndex > minCollectorIndex) return null // nothing changes, it was not min + if (oldIndex > minCollectorIndex) return EMPTY_RESUMES // nothing changes, it was not min // start computing new minimal index of active collectors val head = head var newMinCollectorIndex = head + bufferSize @@ -467,7 +467,7 @@ private class SharedFlowImpl( if (slot.index >= 0 && slot.index < newMinCollectorIndex) newMinCollectorIndex = slot.index } assert { newMinCollectorIndex >= minCollectorIndex } // can only grow - if (newMinCollectorIndex <= minCollectorIndex) return null // nothing changes + if (newMinCollectorIndex <= minCollectorIndex) return EMPTY_RESUMES // nothing changes // Compute new buffer size if we drop items we no longer need and no emitter is resumed: // We must keep all the items from newMinIndex to the end of buffer var newBufferEndIndex = bufferEndIndex // var to grow when waiters are resumed @@ -481,20 +481,21 @@ private class SharedFlowImpl( // If we don't have collectors anymore we must resume all waiting emitters queueSize // that's how many waiting emitters we have (at most) } - var resumeList: ArrayList>? = null + var resumes: Array?> = EMPTY_RESUMES val newQueueEndIndex = newBufferEndIndex + queueSize if (maxResumeCount > 0) { // collect emitters to resume if we have them - resumeList = ArrayList(maxResumeCount) + resumes = arrayOfNulls(maxResumeCount) + var resumeCount = 0 val buffer = buffer!! for (curEmitterIndex in newBufferEndIndex until newQueueEndIndex) { val emitter = buffer.getBufferAt(curEmitterIndex) if (emitter !== NO_VALUE) { emitter as Emitter // must have Emitter class - resumeList.add(emitter.cont) + resumes[resumeCount++] = emitter.cont buffer.setBufferAt(curEmitterIndex, NO_VALUE) // make as canceled if we moved ahead buffer.setBufferAt(newBufferEndIndex, emitter.value) newBufferEndIndex++ - if (resumeList.size >= maxResumeCount) break // enough resumed, done + if (resumeCount >= maxResumeCount) break // enough resumed, done } } } @@ -511,7 +512,7 @@ private class SharedFlowImpl( updateBufferLocked(newReplayIndex, newMinCollectorIndex, newBufferEndIndex, newQueueEndIndex) // just in case we've moved all buffered emitters and have NO_VALUE's at the tail now cleanupTailLocked() - return resumeList + return resumes } private fun updateBufferLocked( @@ -549,7 +550,7 @@ private class SharedFlowImpl( // returns NO_VALUE if cannot take value without suspension private fun tryTakeValue(slot: SharedFlowSlot): Any? { - var resumeList: List>? = null + var resumes: Array?> = EMPTY_RESUMES val value = synchronized(this) { val index = tryPeekLocked(slot) if (index < 0) { @@ -558,11 +559,11 @@ private class SharedFlowImpl( val oldIndex = slot.index val newValue = getPeekedValueLockedAt(index) slot.index = index + 1 // points to the next index after peeked one - resumeList = updateCollectorIndexLocked(oldIndex) + resumes = updateCollectorIndexLocked(oldIndex) newValue } } - resumeList?.forEach { it.resume(Unit) } + for (resume in resumes) resume?.resume(Unit) return value } @@ -597,16 +598,17 @@ private class SharedFlowImpl( } } - private fun findSlotsToResumeLocked(): List>? { - var result: ArrayList>? = null + private fun findSlotsToResumeLocked(): Array?> { + var resumes: Array?> = EMPTY_RESUMES + var resumeCount = 0 forEachSlotLocked loop@{ slot -> val cont = slot.cont ?: return@loop // only waiting slots if (tryPeekLocked(slot) < 0) return@loop // only slots that can peek a value - val a = result ?: ArrayList>(2).also { result = it } - a.add(cont) + if (resumeCount >= resumes.size) resumes = resumes.copyOf(maxOf(2, 2 * resumes.size)) + resumes[resumeCount++] = cont slot.cont = null // not waiting anymore } - return result + return resumes } override fun createSlot() = SharedFlowSlot() diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 2badc8b453..b3434760a4 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -209,9 +209,9 @@ private class StateFlowSlot : AbstractSharedFlowSlot>() { return true } - override fun freeLocked(flow: StateFlowImpl<*>): List>? { + override fun freeLocked(flow: StateFlowImpl<*>): Array?> { _state.value = null // free now - return null // nothing more to do + return EMPTY_RESUMES // nothing more to do } @Suppress("UNCHECKED_CAST") From faaad4f66e03181bee1e804173a6b8eeb40b0832 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 18:12:29 +0300 Subject: [PATCH 20/30] ~ Optimized delay(Long.MAX_VALUE), SharingStarted docs --- kotlinx-coroutines-core/common/src/Delay.kt | 5 ++++- kotlinx-coroutines-core/common/src/flow/SharingStarted.kt | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index ee28ec3b0e..8b6cbea143 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -111,7 +111,10 @@ public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} public suspend fun delay(timeMillis: Long) { if (timeMillis <= 0) return // don't delay return suspendCancellableCoroutine sc@ { cont: CancellableContinuation -> - cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) + // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule. + if (timeMillis < Long.MAX_VALUE) { + cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) + } } } diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index 66e43e1f79..935efdae2b 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -101,7 +101,8 @@ public interface SharingStarted { * * [replayExpirationMillis] — configures a delay (in milliseconds) between the stopping of * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator * and resets the cached value to the original `initialValue` for the [stateIn] operator). - * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer) + * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer). + * Use zero value to expire the cache immediately. * * This function throws [IllegalArgumentException] when either [stopTimeoutMillis] or [replayExpirationMillis] * are negative. @@ -134,7 +135,8 @@ public interface SharingStarted { * * [replayExpiration] — configures a delay between the stopping of * the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the [shareIn] operator * and resets the cached value to the original `initialValue` for the [stateIn] operator). - * It defaults to `Long.MAX_VALUE` (keep replay cache forever, never reset buffer) + * It defaults to [Duration.INFINITE] (keep replay cache forever, never reset buffer). + * Use [Duration.ZERO] value to expire the cache immediately. * * This function throws [IllegalArgumentException] when either [stopTimeout] or [replayExpiration] * are negative. From 56ff4fb6ba6e3965d3f66bfd0634c1ab0099455b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 18:21:27 +0300 Subject: [PATCH 21/30] ~ Moved AbstractSharedFlow to internal package --- .../common/src/flow/{ => internal}/AbstractSharedFlow.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename kotlinx-coroutines-core/common/src/flow/{ => internal}/AbstractSharedFlow.kt (97%) diff --git a/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt similarity index 97% rename from kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt rename to kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt index 92055fcf53..ee8d06359e 100644 --- a/kotlinx-coroutines-core/common/src/flow/AbstractSharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -2,8 +2,9 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines.flow +package kotlinx.coroutines.flow.internal +import kotlinx.coroutines.flow.* import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* From 17c8fac6be1d2e96eb715dc5fdbc35465382171d Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 18:33:09 +0300 Subject: [PATCH 22/30] ~ More detailed docs on StateFlow.compareAndSet and test --- .../common/src/flow/StateFlow.kt | 6 +++++- .../common/test/flow/sharing/StateFlowTest.kt | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index b3434760a4..441035ca10 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -167,7 +167,11 @@ public interface MutableStateFlow : StateFlow, MutableSharedFlow { /** * Atomically compares the current [value] with [expect] and sets it to [update] if it is equal to [expect]. - * The result is `true` if the [value] was set to [update] and `false` otherwise. + * The result is `true` if the [value] was set to [update] and `false` otherwise. + * + * This function use a regular comparison using [Any.equals]. If both [expect] and [update] are equal to the + * current [value], this function returns `true`, but it does not actually change the reference that is + * stored in the [value]. */ public fun compareAndSet(expect: T, update: T): Boolean } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt index 47bee4f665..0a2c0458c4 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt @@ -172,4 +172,25 @@ class StateFlowTest : TestBase() { assertEquals(42, state.value) assertEquals(listOf(42), state.replayCache) } + + @Test + fun testReferenceUpdatesAndCAS() { + val d0 = Data(0) + val d0_1 = Data(0) + val d1 = Data(1) + val d1_1 = Data(1) + val d1_2 = Data(1) + val state = MutableStateFlow(d0) + assertSame(d0, state.value) + state.value = d0_1 // equal, nothing changes + assertSame(d0, state.value) + state.value = d1 // updates + assertSame(d1, state.value) + assertFalse(state.compareAndSet(d0, d0)) // wrong value + assertSame(d1, state.value) + assertTrue(state.compareAndSet(d1_1, d1_2)) // "updates", but ref stays + assertSame(d1, state.value) + assertTrue(state.compareAndSet(d1_1, d0)) // updates, reference changes + assertSame(d0, state.value) + } } \ No newline at end of file From 25e08d49976c31386833099571049db33d3b56a6 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 18:47:57 +0300 Subject: [PATCH 23/30] ~ Fixed Robolectric test for optimized delay impl --- .../android-unit-tests/test/ordered/tests/TestComponent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt index 9cf813bc3a..c677d9911a 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/TestComponent.kt @@ -21,7 +21,7 @@ public class TestComponent { fun launchDelayed() { scope.launch { - delay(Long.MAX_VALUE) + delay(Long.MAX_VALUE / 2) delayedLaunchCompleted = true } } From fe64780545e9418916431dda3ec20e5e8e7bad4f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 18:59:27 +0300 Subject: [PATCH 24/30] ~ Added comment on how freeSlot can resume coroutines --- .../common/src/flow/internal/AbstractSharedFlow.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt index ee8d06359e..862cd465a3 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -82,7 +82,11 @@ internal abstract class AbstractSharedFlow> : Sync if (nCollectors == 0) nextIndex = 0 (slot as AbstractSharedFlowSlot).freeLocked(this) } - // Resume suspended coroutines + /* + Resume suspended coroutines. + This can happens when the subscriber that was freed was a slow one and was holding up buffer. + When this subscriber was freed, previously queued emitted can now wake up and are resumed here. + */ for (cont in resumes) cont?.resume(Unit) // decrement subscription count subscriptionCount?.increment(-1) From a0f7666976217ade7e21448ca4ade7c1de81c946 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 19:02:20 +0300 Subject: [PATCH 25/30] ~ Code style --- .../common/src/flow/internal/AbstractSharedFlow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt index 862cd465a3..ccb5343084 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -45,7 +45,7 @@ internal abstract class AbstractSharedFlow> : Sync // Actually create slot under lock var subscriptionCount: MutableStateFlow? = null val slot = synchronized(this) { - val slots = when(val curSlots = slots) { + val slots = when (val curSlots = slots) { null -> createSlotArray(2).also { slots = it } else -> if (nCollectors >= curSlots.size) { curSlots.copyOf(2 * curSlots.size).also { slots = it } From 0fa4de61d35211a5265947350c322cca18c35e3b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 20:53:44 +0300 Subject: [PATCH 26/30] ~ One more fix for delay(MAX_VALUE) optimization --- kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt index 0013a654a6..e21c82b95c 100644 --- a/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt @@ -54,11 +54,11 @@ class TestRunBlockingOrderTest : TestBase() { } @Test - fun testInfiniteDelay() = runBlockingTest { + fun testVeryLongDelay() = runBlockingTest { expect(1) delay(100) // move time forward a bit some that naive time + delay gives an overflow launch { - delay(Long.MAX_VALUE) // infinite delay + delay(Long.MAX_VALUE / 2) // very long delay finish(4) } launch { From 996e6180441af3e91c59533b8f12f2fb48c9db7f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 21:52:42 +0300 Subject: [PATCH 27/30] ~ Fixed distinctUntilChanged on Kotlin/Native --- kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index d22445c28e..1a34af776f 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -10,6 +10,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* +import kotlin.native.concurrent.* /** * Returns flow where all subsequent repetitions of the same value are filtered out. @@ -44,7 +45,10 @@ public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> B public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent) +@SharedImmutable private val defaultKeySelector: (Any?) -> Any? = { it } + +@SharedImmutable private val defaultAreEquivalent: (Any?, Any?) -> Boolean = { old, new -> old == new } /** From bd32090a6d8c63bbdef35c065295115394cd4048 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 13 Oct 2020 13:18:44 +0300 Subject: [PATCH 28/30] ~ Tweaked defaults and parameter order for shareIn --- .../api/kotlinx-coroutines-core.api | 3 +- .../common/src/flow/SharedFlow.kt | 12 +++--- .../common/src/flow/StateFlow.kt | 2 +- .../common/src/flow/operators/Share.kt | 22 +++++------ .../test/flow/sharing/ShareInBufferTest.kt | 16 ++++---- .../flow/sharing/ShareInConflationTest.kt | 38 +++++++++---------- .../test/flow/sharing/ShareInFusionTest.kt | 6 +-- .../common/test/flow/sharing/ShareInTest.kt | 8 ++-- .../test/flow/sharing/SharedFlowTest.kt | 10 ++--- .../jvm/test/flow/SharingStressTest.kt | 2 +- 10 files changed, 60 insertions(+), 59 deletions(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index f7acce8e3e..bb1c0f36ab 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -1021,7 +1021,8 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; - public static final fun shareIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;ILkotlinx/coroutines/flow/SharingStarted;)Lkotlinx/coroutines/flow/SharedFlow; + public static final fun shareIn (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;I)Lkotlinx/coroutines/flow/SharedFlow; + public static synthetic fun shareIn$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;IILjava/lang/Object;)Lkotlinx/coroutines/flow/SharedFlow; public static final fun single (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun singleOrNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun skip (Lkotlinx/coroutines/flow/Flow;I)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 4eda99bcb1..88dc775842 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -37,7 +37,7 @@ import kotlin.native.concurrent.* * * ``` * class EventBus { - * private val _events = MutableSharedFlow(0) // private mutable shared flow + * private val _events = MutableSharedFlow() // private mutable shared flow * val events = _events.asSharedFlow() // publicly exposed as read-only shared flow * * suspend fun produceEvent(event: Event) { @@ -194,7 +194,7 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { * * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. * - * @param replay the number of values replayed to new subscribers (cannot be negative). + * @param replay the number of values replayed to new subscribers (cannot be negative, defaults to zero). * @param extraBufferCapacity the number of values buffered in addition to `replay`. * [emit][MutableSharedFlow.emit] does not suspend while there is a buffer space remaining (optional, cannot be negative, defaults to zero). * @param onBufferOverflow configures an action on buffer overflow (optional, defaults to @@ -204,14 +204,14 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { @Suppress("FunctionName", "UNCHECKED_CAST") @ExperimentalCoroutinesApi public fun MutableSharedFlow( - replay: Int, + replay: Int = 0, extraBufferCapacity: Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ): MutableSharedFlow { - require(replay >= 0) { "replay cannot be negative" } - require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative" } + require(replay >= 0) { "replay cannot be negative, but was $replay" } + require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" } require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) { - "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy" + "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow" } val bufferCapacity0 = replay + extraBufferCapacity val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 441035ca10..8587606633 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -76,7 +76,7 @@ import kotlin.native.concurrent.* * // MutableStateFlow(initialValue) is a shared flow with the following parameters: * val shared = MutableSharedFlow( * replay = 1, - * onBufferOverflow = BufferOverflow.DROP_OLDEST, + * onBufferOverflow = BufferOverflow.DROP_OLDEST * ) * shared.tryEmit(initialValue) // emit the initial value * val state = shared.distinctUntilChanged() // get StateFlow-like behavior diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt index 9db242f50c..4dd89ee4bf 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -59,7 +59,7 @@ import kotlin.jvm.* * and establish it eagerly like this: * * ``` - * val messages: SharedFlow = backendMessages.shareIn(scope, 0) + * val messages: SharedFlow = backendMessages.shareIn(scope, SharingStarted.Eagerly) * ``` * * Now a single connection is shared between all collectors from `messages`, and there is a chance that the connection @@ -75,7 +75,7 @@ import kotlin.jvm.* * ``` * backendMessages * .onCompletion { cause -> if (cause == null) emit(UpstreamHasCompletedMessage) } - * .shareIn(scope, 0) + * .shareIn(scope, SharingStarted.Eagerly) * ``` * * Any exception in the upstream flow terminates the sharing coroutine without affecting any of the subscribers, @@ -90,7 +90,7 @@ import kotlin.jvm.* * if (shallRetry) delay(1000) * shallRetry * } - * .shareIn(scope, 0) + * .shareIn(scope, SharingStarted.Eagerly) * ``` * * ### Initial value @@ -101,7 +101,7 @@ import kotlin.jvm.* * ``` * backendMessages * .onStart { emit(UpstreamIsStartingMessage) } - * .shareIn(scope, 1) // replay one most recent message + * .shareIn(scope, SharingStarted.Eagerly, 1) // replay one most recent message * ``` * * ### Buffering and conflation @@ -111,12 +111,12 @@ import kotlin.jvm.* * This default buffering can be overridden with an explicit buffer configuration by preceding the `shareIn` call * with [buffer] or [conflate], for example: * - * * `buffer(0).shareIn(scope, 0)` — overrides the default buffer size and creates a [SharedFlow] without a buffer. + * * `buffer(0).shareIn(scope, started, 0)` — overrides the default buffer size and creates a [SharedFlow] without a buffer. * Effectively, it configures sequential processing between the upstream emitter and subscribers, * as the emitter is suspended until all subscribers process the value. Note, that the value is still immediately * discarded when there are no subscribers. - * * `buffer(b).shareIn(scope, r)` — creates a [SharedFlow] with `replay = r` and `extraBufferCapacity = b`. - * * `conflate().shareIn(scope, r)` — creates a [SharedFlow] with `replay = r`, `onBufferOverflow = DROP_OLDEST`, + * * `buffer(b).shareIn(scope, started, r)` — creates a [SharedFlow] with `replay = r` and `extraBufferCapacity = b`. + * * `conflate().shareIn(scope, started, r)` — creates a [SharedFlow] with `replay = r`, `onBufferOverflow = DROP_OLDEST`, * and `extraBufferCapacity = 1` when `replay == 0` to support this strategy. * * ### Operator fusion @@ -129,14 +129,14 @@ import kotlin.jvm.* * This function throws [IllegalArgumentException] on unsupported values of parameters or combinations thereof. * * @param scope the coroutine scope in which sharing is started. - * @param replay the number of values replayed to new subscribers (cannot be negative). * @param started the strategy that controls when sharing is started and stopped. + * @param replay the number of values replayed to new subscribers (cannot be negative, defaults to zero). */ @ExperimentalCoroutinesApi public fun Flow.shareIn( scope: CoroutineScope, - replay: Int, - started: SharingStarted + started: SharingStarted, + replay: Int = 0 ): SharedFlow { val config = configureSharing(replay) val shared = MutableSharedFlow( @@ -267,7 +267,7 @@ private fun CoroutineScope.launchSharing( * and establish it eagerly like this: * * ``` - * val state: StateFlow = backendMessages.stateIn(scope, initialValue = State.LOADING) + * val state: StateFlow = backendMessages.stateIn(scope, SharingStarted.Eagerly, State.LOADING) * ``` * * Now, a single connection is shared between all collectors from `state`, and there is a chance that the connection diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt index 426d0ba5ce..9c6aed211a 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInBufferTest.kt @@ -51,48 +51,48 @@ class ShareInBufferTest : TestBase() { @Test fun testReplay0DefaultBuffer() = checkBuffer(defaultBufferSize) { - shareIn(it, 0, SharingStarted.Eagerly) + shareIn(it, SharingStarted.Eagerly) } @Test fun testReplay1DefaultBuffer() = checkBuffer(defaultBufferSize) { - shareIn(it, 1, SharingStarted.Eagerly) + shareIn(it, SharingStarted.Eagerly, 1) } @Test // buffer is padded to default size as needed fun testReplay10DefaultBuffer() = checkBuffer(maxOf(10, defaultBufferSize)) { - shareIn(it, 10, SharingStarted.Eagerly) + shareIn(it, SharingStarted.Eagerly, 10) } @Test // buffer is padded to default size as needed fun testReplay100DefaultBuffer() = checkBuffer( maxOf(100, defaultBufferSize)) { - shareIn(it, 100, SharingStarted.Eagerly) + shareIn(it, SharingStarted.Eagerly, 100) } @Test fun testDefaultBufferKeepsDefault() = checkBuffer(defaultBufferSize) { - buffer().shareIn(it, 0, SharingStarted.Eagerly) + buffer().shareIn(it, SharingStarted.Eagerly) } @Test fun testOverrideDefaultBuffer0() = checkBuffer(0) { - buffer(0).shareIn(it, 0, SharingStarted.Eagerly) + buffer(0).shareIn(it, SharingStarted.Eagerly) } @Test fun testOverrideDefaultBuffer10() = checkBuffer(10) { - buffer(10).shareIn(it, 0, SharingStarted.Eagerly) + buffer(10).shareIn(it, SharingStarted.Eagerly) } @Test // buffer and replay sizes add up fun testBufferReplaySum() = checkBuffer(41) { - buffer(10).buffer(20).shareIn(it, 11, SharingStarted.Eagerly) + buffer(10).buffer(20).shareIn(it, SharingStarted.Eagerly, 11) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt index 612c8cded7..0528e97e7d 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInConflationTest.kt @@ -49,114 +49,114 @@ class ShareInConflationTest : TestBase() { @Test fun testConflateReplay1() = checkConflation(1) { - conflate().shareIn(it, 1, SharingStarted.Eagerly) + conflate().shareIn(it, SharingStarted.Eagerly, 1) } @Test // still looks like conflating the last value for the first subscriber (will not replay to others though) fun testConflateReplay0() = checkConflation(1) { - conflate().shareIn(it, 0, SharingStarted.Eagerly) + conflate().shareIn(it, SharingStarted.Eagerly, 0) } @Test fun testConflateReplay5() = checkConflation(5) { - conflate().shareIn(it, 5, SharingStarted.Eagerly) + conflate().shareIn(it, SharingStarted.Eagerly, 5) } @Test fun testBufferDropOldestReplay1() = checkConflation(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1, SharingStarted.Eagerly) + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) } @Test fun testBufferDropOldestReplay0() = checkConflation(1) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0, SharingStarted.Eagerly) + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) } @Test fun testBufferDropOldestReplay10() = checkConflation(10) { - buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 10, SharingStarted.Eagerly) + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 10) } @Test fun testBuffer20DropOldestReplay0() = checkConflation(20) { - buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 0, SharingStarted.Eagerly) + buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) } @Test fun testBuffer7DropOldestReplay11() = checkConflation(18) { - buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 11, SharingStarted.Eagerly) + buffer(7, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 11) } @Test // a preceding buffer() gets overridden by conflate() fun testBufferConflateOverride() = checkConflation(1) { - buffer(23).conflate().shareIn(it, 1, SharingStarted.Eagerly) + buffer(23).conflate().shareIn(it, SharingStarted.Eagerly, 1) } @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) fun testBufferDropOldestOverride() = checkConflation(1) { - buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, 1, SharingStarted.Eagerly) + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) } @Test fun testBufferDropLatestReplay0() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) } @Test fun testBufferDropLatestReplay1() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1, SharingStarted.Eagerly) + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 1) } @Test fun testBufferDropLatestReplay10() = checkConflation(10, BufferOverflow.DROP_LATEST) { - buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10, SharingStarted.Eagerly) + buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) } @Test fun testBuffer0DropLatestReplay0() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) } @Test fun testBuffer0DropLatestReplay1() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 1, SharingStarted.Eagerly) + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 1) } @Test fun testBuffer0DropLatestReplay10() = checkConflation(10, BufferOverflow.DROP_LATEST) { - buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10, SharingStarted.Eagerly) + buffer(0, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) } @Test fun testBuffer5DropLatestReplay0() = checkConflation(5, BufferOverflow.DROP_LATEST) { - buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) } @Test fun testBuffer5DropLatestReplay10() = checkConflation(15, BufferOverflow.DROP_LATEST) { - buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 10, SharingStarted.Eagerly) + buffer(5, onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 10) } @Test // a preceding buffer() gets overridden by buffer(onBufferOverflow = BufferOverflow.DROP_LATEST) fun testBufferDropLatestOverride() = checkConflation(1, BufferOverflow.DROP_LATEST) { - buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, 0, SharingStarted.Eagerly) + buffer(23).buffer(onBufferOverflow = BufferOverflow.DROP_LATEST).shareIn(it, SharingStarted.Eagerly, 0) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt index a1f40cb6c6..371d01472e 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInFusionTest.kt @@ -14,7 +14,7 @@ class ShareInFusionTest : TestBase() { */ @Test fun testOperatorFusion() = runTest { - val sh = emptyFlow().shareIn(this, 0, SharingStarted.Eagerly) + val sh = emptyFlow().shareIn(this, SharingStarted.Eagerly) assertTrue(sh !is MutableSharedFlow<*>) // cannot be cast to mutable shared flow!!! assertSame(sh, (sh as Flow<*>).cancellable()) assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) @@ -28,7 +28,7 @@ class ShareInFusionTest : TestBase() { assertEquals("FlowCtx", currentCoroutineContext()[CoroutineName]?.name) emit("OK") }.flowOn(CoroutineName("FlowCtx")) - assertEquals("OK", flow.shareIn(this, 1, SharingStarted.Eagerly).first()) + assertEquals("OK", flow.shareIn(this, SharingStarted.Eagerly, 1).first()) coroutineContext.cancelChildren() } @@ -47,7 +47,7 @@ class ShareInFusionTest : TestBase() { send(0) // done }.buffer(10) // request a buffer of 10 // ^^^^^^^^^ buffer stays here - val shared = flow.shareIn(this, 0, SharingStarted.Eagerly) + val shared = flow.shareIn(this, SharingStarted.Eagerly) shared .takeWhile { it > 0 } .collect { i -> expect(i + 1) } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt index 1aaeea3508..9020f5f311 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -13,7 +13,7 @@ class ShareInTest : TestBase() { fun testReplay0Eager() = runTest { expect(1) val flow = flowOf("OK") - val shared = flow.shareIn(this, 0, SharingStarted.Eagerly) + val shared = flow.shareIn(this, SharingStarted.Eagerly) yield() // actually start sharing // all subscribers miss "OK" val jobs = List(10) { @@ -40,7 +40,7 @@ class ShareInTest : TestBase() { emit("DONE") } val sharingJob = Job() - val shared = flow.shareIn(this + sharingJob, replay, started = SharingStarted.Lazily) + val shared = flow.shareIn(this + sharingJob, started = SharingStarted.Lazily, replay = replay) yield() // should not start sharing // first subscriber gets "OK", other subscribers miss "OK" val n = 10 @@ -95,7 +95,7 @@ class ShareInTest : TestBase() { terminate.join() if (failed) throw TestException() } - val shared = upstream.shareIn(this + sharingJob, 1, SharingStarted.Eagerly) + val shared = upstream.shareIn(this + sharingJob, SharingStarted.Eagerly, 1) assertEquals(emptyList(), shared.replayCache) emitted.join() // should start sharing, emit & cache assertEquals(listOf("OK"), shared.replayCache) @@ -157,7 +157,7 @@ class ShareInTest : TestBase() { } } - val shared = flow.shareIn(this, 0, started = started) + val shared = flow.shareIn(this, started) repeat(5) { // repeat scenario a few times yield() assertFalse(flowState.started) // flow is not running even if we yield diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt index ec8e3eb790..32d88f3c99 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowTest.kt @@ -18,7 +18,7 @@ class SharedFlowTest : TestBase() { @Test fun testRendezvousSharedFlowBasic() = runTest { expect(1) - val sh = MutableSharedFlow(0) + val sh = MutableSharedFlow() assertTrue(sh.replayCache.isEmpty()) assertEquals(0, sh.subscriptionCount.value) sh.emit(1) // no suspend @@ -91,7 +91,7 @@ class SharedFlowTest : TestBase() { @Test fun testRendezvousSharedFlowReset() = runTest { expect(1) - val sh = MutableSharedFlow(0) + val sh = MutableSharedFlow() val barrier = Channel(1) val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) @@ -383,7 +383,7 @@ class SharedFlowTest : TestBase() { @Test fun testSynchronousSharedFlowEmitterCancel() = runTest { expect(1) - val sh = MutableSharedFlow(0) + val sh = MutableSharedFlow() val barrier1 = Job() val barrier2 = Job() val barrier3 = Job() @@ -562,7 +562,7 @@ class SharedFlowTest : TestBase() { @Test public fun testOnSubscription() = runTest { expect(1) - val sh = MutableSharedFlow(0) + val sh = MutableSharedFlow() fun share(s: String) { launch(start = CoroutineStart.UNDISPATCHED) { sh.emit(s) } } sh .onSubscription { @@ -747,7 +747,7 @@ class SharedFlowTest : TestBase() { @Test fun testOperatorFusion() { - val sh = MutableSharedFlow(0) + val sh = MutableSharedFlow() assertSame(sh, (sh as Flow<*>).cancellable()) assertSame(sh, (sh as Flow<*>).flowOn(Dispatchers.Default)) assertSame(sh, sh.buffer(Channel.RENDEZVOUS)) diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt index dc1cfa9c67..7d346bdc33 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt @@ -94,7 +94,7 @@ class SharingStressTest : TestBase() { val sharedFlow = if (usingStateFlow) upstream.stateIn(sharingScope, started, 0L) else - upstream.shareIn(sharingScope, replay, started) + upstream.shareIn(sharingScope, started, replay) try { val subscribers = ArrayList() withTimeoutOrNull(testDuration) { From db7e689fb3420e54ffc4dec5429f05e750786fc0 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 13 Oct 2020 13:30:01 +0300 Subject: [PATCH 29/30] ~ Code style --- .../kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt index e8e39d0c7c..04833e9814 100644 --- a/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/PublisherAsFlowTest.kt @@ -253,7 +253,7 @@ class PublisherAsFlowTest : TestBase() { .buffer(capacity, onBufferOverflow) val list = flow.toList() val runSize = if (capacity == Channel.BUFFERED) 1 else capacity - val expected = when(onBufferOverflow) { + val expected = when (onBufferOverflow) { // Everything is expected to be delivered BufferOverflow.SUSPEND -> (1..m).toList() // Only the last one (by default) or the last "capacity" items delivered @@ -263,5 +263,4 @@ class PublisherAsFlowTest : TestBase() { } assertEquals(expected, list) } - } From 5db8a5d2215aa13f14c1abc8178aa84252bbafc9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 13 Oct 2020 13:31:39 +0300 Subject: [PATCH 30/30] ~ Text typo --- kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 8e802c753a..3ffe5fe943 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -58,7 +58,7 @@ internal inline fun Flow.unsafeTransform( * Returns a flow that invokes the given [action] **before** this flow starts to be collected. * * The [action] is called before the upstream flow is started, so if it is used with a [SharedFlow] - * there is **no guarantee** that emissions to the upstream flow that happen inside or immediately + * there is **no guarantee** that emissions from the upstream flow that happen inside or immediately * after this `onStart` action will be collected * (see [onSubscription] for an alternative operator on shared flows). *