From 3fdd3fe70489f7ee8bca9b690eff5f074a939ce1 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 19 Mar 2020 10:09:28 +0300 Subject: [PATCH 001/257] Refactor select impl to make its internal state easier to understand The representation of not selected state is changed from _state === this to _state === NOT_SELECTED (a special marker). --- .../common/src/selects/Select.kt | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 1df5c62bf5..1b2be7b389 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -211,6 +211,8 @@ public suspend inline fun select(crossinline builder: SelectBuilder.() -> } +@SharedImmutable +internal val NOT_SELECTED: Any = Symbol("NOT_SELECTED") @SharedImmutable internal val ALREADY_SELECTED: Any = Symbol("ALREADY_SELECTED") @SharedImmutable @@ -240,8 +242,8 @@ internal class SelectBuilderImpl( override fun getStackTraceElement(): StackTraceElement? = null - // selection state is "this" (list of nodes) initially and is replaced by idempotent marker (or null) when selected - private val _state = atomic(this) + // selection state is NOT_SELECTED initially and is replaced by idempotent marker (or null) when selected + private val _state = atomic(NOT_SELECTED) // this is basically our own SafeContinuation private val _result = atomic(UNDECIDED) @@ -360,7 +362,7 @@ internal class SelectBuilderImpl( override val isSelected: Boolean get() = _state.loop { state -> when { - state === this -> return false + state === NOT_SELECTED -> return false state is OpDescriptor -> state.perform(this) // help else -> return true // already selected } @@ -483,14 +485,14 @@ internal class SelectBuilderImpl( _state.loop { state -> // lock-free loop on state when { // Found initial state (not selected yet) -- try to make it selected - state === this -> { + state === NOT_SELECTED -> { if (otherOp == null) { // regular trySelect -- just mark as select - if (!_state.compareAndSet(this, null)) return@loop + if (!_state.compareAndSet(NOT_SELECTED, null)) return@loop } else { // Rendezvous with another select instance -- install PairSelectOp val pairSelectOp = PairSelectOp(otherOp) - if (!_state.compareAndSet(this, pairSelectOp)) return@loop + if (!_state.compareAndSet(NOT_SELECTED, pairSelectOp)) return@loop val decision = pairSelectOp.perform(this) if (decision !== null) return decision } @@ -546,7 +548,7 @@ internal class SelectBuilderImpl( // we must finish preparation of another operation before attempting to reach decision to select otherOp.finishPrepare() val decision = otherOp.atomicOp.decide(null) // try decide for success of operation - val update: Any = if (decision == null) otherOp.desc else impl + val update: Any = if (decision == null) otherOp.desc else NOT_SELECTED impl._state.compareAndSet(this, update) return decision } @@ -558,10 +560,7 @@ internal class SelectBuilderImpl( override fun performAtomicTrySelect(desc: AtomicDesc): Any? = AtomicSelectOp(this, desc).perform(null) - override fun toString(): String { - val state = _state.value - return "SelectInstance(state=${if (state === this) "this" else state.toString()}, result=${_result.value})" - } + override fun toString(): String = "SelectInstance(state=${_state.value}, result=${_result.value})" private class AtomicSelectOp( @JvmField val impl: SelectBuilderImpl<*>, @@ -600,8 +599,8 @@ internal class SelectBuilderImpl( when { state === this -> return null // already in progress state is OpDescriptor -> state.perform(impl) // help - state === impl -> { - if (impl._state.compareAndSet(impl, this)) + state === NOT_SELECTED -> { + if (impl._state.compareAndSet(NOT_SELECTED, this)) return null // success } else -> return ALREADY_SELECTED @@ -611,12 +610,12 @@ internal class SelectBuilderImpl( // reverts the change done by prepareSelectedOp private fun undoPrepare() { - impl._state.compareAndSet(this, impl) + impl._state.compareAndSet(this, NOT_SELECTED) } private fun completeSelect(failure: Any?) { val selectSuccess = failure == null - val update = if (selectSuccess) null else impl + val update = if (selectSuccess) null else NOT_SELECTED if (impl._state.compareAndSet(this, update)) { if (selectSuccess) impl.doAfterSelect() From 5b610bb1d9a8b66de0f1eb9cdcc82af4c617a54d Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Fri, 27 Mar 2020 17:41:20 +0300 Subject: [PATCH 002/257] Fix publication validator not looking in the local maven repository (#1880) During the change of the publication validator, a bug was introduced that led to MavenPublicationValidator being run not on the artifacts from the local Maven repository but on classfiles from the corresponding subproject. This is a problem because this test is for the behavior of the atomicfu plugin, which could in theory produce nice classfiles in one place but wrong ones in the other, and the only important thing to test is whether the published classfiles are good. Now, this is fixed. --- publication-validator/build.gradle | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/publication-validator/build.gradle b/publication-validator/build.gradle index a22ccf46d2..c94b317f4f 100644 --- a/publication-validator/build.gradle +++ b/publication-validator/build.gradle @@ -4,6 +4,8 @@ apply from: rootProject.file("gradle/compile-jvm.gradle") +def coroutines_version = version // all modules share the same version + repositories { mavenLocal() mavenCentral() @@ -14,8 +16,6 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.apache.commons:commons-compress:1.18' testCompile 'com.google.code.gson:gson:2.8.5' - testCompile project(':kotlinx-coroutines-core') - testCompile project(':kotlinx-coroutines-android') } compileTestKotlin { @@ -26,7 +26,14 @@ def dryRunNpm = properties['dryRun'] test { onlyIf { dryRunNpm == "true" } // so that we don't accidentally publish anything, especially before the test - doFirst { println "Verifying publishing version $version" } // all modules share the same version + doFirst { + println "Verifying publishing version $coroutines_version" + // we can't depend on the subprojects because we need to test the classfiles that are published in the end. + // also, we can't put this in the `dependencies` block because the resolution would happen before publication. + classpath += project.configurations.detachedConfiguration( + project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"), + project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")) + } environment "projectRoot", project.rootDir environment "deployVersion", version if (dryRunNpm == "true") { // `onlyIf` only affects execution of the task, not the dependency subtree From 9cbad7d41a059ca742c7de15efca3f4b38911a97 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 27 Mar 2020 17:43:45 +0300 Subject: [PATCH 003/257] Enable strict explicit mode (#1877) --- .../benchmarks/common/BenchmarkUtils.kt | 2 +- gradle/compile-common.gradle | 5 +++++ gradle/compile-jvm.gradle | 6 +++++ .../kotlinx-coroutines-jdk8/src/time/Time.kt | 9 ++++---- .../src/MDCContext.kt | 2 +- js/js-stub/src/Performance.kt | 2 +- .../common/src/AbstractCoroutine.kt | 2 +- .../common/src/CancellableContinuation.kt | 2 +- .../common/src/CompletableDeferred.kt | 3 ++- .../common/src/CoroutineDispatcher.kt | 4 ++-- .../common/src/CoroutineStart.kt | 22 +++++++++---------- kotlinx-coroutines-core/common/src/Delay.kt | 8 +++---- kotlinx-coroutines-core/common/src/Job.kt | 12 +++++----- .../common/src/JobSupport.kt | 2 +- .../common/src/channels/Broadcast.kt | 2 +- .../common/src/channels/Channel.kt | 20 ++++++++--------- .../common/src/channels/Channels.common.kt | 4 ++-- .../src/channels/ConflatedBroadcastChannel.kt | 2 +- .../common/src/channels/Produce.kt | 2 +- .../common/src/flow/Channels.kt | 2 +- .../common/src/flow/Migration.kt | 2 +- .../common/src/flow/internal/ChannelFlow.kt | 12 +++++----- .../src/flow/internal/SendingCollector.kt | 2 +- .../common/src/flow/operators/Emitters.kt | 2 +- .../common/src/flow/operators/Merge.kt | 4 ++-- .../common/src/flow/terminal/Collect.kt | 4 ++-- .../common/src/internal/Atomic.kt | 1 + .../src/internal/DispatchedContinuation.kt | 2 +- .../src/internal/LockFreeLinkedList.common.kt | 1 + .../src/internal/MainDispatcherFactory.kt | 6 ++--- .../common/src/internal/ThreadSafeHeap.kt | 4 ++-- .../common/src/intrinsics/Cancellable.kt | 2 +- .../common/src/selects/Select.kt | 4 ++-- kotlinx-coroutines-core/js/src/Exceptions.kt | 2 +- .../js/src/internal/LinkedList.kt | 3 +-- kotlinx-coroutines-core/jvm/src/Debug.kt | 8 +++---- .../jvm/src/Dispatchers.kt | 2 +- kotlinx-coroutines-core/jvm/src/Future.kt | 2 +- .../jvm/src/ThreadPoolDispatcher.kt | 4 ++-- .../jvm/src/channels/Actor.kt | 2 +- .../jvm/src/channels/TickerChannels.kt | 3 ++- .../jvm/src/internal/LockFreeLinkedList.kt | 3 ++- .../jvm/src/internal/StackTraceRecovery.kt | 4 ++-- .../jvm/src/scheduling/Dispatcher.kt | 8 +++---- .../jvm/src/test_/TestCoroutineContext.kt | 8 +++---- kotlinx-coroutines-core/native/src/Debug.kt | 2 +- .../native/src/Exceptions.kt | 2 +- .../native/src/internal/LinkedList.kt | 1 + kotlinx-coroutines-debug/src/CoroutineInfo.kt | 4 ++-- kotlinx-coroutines-debug/src/DebugProbes.kt | 2 +- .../src/junit4/CoroutinesTimeout.kt | 8 +++++-- kotlinx-coroutines-test/src/TestBuilders.kt | 6 +++-- .../src/TestCoroutineDispatcher.kt | 4 ++-- .../src/TestCoroutineExceptionHandler.kt | 2 +- .../src/ReactiveFlow.kt | 2 +- .../src/Channel.kt | 4 ++-- .../src/Publish.kt | 2 +- .../src/ReactiveFlow.kt | 4 ++-- .../src/ReactorContext.kt | 2 +- .../src/Scheduler.kt | 2 +- .../kotlinx-coroutines-rx2/src/RxChannel.kt | 8 +++---- stdlib-stubs/src/ContinuationInterceptor.kt | 2 +- stdlib-stubs/src/Result.kt | 2 +- .../src/JavaFxDispatcher.kt | 2 +- .../src/SwingDispatcher.kt | 2 +- 65 files changed, 146 insertions(+), 125 deletions(-) diff --git a/benchmarks/src/main/kotlin/benchmarks/common/BenchmarkUtils.kt b/benchmarks/src/main/kotlin/benchmarks/common/BenchmarkUtils.kt index 0057573bc6..27bc6b7d85 100644 --- a/benchmarks/src/main/kotlin/benchmarks/common/BenchmarkUtils.kt +++ b/benchmarks/src/main/kotlin/benchmarks/common/BenchmarkUtils.kt @@ -6,7 +6,7 @@ package benchmarks.common import java.util.concurrent.* -fun doGeomDistrWork(work: Int) { +public fun doGeomDistrWork(work: Int) { // We use geometric distribution here. We also checked on macbook pro 13" (2017) that the resulting work times // are distributed geometrically, see https://github.com/Kotlin/kotlinx.coroutines/pull/1464#discussion_r355705325 val p = 1.0 / work diff --git a/gradle/compile-common.gradle b/gradle/compile-common.gradle index 403c3345d0..bee61429df 100644 --- a/gradle/compile-common.gradle +++ b/gradle/compile-common.gradle @@ -12,3 +12,8 @@ kotlin.sourceSets { api "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version" } } + + +kotlin.sourceSets.matching({ it.name.contains("Main") }).all { srcSet -> + project.ext.set("kotlin.mpp.freeCompilerArgsForSourceSet.${srcSet.name}", "-Xexplicit-api=strict") +} diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index 261136bc87..a8116595f5 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -23,6 +23,12 @@ repositories { maven { url "https://dl.bintray.com/devexperts/Maven/" } } +compileKotlin { + kotlinOptions { + freeCompilerArgs += ['-Xexplicit-api=strict'] + } +} + tasks.withType(Test) { testLogging { showStandardStreams = true diff --git a/integration/kotlinx-coroutines-jdk8/src/time/Time.kt b/integration/kotlinx-coroutines-jdk8/src/time/Time.kt index 56f1e26d85..807a3bbc3b 100644 --- a/integration/kotlinx-coroutines-jdk8/src/time/Time.kt +++ b/integration/kotlinx-coroutines-jdk8/src/time/Time.kt @@ -12,25 +12,24 @@ import java.time.temporal.* /** * "java.time" adapter method for [kotlinx.coroutines.delay]. */ -public suspend fun delay(duration: Duration) = - kotlinx.coroutines.delay(duration.coerceToMillis()) +public suspend fun delay(duration: Duration): Unit = delay(duration.coerceToMillis()) /** * "java.time" adapter method for [kotlinx.coroutines.flow.debounce]. */ @FlowPreview -public fun Flow.debounce(timeout: Duration) = debounce(timeout.coerceToMillis()) +public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.coerceToMillis()) /** * "java.time" adapter method for [kotlinx.coroutines.flow.sample]. */ @FlowPreview -public fun Flow.sample(period: Duration) = sample(period.coerceToMillis()) +public fun Flow.sample(period: Duration): Flow = sample(period.coerceToMillis()) /** * "java.time" adapter method for [SelectBuilder.onTimeout]. */ -public fun SelectBuilder.onTimeout(duration: Duration, block: suspend () -> R) = +public fun SelectBuilder.onTimeout(duration: Duration, block: suspend () -> R): Unit = onTimeout(duration.coerceToMillis(), block) /** diff --git a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt index 6dbcef6ebe..078800d0ee 100644 --- a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt +++ b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt @@ -48,7 +48,7 @@ public class MDCContext( /** * Key of [MDCContext] in [CoroutineContext]. */ - companion object Key : CoroutineContext.Key + public companion object Key : CoroutineContext.Key /** @suppress */ override fun updateThreadContext(context: CoroutineContext): MDCContextMap { diff --git a/js/js-stub/src/Performance.kt b/js/js-stub/src/Performance.kt index 353a08fff8..0b85c93df0 100644 --- a/js/js-stub/src/Performance.kt +++ b/js/js-stub/src/Performance.kt @@ -5,5 +5,5 @@ package org.w3c.performance public abstract class Performance { - abstract fun now(): Double + public abstract fun now(): Double } diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt index 22111f0ce2..742c9670b7 100644 --- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt @@ -113,7 +113,7 @@ public abstract class AbstractCoroutine( afterResume(state) } - protected open fun afterResume(state: Any?) = afterCompletion(state) + protected open fun afterResume(state: Any?): Unit = afterCompletion(state) internal final override fun handleOnCompletionException(exception: Throwable) { handleCoroutineException(context, exception) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index fd5cd083e2..e01dc82521 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -288,7 +288,7 @@ internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinke * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHandle) = +public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHandle): Unit = invokeOnCancellation(handler = DisposeOnCancel(handle).asHandler) // --------------- implementation details --------------- diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index f6cf90d515..cba8a63929 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -55,7 +55,8 @@ public interface CompletableDeferred : Deferred { * [CompletableDeferred.completeExceptionally]. */ @ExperimentalCoroutinesApi // since 1.3.2, tentatively until 1.4.0 -public fun CompletableDeferred.completeWith(result: Result) = result.fold({ complete(it) }, { completeExceptionally(it) }) +public fun CompletableDeferred.completeWith(result: Result): Boolean = + result.fold({ complete(it) }, { completeExceptionally(it) }) /** * Creates a [CompletableDeferred] in an _active_ state. diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index fe4c263e18..1b6e7eb00f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -87,7 +87,7 @@ public abstract class CoroutineDispatcher : * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi - public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block) + public open fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = dispatch(context, block) /** * Returns a continuation that wraps the provided [continuation], thus intercepting all resumptions. @@ -115,7 +115,7 @@ public abstract class CoroutineDispatcher : "The dispatcher to the right of `+` just replaces the dispatcher to the left.", level = DeprecationLevel.ERROR ) - public operator fun plus(other: CoroutineDispatcher) = other + public operator fun plus(other: CoroutineDispatcher): CoroutineDispatcher = other /** @suppress for nicer debugging */ override fun toString(): String = "$classSimpleName@$hexAddress" diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 1272ce7c3a..05e80e3ed7 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -1,7 +1,7 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - +@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines import kotlinx.coroutines.CoroutineStart.* @@ -85,12 +85,12 @@ public enum class CoroutineStart { * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi - public operator fun invoke(block: suspend () -> T, completion: Continuation) = + public operator fun invoke(block: suspend () -> T, completion: Continuation): Unit = when (this) { - CoroutineStart.DEFAULT -> block.startCoroutineCancellable(completion) - CoroutineStart.ATOMIC -> block.startCoroutine(completion) - CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(completion) - CoroutineStart.LAZY -> Unit // will start lazily + DEFAULT -> block.startCoroutineCancellable(completion) + ATOMIC -> block.startCoroutine(completion) + UNDISPATCHED -> block.startCoroutineUndispatched(completion) + LAZY -> Unit // will start lazily } /** @@ -104,12 +104,12 @@ public enum class CoroutineStart { * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi - public operator fun invoke(block: suspend R.() -> T, receiver: R, completion: Continuation) = + public operator fun invoke(block: suspend R.() -> T, receiver: R, completion: Continuation): Unit = when (this) { - CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion) - CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion) - CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion) - CoroutineStart.LAZY -> Unit // will start lazily + DEFAULT -> block.startCoroutineCancellable(receiver, completion) + ATOMIC -> block.startCoroutine(receiver, completion) + UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion) + LAZY -> Unit // will start lazily } /** diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index cc205ecaf0..ab80912269 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -25,7 +25,7 @@ public interface Delay { * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. */ - suspend fun delay(time: Long) { + public suspend fun delay(time: Long) { if (time <= 0) return // don't delay return suspendCancellableCoroutine { scheduleResumeAfterDelay(time, it) } } @@ -45,7 +45,7 @@ public interface Delay { * with(continuation) { resumeUndispatchedWith(Unit) } * ``` */ - fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) + public fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) /** * Schedules invocation of a specified [block] after a specified delay [timeMillis]. @@ -54,7 +54,7 @@ public interface Delay { * * This implementation uses a built-in single-threaded scheduled executor service. */ - fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + public fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = DefaultDelay.invokeOnTimeout(timeMillis, block) } @@ -87,7 +87,7 @@ public suspend fun delay(timeMillis: Long) { * Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context. */ @ExperimentalTime -public suspend fun delay(duration: Duration) = delay(duration.toDelayMillis()) +public suspend fun delay(duration: Duration): Unit = delay(duration.toDelayMillis()) /** Returns [Delay] implementation of the given context */ internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index 4d4e37ee25..4dd783ca21 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -167,7 +167,7 @@ public interface Job : CoroutineContext.Element { * @suppress This method implements old version of JVM ABI. Use [cancel]. */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") - public fun cancel() = cancel(null) + public fun cancel(): Unit = cancel(null) /** * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. @@ -337,7 +337,7 @@ public interface Job : CoroutineContext.Element { "Job is a coroutine context element and `+` is a set-sum operator for coroutine contexts. " + "The job to the right of `+` just replaces the job the left of `+`.", level = DeprecationLevel.ERROR) - public operator fun plus(other: Job) = other + public operator fun plus(other: Job): Job = other } /** @@ -382,7 +382,7 @@ public interface DisposableHandle { */ @Suppress("FunctionName") @InternalCoroutinesApi -public inline fun DisposableHandle(crossinline block: () -> Unit) = +public inline fun DisposableHandle(crossinline block: () -> Unit): DisposableHandle = object : DisposableHandle { override fun dispose() { block() @@ -496,7 +496,7 @@ public fun Job.cancelChildren(cause: CancellationException? = null) { * @suppress This method implements old version of JVM ABI. Use [cancel]. */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") -public fun Job.cancelChildren() = cancelChildren(null) +public fun Job.cancelChildren(): Unit = cancelChildren(null) /** * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [Job.cancelChildren]. @@ -539,7 +539,7 @@ public fun CoroutineContext.cancel(cause: CancellationException? = null) { * @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancel]. */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") -public fun CoroutineContext.cancel() = cancel(null) +public fun CoroutineContext.cancel(): Unit = cancel(null) /** * Ensures that current job is [active][Job.isActive]. @@ -605,7 +605,7 @@ public fun CoroutineContext.cancelChildren(cause: CancellationException? = null) * @suppress This method implements old version of JVM ABI. Use [CoroutineContext.cancelChildren]. */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") -public fun CoroutineContext.cancelChildren() = cancelChildren(null) +public fun CoroutineContext.cancelChildren(): Unit = cancelChildren(null) /** * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancelChildren]. diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index e52aaeaa8e..020d00a32c 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -652,7 +652,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren * Makes this [Job] cancelled with a specified [cause]. * It is used in [AbstractCoroutine]-derived classes when there is an internal failure. */ - public fun cancelCoroutine(cause: Throwable?) = cancelImpl(cause) + public fun cancelCoroutine(cause: Throwable?): Boolean = cancelImpl(cause) // cause is Throwable or ParentJob when cancelChild was invoked // returns true is exception was handled, false otherwise diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index e2891c9500..863d1387fc 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -39,7 +39,7 @@ import kotlin.native.concurrent.* * * @param start coroutine start option. The default value is [CoroutineStart.LAZY]. */ -fun ReceiveChannel.broadcast( +public fun ReceiveChannel.broadcast( capacity: Int = 1, start: CoroutineStart = CoroutineStart.LAZY ): BroadcastChannel { diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index 8dff4ec2b7..c4b4a9b25e 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -7,15 +7,15 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* +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.RENDEZVOUS import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.channels.Channel.Factory.BUFFERED -import kotlinx.coroutines.channels.Channel.Factory.CHANNEL_DEFAULT_CAPACITY -import kotlinx.coroutines.internal.systemProp +import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* -import kotlin.jvm.* import kotlin.internal.* +import kotlin.jvm.* /** * Sender's interface to [Channel]. @@ -314,7 +314,7 @@ public interface ReceiveChannel { * @suppress This method implements old version of JVM ABI. Use [cancel]. */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") - public fun cancel() = cancel(null) + public fun cancel(): Unit = cancel(null) /** * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [cancel]. @@ -517,17 +517,17 @@ public interface Channel : SendChannel, ReceiveChannel { /** * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function */ - public const val UNLIMITED = Int.MAX_VALUE + public const val UNLIMITED: Int = Int.MAX_VALUE /** * Requests a rendezvous channel in the `Channel(...)` factory function — a `RendezvousChannel` gets created. */ - public const val RENDEZVOUS = 0 + public const val RENDEZVOUS: Int = 0 /** * Requests a conflated channel in the `Channel(...)` factory function — a `ConflatedChannel` gets created. */ - public const val CONFLATED = -1 + public const val CONFLATED: Int = -1 /** * Requests a buffered channel with the default buffer capacity in the `Channel(...)` factory function — @@ -535,7 +535,7 @@ public interface Channel : SendChannel, ReceiveChannel { * The default capacity is 64 and can be overridden by setting * [DEFAULT_BUFFER_PROPERTY_NAME] on JVM. */ - public const val BUFFERED = -2 + public const val BUFFERED: Int = -2 // only for internal use, cannot be used with Channel(...) internal const val OPTIONAL_CHANNEL = -3 @@ -544,7 +544,7 @@ public interface Channel : SendChannel, ReceiveChannel { * Name of the property that defines the default channel capacity when * [BUFFERED] is used as parameter in `Channel(...)` factory function. */ - public const val DEFAULT_BUFFER_PROPERTY_NAME = "kotlinx.coroutines.channels.defaultBuffer" + public const val DEFAULT_BUFFER_PROPERTY_NAME: String = "kotlinx.coroutines.channels.defaultBuffer" internal val CHANNEL_DEFAULT_CAPACITY = systemProp(DEFAULT_BUFFER_PROPERTY_NAME, 64, 1, UNLIMITED - 1 diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index 4a73d5d59d..8c61928aa4 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -84,7 +84,7 @@ public fun ReceiveChannel.onReceiveOrNull(): SelectClause1 { * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @ObsoleteCoroutinesApi -public suspend inline fun BroadcastChannel.consumeEach(action: (E) -> Unit) = +public suspend inline fun BroadcastChannel.consumeEach(action: (E) -> Unit): Unit = consume { for (element in this) action(element) } @@ -175,7 +175,7 @@ public inline fun ReceiveChannel.consume(block: ReceiveChannel.() - * This function [consumes][ReceiveChannel.consume] all elements of the original [ReceiveChannel]. */ @ExperimentalCoroutinesApi // since 1.3.0, tentatively graduates in 1.4.0 -public suspend inline fun ReceiveChannel.consumeEach(action: (E) -> Unit) = +public suspend inline fun ReceiveChannel.consumeEach(action: (E) -> Unit): Unit = consume { for (e in this) action(e) } diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 4990c933ec..9e093b77ec 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -37,7 +37,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { * It is as a shortcut to creating an instance with a default constructor and * immediately sending an element: `ConflatedBroadcastChannel().apply { offer(value) }`. */ - constructor(value: E) : this() { + public constructor(value: E) : this() { _state.lazySet(State(value, null)) } diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 24fd399bb7..1b1581a99e 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -22,7 +22,7 @@ public interface ProducerScope : CoroutineScope, SendChannel { * All the [SendChannel] functions on this interface delegate to * the channel instance returned by this property. */ - val channel: SendChannel + public val channel: SendChannel } /** diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index e3a64b9568..130ffc72dc 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * See [consumeEach][ReceiveChannel.consumeEach]. */ @ExperimentalCoroutinesApi // since version 1.3.0 -public suspend fun FlowCollector.emitAll(channel: ReceiveChannel) = +public suspend fun FlowCollector.emitAll(channel: ReceiveChannel): Unit = emitAllImpl(channel, consume = true) private suspend fun FlowCollector.emitAllImpl(channel: ReceiveChannel, consume: Boolean) { diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 16bde89f44..bdc205e082 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -4,7 +4,7 @@ @file:JvmMultifileClass @file:JvmName("FlowKt") -@file:Suppress("unused", "DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER") +@file:Suppress("unused", "DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER", "NO_EXPLICIT_RETURN_TYPE_IN_API_MODE") package kotlinx.coroutines.flow diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt index 8a18bff30e..f2f748b2ff 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -23,9 +23,9 @@ internal fun Flow.asChannelFlow(): ChannelFlow = @InternalCoroutinesApi public abstract class ChannelFlow( // upstream context - @JvmField val context: CoroutineContext, + @JvmField public val context: CoroutineContext, // buffer capacity between upstream and downstream context - @JvmField val capacity: Int + @JvmField public val capacity: Int ) : Flow { // shared code to create a suspend lambda from collectTo function in one place @@ -65,7 +65,7 @@ public abstract class ChannelFlow( protected abstract suspend fun collectTo(scope: ProducerScope) - open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel = + public open fun broadcastImpl(scope: CoroutineScope, start: CoroutineStart): BroadcastChannel = scope.broadcast(context, produceCapacity, start, block = collectToFun) /** @@ -76,15 +76,15 @@ public abstract class ChannelFlow( * handlers, while the pipeline before does not, because it was cancelled during its dispatch. * Thus `onCompletion` and `finally` blocks won't be executed and it may lead to a different kinds of memory leaks. */ - open fun produceImpl(scope: CoroutineScope): ReceiveChannel = + public open fun produceImpl(scope: CoroutineScope): ReceiveChannel = scope.produce(context, produceCapacity, start = CoroutineStart.ATOMIC, block = collectToFun) - override suspend fun collect(collector: FlowCollector) = + override suspend fun collect(collector: FlowCollector): Unit = coroutineScope { collector.emitAll(produceImpl(this)) } - open fun additionalToStringProps() = "" + public open fun additionalToStringProps(): String = "" // debug toString override fun toString(): String = diff --git a/kotlinx-coroutines-core/common/src/flow/internal/SendingCollector.kt b/kotlinx-coroutines-core/common/src/flow/internal/SendingCollector.kt index 1620a2ac7c..c2abafd2fd 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/SendingCollector.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/SendingCollector.kt @@ -16,5 +16,5 @@ import kotlinx.coroutines.flow.* public class SendingCollector( private val channel: SendChannel ) : FlowCollector { - override suspend fun emit(value: T) = channel.send(value) + override suspend fun emit(value: T): Unit = channel.send(value) } diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 9236fd26d8..d8016e018d 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -164,7 +164,7 @@ private class ThrowingCollector(private val e: Throwable) : FlowCollector // It was only released in 1.3.0-M2, remove in 1.4.0 /** @suppress */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "binary compatibility with a version w/o FlowCollector receiver") -public fun Flow.onCompletion(action: suspend (cause: Throwable?) -> Unit) = +public fun Flow.onCompletion(action: suspend (cause: Throwable?) -> Unit): Flow = onCompletion { action(it) } private suspend fun FlowCollector.invokeSafely( diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index 85fe90b91f..340d8e313e 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -19,14 +19,14 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * Name of the property that defines the value of [DEFAULT_CONCURRENCY]. */ @FlowPreview -public const val DEFAULT_CONCURRENCY_PROPERTY_NAME = "kotlinx.coroutines.flow.defaultConcurrency" +public const val DEFAULT_CONCURRENCY_PROPERTY_NAME: String = "kotlinx.coroutines.flow.defaultConcurrency" /** * Default concurrency limit that is used by [flattenMerge] and [flatMapMerge] operators. * It is 16 by default and can be changed on JVM using [DEFAULT_CONCURRENCY_PROPERTY_NAME] property. */ @FlowPreview -public val DEFAULT_CONCURRENCY = systemProp(DEFAULT_CONCURRENCY_PROPERTY_NAME, +public val DEFAULT_CONCURRENCY: Int = systemProp(DEFAULT_CONCURRENCY_PROPERTY_NAME, 16, 1, Int.MAX_VALUE ) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index 52d060f255..9d463df073 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -27,7 +27,7 @@ import kotlin.jvm.* * .collect() // trigger collection of the flow * ``` */ -public suspend fun Flow<*>.collect() = collect(NopCollector) +public suspend fun Flow<*>.collect(): Unit = collect(NopCollector) /** * Terminal flow operator that [launches][launch] the [collection][collect] of the given flow in the [scope]. @@ -132,4 +132,4 @@ public suspend fun Flow.collectLatest(action: suspend (value: T) -> Unit) */ @BuilderInference @ExperimentalCoroutinesApi -public suspend inline fun FlowCollector.emitAll(flow: Flow) = flow.collect(this) +public suspend inline fun FlowCollector.emitAll(flow: Flow): Unit = flow.collect(this) diff --git a/kotlinx-coroutines-core/common/src/internal/Atomic.kt b/kotlinx-coroutines-core/common/src/internal/Atomic.kt index 56fd35b731..94f6ab9cf2 100644 --- a/kotlinx-coroutines-core/common/src/internal/Atomic.kt +++ b/kotlinx-coroutines-core/common/src/internal/Atomic.kt @@ -1,6 +1,7 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.internal diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index 50758146af..cf31fcf07d 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -245,7 +245,7 @@ internal class DispatchedContinuation( * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public fun Continuation.resumeCancellableWith(result: Result) = when (this) { +public fun Continuation.resumeCancellableWith(result: Result): Unit = when (this) { is DispatchedContinuation -> resumeCancellableWith(result) else -> resumeWith(result) } diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index 216ce7b56b..f1663c3ddc 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -1,6 +1,7 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.internal diff --git a/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt b/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt index 93142df2cd..c3587af6e1 100644 --- a/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt +++ b/kotlinx-coroutines-core/common/src/internal/MainDispatcherFactory.kt @@ -9,16 +9,16 @@ import kotlinx.coroutines.* /** @suppress */ @InternalCoroutinesApi // Emulating DI for Kotlin object's public interface MainDispatcherFactory { - val loadPriority: Int // higher priority wins + public val loadPriority: Int // higher priority wins /** * Creates the main dispatcher. [allFactories] parameter contains all factories found by service loader. * This method is not guaranteed to be idempotent. */ - fun createDispatcher(allFactories: List): MainCoroutineDispatcher + public fun createDispatcher(allFactories: List): MainCoroutineDispatcher /** * Hint used along with error message when the factory failed to create a dispatcher. */ - fun hintOnError(): String? = null + public fun hintOnError(): String? = null } diff --git a/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt b/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt index 12d6a38a81..df55c28f13 100644 --- a/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt +++ b/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt @@ -32,7 +32,7 @@ public open class ThreadSafeHeap : SynchronizedObject() where T: ThreadSafeHe public val isEmpty: Boolean get() = size == 0 - public fun clear() = synchronized(this) { + public fun clear(): Unit = synchronized(this) { a?.fill(null) _size.value = 0 } @@ -57,7 +57,7 @@ public open class ThreadSafeHeap : SynchronizedObject() where T: ThreadSafeHe } } - public fun addLast(node: T) = synchronized(this) { addImpl(node) } + public fun addLast(node: T): Unit = synchronized(this) { addImpl(node) } // @Synchronized // NOTE! NOTE! NOTE! inline fun cannot be @Synchronized // Condition also receives current first node in the heap diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt index 0951349ebd..1b1c389dc4 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt @@ -13,7 +13,7 @@ import kotlin.coroutines.intrinsics.* * while waiting to be dispatched. */ @InternalCoroutinesApi -public fun (suspend () -> T).startCoroutineCancellable(completion: Continuation) = runSafely(completion) { +public fun (suspend () -> T).startCoroutineCancellable(completion: Continuation): Unit = runSafely(completion) { createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit)) } diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 1b2be7b389..0595341f92 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -39,7 +39,7 @@ public interface SelectBuilder { * Registers clause in this [select] expression with additional nullable parameter of type [P] * with the `null` value for this parameter that selects value of type [Q]. */ - public operator fun SelectClause2.invoke(block: suspend (Q) -> R) = invoke(null, block) + public operator fun SelectClause2.invoke(block: suspend (Q) -> R): Unit = invoke(null, block) /** * Clause that selects the given [block] after a specified timeout passes. @@ -61,7 +61,7 @@ public interface SelectBuilder { */ @ExperimentalCoroutinesApi @ExperimentalTime -public fun SelectBuilder.onTimeout(timeout: Duration, block: suspend () -> R) = +public fun SelectBuilder.onTimeout(timeout: Duration, block: suspend () -> R): Unit = onTimeout(timeout.toDelayMillis(), block) /** diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/js/src/Exceptions.kt index 39b3344ac1..c82199a429 100644 --- a/kotlinx-coroutines-core/js/src/Exceptions.kt +++ b/kotlinx-coroutines-core/js/src/Exceptions.kt @@ -14,7 +14,7 @@ public actual open class CancellationException( message: String?, cause: Throwable? ) : IllegalStateException(message, cause) { - actual constructor(message: String?) : this(message, null) + public actual constructor(message: String?) : this(message, null) } /** diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index 7daeef2d94..342b11c69a 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -2,12 +2,11 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("unused") +@file:Suppress("unused", "NO_EXPLICIT_RETURN_TYPE_IN_API_MODE", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.internal private typealias Node = LinkedListNode - /** @suppress **This is unstable API and it is subject to change.** */ @Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS") // :TODO: Remove when fixed: https://youtrack.jetbrains.com/issue/KT-23703 public actual typealias LockFreeLinkedListNode = LinkedListNode diff --git a/kotlinx-coroutines-core/jvm/src/Debug.kt b/kotlinx-coroutines-core/jvm/src/Debug.kt index aac06ade79..8108d235c1 100644 --- a/kotlinx-coroutines-core/jvm/src/Debug.kt +++ b/kotlinx-coroutines-core/jvm/src/Debug.kt @@ -33,7 +33,7 @@ import kotlin.internal.InlineOnly * Debugging facilities are implemented by [newCoroutineContext][CoroutineScope.newCoroutineContext] function that * is used in all coroutine builders to create context of a new coroutine. */ -public const val DEBUG_PROPERTY_NAME = "kotlinx.coroutines.debug" +public const val DEBUG_PROPERTY_NAME: String = "kotlinx.coroutines.debug" /** * Name of the boolean property that controls stacktrace recovery (enabled by default) on JVM. @@ -51,17 +51,17 @@ internal const val STACKTRACE_RECOVERY_PROPERTY_NAME = "kotlinx.coroutines.stack /** * Automatic debug configuration value for [DEBUG_PROPERTY_NAME]. */ -public const val DEBUG_PROPERTY_VALUE_AUTO = "auto" +public const val DEBUG_PROPERTY_VALUE_AUTO: String = "auto" /** * Debug turned on value for [DEBUG_PROPERTY_NAME]. */ -public const val DEBUG_PROPERTY_VALUE_ON = "on" +public const val DEBUG_PROPERTY_VALUE_ON: String = "on" /** * Debug turned on value for [DEBUG_PROPERTY_NAME]. */ -public const val DEBUG_PROPERTY_VALUE_OFF = "off" +public const val DEBUG_PROPERTY_VALUE_OFF: String = "off" // @JvmField: Don't use JvmField here to enable R8 optimizations via "assumenosideeffects" internal val ASSERTIONS_ENABLED = CoroutineId::class.java.desiredAssertionStatus() diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt index b9df7ab2e6..8cd3bb1bae 100644 --- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* /** * Name of the property that defines the maximal number of threads that are used by [Dispatchers.IO] coroutines dispatcher. */ -public const val IO_PARALLELISM_PROPERTY_NAME = "kotlinx.coroutines.io.parallelism" +public const val IO_PARALLELISM_PROPERTY_NAME: String = "kotlinx.coroutines.io.parallelism" /** * Groups various implementations of [CoroutineDispatcher]. diff --git a/kotlinx-coroutines-core/jvm/src/Future.kt b/kotlinx-coroutines-core/jvm/src/Future.kt index 54825c309d..bd16f49af0 100644 --- a/kotlinx-coroutines-core/jvm/src/Future.kt +++ b/kotlinx-coroutines-core/jvm/src/Future.kt @@ -29,7 +29,7 @@ public fun Job.cancelFutureOnCompletion(future: Future<*>): DisposableHandle = * invokeOnCancellation { future.cancel(false) } * ``` */ -public fun CancellableContinuation<*>.cancelFutureOnCancellation(future: Future<*>) = +public fun CancellableContinuation<*>.cancelFutureOnCancellation(future: Future<*>): Unit = invokeOnCancellation(handler = CancelFutureOnCancel(future)) private class CancelFutureOnCompletion( diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt index 7291f0c4fc..a0e1ffa2c7 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt @@ -27,7 +27,7 @@ import kotlin.coroutines.* * @param name the base name of the created thread. */ @ObsoleteCoroutinesApi -fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = +public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = newFixedThreadPoolContext(1, name) /** @@ -49,7 +49,7 @@ fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = * @param name the base name of the created threads. */ @ObsoleteCoroutinesApi -fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher { +public fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher { require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" } return ThreadPoolDispatcher(nThreads, name) } diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index ea48630b12..a905426585 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -25,7 +25,7 @@ public interface ActorScope : CoroutineScope, ReceiveChannel { * All the [ReceiveChannel] functions on this interface delegate to * the channel instance returned by this function. */ - val channel: Channel + public val channel: Channel } /** diff --git a/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt b/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt index 0f9321a58d..b9f39af9cc 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt @@ -13,7 +13,8 @@ import kotlin.coroutines.* * **Note: Ticker channels are not currently integrated with structured concurrency and their api will change in the future.** */ @ObsoleteCoroutinesApi -enum class TickerMode { +@Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE") +public enum class TickerMode { /** * Adjust delay to maintain fixed period if consumer cannot keep up or is otherwise slow. * **This is a default mode.** diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index f718df04b5..29f37dac28 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -1,6 +1,7 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.internal @@ -433,7 +434,7 @@ public actual open class LockFreeLinkedListNode { return null } - public fun finishPrepare() = desc.finishPrepare(this) + public fun finishPrepare(): Unit = desc.finishPrepare(this) override fun toString(): String = "PrepareOp(op=$atomicOp)" } diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index f5c5c24b47..208d3f2e75 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -2,7 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("UNCHECKED_CAST") +@file:Suppress("UNCHECKED_CAST", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.internal @@ -191,7 +191,7 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque.frameIndex(methodName: String) = indexOfFirst { methodName == it.className } diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt index 7398f12fe7..35ebf92d1b 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt @@ -32,20 +32,20 @@ internal object DefaultScheduler : ExperimentalCoroutineDispatcher() { */ // TODO make internal (and rename) after complete integration @InternalCoroutinesApi -open class ExperimentalCoroutineDispatcher( +public open class ExperimentalCoroutineDispatcher( private val corePoolSize: Int, private val maxPoolSize: Int, private val idleWorkerKeepAliveNs: Long, private val schedulerName: String = "CoroutineScheduler" ) : ExecutorCoroutineDispatcher() { - constructor( + public constructor( corePoolSize: Int = CORE_POOL_SIZE, maxPoolSize: Int = MAX_POOL_SIZE, schedulerName: String = DEFAULT_SCHEDULER_NAME ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName) @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN) - constructor( + public constructor( corePoolSize: Int = CORE_POOL_SIZE, maxPoolSize: Int = MAX_POOL_SIZE ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS) @@ -70,7 +70,7 @@ open class ExperimentalCoroutineDispatcher( DefaultExecutor.dispatchYield(context, block) } - override fun close() = coroutineScheduler.close() + override fun close(): Unit = coroutineScheduler.close() override fun toString(): String { return "${super.toString()}[scheduler = $coroutineScheduler]" diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt index 8a0bd1a187..e7c8b6b671 100644 --- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt @@ -33,7 +33,7 @@ import kotlin.coroutines.* @Deprecated("This API has been deprecated to integrate with Structured Concurrency.", ReplaceWith("TestCoroutineScope", "kotlin.coroutines.test"), level = DeprecationLevel.WARNING) -class TestCoroutineContext(private val name: String? = null) : CoroutineContext { +public class TestCoroutineContext(private val name: String? = null) : CoroutineContext { private val uncaughtExceptions = mutableListOf() private val ctxDispatcher = Dispatcher() @@ -80,7 +80,7 @@ class TestCoroutineContext(private val name: String? = null) : CoroutineContext * @param unit The [TimeUnit] in which the clock-time must be returned. * @return The virtual clock-time */ - public fun now(unit: TimeUnit = TimeUnit.MILLISECONDS)= + public fun now(unit: TimeUnit = TimeUnit.MILLISECONDS): Long= unit.convert(time, TimeUnit.NANOSECONDS) /** @@ -105,7 +105,7 @@ class TestCoroutineContext(private val name: String? = null) : CoroutineContext * @param targetTime The point in time to which to move the CoroutineContext's clock. * @param unit The [TimeUnit] in which [targetTime] is expressed. */ - fun advanceTimeTo(targetTime: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) { + public fun advanceTimeTo(targetTime: Long, unit: TimeUnit = TimeUnit.MILLISECONDS) { val nanoTime = unit.toNanos(targetTime) triggerActions(nanoTime) if (nanoTime > time) time = nanoTime @@ -115,7 +115,7 @@ class TestCoroutineContext(private val name: String? = null) : CoroutineContext * Triggers any actions that have not yet been triggered and that are scheduled to be triggered at or * before this CoroutineContext's present virtual clock-time. */ - public fun triggerActions() = triggerActions(time) + public fun triggerActions(): Unit = triggerActions(time) /** * Cancels all not yet triggered actions. Be careful calling this, since it can seriously diff --git a/kotlinx-coroutines-core/native/src/Debug.kt b/kotlinx-coroutines-core/native/src/Debug.kt index 26787f865a..1fa0ec7ff3 100644 --- a/kotlinx-coroutines-core/native/src/Debug.kt +++ b/kotlinx-coroutines-core/native/src/Debug.kt @@ -13,6 +13,6 @@ internal actual val Any.hexAddress: String get() = abs(id().let { if (it == Int. internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown" @SymbolName("Kotlin_Any_hashCode") -external fun Any.id(): Int // Note: can return negative value on K/N +public external fun Any.id(): Int // Note: can return negative value on K/N internal actual inline fun assert(value: () -> Boolean) {} diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt index 39b3344ac1..c82199a429 100644 --- a/kotlinx-coroutines-core/native/src/Exceptions.kt +++ b/kotlinx-coroutines-core/native/src/Exceptions.kt @@ -14,7 +14,7 @@ public actual open class CancellationException( message: String?, cause: Throwable? ) : IllegalStateException(message, cause) { - actual constructor(message: String?) : this(message, null) + public actual constructor(message: String?) : this(message, null) } /** diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt index 60d0857be5..9657830e35 100644 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt @@ -1,6 +1,7 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("NO_EXPLICIT_RETURN_TYPE_IN_API_MODE", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.internal diff --git a/kotlinx-coroutines-debug/src/CoroutineInfo.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt index d92d9b6b8b..10540d1608 100644 --- a/kotlinx-coroutines-debug/src/CoroutineInfo.kt +++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt @@ -2,7 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("PropertyName") +@file:Suppress("PropertyName", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") package kotlinx.coroutines.debug @@ -15,7 +15,7 @@ import kotlin.coroutines.jvm.internal.* */ @ExperimentalCoroutinesApi public class CoroutineInfo internal constructor( - val context: CoroutineContext, + public val context: CoroutineContext, private val creationStackBottom: CoroutineStackFrame?, @JvmField internal val sequenceNumber: Long ) { diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt index f164e7ccdc..710300cb37 100644 --- a/kotlinx-coroutines-debug/src/DebugProbes.kt +++ b/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -135,7 +135,7 @@ public object DebugProbes { * ... * ``` */ - public fun dumpCoroutines(out: PrintStream = System.out) = DebugProbesImpl.dumpCoroutines(out) + public fun dumpCoroutines(out: PrintStream = System.out): Unit = DebugProbesImpl.dumpCoroutines(out) } // Stubs which are injected as coroutine probes. Require direct match of signatures diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt index 0f14ade89c..0510764a0c 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt @@ -42,7 +42,11 @@ public class CoroutinesTimeout( ) : TestRule { @Suppress("UNUSED") // Binary compatibility - constructor(testTimeoutMs: Long, cancelOnTimeout: Boolean = false) : this(testTimeoutMs, cancelOnTimeout, true) + public constructor(testTimeoutMs: Long, cancelOnTimeout: Boolean = false) : this( + testTimeoutMs, + cancelOnTimeout, + true + ) init { require(testTimeoutMs > 0) { "Expected positive test timeout, but had $testTimeoutMs" } @@ -55,7 +59,7 @@ public class CoroutinesTimeout( DebugProbes.install() } - companion object { + public companion object { /** * Creates [CoroutinesTimeout] rule with the given timeout in seconds. */ diff --git a/kotlinx-coroutines-test/src/TestBuilders.kt b/kotlinx-coroutines-test/src/TestBuilders.kt index 3b8efb9fe1..88fa01b7b8 100644 --- a/kotlinx-coroutines-test/src/TestBuilders.kt +++ b/kotlinx-coroutines-test/src/TestBuilders.kt @@ -69,13 +69,15 @@ private fun CoroutineContext.activeJobs(): Set { */ // todo: need documentation on how this extension is supposed to be used @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(coroutineContext, block) +public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = + runBlockingTest(coroutineContext, block) /** * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher]. */ @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(this, block) +public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit = + runBlockingTest(this, block) private fun CoroutineContext.checkArguments(): Pair { // TODO optimize it diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt index aab869c98b..4706d627ef 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt @@ -105,7 +105,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl } /** @suppress */ - override val currentTime get() = _time.value + override val currentTime: Long get() = _time.value /** @suppress */ override fun advanceTimeBy(delayTimeMillis: Long): Long { @@ -136,7 +136,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl } /** @suppress */ - override fun runCurrent() = doActionsUntil(currentTime) + override fun runCurrent(): Unit = doActionsUntil(currentTime) /** @suppress */ override suspend fun pauseDispatcher(block: suspend () -> Unit) { diff --git a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt b/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt index f585aa03ab..ed08fbcdd4 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt @@ -48,7 +48,7 @@ public class TestCoroutineExceptionHandler : } /** @suppress **/ - override val uncaughtExceptions + override val uncaughtExceptions: List get() = synchronized(_exceptions) { _exceptions.toList() } /** @suppress **/ diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index 6568c73a4a..89caf82c54 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -35,5 +35,5 @@ public fun Flow.asPublisher(): JFlow.Publisher { * Subscribes to this [Publisher] and performs the specified action for each received element. * Cancels subscription if any exception happens during collect. */ -public suspend inline fun JFlow.Publisher.collect(action: (T) -> Unit) = +public suspend inline fun JFlow.Publisher.collect(action: (T) -> Unit): Unit = FlowAdapters.toPublisher(this).collect(action) diff --git a/reactive/kotlinx-coroutines-reactive/src/Channel.kt b/reactive/kotlinx-coroutines-reactive/src/Channel.kt index 8c8187c130..379fc4ed53 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Channel.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Channel.kt @@ -35,14 +35,14 @@ public fun Publisher.openSubscription(request: Int = 1): ReceiveChannel Publisher.consumeEach(action: (T) -> Unit) = +public suspend inline fun Publisher.consumeEach(action: (T) -> Unit): Unit = openSubscription().consumeEach(action) /** * Subscribes to this [Publisher] and performs the specified action for each received element. * Cancels subscription if any exception happens during collect. */ -public suspend inline fun Publisher.collect(action: (T) -> Unit) = +public suspend inline fun Publisher.collect(action: (T) -> Unit): Unit = openSubscription().consumeEach(action) @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") diff --git a/reactive/kotlinx-coroutines-reactive/src/Publish.kt b/reactive/kotlinx-coroutines-reactive/src/Publish.kt index 68c1702913..ddfd7f8aa3 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Publish.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Publish.kt @@ -93,7 +93,7 @@ public class PublisherCoroutine( override val isClosedForSend: Boolean get() = isCompleted override val isFull: Boolean = mutex.isLocked override fun close(cause: Throwable?): Boolean = cancelCoroutine(cause) - override fun invokeOnClose(handler: (Throwable?) -> Unit) = + override fun invokeOnClose(handler: (Throwable?) -> Unit): Nothing = throw UnsupportedOperationException("PublisherCoroutine doesn't support invokeOnClose") override fun offer(element: T): Boolean { diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index 20e165e705..20e59f6a3a 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -163,8 +163,8 @@ private class FlowAsPublisher(private val flow: Flow) : Publisher /** @suppress */ @InternalCoroutinesApi public class FlowSubscription( - @JvmField val flow: Flow, - @JvmField val subscriber: Subscriber + @JvmField public val flow: Flow, + @JvmField public val subscriber: Subscriber ) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, false) { private val requested = atomic(0L) private val producer = atomic?>(null) diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt index 9f5eb23169..69467ad8b1 100644 --- a/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt +++ b/reactive/kotlinx-coroutines-reactor/src/ReactorContext.kt @@ -49,7 +49,7 @@ import kotlinx.coroutines.reactive.* */ @ExperimentalCoroutinesApi public class ReactorContext(public val context: Context) : AbstractCoroutineContextElement(ReactorContext) { - companion object Key : CoroutineContext.Key + public companion object Key : CoroutineContext.Key } /** diff --git a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt index 833ceb2d6a..e176c07bb9 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt @@ -13,7 +13,7 @@ import kotlin.coroutines.CoroutineContext /** * Converts an instance of [Scheduler] to an implementation of [CoroutineDispatcher]. */ -fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this) +public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this) /** * Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler]. diff --git a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt index 9f31b2aaa8..e253161db0 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt @@ -40,26 +40,26 @@ public fun ObservableSource.openSubscription(): ReceiveChannel { // Will be promoted to error in 1.3.0, removed in 1.4.0 @Deprecated(message = "Use collect instead", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.collect(action)")) -public suspend inline fun MaybeSource.consumeEach(action: (T) -> Unit) = +public suspend inline fun MaybeSource.consumeEach(action: (T) -> Unit): Unit = openSubscription().consumeEach(action) // Will be promoted to error in 1.3.0, removed in 1.4.0 @Deprecated(message = "Use collect instead", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("this.collect(action)")) -public suspend inline fun ObservableSource.consumeEach(action: (T) -> Unit) = +public suspend inline fun ObservableSource.consumeEach(action: (T) -> Unit): Unit = openSubscription().consumeEach(action) /** * Subscribes to this [MaybeSource] and performs the specified action for each received element. * Cancels subscription if any exception happens during collect. */ -public suspend inline fun MaybeSource.collect(action: (T) -> Unit) = +public suspend inline fun MaybeSource.collect(action: (T) -> Unit): Unit = openSubscription().consumeEach(action) /** * Subscribes to this [ObservableSource] and performs the specified action for each received element. * Cancels subscription if any exception happens during collect. */ -public suspend inline fun ObservableSource.collect(action: (T) -> Unit) = +public suspend inline fun ObservableSource.collect(action: (T) -> Unit): Unit = openSubscription().consumeEach(action) @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") diff --git a/stdlib-stubs/src/ContinuationInterceptor.kt b/stdlib-stubs/src/ContinuationInterceptor.kt index 47935e3b03..ebf0a33532 100644 --- a/stdlib-stubs/src/ContinuationInterceptor.kt +++ b/stdlib-stubs/src/ContinuationInterceptor.kt @@ -5,7 +5,7 @@ package kotlin.coroutines // DOKKA STUB public interface ContinuationInterceptor : CoroutineContext.Element { - companion object Key : CoroutineContext.Key + public companion object Key : CoroutineContext.Key public fun interceptContinuation(continuation: Continuation): Continuation public fun releaseInterceptedContinuation(continuation: Continuation<*>): Continuation<*> { return continuation diff --git a/stdlib-stubs/src/Result.kt b/stdlib-stubs/src/Result.kt index 6dc8d9c8f7..611074a795 100644 --- a/stdlib-stubs/src/Result.kt +++ b/stdlib-stubs/src/Result.kt @@ -4,7 +4,7 @@ package kotlin -interface Result { +public interface Result { public val value: T public val isSuccess: Boolean public val isFailure: Boolean diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index ed74ad6a56..5f39bf758c 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -31,7 +31,7 @@ public val Dispatchers.JavaFx: JavaFxDispatcher public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay { /** @suppress */ - override fun dispatch(context: CoroutineContext, block: Runnable) = Platform.runLater(block) + override fun dispatch(context: CoroutineContext, block: Runnable): Unit = Platform.runLater(block) /** @suppress */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt index 3fad55f314..50efe47126 100644 --- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt +++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt @@ -25,7 +25,7 @@ public val Dispatchers.Swing : SwingDispatcher */ public sealed class SwingDispatcher : MainCoroutineDispatcher(), Delay { /** @suppress */ - override fun dispatch(context: CoroutineContext, block: Runnable) = SwingUtilities.invokeLater(block) + override fun dispatch(context: CoroutineContext, block: Runnable): Unit = SwingUtilities.invokeLater(block) /** @suppress */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { From 96b41b4e609e78344cbef67f793ad93fc6bb97d9 Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Mon, 30 Mar 2020 18:54:09 +0300 Subject: [PATCH 004/257] Fix DefaultExecutor not being able to exit (#1876) * Fix DefaultExecutor not being able to exit. * Also adds the performance optimization. See the discussion on the PR. * Add a stress test for the DefaultExecutor worker shutting down. * Make `testDelayChannelBackpressure2` not fail: This test could in theory already fail on the second `checkNotEmpty`: after the first `checkNotEmpty` has passed, first, the ticker channel awakens to produce a new element, and then the main thread awakens to check if it's there. However, if the ticker channel is sufficiently slowed down, it may not produce the element in time for the main thread to find it. After introducing the change that allows the worker thread in `DefaultExecutor` to shut down, the initial delay of 2500 ms is sufficient for the worker to shut down (which, by default, happens after 1000 ms of inactivity), and then the aforementioned race condition worsens: additional time is required to launch a new worker thread and it's much easier to miss the deadline. Now, the delays are much shorter, meaning that the worker thread is never inactive long enough to shut down. Fixes #856 --- .../common/src/EventLoop.common.kt | 10 +++++-- .../jvm/src/DefaultExecutor.kt | 19 ++++++------ .../jvm/test/DefaultExecutorStressTest.kt | 29 ++++++++++++++++++- .../jvm/test/channels/TickerChannelTest.kt | 10 +++---- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt index ba331e20df..69ea9fe312 100644 --- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt +++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt @@ -52,7 +52,7 @@ internal abstract class EventLoop : CoroutineDispatcher() { */ public open fun processNextEvent(): Long { if (!processUnconfinedEvent()) return Long.MAX_VALUE - return nextTime + return 0 } protected open val isEmpty: Boolean get() = isUnconfinedQueueEmpty @@ -251,7 +251,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { override fun processNextEvent(): Long { // unconfined events take priority - if (processUnconfinedEvent()) return nextTime + if (processUnconfinedEvent()) return 0 // queue all delayed tasks that are due to be executed val delayed = _delayed.value if (delayed != null && !delayed.isEmpty) { @@ -269,7 +269,11 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { } } // then process one event from queue - dequeue()?.run() + val task = dequeue() + if (task != null) { + task.run() + return 0 + } return nextTime } diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index 4e107a7b1d..ed84f55e74 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -68,15 +68,13 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { var parkNanos = processNextEvent() if (parkNanos == Long.MAX_VALUE) { // nothing to do, initialize shutdown timeout - if (shutdownNanos == Long.MAX_VALUE) { - val now = nanoTime() - if (shutdownNanos == Long.MAX_VALUE) shutdownNanos = now + KEEP_ALIVE_NANOS - val tillShutdown = shutdownNanos - now - if (tillShutdown <= 0) return // shut thread down - parkNanos = parkNanos.coerceAtMost(tillShutdown) - } else - parkNanos = parkNanos.coerceAtMost(KEEP_ALIVE_NANOS) // limit wait time anyway - } + val now = nanoTime() + if (shutdownNanos == Long.MAX_VALUE) shutdownNanos = now + KEEP_ALIVE_NANOS + val tillShutdown = shutdownNanos - now + if (tillShutdown <= 0) return // shut thread down + parkNanos = parkNanos.coerceAtMost(tillShutdown) + } else + shutdownNanos = Long.MAX_VALUE if (parkNanos > 0) { // check if shutdown was requested and bail out in this case if (isShutdownRequested) return @@ -142,4 +140,7 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { resetAll() // clear queues (this as Object).notifyAll() } + + internal val isThreadPresent + get() = _thread != null } diff --git a/kotlinx-coroutines-core/jvm/test/DefaultExecutorStressTest.kt b/kotlinx-coroutines-core/jvm/test/DefaultExecutorStressTest.kt index b4c6aaed4d..bc2de8c998 100644 --- a/kotlinx-coroutines-core/jvm/test/DefaultExecutorStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/DefaultExecutorStressTest.kt @@ -4,7 +4,8 @@ package kotlinx.coroutines -import org.junit.* +import org.junit.Test +import kotlin.test.* class DefaultExecutorStressTest : TestBase() { @Test @@ -35,4 +36,30 @@ class DefaultExecutorStressTest : TestBase() { } finish(2 + iterations * 4) } + + @Test + fun testWorkerShutdown() = withVirtualTimeSource { + val iterations = 1_000 * stressTestMultiplier + // wait for the worker to shut down + suspend fun awaitWorkerShutdown() { + val executorTimeoutMs = 1000L + delay(executorTimeoutMs) + while (DefaultExecutor.isThreadPresent) { delay(10) } // hangs if the thread refuses to stop + assertFalse(DefaultExecutor.isThreadPresent) // just to make sure + } + runTest { + awaitWorkerShutdown() // so that the worker shuts down after the initial launch + repeat (iterations) { + val job = launch(Dispatchers.Unconfined) { + // this line runs in the main thread + delay(1) + // this line runs in the DefaultExecutor worker + } + delay(100) // yield the execution, allow the worker to spawn + assertTrue(DefaultExecutor.isThreadPresent) // the worker spawned + job.join() + awaitWorkerShutdown() + } + } + } } diff --git a/kotlinx-coroutines-core/jvm/test/channels/TickerChannelTest.kt b/kotlinx-coroutines-core/jvm/test/channels/TickerChannelTest.kt index c421bd334a..fcdc6bb4ad 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/TickerChannelTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/TickerChannelTest.kt @@ -47,17 +47,17 @@ class TickerChannelTest : TestBase() { @Test fun testDelayChannelBackpressure2() = withVirtualTimeSource { runTest { - val delayChannel = ticker(delayMillis = 1000, initialDelayMillis = 0) + val delayChannel = ticker(delayMillis = 200, initialDelayMillis = 0) delayChannel.checkNotEmpty() delayChannel.checkEmpty() - delay(2500) + delay(500) delayChannel.checkNotEmpty() - delay(510) + delay(110) delayChannel.checkNotEmpty() - delay(510) + delay(110) delayChannel.checkEmpty() - delay(510) + delay(110) delayChannel.checkNotEmpty() delayChannel.cancel() } From a3384a80659632f22dc2f032e7199b2ac178994a Mon Sep 17 00:00:00 2001 From: LepilkinaElena Date: Tue, 31 Mar 2020 13:34:28 +0300 Subject: [PATCH 005/257] Removed extra non working SharedImmutable annotations (#1892) --- .../common/src/channels/ConflatedBroadcastChannel.kt | 2 -- kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 4990c933ec..c58507ba93 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -47,9 +47,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { private val onCloseHandler = atomic(null) private companion object { - @SharedImmutable private val CLOSED = Closed(null) - @SharedImmutable private val UNDEFINED = Symbol("UNDEFINED") private val INITIAL_STATE = State(UNDEFINED, null) } diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 399019c3ee..4734766914 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -31,7 +31,6 @@ internal open class ConflatedChannel : AbstractChannel() { private var value: Any? = EMPTY private companion object { - @SharedImmutable private val EMPTY = Symbol("EMPTY") } From 6e66695bbbfc872c38f8e9e88d1a9d00cb43169f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 31 Mar 2020 16:10:12 +0300 Subject: [PATCH 006/257] Fix leak of subscription reference in closed ArrayBroadcastChannel (#1885) When ArrayBroadcastChannel was closed it was still retaining a reference to its subscription (even if that subscription was cancelled) while, in fact, either closing a broadcast channel or cancelling subscription should remove the reference. This is no problem with ConflatedBroadcastChannel, but it is added to the test for completeness. --- .../src/channels/ArrayBroadcastChannel.kt | 4 ++- .../test/channels/BroadcastChannelLeakTest.kt | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelLeakTest.kt diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index 19334ea706..155652fd6f 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -218,13 +218,15 @@ internal class ArrayBroadcastChannel( override val isBufferAlwaysFull: Boolean get() = error("Should not be used") override val isBufferFull: Boolean get() = error("Should not be used") - override fun onCancelIdempotent(wasClosed: Boolean) { + override fun close(cause: Throwable?): Boolean { + val wasClosed = super.close(cause) if (wasClosed) { broadcastChannel.updateHead(removeSub = this) subLock.withLock { subHead = broadcastChannel.tail } } + return wasClosed } // returns true if subHead was updated and broadcast channel's head must be checked diff --git a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelLeakTest.kt b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelLeakTest.kt new file mode 100644 index 0000000000..66b08c74e4 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelLeakTest.kt @@ -0,0 +1,34 @@ +package kotlinx.coroutines.channels + +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.* + +class BroadcastChannelLeakTest : TestBase() { + @Test + fun testArrayBroadcastChannelSubscriptionLeak() { + checkLeak { ArrayBroadcastChannel(1) } + } + + @Test + fun testConflatedBroadcastChannelSubscriptionLeak() { + checkLeak { ConflatedBroadcastChannel() } + } + + enum class TestKind { BROADCAST_CLOSE, SUB_CANCEL, BOTH } + + private fun checkLeak(factory: () -> BroadcastChannel) = runTest { + for (kind in TestKind.values()) { + val broadcast = factory() + val sub = broadcast.openSubscription() + broadcast.send("OK") + assertEquals("OK", sub.receive()) + // now close broadcast + if (kind != TestKind.SUB_CANCEL) broadcast.close() + // and then cancel subscription + if (kind != TestKind.BROADCAST_CLOSE) sub.cancel() + // subscription should not be reachable from the channel anymore + FieldWalker.assertReachableCount(0, broadcast) { it === sub } + } + } +} \ No newline at end of file From b2bc70b6c3f2561f9271b1438fa6fe24d9adf460 Mon Sep 17 00:00:00 2001 From: MMoskowitz9 Date: Sun, 5 Apr 2020 04:35:37 -0600 Subject: [PATCH 007/257] Correct JDK version typo (#1903) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5456857fe..cd5a0b1676 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Grad * JDK >= 11 referred to by the `JAVA_HOME` environment variable. * JDK 1.6 referred to by the `JDK_16` environment variable. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. -* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. +* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is okay to have `JDK_18` pointing to `JAVA_HOME` for external contributions. ## Contributions and releases From df8316f063a0313b8f30bbd6f426b79d06985418 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 6 Apr 2020 12:29:07 +0300 Subject: [PATCH 008/257] Update CoroutineScope docs (#1882) * Update CoroutineScope docs * Fixed scope examples in guides, added notes on first-party support in Android. * Simplified scopes section in UI guide since it is mostly irrelevant. Fixes #1581 --- docs/coroutine-context-and-dispatchers.md | 34 +++----- .../common/src/CoroutineScope.kt | 38 ++++++--- .../jvm/test/guide/example-context-10.kt | 12 ++- ui/coroutines-guide-ui.md | 81 +++++-------------- 4 files changed, 63 insertions(+), 102 deletions(-) diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index e379842443..d0ce20945e 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -501,21 +501,8 @@ class Activity { -Alternatively, we can implement the [CoroutineScope] interface in this `Activity` class. The best way to do it is -to use delegation with default factory functions. -We also can combine the desired dispatcher (we used [Dispatchers.Default] in this example) with the scope: - -
- -```kotlin - class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) { - // to be continued ... -``` - -
- -Now, we can launch coroutines in the scope of this `Activity` without having to explicitly -specify their context. For the demo, we launch ten coroutines that delay for a different time: +Now, we can launch coroutines in the scope of this `Activity` using the defined `scope`. +For the demo, we launch ten coroutines that delay for a different time:
@@ -524,7 +511,7 @@ specify their context. For the demo, we launch ten coroutines that delay for a d fun doSomething() { // launch ten coroutines for a demo, each working for a different time repeat(10) { i -> - launch { + mainScope.launch { delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc println("Coroutine $i is done") } @@ -544,21 +531,19 @@ of the activity no more messages are printed, even if we wait a little longer.
```kotlin -import kotlin.coroutines.* import kotlinx.coroutines.* -class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) { - +class Activity { + private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes + fun destroy() { - cancel() // Extension on CoroutineScope + mainScope.cancel() } - // to be continued ... - // class Activity continues fun doSomething() { // launch ten coroutines for a demo, each working for a different time repeat(10) { i -> - launch { + mainScope.launch { delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc println("Coroutine $i is done") } @@ -597,6 +582,9 @@ Destroying activity! As you can see, only the first two coroutines print a message and the others are cancelled by a single invocation of `job.cancel()` in `Activity.destroy()`. +> Note, that Android has first-party support for coroutine scope in all entities with the lifecycle. +See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope). + ### Thread-local data Sometimes it is convenient to have an ability to pass some thread-local data to or between coroutines. diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index a6b79bdb5a..7b5c645d1f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -10,37 +10,49 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* /** - * Defines a scope for new coroutines. Every coroutine builder + * Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc) * is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext] - * to automatically propagate both context elements and cancellation. + * to automatically propagate all its elements and cancellation. * * The best ways to obtain a standalone instance of the scope are [CoroutineScope()] and [MainScope()] factory functions. * Additional context elements can be appended to the scope using the [plus][CoroutineScope.plus] operator. * + * ### Convention for structured concurrency + * * Manual implementation of this interface is not recommended, implementation by delegation should be preferred instead. - * By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a [job][Job] to enforce structured concurrency. + * By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a + * [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation. * - * Every coroutine builder (like [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc) + * Every coroutine builder (like [launch], [async], etc) * and every scoping function (like [coroutineScope], [withContext], etc) provides _its own_ scope * with its own [Job] instance into the inner block of code it runs. * By convention, they all wait for all the coroutines inside their block to complete before completing themselves, - * thus enforcing the discipline of **structured concurrency**. + * thus enforcing the structured concurrency. See [Job] documentation for more details. * - * [CoroutineScope] should be implemented (or used as a field) on entities with a well-defined lifecycle that are responsible - * for launching children coroutines. Example of such entity on Android is Activity. - * Usage of this interface may look like this: + * ### Android usage + * + * Android has first-party support for coroutine scope in all entities with the lifecycle. + * See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope). + * + * ### Custom usage + * + * [CoroutineScope] should be implemented or declared as a property on entities with a well-defined lifecycle that are + * responsible for launching children coroutines, for example: * * ``` - * class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() { - * override fun onDestroy() { - * cancel() // cancel is extension on CoroutineScope + * class MyUIClass { + * val scope = MainScope() // the scope of MyUIClass + * + * fun destroy() { // destroys an instance of MyUIClass + * scope.cancel() // cancels all coroutines launched in this scope + * // ... do the rest of cleanup here ... * } * * /* - * * Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines + * * Note: if this instance is destroyed or any of the launched coroutines * * in this method throws an exception, then all nested coroutines are cancelled. * */ - * fun showSomeData() = launch { // <- extension on current activity, launched in the main thread + * fun showSomeData() = scope.launch { // launched in the main thread * // ... here we can use suspending functions or coroutine builders with other dispatchers * draw(data) // draw in the main thread * } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt b/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt index 0256004405..73ecbd2b3e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-context-10.kt @@ -5,21 +5,19 @@ // This file was automatically generated from coroutine-context-and-dispatchers.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.exampleContext10 -import kotlin.coroutines.* import kotlinx.coroutines.* -class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) { - +class Activity { + private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes + fun destroy() { - cancel() // Extension on CoroutineScope + mainScope.cancel() } - // to be continued ... - // class Activity continues fun doSomething() { // launch ten coroutines for a demo, each working for a different time repeat(10) { i -> - launch { + mainScope.launch { delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc println("Coroutine $i is done") } diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 20607be21a..e6cc2081e1 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -408,47 +408,40 @@ during UI freeze. ### Structured concurrency, lifecycle and coroutine parent-child hierarchy A typical UI application has a number of elements with a lifecycle. Windows, UI controls, activities, views, fragments -and other visual elements are created and destroyed. A long-running coroutine, performing some IO or a background -computation, can retain references to the corresponding UI elements for longer than it is needed, preventing garbage +and other visual elements are created and destroyed. A long-running coroutine, performing some IO or a background +computation, can retain references to the corresponding UI elements for longer than it is needed, preventing garbage collection of the whole trees of UI objects that were already destroyed and will not be displayed anymore. -The natural solution to this problem is to associate a [Job] object with each UI object that has a lifecycle and create -all the coroutines in the context of this job. But passing associated job object to every coroutine builder is error-prone, -it is easy to forget it. For this purpose, [CoroutineScope] interface could be implemented by UI owner, and then every -coroutine builder defined as an extension on [CoroutineScope] inherits UI job without explicitly mentioning it. -For the sake of simplicity, [MainScope()] factory can be used. It automatically provides `Dispatchers.Main` and parent -job. +The natural solution to this problem is to associate a [CoroutineScope] object with each UI object that has a +lifecycle and create all the coroutines in the context of this scope. +For the sake of simplicity, [MainScope()] factory can be used. It automatically provides `Dispatchers.Main` and +a parent job for all the children coroutines. + +For example, in Android application an `Activity` is initially _created_ and is _destroyed_ when it is no longer +needed and when its memory must be released. A natural solution is to attach an +instance of a `CoroutineScope` to an instance of an `Activity`: -For example, in Android application an `Activity` is initially _created_ and is _destroyed_ when it is no longer -needed and when its memory must be released. A natural solution is to attach an -instance of a `Job` to an instance of an `Activity`: ```kotlin -abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() { +class MainActivity : AppCompatActivity() { + private val scope = MainScope() + override fun onDestroy() { super.onDestroy() - cancel() // CoroutineScope.cancel + scope.cancel() } -} -``` - -Now, an activity that is associated with a job has to extend ScopedAppActivity - -```kotlin -class MainActivity : ScopedAppActivity() { - fun asyncShowData() = launch { // Is invoked in UI context with Activity's job as a parent + fun asyncShowData() = scope.launch { // Is invoked in UI context with Activity's scope as a parent // actual implementation } suspend fun showIOData() { - val deferred = async(Dispatchers.IO) { - // impl + val data = withContext(Dispatchers.IO) { + // compute data in background thread } withContext(Dispatchers.Main) { - val data = deferred.await() - // Show data in UI + // Show data in UI } } } @@ -457,43 +450,14 @@ class MainActivity : ScopedAppActivity() { Every coroutine launched from within a `MainActivity` has its job as a parent and is immediately cancelled when activity is destroyed. -To propagate activity scope to its views and presenters, multiple techniques can be used: -- [coroutineScope] builder to provide a nested scope -- Receive [CoroutineScope] in presenter method parameters -- Make method extension on [CoroutineScope] (applicable only for top-level methods) - -```kotlin -class ActivityWithPresenters: ScopedAppActivity() { - fun init() { - val presenter = Presenter() - val presenter2 = ScopedPresenter(this) - } -} - -class Presenter { - suspend fun loadData() = coroutineScope { - // Nested scope of outer activity - } - - suspend fun loadData(uiScope: CoroutineScope) = uiScope.launch { - // Invoked in the uiScope - } -} - -class ScopedPresenter(scope: CoroutineScope): CoroutineScope by scope { - fun loadData() = launch { // Extension on ActivityWithPresenters's scope - } -} - -suspend fun CoroutineScope.launchInIO() = launch(Dispatchers.IO) { - // Launched in the scope of the caller, but with IO dispatcher -} -``` +> Note, that Android has first-party support for coroutine scope in all entities with the lifecycle. +See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope). Parent-child relation between jobs forms a hierarchy. A coroutine that performs some background job on behalf of -the view and in its context can create further children coroutines. The whole tree of coroutines gets cancelled +the activity can create further children coroutines. The whole tree of coroutines gets cancelled when the parent job is cancelled. An example of that is shown in the ["Children of a coroutine"](../docs/coroutine-context-and-dispatchers.md#children-of-a-coroutine) section of the guide to coroutines. + ### Blocking operations @@ -649,7 +613,6 @@ After delay [Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html -[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html [withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html [CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html From 02b403d6625ca94d29a22ed836ededb845531985 Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Mon, 6 Apr 2020 16:33:22 +0300 Subject: [PATCH 009/257] BlockHound integration (#1873) * Integration with BlockHound * Improve build configuration of integration tests * publication-validator is renamed to integration-testing; * Add an integration test for coroutine debugger java agent * Use JNA-based attach mechanism for dynamic attach Fixes #1821 Fixes #1060 Co-authored-by: Vsevolod Tolstopyatov Co-authored-by: Sergei Egorov --- build.gradle | 4 +- gradle.properties | 4 +- integration-testing/README.md | 14 ++++ integration-testing/build.gradle | 80 +++++++++++++++++++ .../debugAgentTest/kotlin/DebugAgentTest.kt | 20 +++++ .../kotlin}/MavenPublicationValidator.kt | 1 - .../kotlin}/NpmPublicationValidator.kt | 0 .../jvm/src/scheduling/CoroutineScheduler.kt | 17 ++++ kotlinx-coroutines-debug/README.md | 5 ++ .../api/kotlinx-coroutines-debug.api | 5 ++ kotlinx-coroutines-debug/build.gradle | 8 +- ...ockhound.integration.BlockHoundIntegration | 1 + .../src/CoroutinesBlockHoundIntegration.kt | 16 ++++ .../src/internal/DebugProbesImpl.kt | 2 +- .../test/BlockHoundTest.kt | 73 +++++++++++++++++ .../test/CoroutinesDumpTest.kt | 13 +-- .../test/RunningThreadStackMergeTest.kt | 2 +- .../test/StracktraceUtils.kt | 29 ++++++- publication-validator/README.md | 13 --- publication-validator/build.gradle | 44 ---------- settings.gradle | 2 +- 21 files changed, 280 insertions(+), 73 deletions(-) create mode 100644 integration-testing/README.md create mode 100644 integration-testing/build.gradle create mode 100644 integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt rename {publication-validator/src/test/kotlin/kotlinx/coroutines/tools => integration-testing/src/mavenTest/kotlin}/MavenPublicationValidator.kt (99%) rename {publication-validator/src/test/kotlin/kotlinx/coroutines/tools => integration-testing/src/npmTest/kotlin}/NpmPublicationValidator.kt (100%) create mode 100644 kotlinx-coroutines-debug/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration create mode 100644 kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt create mode 100644 kotlinx-coroutines-debug/test/BlockHoundTest.kt delete mode 100644 publication-validator/README.md delete mode 100644 publication-validator/build.gradle diff --git a/build.gradle b/build.gradle index 87aac2a1e0..c679109ca3 100644 --- a/build.gradle +++ b/build.gradle @@ -8,8 +8,8 @@ apply from: rootProject.file("gradle/experimental.gradle") def rootModule = "kotlinx.coroutines" def coreModule = "kotlinx-coroutines-core" // Not applicable for Kotlin plugin -def sourceless = ['kotlinx.coroutines', 'site', 'kotlinx-coroutines-bom', 'publication-validator'] -def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-stubs', 'publication-validator'] +def sourceless = ['kotlinx.coroutines', 'site', 'kotlinx-coroutines-bom', 'integration-testing'] +def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-stubs', 'integration-testing'] // Not published def unpublished = internal + ['example-frontend-js', 'android-unit-tests'] diff --git a/gradle.properties b/gradle.properties index 802704029a..ac145da51c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,13 +14,15 @@ knit_version=0.1.3 html_version=0.6.8 lincheck_version=2.5.3 dokka_version=0.9.16-rdev-2-mpp-hacks -byte_buddy_version=1.9.3 +byte_buddy_version=1.10.9 reactor_vesion=3.2.5.RELEASE reactive_streams_version=1.0.2 rxjava2_version=2.2.8 javafx_version=11.0.2 javafx_plugin_version=0.0.8 binary_compatibility_validator_version=0.2.2 +blockhound_version=1.0.2.RELEASE +jna_version=5.5.0 # Android versions android_version=4.1.1.4 diff --git a/integration-testing/README.md b/integration-testing/README.md new file mode 100644 index 0000000000..4754081a45 --- /dev/null +++ b/integration-testing/README.md @@ -0,0 +1,14 @@ +# Integration tests + +This is a supplementary subproject of kotlinx.coroutines that provides +integration tests. + +The tests are the following: +* `NpmPublicationValidator` tests that version of NPM artifact is correct and that it has neither source nor package dependencies on atomicfu + In order for the test to work, one needs to run gradle with `-PdryRun=true`. + `-PdryRun` affects `npmPublish` so that it only provides a packed publication + and does not in fact attempt to send the build for publication. +* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath +* `DebugAgentTest` checks that the coroutine debugger can be run as a Java agent. + +All the available tests can be run with `integration-testing:test`. diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle new file mode 100644 index 0000000000..f376f667e0 --- /dev/null +++ b/integration-testing/build.gradle @@ -0,0 +1,80 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +apply from: rootProject.file("gradle/compile-jvm.gradle") + +repositories { + mavenLocal() + mavenCentral() +} + +sourceSets { + npmTest { + kotlin + compileClasspath += sourceSets.test.runtimeClasspath + runtimeClasspath += sourceSets.test.runtimeClasspath + } + mavenTest { + kotlin + compileClasspath += sourceSets.test.runtimeClasspath + runtimeClasspath += sourceSets.test.runtimeClasspath + } + debugAgentTest { + kotlin + compileClasspath += sourceSets.test.runtimeClasspath + runtimeClasspath += sourceSets.test.runtimeClasspath + } +} + +task npmTest(type: Test) { + def sourceSet = sourceSets.npmTest + environment "projectRoot", project.rootDir + environment "deployVersion", version + def dryRunNpm = project.properties['dryRun'] + def doRun = dryRunNpm == "true" // so that we don't accidentally publish anything, especially before the test + onlyIf { doRun } + if (doRun) { // `onlyIf` only affects execution of the task, not the dependency subtree + dependsOn(project(':').getTasksByName("publishNpm", true)) + } + testClassesDirs = sourceSet.output.classesDirs + classpath = sourceSet.runtimeClasspath +} + +task mavenTest(type: Test) { + def sourceSet = sourceSets.mavenTest + dependsOn(project(':').getTasksByName("publishToMavenLocal", true)) + dependsOn.remove(project(':').getTasksByName("dokka", true)) + testClassesDirs = sourceSet.output.classesDirs + classpath = sourceSet.runtimeClasspath + // we can't depend on the subprojects because we need to test the classfiles that are published in the end. + // also, we can't put this in the `dependencies` block because the resolution would happen before publication. + classpath += project.configurations.detachedConfiguration( + project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"), + project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-android:$version")) +} + +task debugAgentTest(type: Test) { + def sourceSet = sourceSets.debugAgentTest + dependsOn(project(':kotlinx-coroutines-debug').shadowJar) + jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-debug').shadowJar.outputs.files.getFiles()[0]) + testClassesDirs = sourceSet.output.classesDirs + classpath = sourceSet.runtimeClasspath +} + +dependencies { + testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testCompile 'junit:junit:4.12' + npmTestCompile 'org.apache.commons:commons-compress:1.18' + npmTestCompile 'com.google.code.gson:gson:2.8.5' + debugAgentTestCompile project(':kotlinx-coroutines-core') + debugAgentTestCompile project(':kotlinx-coroutines-debug') +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +test { + dependsOn([npmTest, mavenTest, debugAgentTest]) +} diff --git a/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt b/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt new file mode 100644 index 0000000000..925fe077ea --- /dev/null +++ b/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.junit.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import java.io.* + +class DebugAgentTest { + + @Test + fun agentDumpsCoroutines() = runBlocking { + val baos = ByteArrayOutputStream() + DebugProbes.dumpCoroutines(PrintStream(baos)) + // if the agent works, then dumps should contain something, + // at least the fact that this test is running. + Assert.assertTrue(baos.toString().contains("agentDumpsCoroutines")) + } + +} diff --git a/publication-validator/src/test/kotlin/kotlinx/coroutines/tools/MavenPublicationValidator.kt b/integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt similarity index 99% rename from publication-validator/src/test/kotlin/kotlinx/coroutines/tools/MavenPublicationValidator.kt rename to integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt index 53fd65d31f..5089c535ae 100644 --- a/publication-validator/src/test/kotlin/kotlinx/coroutines/tools/MavenPublicationValidator.kt +++ b/integration-testing/src/mavenTest/kotlin/MavenPublicationValidator.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.validator import org.junit.* import org.junit.Assert.assertTrue -import java.io.* import java.util.jar.* class MavenPublicationValidator { diff --git a/publication-validator/src/test/kotlin/kotlinx/coroutines/tools/NpmPublicationValidator.kt b/integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt similarity index 100% rename from publication-validator/src/test/kotlin/kotlinx/coroutines/tools/NpmPublicationValidator.kt rename to integration-testing/src/npmTest/kotlin/NpmPublicationValidator.kt diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 815fa26941..62cf80f7f8 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -951,3 +951,20 @@ internal class CoroutineScheduler( TERMINATED } } + +/** + * Checks if the thread is part of a thread pool that supports coroutines. + * This function is needed for integration with BlockHound. + */ +@Suppress("UNUSED") +@JvmName("isSchedulerWorker") +internal fun isSchedulerWorker(thread: Thread) = thread is CoroutineScheduler.Worker + +/** + * Checks if the thread is running a CPU-bound task. + * This function is needed for integration with BlockHound. + */ +@Suppress("UNUSED") +@JvmName("mayNotBlock") +internal fun mayNotBlock(thread: Thread) = thread is CoroutineScheduler.Worker && + thread.state == CoroutineScheduler.WorkerState.CPU_ACQUIRED diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index c552715c01..a8ed2214d8 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -13,6 +13,11 @@ suspension stacktraces. Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesInfo] or dump isolated parts of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances using [DebugProbes.printJob] and [DebugProbes.printScope] respectively. +This module also provides an automatic [BlockHound](https://github.com/reactor/BlockHound) integration +that detects when a blocking operation was called in a coroutine context that prohibits it. In order to use it, +please follow the BlockHound [quick start guide]( +https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). + ### Using in your project Add `kotlinx-coroutines-debug` to your project test dependencies: diff --git a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api index 6061f03899..749c94619e 100644 --- a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api +++ b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api @@ -8,6 +8,11 @@ public final class kotlinx/coroutines/debug/CoroutineInfo { public fun toString ()Ljava/lang/String; } +public final class kotlinx/coroutines/debug/CoroutinesBlockHoundIntegration : reactor/blockhound/integration/BlockHoundIntegration { + public fun ()V + public fun applyTo (Lreactor/blockhound/BlockHound$Builder;)V +} + public final class kotlinx/coroutines/debug/DebugProbes { public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes; public final fun dumpCoroutines (Ljava/io/PrintStream;)V diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle index 7fc2e22369..ab7f28c6a8 100644 --- a/kotlinx-coroutines-debug/build.gradle +++ b/kotlinx-coroutines-debug/build.gradle @@ -14,7 +14,7 @@ configurations { * but in that case it changes dependency type to "runtime" and resolves it * (so it cannot be further modified). Otherwise, shadow just ignores all dependencies. */ - shadow.extendsFrom(compile) // shadow - resulting configuration with shaded jar file + shadow.extendsFrom(api) // shadow - resulting configuration with shaded jar file configureKotlinJvmPlatform(shadow) } @@ -22,6 +22,10 @@ dependencies { compileOnly "junit:junit:$junit_version" shadowDeps "net.bytebuddy:byte-buddy:$byte_buddy_version" shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" + compileOnly "io.projectreactor.tools:blockhound:$blockhound_version" + testCompile "io.projectreactor.tools:blockhound:$blockhound_version" + api "net.java.dev.jna:jna:$jna_version" + api "net.java.dev.jna:jna-platform:$jna_version" } jar { @@ -35,5 +39,5 @@ shadowJar { classifier null // Shadow only byte buddy, do not package kotlin stdlib configurations = [project.configurations.shadowDeps] - relocate 'net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy' + relocate('net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy') } diff --git a/kotlinx-coroutines-debug/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/kotlinx-coroutines-debug/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration new file mode 100644 index 0000000000..c2f1e9cf38 --- /dev/null +++ b/kotlinx-coroutines-debug/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration @@ -0,0 +1 @@ +kotlinx.coroutines.debug.CoroutinesBlockHoundIntegration \ No newline at end of file diff --git a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt new file mode 100644 index 0000000000..f89d2be23f --- /dev/null +++ b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt @@ -0,0 +1,16 @@ +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +package kotlinx.coroutines.debug + +import reactor.blockhound.BlockHound +import kotlinx.coroutines.scheduling.* +import reactor.blockhound.integration.* + +@Suppress("UNUSED") +public class CoroutinesBlockHoundIntegration: BlockHoundIntegration { + + override fun applyTo(builder: BlockHound.Builder) { + builder.addDynamicThreadPredicate { isSchedulerWorker(it) } + builder.nonBlockingThreadPredicate { p -> p.or { mayNotBlock(it) } } + } + +} diff --git a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt index 090d3e5d89..8b7d8e7998 100644 --- a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt @@ -57,7 +57,7 @@ internal object DebugProbesImpl { public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return - ByteBuddyAgent.install() + ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE) val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") diff --git a/kotlinx-coroutines-debug/test/BlockHoundTest.kt b/kotlinx-coroutines-debug/test/BlockHoundTest.kt new file mode 100644 index 0000000000..ff5c95cdb1 --- /dev/null +++ b/kotlinx-coroutines-debug/test/BlockHoundTest.kt @@ -0,0 +1,73 @@ +package kotlinx.coroutines.debug +import kotlinx.coroutines.* +import org.junit.* +import reactor.blockhound.* + +class BlockHoundTest : TestBase() { + + @Before + fun init() { + BlockHound.install() + } + + @Test(expected = BlockingOperationError::class) + fun shouldDetectBlockingInDefault() = runTest { + withContext(Dispatchers.Default) { + Thread.sleep(1) + } + } + + @Test + fun shouldNotDetectBlockingInIO() = runTest { + withContext(Dispatchers.IO) { + Thread.sleep(1) + } + } + + @Test + fun shouldNotDetectNonblocking() = runTest { + withContext(Dispatchers.Default) { + val a = 1 + val b = 2 + assert(a + b == 3) + } + } + + @Test + fun testReusingThreads() = runTest { + val n = 100 + repeat(n) { + async(Dispatchers.IO) { + Thread.sleep(1) + } + } + repeat(n) { + async(Dispatchers.Default) { + } + } + repeat(n) { + async(Dispatchers.IO) { + Thread.sleep(1) + } + } + } + + @Test(expected = BlockingOperationError::class) + fun testReusingThreadsFailure() = runTest { + val n = 100 + repeat(n) { + async(Dispatchers.IO) { + Thread.sleep(1) + } + } + async(Dispatchers.Default) { + Thread.sleep(1) + } + repeat(n) { + async(Dispatchers.IO) { + Thread.sleep(1) + } + } + } + +} diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index 91bd4f287d..8507721e30 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -39,7 +39,7 @@ class CoroutinesDumpTest : DebugTestBase() { @Test fun testRunningCoroutine() = runBlocking { - val deferred = async(Dispatchers.Default) { + val deferred = async(Dispatchers.IO) { activeMethod(shouldSuspend = false) assertTrue(true) } @@ -70,7 +70,7 @@ class CoroutinesDumpTest : DebugTestBase() { @Test fun testRunningCoroutineWithSuspensionPoint() = runBlocking { - val deferred = async(Dispatchers.Default) { + val deferred = async(Dispatchers.IO) { activeMethod(shouldSuspend = true) yield() // tail-call } @@ -100,7 +100,7 @@ class CoroutinesDumpTest : DebugTestBase() { @Test fun testCreationStackTrace() = runBlocking { - val deferred = async(Dispatchers.Default) { + val deferred = async(Dispatchers.IO) { activeMethod(shouldSuspend = true) } @@ -129,7 +129,7 @@ class CoroutinesDumpTest : DebugTestBase() { @Test fun testFinishedCoroutineRemoved() = runBlocking { - val deferred = async(Dispatchers.Default) { + val deferred = async(Dispatchers.IO) { activeMethod(shouldSuspend = true) } @@ -149,7 +149,10 @@ class CoroutinesDumpTest : DebugTestBase() { if (shouldSuspend) yield() notifyCoroutineStarted() while (coroutineContext[Job]!!.isActive) { - runCatching { Thread.sleep(60_000) } + try { + Thread.sleep(60_000) + } catch (_ : InterruptedException) { + } } } diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index c0b7f50134..85aa657be4 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -133,7 +133,7 @@ class RunningThreadStackMergeTest : DebugTestBase() { } private fun CoroutineScope.launchEscapingCoroutineWithoutContext() { - launch(Dispatchers.Default) { + launch(Dispatchers.IO) { suspendingFunctionWithoutContext() assertTrue(true) } diff --git a/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/kotlinx-coroutines-debug/test/StracktraceUtils.kt index 12a39c0041..8c591ebd44 100644 --- a/kotlinx-coroutines-debug/test/StracktraceUtils.kt +++ b/kotlinx-coroutines-debug/test/StracktraceUtils.kt @@ -13,7 +13,7 @@ public fun String.trimStackTrace(): String = .replace(Regex("#[0-9]+"), "") .replace(Regex("(?<=\tat )[^\n]*/"), "") .replace(Regex("\t"), "") - .replace("sun.misc.Unsafe.park", "jdk.internal.misc.Unsafe.park") // JDK8->JDK11 + .replace("sun.misc.Unsafe.", "jdk.internal.misc.Unsafe.") // JDK8->JDK11 .applyBackspace() public fun String.applyBackspace(): String { @@ -62,6 +62,31 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null, f } } +/** Clean the stacktraces from artifacts of BlockHound instrumentation + * + * BlockHound works by switching a native call by a class generated with ByteBuddy, which, if the blocking + * call is allowed in this context, in turn calls the real native call that is now available under a + * different name. + * + * The traces thus undergo the following two changes when the execution is instrumented: + * - The original native call is replaced with a non-native one with the same FQN, and + * - An additional native call is placed on top of the stack, with the original name that also has + * `$$BlockHound$$_` prepended at the last component. + */ +private fun cleanBlockHoundTraces(frames: List): List { + var result = mutableListOf() + val blockHoundSubstr = "\$\$BlockHound\$\$_" + var i = 0 + while (i < frames.size) { + result.add(frames[i].replace(blockHoundSubstr, "")) + if (frames[i].contains(blockHoundSubstr)) { + i += 1 + } + i += 1 + } + return result +} + public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) @@ -85,7 +110,7 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { expected.withIndex().forEach { (index, trace) -> val actualTrace = actual[index].trimStackTrace().sanitizeAddresses() val expectedTrace = trace.trimStackTrace().sanitizeAddresses() - val actualLines = actualTrace.split("\n") + val actualLines = cleanBlockHoundTraces(actualTrace.split("\n")) val expectedLines = expectedTrace.split("\n") for (i in expectedLines.indices) { assertEquals(expectedLines[i], actualLines[i]) diff --git a/publication-validator/README.md b/publication-validator/README.md deleted file mode 100644 index a60ff00e3c..0000000000 --- a/publication-validator/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Publication validator - -This is a supplementary subproject of kotlinx.coroutines that provides a new -task, `testPublishing`, to test its publication correctness. - -The tests are the following: -* `NpmPublicationValidator` tests that version of NPM artifact is correct and that it has neither source nor package dependencies on atomicfu -* `MavenPublicationValidator` depends on the published artifacts and tests artifacts binary content and absence of atomicfu in the classpath - -To test publication, one needs to run gradle with `-PdryRun=true`, and the -task that actually does the testing is `publication-validator:test`. -`-PdryRun` affects `npmPublish` so that it only provides a packed publication -and does not in fact attempt to send the build for publication. diff --git a/publication-validator/build.gradle b/publication-validator/build.gradle deleted file mode 100644 index c94b317f4f..0000000000 --- a/publication-validator/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -apply from: rootProject.file("gradle/compile-jvm.gradle") - -def coroutines_version = version // all modules share the same version - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - testCompile 'junit:junit:4.12' - testCompile 'org.apache.commons:commons-compress:1.18' - testCompile 'com.google.code.gson:gson:2.8.5' -} - -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" -} - -def dryRunNpm = properties['dryRun'] - -test { - onlyIf { dryRunNpm == "true" } // so that we don't accidentally publish anything, especially before the test - doFirst { - println "Verifying publishing version $coroutines_version" - // we can't depend on the subprojects because we need to test the classfiles that are published in the end. - // also, we can't put this in the `dependencies` block because the resolution would happen before publication. - classpath += project.configurations.detachedConfiguration( - project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"), - project.dependencies.create("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")) - } - environment "projectRoot", project.rootDir - environment "deployVersion", version - if (dryRunNpm == "true") { // `onlyIf` only affects execution of the task, not the dependency subtree - dependsOn(project(':').getTasksByName("publishNpm", true) + - project(':').getTasksByName("publishToMavenLocal", true)) - dependsOn.remove(project(':').getTasksByName("dokka", true)) - } -} diff --git a/settings.gradle b/settings.gradle index 64ae2ffad3..95fcd7cb2d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,4 +45,4 @@ if (!build_snapshot_train) { include('site') } -module('publication-validator') +module('integration-testing') From de29acd404e4dd9afbbff57300161547516b5604 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 7 Apr 2020 17:32:00 +0300 Subject: [PATCH 010/257] Flow onEmpty (#1904) * Introduce Flow.onEmpty operator Fixes #1890 --- .../api/kotlinx-coroutines-core.api | 1 + .../common/src/flow/operators/Emitters.kt | 39 ++++++++- .../test/flow/operators/OnCompletionTest.kt | 19 +++++ .../common/test/flow/operators/OnEmptyTest.kt | 81 +++++++++++++++++++ .../common/test/flow/operators/OnStartTest.kt | 21 ++++- 5 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/OnEmptyTest.kt diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index a6e5fd513e..54e355ec37 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -942,6 +942,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final synthetic fun onCompletion (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onCompletion (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun onEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun onEmpty (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorCollect (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun onErrorCollect$default (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun onErrorResume (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index d8016e018d..cbed2df929 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -56,9 +56,9 @@ internal inline fun Flow.unsafeTransform( } /** - * Invokes the given [action] when the this flow starts to be collected. + * Invokes the given [action] when this flow starts to be collected. * - * The receiver of the [action] is [FlowCollector] and thus `onStart` can emit additional elements. + * The receiver of the [action] is [FlowCollector], so `onStart` can emit additional elements. * For example: * * ``` @@ -67,7 +67,7 @@ internal inline fun Flow.unsafeTransform( * .collect { println(it) } // prints Begin, a, b, c * ``` */ -@ExperimentalCoroutinesApi // tentatively stable in 1.3.0 +@ExperimentalCoroutinesApi public fun Flow.onStart( action: suspend FlowCollector.() -> Unit ): Flow = unsafeFlow { // Note: unsafe flow is used here, but safe collector is used to invoke start action @@ -129,7 +129,7 @@ public fun Flow.onStart( * .collect { println(it) } // prints a, b, c, Done * ``` */ -@ExperimentalCoroutinesApi // tentatively stable in 1.3.0 +@ExperimentalCoroutinesApi public fun Flow.onCompletion( action: suspend FlowCollector.(cause: Throwable?) -> Unit ): Flow = unsafeFlow { // Note: unsafe flow is used here, but safe collector is used to invoke completion action @@ -155,6 +155,37 @@ public fun Flow.onCompletion( exception?.let { throw it } } +/** + * Invokes the given [action] when this flow completes without emitting any elements. + * The receiver of the [action] is [FlowCollector], so `onEmpty` can emit additional elements. + * For example: + * + * ``` + * emptyFlow().onEmpty { + * emit(1) + * emit(2) + * }.collect { println(it) } // prints 1, 2 + * ``` + */ +@ExperimentalCoroutinesApi +public fun Flow.onEmpty( + action: suspend FlowCollector.() -> Unit +): Flow = unsafeFlow { + var isEmpty = true + collect { + isEmpty = false + emit(it) + } + if (isEmpty) { + val collector = SafeCollector(this, coroutineContext) + try { + collector.action() + } finally { + collector.releaseIntercepted() + } + } +} + private class ThrowingCollector(private val e: Throwable) : FlowCollector { override suspend fun emit(value: Any?) { throw e diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt index c079500ef7..f56632d5fd 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt @@ -259,4 +259,23 @@ class OnCompletionTest : TestBase() { assertEquals(42, value) finish(2) } + + @Test + fun testTransparencyViolation() = runTest { + val flow = emptyFlow().onCompletion { + expect(2) + coroutineScope { + launch { + try { + emit(1) + } catch (e: IllegalStateException) { + expect(3) + } + } + } + } + expect(1) + assertNull(flow.singleOrNull()) + finish(4) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnEmptyTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnEmptyTest.kt new file mode 100644 index 0000000000..3da166645b --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnEmptyTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.operators + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class OnEmptyTest : TestBase() { + + @Test + fun testOnEmptyInvoked() = runTest { + val flow = emptyFlow().onEmpty { emit(1) } + assertEquals(1, flow.single()) + } + + @Test + fun testOnEmptyNotInvoked() = runTest { + val flow = flowOf(1).onEmpty { emit(2) } + assertEquals(1, flow.single()) + } + + @Test + fun testOnEmptyNotInvokedOnError() = runTest { + val flow = flow { + throw TestException() + }.onEmpty { expectUnreached() } + assertFailsWith(flow) + } + + @Test + fun testOnEmptyNotInvokedOnCancellation() = runTest { + val flow = flow { + expect(2) + hang { expect(4) } + }.onEmpty { expectUnreached() } + + expect(1) + val job = flow.onEach { expectUnreached() }.launchIn(this) + yield() + expect(3) + job.cancelAndJoin() + finish(5) + } + + @Test + fun testOnEmptyCancellation() = runTest { + val flow = emptyFlow().onEmpty { + expect(2) + hang { expect(4) } + emit(1) + } + expect(1) + val job = flow.onEach { expectUnreached() }.launchIn(this) + yield() + expect(3) + job.cancelAndJoin() + finish(5) + } + + @Test + fun testTransparencyViolation() = runTest { + val flow = emptyFlow().onEmpty { + expect(2) + coroutineScope { + launch { + try { + emit(1) + } catch (e: IllegalStateException) { + expect(3) + } + } + } + } + expect(1) + assertNull(flow.singleOrNull()) + finish(4) + } +} diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnStartTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnStartTest.kt index a0981ab042..0443e56a2c 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnStartTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnStartTest.kt @@ -14,4 +14,23 @@ class OnStartTest : TestBase() { .onStart { emit("Begin") } assertEquals(listOf("Begin", "a", "b", "c"), flow.toList()) } -} \ No newline at end of file + + @Test + fun testTransparencyViolation() = runTest { + val flow = emptyFlow().onStart { + expect(2) + coroutineScope { + launch { + try { + emit(1) + } catch (e: IllegalStateException) { + expect(3) + } + } + } + } + expect(1) + assertNull(flow.singleOrNull()) + finish(4) + } +} From 532368fce9bd4e67c1428fc8d5a59264c9c46cc7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 7 Apr 2020 17:32:17 +0300 Subject: [PATCH 011/257] Update Kotlin version to 1.3.71 (#1906) --- README.md | 8 ++++---- gradle.properties | 2 +- .../jvm/src/channels/TickerChannels.kt | 1 - kotlinx-coroutines-debug/src/CoroutineInfo.kt | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b5456857fe..8bafa78efa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.5) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.5) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.3.70` release. +This is a companion version for Kotlin `1.3.71` release. ```kotlin suspend fun main() = coroutineScope { @@ -91,7 +91,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.3.70 + 1.3.71 ``` @@ -109,7 +109,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.3.70' + ext.kotlin_version = '1.3.71' } ``` @@ -135,7 +135,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.3.70" + kotlin("jvm") version "1.3.71" } ``` diff --git a/gradle.properties b/gradle.properties index ac145da51c..daf5b67f44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ # Kotlin version=1.3.5-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.3.70 +kotlin_version=1.3.71 # Dependencies junit_version=4.12 diff --git a/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt b/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt index b9f39af9cc..1e6797accc 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/TickerChannels.kt @@ -13,7 +13,6 @@ import kotlin.coroutines.* * **Note: Ticker channels are not currently integrated with structured concurrency and their api will change in the future.** */ @ObsoleteCoroutinesApi -@Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE") public enum class TickerMode { /** * Adjust delay to maintain fixed period if consumer cannot keep up or is otherwise slow. diff --git a/kotlinx-coroutines-debug/src/CoroutineInfo.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt index 10540d1608..c9517fe897 100644 --- a/kotlinx-coroutines-debug/src/CoroutineInfo.kt +++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt @@ -2,7 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("PropertyName", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") +@file:Suppress("PropertyName") package kotlinx.coroutines.debug diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index e9d79d0e50..da02aabb5a 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -20,7 +20,7 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.3.70 +kotlin_version=1.3.71 coroutines_version=1.3.5 android.useAndroidX=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index e9d79d0e50..da02aabb5a 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -20,7 +20,7 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.3.70 +kotlin_version=1.3.71 coroutines_version=1.3.5 android.useAndroidX=true From 2ff0bbb33ee1e1404167883b13b9932bbd1e35b6 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 8 Apr 2020 18:24:30 +0300 Subject: [PATCH 012/257] Full Apache 2.0 license text --- LICENSE.txt | 209 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 199 insertions(+), 10 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index dead69c1be..9c308d958b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,13 +1,202 @@ -Copyright 2016-2019 JetBrains s.r.o. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From d75d02a4a717d69d9a41dd3ea39b41bad3ff8000 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Fri, 10 Apr 2020 20:11:27 +0300 Subject: [PATCH 013/257] Make the list of segments more abstract (#1563) * Make the list of segments more abstract, so that it can be used for other synchronization and communication primitives Co-authored-by: Roman Elizarov --- .../src/internal/ConcurrentLinkedList.kt | 240 ++++++++++++++++++ .../common/src/internal/SegmentQueue.kt | 179 ------------- .../common/src/sync/Semaphore.kt | 67 +++-- .../jvm/test/internal/SegmentBasedQueue.kt | 87 +++++-- .../jvm/test/internal/SegmentListTest.kt | 41 +++ .../jvm/test/internal/SegmentQueueTest.kt | 17 +- .../SegmentQueueLCStressTest.kt | 15 +- 7 files changed, 405 insertions(+), 241 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/internal/ConcurrentLinkedList.kt delete mode 100644 kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/SegmentListTest.kt diff --git a/kotlinx-coroutines-core/common/src/internal/ConcurrentLinkedList.kt b/kotlinx-coroutines-core/common/src/internal/ConcurrentLinkedList.kt new file mode 100644 index 0000000000..128a1998ef --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/ConcurrentLinkedList.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlin.native.concurrent.SharedImmutable + +/** + * Returns the first segment `s` with `s.id >= id` or `CLOSED` + * if all the segments in this linked list have lower `id`, and the list is closed for further segment additions. + */ +private inline fun > S.findSegmentInternal( + id: Long, + createNewSegment: (id: Long, prev: S?) -> S +): SegmentOrClosed { + /* + Go through `next` references and add new segments if needed, similarly to the `push` in the Michael-Scott + queue algorithm. The only difference is that "CAS failure" means that the required segment has already been + added, so the algorithm just uses it. This way, only one segment with each id can be added. + */ + var cur: S = this + while (cur.id < id || cur.removed) { + val next = cur.nextOrIfClosed { return SegmentOrClosed(CLOSED) } + if (next != null) { // there is a next node -- move there + cur = next + continue + } + val newTail = createNewSegment(cur.id + 1, cur) + if (cur.trySetNext(newTail)) { // successfully added new node -- move there + if (cur.removed) cur.remove() + cur = newTail + } + } + return SegmentOrClosed(cur) +} + +/** + * Returns `false` if the segment `to` is logically removed, `true` on a successful update. + */ +@Suppress("NOTHING_TO_INLINE") // Must be inline because it is an AtomicRef extension +private inline fun > AtomicRef.moveForward(to: S): Boolean = loop { cur -> + if (cur.id >= to.id) return true + if (!to.tryIncPointers()) return false + if (compareAndSet(cur, to)) { // the segment is moved + if (cur.decPointers()) cur.remove() + return true + } + if (to.decPointers()) to.remove() // undo tryIncPointers +} + +/** + * Tries to find a segment with the specified [id] following by next references from the + * [startFrom] segment and creating new ones if needed. The typical use-case is reading this `AtomicRef` values, + * doing some synchronization, and invoking this function to find the required segment and update the pointer. + * At the same time, [Segment.cleanPrev] should also be invoked if the previous segments are no longer needed + * (e.g., queues should use it in dequeue operations). + * + * Since segments can be removed from the list, or it can be closed for further segment additions. + * Returns the segment `s` with `s.id >= id` or `CLOSED` if all the segments in this linked list have lower `id`, + * and the list is closed. + */ +internal inline fun > AtomicRef.findSegmentAndMoveForward( + id: Long, + startFrom: S, + createNewSegment: (id: Long, prev: S?) -> S +): SegmentOrClosed { + while (true) { + val s = startFrom.findSegmentInternal(id, createNewSegment) + if (s.isClosed || moveForward(s.segment)) return s + } +} + +/** + * Closes this linked list of nodes by forbidding adding new ones, + * returns the last node in the list. + */ +internal fun > N.close(): N { + var cur: N = this + while (true) { + val next = cur.nextOrIfClosed { return cur } + if (next === null) { + if (cur.markAsClosed()) return cur + } else { + cur = next + } + } +} + +internal abstract class ConcurrentLinkedListNode>(prev: N?) { + // Pointer to the next node, updates similarly to the Michael-Scott queue algorithm. + private val _next = atomic(null) + // Pointer to the previous node, updates in [remove] function. + private val _prev = atomic(prev) + + private val nextOrClosed get() = _next.value + + /** + * Returns the next segment or `null` of the one does not exist, + * and invokes [onClosedAction] if this segment is marked as closed. + */ + @Suppress("UNCHECKED_CAST") + inline fun nextOrIfClosed(onClosedAction: () -> Nothing): N? = nextOrClosed.let { + if (it === CLOSED) { + onClosedAction() + } else { + it as N? + } + } + + val next: N? get() = nextOrIfClosed { return null } + + /** + * Tries to set the next segment if it is not specified and this segment is not marked as closed. + */ + fun trySetNext(value: N): Boolean = _next.compareAndSet(null, value) + + /** + * Checks whether this node is the physical tail of the current linked list. + */ + val isTail: Boolean get() = next == null + + val prev: N? get() = _prev.value + + /** + * Cleans the pointer to the previous node. + */ + fun cleanPrev() { _prev.lazySet(null) } + + /** + * Tries to mark the linked list as closed by forbidding adding new nodes after this one. + */ + fun markAsClosed() = _next.compareAndSet(null, CLOSED) + + /** + * This property indicates whether the current node is logically removed. + * The expected use-case is removing the node logically (so that [removed] becomes true), + * and invoking [remove] after that. Note that this implementation relies on the contract + * that the physical tail cannot be logically removed. Please, do not break this contract; + * otherwise, memory leaks and unexpected behavior can occur. + */ + abstract val removed: Boolean + + /** + * Removes this node physically from this linked list. The node should be + * logically removed (so [removed] returns `true`) at the point of invocation. + */ + fun remove() { + assert { removed } // The node should be logically removed at first. + assert { !isTail } // The physical tail cannot be removed. + while (true) { + // Read `next` and `prev` pointers ignoring logically removed nodes. + val prev = leftmostAliveNode + val next = rightmostAliveNode + // Link `next` and `prev`. + next._prev.value = prev + if (prev !== null) prev._next.value = next + // Checks that prev and next are still alive. + if (next.removed) continue + if (prev !== null && prev.removed) continue + // This node is removed. + return + } + } + + private val leftmostAliveNode: N? get() { + var cur = prev + while (cur !== null && cur.removed) + cur = cur._prev.value + return cur + } + + private val rightmostAliveNode: N get() { + assert { !isTail } // Should not be invoked on the tail node + var cur = next!! + while (cur.removed) + cur = cur.next!! + return cur + } +} + +/** + * Each segment in the list has a unique id and is created by the provided to [findSegmentAndMoveForward] method. + * Essentially, this is a node in the Michael-Scott queue algorithm, + * but with maintaining [prev] pointer for efficient [remove] implementation. + */ +internal abstract class Segment>(val id: Long, prev: S?, pointers: Int): ConcurrentLinkedListNode(prev) { + /** + * This property should return the maximal number of slots in this segment, + * it is used to define whether the segment is logically removed. + */ + abstract val maxSlots: Int + + /** + * Numbers of cleaned slots (the lowest bits) and AtomicRef pointers to this segment (the highest bits) + */ + private val cleanedAndPointers = atomic(pointers shl POINTERS_SHIFT) + + /** + * The segment is considered as removed if all the slots are cleaned. + * There are no pointers to this segment from outside, and + * it is not a physical tail in the linked list of segments. + */ + override val removed get() = cleanedAndPointers.value == maxSlots && !isTail + + // increments the number of pointers if this segment is not logically removed. + internal fun tryIncPointers() = cleanedAndPointers.addConditionally(1 shl POINTERS_SHIFT) { it != maxSlots || isTail } + + // returns `true` if this segment is logically removed after the decrement. + internal fun decPointers() = cleanedAndPointers.addAndGet(-(1 shl POINTERS_SHIFT)) == maxSlots && !isTail + + /** + * Invoked on each slot clean-up; should not be invoked twice for the same slot. + */ + fun onSlotCleaned() { + if (cleanedAndPointers.incrementAndGet() == maxSlots && !isTail) remove() + } +} + +private inline fun AtomicInt.addConditionally(delta: Int, condition: (cur: Int) -> Boolean): Boolean { + while (true) { + val cur = this.value + if (!condition(cur)) return false + if (this.compareAndSet(cur, cur + delta)) return true + } +} + +@Suppress("EXPERIMENTAL_FEATURE_WARNING") // We are using inline class only internally, so it is Ok +internal inline class SegmentOrClosed>(private val value: Any?) { + val isClosed: Boolean get() = value === CLOSED + @Suppress("UNCHECKED_CAST") + val segment: S get() = if (value === CLOSED) error("Does not contain segment") else value as S +} + +private const val POINTERS_SHIFT = 16 + +@SharedImmutable +private val CLOSED = Symbol("CLOSED") \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt b/kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt deleted file mode 100644 index 0091d13671..0000000000 --- a/kotlinx-coroutines-core/common/src/internal/SegmentQueue.kt +++ /dev/null @@ -1,179 +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.internal - -import kotlinx.atomicfu.* -import kotlinx.coroutines.* - -/** - * Essentially, this segment queue is an infinite array of segments, which is represented as - * a Michael-Scott queue of them. All segments are instances of [Segment] class and - * follow in natural order (see [Segment.id]) in the queue. - */ -internal abstract class SegmentQueue>() { - private val _head: AtomicRef - /** - * Returns the first segment in the queue. - */ - protected val head: S get() = _head.value - - private val _tail: AtomicRef - /** - * Returns the last segment in the queue. - */ - protected val tail: S get() = _tail.value - - init { - val initialSegment = newSegment(0) - _head = atomic(initialSegment) - _tail = atomic(initialSegment) - } - - /** - * The implementation should create an instance of segment [S] with the specified id - * and initial reference to the previous one. - */ - abstract fun newSegment(id: Long, prev: S? = null): S - - /** - * Finds a segment with the specified [id] following by next references from the - * [startFrom] segment. The typical use-case is reading [tail] or [head], doing some - * synchronization, and invoking [getSegment] or [getSegmentAndMoveHead] correspondingly - * to find the required segment. - */ - protected fun getSegment(startFrom: S, id: Long): S? { - // Go through `next` references and add new segments if needed, - // similarly to the `push` in the Michael-Scott queue algorithm. - // The only difference is that `CAS failure` means that the - // required segment has already been added, so the algorithm just - // uses it. This way, only one segment with each id can be in the queue. - var cur: S = startFrom - while (cur.id < id) { - var curNext = cur.next - if (curNext == null) { - // Add a new segment. - val newTail = newSegment(cur.id + 1, cur) - curNext = if (cur.casNext(null, newTail)) { - if (cur.removed) { - cur.remove() - } - moveTailForward(newTail) - newTail - } else { - cur.next!! - } - } - cur = curNext - } - if (cur.id != id) return null - return cur - } - - /** - * Invokes [getSegment] and replaces [head] with the result if its [id] is greater. - */ - protected fun getSegmentAndMoveHead(startFrom: S, id: Long): S? { - @Suppress("LeakingThis") - if (startFrom.id == id) return startFrom - val s = getSegment(startFrom, id) ?: return null - moveHeadForward(s) - return s - } - - /** - * Updates [head] to the specified segment - * if its `id` is greater. - */ - private fun moveHeadForward(new: S) { - _head.loop { curHead -> - if (curHead.id > new.id) return - if (_head.compareAndSet(curHead, new)) { - new.prev.value = null - return - } - } - } - - /** - * Updates [tail] to the specified segment - * if its `id` is greater. - */ - private fun moveTailForward(new: S) { - _tail.loop { curTail -> - if (curTail.id > new.id) return - if (_tail.compareAndSet(curTail, new)) return - } - } -} - -/** - * Each segment in [SegmentQueue] has a unique id and is created by [SegmentQueue.newSegment]. - * Essentially, this is a node in the Michael-Scott queue algorithm, but with - * maintaining [prev] pointer for efficient [remove] implementation. - */ -internal abstract class Segment>(val id: Long, prev: S?) { - // Pointer to the next segment, updates similarly to the Michael-Scott queue algorithm. - private val _next = atomic(null) - val next: S? get() = _next.value - fun casNext(expected: S?, value: S?): Boolean = _next.compareAndSet(expected, value) - // Pointer to the previous segment, updates in [remove] function. - val prev = atomic(null) - - /** - * Returns `true` if this segment is logically removed from the queue. - * The [remove] function should be called right after it becomes logically removed. - */ - abstract val removed: Boolean - - init { - this.prev.value = prev - } - - /** - * Removes this segment physically from the segment queue. The segment should be - * logically removed (so [removed] returns `true`) at the point of invocation. - */ - fun remove() { - assert { removed } // The segment should be logically removed at first - // Read `next` and `prev` pointers. - var next = this._next.value ?: return // tail cannot be removed - var prev = prev.value ?: return // head cannot be removed - // Link `next` and `prev`. - prev.moveNextToRight(next) - while (prev.removed) { - prev = prev.prev.value ?: break - prev.moveNextToRight(next) - } - next.movePrevToLeft(prev) - while (next.removed) { - next = next.next ?: break - next.movePrevToLeft(prev) - } - } - - /** - * Updates [next] pointer to the specified segment if - * the [id] of the specified segment is greater. - */ - private fun moveNextToRight(next: S) { - while (true) { - val curNext = this._next.value as S - if (next.id <= curNext.id) return - if (this._next.compareAndSet(curNext, next)) return - } - } - - /** - * Updates [prev] pointer to the specified segment if - * the [id] of the specified segment is lower. - */ - private fun movePrevToLeft(prev: S) { - while (true) { - val curPrev = this.prev.value ?: return - if (curPrev.id <= prev.id) return - if (this.prev.compareAndSet(curPrev, prev)) return - } - } -} diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index aa7ed63d3d..7cdc736197 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -8,9 +8,8 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlin.coroutines.* -import kotlin.jvm.* import kotlin.math.* -import kotlin.native.concurrent.* +import kotlin.native.concurrent.SharedImmutable /** * A counting semaphore for coroutines that logically maintains a number of available permits. @@ -84,16 +83,26 @@ public suspend inline fun Semaphore.withPermit(action: () -> T): T { } } -private class SemaphoreImpl( - private val permits: Int, acquiredPermits: Int -) : Semaphore, SegmentQueue() { +private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Semaphore { + + // The queue of waiting acquirers is essentially an infinite array based on the list of segments + // (see `SemaphoreSegment`); each segment contains a fixed number of slots. To determine a slot for each enqueue + // and dequeue operation, we increment the corresponding counter at the beginning of the operation + // and use the value before the increment as a slot number. This way, each enqueue-dequeue pair + // works with an individual cell.We use the corresponding segment pointer to find the required ones. + private val head: AtomicRef + private val deqIdx = atomic(0L) + private val tail: AtomicRef + private val enqIdx = atomic(0L) + init { require(permits > 0) { "Semaphore should have at least 1 permit, but had $permits" } require(acquiredPermits in 0..permits) { "The number of acquired permits should be in 0..$permits" } + val s = SemaphoreSegment(0, null, 2) + head = atomic(s) + tail = atomic(s) } - override fun newSegment(id: Long, prev: SemaphoreSegment?) = SemaphoreSegment(id, prev) - /** * This counter indicates a number of available permits if it is non-negative, * or the size with minus sign otherwise. Note, that 32-bit counter is enough here @@ -104,14 +113,6 @@ private class SemaphoreImpl( private val _availablePermits = atomic(permits - acquiredPermits) override val availablePermits: Int get() = max(_availablePermits.value, 0) - // The queue of waiting acquirers is essentially an infinite array based on `SegmentQueue`; - // each segment contains a fixed number of slots. To determine a slot for each enqueue - // and dequeue operation, we increment the corresponding counter at the beginning of the operation - // and use the value before the increment as a slot number. This way, each enqueue-dequeue pair - // works with an individual cell. - private val enqIdx = atomic(0L) - private val deqIdx = atomic(0L) - override fun tryAcquire(): Boolean { _availablePermits.loop { p -> if (p <= 0) return false @@ -136,12 +137,13 @@ private class SemaphoreImpl( cur + 1 } - private suspend fun addToQueueAndSuspend() = suspendAtomicCancellableCoroutineReusable sc@ { cont -> - val last = this.tail + private suspend fun addToQueueAndSuspend() = suspendAtomicCancellableCoroutineReusable sc@{ cont -> + val curTail = this.tail.value val enqIdx = enqIdx.getAndIncrement() - val segment = getSegment(last, enqIdx / SEGMENT_SIZE) + val segment = this.tail.findSegmentAndMoveForward(id = enqIdx / SEGMENT_SIZE, startFrom = curTail, + createNewSegment = ::createSegment).run { segment } // cannot be closed val i = (enqIdx % SEGMENT_SIZE).toInt() - if (segment === null || segment.get(i) === RESUMED || !segment.cas(i, null, cont)) { + if (segment.get(i) === RESUMED || !segment.cas(i, null, cont)) { // already resumed cont.resume(Unit) return@sc @@ -151,10 +153,17 @@ private class SemaphoreImpl( @Suppress("UNCHECKED_CAST") internal fun resumeNextFromQueue() { - try_again@while (true) { - val first = this.head + try_again@ while (true) { + val curHead = this.head.value val deqIdx = deqIdx.getAndIncrement() - val segment = getSegmentAndMoveHead(first, deqIdx / SEGMENT_SIZE) ?: continue@try_again + val id = deqIdx / SEGMENT_SIZE + val segment = this.head.findSegmentAndMoveForward(id, startFrom = curHead, + createNewSegment = ::createSegment).run { segment } // cannot be closed + segment.cleanPrev() + if (segment.id > id) { + this.deqIdx.updateIfLower(segment.id * SEGMENT_SIZE) + continue@try_again + } val i = (deqIdx % SEGMENT_SIZE).toInt() val cont = segment.getAndSet(i, RESUMED) if (cont === null) return // just resumed @@ -165,6 +174,10 @@ private class SemaphoreImpl( } } +private inline fun AtomicLong.updateIfLower(value: Long): Unit = loop { cur -> + if (cur >= value || compareAndSet(cur, value)) return +} + private class CancelSemaphoreAcquisitionHandler( private val semaphore: SemaphoreImpl, private val segment: SemaphoreSegment, @@ -180,10 +193,11 @@ private class CancelSemaphoreAcquisitionHandler( override fun toString() = "CancelSemaphoreAcquisitionHandler[$semaphore, $segment, $index]" } -private class SemaphoreSegment(id: Long, prev: SemaphoreSegment?): Segment(id, prev) { +private fun createSegment(id: Long, prev: SemaphoreSegment?) = SemaphoreSegment(id, prev, 0) + +private class SemaphoreSegment(id: Long, prev: SemaphoreSegment?, pointers: Int) : Segment(id, prev, pointers) { val acquirers = atomicArrayOfNulls(SEGMENT_SIZE) - private val cancelledSlots = atomic(0) - override val removed get() = cancelledSlots.value == SEGMENT_SIZE + override val maxSlots: Int get() = SEGMENT_SIZE @Suppress("NOTHING_TO_INLINE") inline fun get(index: Int): Any? = acquirers[index].value @@ -200,8 +214,7 @@ private class SemaphoreSegment(id: Long, prev: SemaphoreSegment?): Segment : SegmentQueue>() { - override fun newSegment(id: Long, prev: OneElementSegment?): OneElementSegment = OneElementSegment(id, prev) +internal class SegmentBasedQueue { + private val head: AtomicRef> + private val tail: AtomicRef> private val enqIdx = atomic(0L) private val deqIdx = atomic(0L) - // Returns the segments associated with the enqueued element. - fun enqueue(element: T): OneElementSegment { + init { + val s = OneElementSegment(0, null, 2) + head = atomic(s) + tail = atomic(s) + } + + // Returns the segments associated with the enqueued element, or `null` if the queue is closed. + fun enqueue(element: T): OneElementSegment? { while (true) { - var tail = this.tail + val curTail = this.tail.value val enqIdx = this.enqIdx.getAndIncrement() - tail = getSegment(tail, enqIdx) ?: continue - if (tail.element.value === BROKEN) continue - if (tail.element.compareAndSet(null, element)) return tail + val segmentOrClosed = this.tail.findSegmentAndMoveForward(id = enqIdx, startFrom = curTail, createNewSegment = ::createSegment) + if (segmentOrClosed.isClosed) return null + val s = segmentOrClosed.segment + if (s.element.value === BROKEN) continue + if (s.element.compareAndSet(null, element)) return s } } fun dequeue(): T? { while (true) { if (this.deqIdx.value >= this.enqIdx.value) return null - var firstSegment = this.head + val curHead = this.head.value val deqIdx = this.deqIdx.getAndIncrement() - firstSegment = getSegmentAndMoveHead(firstSegment, deqIdx) ?: continue - var el = firstSegment.element.value + val segmentOrClosed = this.head.findSegmentAndMoveForward(id = deqIdx, startFrom = curHead, createNewSegment = ::createSegment) + if (segmentOrClosed.isClosed) return null + val s = segmentOrClosed.segment + s.cleanPrev() + if (s.id > deqIdx) continue + var el = s.element.value if (el === null) { - if (firstSegment.element.compareAndSet(null, BROKEN)) continue - else el = firstSegment.element.value + if (s.element.compareAndSet(null, BROKEN)) continue + else el = s.element.value } - if (el === REMOVED) continue + if (el === BROKEN) continue + @Suppress("UNCHECKED_CAST") return el as T } } + // `enqueue` should return `null` after the queue is closed + fun close(): OneElementSegment { + val s = this.tail.value.close() + var cur = s + while (true) { + cur.element.compareAndSet(null, BROKEN) + cur = cur.prev ?: break + } + return s + } + val numberOfSegments: Int get() { - var s: OneElementSegment? = head - var i = 0 - while (s != null) { - s = s.next + var cur = head.value + var i = 1 + while (true) { + cur = cur.next ?: return i i++ } - return i + } + + fun checkHeadPrevIsCleaned() { + check(head.value.prev === null) } } -internal class OneElementSegment(id: Long, prev: OneElementSegment?) : Segment>(id, prev) { +private fun createSegment(id: Long, prev: OneElementSegment?) = OneElementSegment(id, prev, 0) + +internal class OneElementSegment(id: Long, prev: OneElementSegment?, pointers: Int) : Segment>(id, prev, pointers) { val element = atomic(null) - override val removed get() = element.value === REMOVED + override val maxSlots get() = 1 fun removeSegment() { - element.value = REMOVED - remove() + element.value = BROKEN + onSlotCleaned() } } -private val BROKEN = Symbol("BROKEN") -private val REMOVED = Symbol("REMOVED") \ No newline at end of file +private val BROKEN = Symbol("BROKEN") \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentListTest.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentListTest.kt new file mode 100644 index 0000000000..ff6a346cda --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentListTest.kt @@ -0,0 +1,41 @@ +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.* +import org.junit.Test +import kotlin.test.* + +class SegmentListTest { + @Test + fun testRemoveTail() { + val initialSegment = TestSegment(0, null, 2) + val head = AtomicRefHolder(initialSegment) + val tail = AtomicRefHolder(initialSegment) + val s1 = tail.ref.findSegmentAndMoveForward(1, tail.ref.value, ::createTestSegment).segment + assertFalse(s1.removed) + tail.ref.value.onSlotCleaned() + assertFalse(s1.removed) + head.ref.findSegmentAndMoveForward(2, head.ref.value, ::createTestSegment) + assertFalse(s1.removed) + tail.ref.findSegmentAndMoveForward(2, head.ref.value, ::createTestSegment) + assertTrue(s1.removed) + } + + @Test + fun testClose() { + val initialSegment = TestSegment(0, null, 2) + val head = AtomicRefHolder(initialSegment) + val tail = AtomicRefHolder(initialSegment) + tail.ref.findSegmentAndMoveForward(1, tail.ref.value, ::createTestSegment) + assertEquals(tail.ref.value, tail.ref.value.close()) + assertTrue(head.ref.findSegmentAndMoveForward(2, head.ref.value, ::createTestSegment).isClosed) + } +} + +private class AtomicRefHolder(initialValue: T) { + val ref = atomic(initialValue) +} + +private class TestSegment(id: Long, prev: TestSegment?, pointers: Int) : Segment(id, prev, pointers) { + override val maxSlots: Int get() = 1 +} +private fun createTestSegment(id: Long, prev: TestSegment?) = TestSegment(id, prev, 0) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt index b59a6488a0..fd2d329088 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt @@ -33,7 +33,7 @@ class SegmentQueueTest : TestBase() { val s = q.enqueue(2) q.enqueue(3) assertEquals(3, q.numberOfSegments) - s.removeSegment() + s!!.removeSegment() assertEquals(2, q.numberOfSegments) assertEquals(1, q.dequeue()) assertEquals(3, q.dequeue()) @@ -47,11 +47,21 @@ class SegmentQueueTest : TestBase() { val s = q.enqueue(2) assertEquals(1, q.dequeue()) q.enqueue(3) - s.removeSegment() + s!!.removeSegment() assertEquals(3, q.dequeue()) assertNull(q.dequeue()) } + @Test + fun testClose() { + val q = SegmentBasedQueue() + q.enqueue(1) + assertEquals(0, q.close().id) + assertEquals(null, q.enqueue(2)) + assertEquals(1, q.dequeue()) + assertEquals(null, q.dequeue()) + } + @Test fun stressTest() { val q = SegmentBasedQueue() @@ -64,6 +74,7 @@ class SegmentQueueTest : TestBase() { expectedQueue.add(el) } else { // remove assertEquals(expectedQueue.poll(), q.dequeue()) + q.checkHeadPrevIsCleaned() } } } @@ -78,7 +89,7 @@ class SegmentQueueTest : TestBase() { val N = 100_000 * stressTestMultiplier val T = 10 val q = SegmentBasedQueue() - val segments = (1..N).map { q.enqueue(it) }.toMutableList() + val segments = (1..N).map { q.enqueue(it)!! }.toMutableList() if (random) segments.shuffle() assertEquals(N, q.numberOfSegments) val nextSegmentIndex = AtomicInteger() diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt index 1bb51a568f..89bf8dfaa4 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt @@ -18,12 +18,17 @@ class SegmentQueueLCStressTest : VerifierState() { private val q = SegmentBasedQueue() @Operation - fun add(@Param(name = "value") value: Int) { - q.enqueue(value) + fun enqueue(@Param(name = "value") x: Int): Boolean { + return q.enqueue(x) !== null } @Operation - fun poll(): Int? = q.dequeue() + fun dequeue(): Int? = q.dequeue() + + @Operation + fun close() { + q.close() + } override fun extractState(): Any { val elements = ArrayList() @@ -31,8 +36,8 @@ class SegmentQueueLCStressTest : VerifierState() { val x = q.dequeue() ?: break elements.add(x) } - - return elements + val closed = q.enqueue(0) === null + return elements to closed } @Test From 5eaf83cb4b63c6e4c52d77fbb29c8b121ff3e169 Mon Sep 17 00:00:00 2001 From: LepilkinaElena Date: Mon, 13 Apr 2020 13:49:04 +0300 Subject: [PATCH 014/257] Fix in gradle for JS IR in case it doesn't exist (#1912) --- gradle/compile-js-multiplatform.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle index e09e8e26b4..2b00d6fc82 100644 --- a/gradle/compile-js-multiplatform.gradle +++ b/gradle/compile-js-multiplatform.gradle @@ -9,9 +9,9 @@ kotlin { fromPreset(presets.js, 'js') { // Enable built-in test runner only for IR target. // These runners don't support changing js module name change. - if (js.hasProperty("irTarget")) { + if (js.hasProperty("irTarget") && irTarget != null) { irTarget.nodejs() - irTarget?.compilations['main']?.dependencies { + irTarget.compilations['main']?.dependencies { api "org.jetbrains.kotlinx:atomicfu-js:$atomicfu_version" } } From a35e5dc29b87c1b468dbb006d4d9ed18a8472200 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Mon, 20 Apr 2020 16:33:35 +0300 Subject: [PATCH 015/257] GitHub issue/pr links in IDEA Git log (#1920) --- .gitignore | 5 +++-- .idea/vcs.xml | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index c447f28b73..a5f8ce2653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -.idea +**/.idea/* +!/.idea/vcs.xml *.iml .gradle .gradletasknamecache build out target -local.properties \ No newline at end of file +local.properties diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..9ea55e627a --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file From ae6b2c47294ece11c87ad2a48fddfaebfc7ccaf9 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Mon, 20 Apr 2020 16:59:17 +0300 Subject: [PATCH 016/257] JetBrains Toolbox icon (#1926) --- .gitignore | 1 + .idea/icon.png | Bin 0 -> 740 bytes 2 files changed, 1 insertion(+) create mode 100644 .idea/icon.png diff --git a/.gitignore b/.gitignore index a5f8ce2653..33ebe3f3a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ **/.idea/* +!/.idea/icon.png !/.idea/vcs.xml *.iml .gradle diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d957c64af5c1fd3d753b95c10df0a2e6b52a2a02 GIT binary patch literal 740 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRS3dtTpz6=aiYC!Q828LfC`4&6 zh2cL4F4(&qB+Xgi5n0T@z;^_M8K-LVNi#4oS$n!ThE&{od&4kGB$1)*;mlG&=bS4I z8@G0`dnk9^=9;Gc!cnDyv05-khlMkul3DTQv0JAud2QI(!=kd|n6;1bp7Y^nOYHaX zA3oD_*yr1qqc{J3N-8X>tTechvfF5SbKUPxuio4{Jbk`c{c-i_+p8}fWpDtxoEn6K z93w*^3&RmX1_5^l2N`Ufp69Qdr`BA4`}$n-?EJjFb-QJc-!8UzUu2l$pECP{XTjTl zX?h?3=PTYUvEEm>yN;VYKFg*Im*m0a)K~BD*f;7%~<`u{NnFZ{59R3(E zd_2r>gr5w~{JPUSWxmwh`Md9CY`sL=$Gtg@4A!^rm*4+uQB(8v`rPM_U%qTDIa}WF zb^hM@^4nKm4chfCoPp&ZvqJzo10#a~FfEWpOu!Y8Sc3SV{c2hNvk$TsWdPF_gQu&X J%Q~loCIA>5>m&dG literal 0 HcmV?d00001 From 94970dfdce6d414949fbdf590545d382fbcc216a Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 22 Apr 2020 13:58:16 +0300 Subject: [PATCH 017/257] Improve docs for CoroutineExceptionHandler (#1886) * Further clarifications and better style for exception handling * Consistent terminology on "uncaught exceptions". * Clarified special relations of exception handling with supervision. * Clearer text in CoroutineExceptionHandler examples. * Minor stylistic corrections. Fixes #1746 Fixes #871 Co-Authored-By: EdwarDDay <4127904+EdwarDDay@users.noreply.github.com> --- docs/exception-handling.md | 122 ++++++++++-------- .../common/src/CoroutineExceptionHandler.kt | 38 +++++- .../jvm/test/guide/example-exceptions-01.kt | 4 +- .../jvm/test/guide/example-exceptions-02.kt | 6 +- .../jvm/test/guide/example-exceptions-04.kt | 2 +- .../jvm/test/guide/example-exceptions-05.kt | 8 +- .../jvm/test/guide/example-exceptions-06.kt | 8 +- .../jvm/test/guide/example-supervision-03.kt | 2 +- .../test/guide/test/ExceptionsGuideTest.kt | 10 +- 9 files changed, 118 insertions(+), 82 deletions(-) diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 08e63ea994..9029e4b155 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -18,22 +18,22 @@ ## Exception Handling - This section covers exception handling and cancellation on exceptions. -We already know that cancelled coroutine throws [CancellationException] in suspension points and that it -is ignored by coroutines machinery. But what happens if an exception is thrown during cancellation or multiple children of the same -coroutine throw an exception? +We already know that cancelled coroutine throws [CancellationException] in suspension points and that it +is ignored by the coroutines' machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same +coroutine throw an exception. ### Exception propagation -Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or +Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or exposing them to users ([async] and [produce]). -The former treat exceptions as unhandled, similar to Java's `Thread.uncaughtExceptionHandler`, -while the latter are relying on the user to consume the final +When these builders are used to create a _root_ coroutine, that is not a _child_ of another coroutine, +the former builder treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`, +while the latter are relying on the user to consume the final exception, for example via [await][Deferred.await] or [receive][ReceiveChannel.receive] ([produce] and [receive][ReceiveChannel.receive] are covered later in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section). -It can be demonstrated by a simple example that creates coroutines in the [GlobalScope]: +It can be demonstrated by a simple example that creates root coroutines using the [GlobalScope]:
@@ -41,13 +41,13 @@ It can be demonstrated by a simple example that creates coroutines in the [Globa import kotlinx.coroutines.* fun main() = runBlocking { - val job = GlobalScope.launch { + val job = GlobalScope.launch { // root coroutine with launch println("Throwing exception from launch") throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler } job.join() println("Joined failed job") - val deferred = GlobalScope.async { + val deferred = GlobalScope.async { // root coroutine with async println("Throwing exception from async") throw ArithmeticException() // Nothing is printed, relying on user to call await } @@ -78,9 +78,13 @@ Caught ArithmeticException ### CoroutineExceptionHandler -But what if one does not want to print all exceptions to the console? -[CoroutineExceptionHandler] context element is used as generic `catch` block of coroutine where custom logging or exception handling may take place. -It is similar to using [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)). +It is possible to customize the default behavior of printing **uncaught** exceptions to the console. +[CoroutineExceptionHandler] context element on a _root_ coroutine can be used as generic `catch` block for +this root coroutine and all its children where custom exception handling may take place. +It is similar to [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)). +You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed +with the corresponding exception when the handler is called. Normally, the handler is used to +log the exception, show some kind of error message, terminate, and/or restart the application. On JVM it is possible to redefine global exception handler for all coroutines by registering [CoroutineExceptionHandler] via [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). @@ -89,8 +93,15 @@ Global exception handler is similar to which is used when no more specific handlers are registered. On Android, `uncaughtExceptionPreHandler` is installed as a global coroutine exception handler. -[CoroutineExceptionHandler] is invoked only on exceptions which are not expected to be handled by the user, -so registering it in [async] builder and the like of it has no effect. +`CoroutineExceptionHandler` is invoked only on **uncaught** exceptions — exceptions that were not handled in any other way. +In particular, all _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of +their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root, +so the `CoroutineExceptionHandler` installed in their context is never used. +In addition to that, [async] builder always catches all exceptions and represents them in the resulting [Deferred] object, +so its `CoroutineExceptionHandler` has no effect either. + +> Coroutines running in supervision scope do not propagate exceptions to their parent and are +excluded from this rule. A further [Supervision](#supervision) section of this document gives more details.
@@ -100,12 +111,12 @@ import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } - val job = GlobalScope.launch(handler) { + val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope throw AssertionError() } - val deferred = GlobalScope.async(handler) { + val deferred = GlobalScope.async(handler) { // also root, but async instead of launch throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await() } joinAll(job, deferred) @@ -120,14 +131,14 @@ fun main() = runBlocking { The output of this code is: ```text -Caught java.lang.AssertionError +CoroutineExceptionHandler got java.lang.AssertionError ``` ### Cancellation and exceptions -Cancellation is tightly bound with exceptions. Coroutines internally use `CancellationException` for cancellation, these +Cancellation is closely related to exceptions. Coroutines internally use `CancellationException` for cancellation, these exceptions are ignored by all handlers, so they should be used only as the source of additional debug information, which can be obtained by `catch` block. When a coroutine is cancelled using [Job.cancel], it terminates, but it does not cancel its parent. @@ -175,15 +186,17 @@ Parent is not cancelled If a coroutine encounters an exception other than `CancellationException`, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for -[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async) which do not depend on -[CoroutineExceptionHandler] implementation. -The original exception is handled by the parent when all its children terminate. +[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async). +[CoroutineExceptionHandler] implementation is not used for child coroutines. -> This also a reason why, in these examples, [CoroutineExceptionHandler] is always installed to a coroutine +> In these examples [CoroutineExceptionHandler] is always installed to a coroutine that is created in [GlobalScope]. It does not make sense to install an exception handler to a coroutine that is launched in the scope of the main [runBlocking], since the main coroutine is going to be always cancelled when its child completes with exception despite the installed handler. +The original exception is handled by the parent only when all its children terminate, +which is demonstrated by the following example. +
```kotlin @@ -192,7 +205,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { launch { // the first child @@ -227,22 +240,15 @@ The output of this code is: Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block -Caught java.lang.ArithmeticException +CoroutineExceptionHandler got java.lang.ArithmeticException ``` ### Exceptions aggregation -What happens if multiple children of a coroutine throw an exception? -The general rule is "the first exception wins", so the first thrown exception is exposed to the handler. -But that may cause lost exceptions, for example if coroutine throws an exception in its `finally` block. -So, additional exceptions are suppressed. - -> One of the solutions would have been to report each exception separately, -but then [Deferred.await] should have had the same mechanism to avoid behavioural inconsistency and this -would cause implementation details of a coroutines (whether it had delegated parts of its work to its children or not) -to leak to its exception handler. - +When multiple children of a coroutine fail with an exception the +general rule is "the first exception wins", so the first exception gets handled. +All additional exceptions that happen after the first one are attached to the first exception as suppressed ones. @@ -293,7 +299,7 @@ Caught java.io.IOException with suppressed [java.lang.ArithmeticException] > Note, this mechanism currently works only on Java version 1.7+. Limitation on JS and Native is temporary and will be fixed in the future. -Cancellation exceptions are transparent and unwrapped by default: +Cancellation exceptions are transparent and are unwrapped by default:
@@ -304,13 +310,13 @@ import java.io.* fun main() = runBlocking { //sampleStart val handler = CoroutineExceptionHandler { _, exception -> - println("Caught original $exception") + println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { - val inner = launch { + val inner = launch { // all this stack of coroutines will get cancelled launch { launch { - throw IOException() + throw IOException() // the original exception } } } @@ -318,7 +324,7 @@ fun main() = runBlocking { inner.join() } catch (e: CancellationException) { println("Rethrowing CancellationException with original cause") - throw e + throw e // cancellation exception is rethrown, yet the original IOException gets to the handler } } job.join() @@ -334,25 +340,26 @@ The output of this code is: ```text Rethrowing CancellationException with original cause -Caught original java.io.IOException +CoroutineExceptionHandler got java.io.IOException ``` ### Supervision As we have studied before, cancellation is a bidirectional relationship propagating through the whole -coroutines hierarchy. But what if unidirectional cancellation is required? +hierarchy of coroutines. Let us take a look at the case when unidirectional cancellation is required. A good example of such a requirement is a UI component with the job defined in its scope. If any of the UI's child tasks have failed, it is not always necessary to cancel (effectively kill) the whole UI component, -but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer required. +but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer needed. Another example is a server process that spawns several children jobs and needs to _supervise_ their execution, tracking their failures and restarting just those children jobs that had failed. #### Supervision job -For these purposes [SupervisorJob][SupervisorJob()] can be used. It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated +For these purposes [SupervisorJob][SupervisorJob()] can be used. +It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated only downwards. It is easy to demonstrate with an example:
@@ -406,8 +413,8 @@ Second child is cancelled because supervisor is cancelled #### Supervision scope -For *scoped* concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation -only in one direction and cancels all children only if it has failed itself. It also waits for all children before completion +For _scoped_ concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation +in one direction only and cancels all children only if it has failed itself. It also waits for all children before completion just like [coroutineScope] does.
@@ -455,8 +462,11 @@ Caught assertion error #### Exceptions in supervised coroutines Another crucial difference between regular and supervisor jobs is exception handling. -Every child should handle its exceptions by itself via exception handling mechanisms. +Every child should handle its exceptions by itself via exception handling mechanism. This difference comes from the fact that child's failure is not propagated to the parent. +It means that coroutines launched directly inside [supervisorScope] _do_ use the [CoroutineExceptionHandler] +that is installed in their scope in the same way as root coroutines do +(see [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details).
@@ -466,7 +476,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } supervisorScope { val child = launch(handler) { @@ -488,7 +498,7 @@ The output of this code is: ```text Scope is completing Child throws an exception -Caught java.lang.AssertionError +CoroutineExceptionHandler got java.lang.AssertionError Scope is completed ``` @@ -501,6 +511,8 @@ Scope is completed [Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html [GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html [CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html +[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html +[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html [Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html [runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html diff --git a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt index cd7fd0d7ca..b49a6faa35 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt @@ -52,11 +52,38 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte } /** - * An optional element in the coroutine context to handle uncaught exceptions. + * An optional element in the coroutine context to handle **uncaught** exceptions. * - * Normally, uncaught exceptions can only result from coroutines created using the [launch][CoroutineScope.launch] builder. + * Normally, uncaught exceptions can only result from _root_ coroutines created using the [launch][CoroutineScope.launch] builder. + * All _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of their + * exceptions to their parent coroutine, which also delegates to the parent, and so on until the root, + * so the `CoroutineExceptionHandler` installed in their context is never used. + * Coroutines running with [SupervisorJob] do not propagate exceptions to their parent and are treated like root coroutines. * A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them - * in the resulting [Deferred] object. + * in the resulting [Deferred] object, so it cannot result in uncaught exceptions. + * + * ### Handling coroutine exceptions + * + * `CoroutineExceptionHandler` is a last-resort mechanism for global "catch all" behavior. + * You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed + * with the corresponding exception when the handler is called. Normally, the handler is used to + * log the exception, show some kind of error message, terminate, and/or restart the application. + * + * If you need to handle exception in a specific part of the code, it is recommended to use `try`/`catch` around + * the corresponding code inside your coroutine. This way you can prevent completion of the coroutine + * with the exception (exception is now _caught_), retry the operation, and/or take other arbitrary actions: + * + * ``` + * scope.launch { // launch child coroutine in a scope + * try { + * // do something + * } catch (e: Throwable) { + * // handle exception + * } + * } + * ``` + * + * ### Implementation details * * By default, when no handler is installed, uncaught exception are handled in the following way: * * If exception is [CancellationException] then it is ignored @@ -66,10 +93,7 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte * * Otherwise, all instances of [CoroutineExceptionHandler] found via [ServiceLoader] * * and current thread's [Thread.uncaughtExceptionHandler] are invoked. * - * [CoroutineExceptionHandler] can be invoked from an arbitrary dispatcher used by coroutines in the current job hierarchy. - * For example, if one has a `MainScope` and launches children of the scope in main and default dispatchers, then exception handler can - * be invoked either in main or in default dispatcher thread regardless of - * which particular dispatcher coroutine that has thrown an exception used. + * [CoroutineExceptionHandler] can be invoked from an arbitrary thread. */ public interface CoroutineExceptionHandler : CoroutineContext.Element { /** diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt index 34d7b68c82..e08ddd0811 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt @@ -8,13 +8,13 @@ package kotlinx.coroutines.guide.exampleExceptions01 import kotlinx.coroutines.* fun main() = runBlocking { - val job = GlobalScope.launch { + val job = GlobalScope.launch { // root coroutine with launch println("Throwing exception from launch") throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler } job.join() println("Joined failed job") - val deferred = GlobalScope.async { + val deferred = GlobalScope.async { // root coroutine with async println("Throwing exception from async") throw ArithmeticException() // Nothing is printed, relying on user to call await } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt index 359eff60e4..67fdaa7177 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt @@ -9,12 +9,12 @@ import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } - val job = GlobalScope.launch(handler) { + val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope throw AssertionError() } - val deferred = GlobalScope.async(handler) { + val deferred = GlobalScope.async(handler) { // also root, but async instead of launch throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await() } joinAll(job, deferred) diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt index e1fc22d725..9c9b43d22e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { launch { // the first child diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt index e97572aba8..04f9385f06 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt @@ -12,19 +12,19 @@ import java.io.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception with suppressed ${exception.suppressed.contentToString()}") + println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}") } val job = GlobalScope.launch(handler) { launch { try { - delay(Long.MAX_VALUE) + delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException } finally { - throw ArithmeticException() + throw ArithmeticException() // the second exception } } launch { delay(100) - throw IOException() + throw IOException() // the first exception } delay(Long.MAX_VALUE) } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt index eec27840e5..5a5b276bc3 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt @@ -10,13 +10,13 @@ import java.io.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught original $exception") + println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { - val inner = launch { + val inner = launch { // all this stack of coroutines will get cancelled launch { launch { - throw IOException() + throw IOException() // the original exception } } } @@ -24,7 +24,7 @@ fun main() = runBlocking { inner.join() } catch (e: CancellationException) { println("Rethrowing CancellationException with original cause") - throw e + throw e // cancellation exception is rethrown, yet the original IOException gets to the handler } } job.join() diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt index b32a004639..5efbe69146 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } supervisorScope { val child = launch(handler) { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt index 4a140208f9..21d2c73b1b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt @@ -22,7 +22,7 @@ class ExceptionsGuideTest { @Test fun testExampleExceptions02() { test("ExampleExceptions02") { kotlinx.coroutines.guide.exampleExceptions02.main() }.verifyLines( - "Caught java.lang.AssertionError" + "CoroutineExceptionHandler got java.lang.AssertionError" ) } @@ -41,14 +41,14 @@ class ExceptionsGuideTest { "Second child throws an exception", "Children are cancelled, but exception is not handled until all children terminate", "The first child finished its non cancellable block", - "Caught java.lang.ArithmeticException" + "CoroutineExceptionHandler got java.lang.ArithmeticException" ) } @Test fun testExampleExceptions05() { test("ExampleExceptions05") { kotlinx.coroutines.guide.exampleExceptions05.main() }.verifyLines( - "Caught java.io.IOException with suppressed [java.lang.ArithmeticException]" + "CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]" ) } @@ -56,7 +56,7 @@ class ExceptionsGuideTest { fun testExampleExceptions06() { test("ExampleExceptions06") { kotlinx.coroutines.guide.exampleExceptions06.main() }.verifyLines( "Rethrowing CancellationException with original cause", - "Caught original java.io.IOException" + "CoroutineExceptionHandler got java.io.IOException" ) } @@ -85,7 +85,7 @@ class ExceptionsGuideTest { test("ExampleSupervision03") { kotlinx.coroutines.guide.exampleSupervision03.main() }.verifyLines( "Scope is completing", "Child throws an exception", - "Caught java.lang.AssertionError", + "CoroutineExceptionHandler got java.lang.AssertionError", "Scope is completed" ) } From dc8eeb09f2e249fac172334a96bc97abdd4b9b35 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 22 Apr 2020 14:55:56 +0300 Subject: [PATCH 018/257] Migrate to androidx.annotation from legacy android.support.annotion (#1932) --- gradle.properties | 2 +- ui/kotlinx-coroutines-android/android-unit-tests/build.gradle | 1 - ui/kotlinx-coroutines-android/build.gradle | 2 +- ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt | 2 +- ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index daf5b67f44..9b54f4f89b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ jna_version=5.5.0 # Android versions android_version=4.1.1.4 -android_support_version=26.1.0 +androidx_annotation_version=1.1.0 robolectric_version=4.0.2 baksmali_version=2.2.7 diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle index c927640cd7..99759b254c 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle +++ b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle @@ -4,7 +4,6 @@ dependencies { testImplementation "com.google.android:android:$android_version" - testImplementation "com.android.support:support-annotations:$android_support_version" testImplementation "org.robolectric:robolectric:$robolectric_version" testImplementation project(":kotlinx-coroutines-test") testImplementation project(":kotlinx-coroutines-android") diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle index 51702cdbe3..6f66e6c5d9 100644 --- a/ui/kotlinx-coroutines-android/build.gradle +++ b/ui/kotlinx-coroutines-android/build.gradle @@ -12,7 +12,7 @@ configurations { dependencies { compileOnly "com.google.android:android:$android_version" - compileOnly "com.android.support:support-annotations:$android_support_version" + compileOnly "androidx.annotation:annotation:$androidx_annotation_version" testImplementation "com.google.android:android:$android_version" testImplementation "org.robolectric:robolectric:$robolectric_version" diff --git a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt index 673c3813d8..b105e5b84c 100644 --- a/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt +++ b/ui/kotlinx-coroutines-android/src/AndroidExceptionPreHandler.kt @@ -5,7 +5,7 @@ package kotlinx.coroutines.android import android.os.* -import android.support.annotation.* +import androidx.annotation.* import kotlinx.coroutines.* import java.lang.reflect.* import kotlin.coroutines.* diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index d415b003bf..af1ab4331b 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -7,7 +7,7 @@ package kotlinx.coroutines.android import android.os.* -import android.support.annotation.* +import androidx.annotation.* import android.view.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* From ac42be944c8f5aefe794f16a1b0ee9b8578375af Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Thu, 23 Apr 2020 20:01:05 +0300 Subject: [PATCH 019/257] Add linearizability tests for Mutex and Semaphore and fix them (#1898) * Make semaphore implementation linearizable (ignoring extra suspensions) * Make mutex implementation linearizable (ignoring extra suspensions) * Add linearizability tests for mutex and semaphore * Fix `SegmentQueueLCStressTest` Fixes #1737 Co-authored-by: Roman Elizarov --- gradle.properties | 2 +- kotlinx-coroutines-core/build.gradle | 2 + .../common/src/sync/Mutex.kt | 27 +-- .../common/src/sync/Semaphore.kt | 164 ++++++++++++------ .../jvm/test/internal/SegmentBasedQueue.kt | 4 +- .../test/linearizability/MutexLCStressTest.kt | 31 ++++ .../linearizability/SemaphoreLCStressTest.kt | 34 ++++ 7 files changed, 192 insertions(+), 72 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt diff --git a/gradle.properties b/gradle.properties index 9b54f4f89b..56a1a23c25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junit_version=4.12 atomicfu_version=0.14.2 knit_version=0.1.3 html_version=0.6.8 -lincheck_version=2.5.3 +lincheck_version=2.6 dokka_version=0.9.16-rdev-2-mpp-hacks byte_buddy_version=1.10.9 reactor_vesion=3.2.5.RELEASE diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 547a12b4c6..8e899016e7 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -101,6 +101,8 @@ task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) { enableAssertions = true testLogging.showStandardStreams = true systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test + systemProperty 'kotlinx.coroutines.semaphore.segmentSize', '2' + systemProperty 'kotlinx.coroutines.semaphore.maxSpinCycles', '10' } task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 1b11bc96cc..769c9f1168 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -380,26 +380,13 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { // atomic unlock operation that checks that waiters queue is empty private class UnlockOp( @JvmField val queue: LockedQueue - ) : OpDescriptor() { - override val atomicOp: AtomicOp<*>? get() = null - - override fun perform(affected: Any?): Any? { - /* - Note: queue cannot change while this UnlockOp is in progress, so all concurrent attempts to - make a decision will reach it consistently. It does not matter what is a proposed - decision when this UnlockOp is no longer active, because in this case the following CAS - will fail anyway. - */ - val success = queue.isEmpty - val update: Any = if (success) EMPTY_UNLOCKED else queue - (affected as MutexImpl)._state.compareAndSet(this@UnlockOp, update) - /* - `perform` invocation from the original `unlock` invocation may be coming too late, when - some other thread had already helped to complete it (either successfully or not). - That operation was unsuccessful if `state` was restored to this `queue` reference and - that is what is being checked below. - */ - return if (affected._state.value === queue) UNLOCK_FAIL else null + ) : AtomicOp() { + override fun prepare(affected: MutexImpl): Any? = + if (queue.isEmpty) null else UNLOCK_FAIL + + override fun complete(affected: MutexImpl, failure: Any?) { + val update: Any = if (failure == null) EMPTY_UNLOCKED else queue + affected._state.compareAndSet(this, update) } } } diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index 7cdc736197..125bbaaeb5 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -84,12 +84,41 @@ public suspend inline fun Semaphore.withPermit(action: () -> T): T { } private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Semaphore { + /* + The queue of waiting acquirers is essentially an infinite array based on the list of segments + (see `SemaphoreSegment`); each segment contains a fixed number of slots. To determine a slot for each enqueue + and dequeue operation, we increment the corresponding counter at the beginning of the operation + and use the value before the increment as a slot number. This way, each enqueue-dequeue pair + works with an individual cell. We use the corresponding segment pointers to find the required ones. + + Here is a state machine for cells. Note that only one `acquire` and at most one `release` operation + can deal with each cell, and that `release` uses `getAndSet(PERMIT)` to perform transitions for performance reasons + so that the state `PERMIT` represents different logical states. + + +------+ `acquire` suspends +------+ `release` tries +--------+ // if `cont.tryResume(..)` succeeds, then + | NULL | -------------------> | cont | -------------------> | PERMIT | (cont RETRIEVED) // the corresponding `acquire` operation gets + +------+ +------+ to resume `cont` +--------+ // a permit and the `release` one completes. + | | + | | `acquire` request is cancelled and the continuation is + | `release` comes | replaced with a special `CANCEL` token to avoid memory leaks + | to the slot before V + | `acquire` and puts +-----------+ `release` has +--------+ + | a permit into the | CANCELLED | -----------------> | PERMIT | (RElEASE FAILED) + | slot, waiting for +-----------+ failed +--------+ + | `acquire` after + | that. + | + | `acquire` gets +-------+ + | +-----------------> | TAKEN | (ELIMINATION HAPPENED) + V | the permit +-------+ + +--------+ | + | PERMIT | -< + +--------+ | + | `release` has waited a bounded time, +--------+ + +---------------------------------------> | BROKEN | (BOTH RELEASE AND ACQUIRE FAILED) + but `acquire` has not come +--------+ + */ - // The queue of waiting acquirers is essentially an infinite array based on the list of segments - // (see `SemaphoreSegment`); each segment contains a fixed number of slots. To determine a slot for each enqueue - // and dequeue operation, we increment the corresponding counter at the beginning of the operation - // and use the value before the increment as a slot number. This way, each enqueue-dequeue pair - // works with an individual cell.We use the corresponding segment pointer to find the required ones. private val head: AtomicRef private val deqIdx = atomic(0L) private val tail: AtomicRef @@ -123,74 +152,100 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se override suspend fun acquire() { val p = _availablePermits.getAndDecrement() if (p > 0) return // permit acquired - addToQueueAndSuspend() + // While it looks better when the following function is inlined, + // it is important to make `suspend` function invocations in a way + // so that the tail-call optimization can be applied. + acquireSlowPath() } - override fun release() { - val p = incPermits() - if (p >= 0) return // no waiters - resumeNextFromQueue() + private suspend fun acquireSlowPath() = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + while (true) { + if (addAcquireToQueue(cont)) return@sc + val p = _availablePermits.getAndDecrement() + if (p > 0) { // permit acquired + cont.resume(Unit) + return@sc + } + } } - fun incPermits() = _availablePermits.getAndUpdate { cur -> - check(cur < permits) { "The number of released permits cannot be greater than $permits" } - cur + 1 + override fun release() { + while (true) { + val p = _availablePermits.getAndUpdate { cur -> + check(cur < permits) { "The number of released permits cannot be greater than $permits" } + cur + 1 + } + if (p >= 0) return + if (tryResumeNextFromQueue()) return + } } - private suspend fun addToQueueAndSuspend() = suspendAtomicCancellableCoroutineReusable sc@{ cont -> + /** + * Returns `false` if the received permit cannot be used and the calling operation should restart. + */ + private fun addAcquireToQueue(cont: CancellableContinuation): Boolean { val curTail = this.tail.value val enqIdx = enqIdx.getAndIncrement() val segment = this.tail.findSegmentAndMoveForward(id = enqIdx / SEGMENT_SIZE, startFrom = curTail, - createNewSegment = ::createSegment).run { segment } // cannot be closed + createNewSegment = ::createSegment).segment // cannot be closed val i = (enqIdx % SEGMENT_SIZE).toInt() - if (segment.get(i) === RESUMED || !segment.cas(i, null, cont)) { - // already resumed + // the regular (fast) path -- if the cell is empty, try to install continuation + if (segment.cas(i, null, cont)) { // installed continuation successfully + cont.invokeOnCancellation(CancelSemaphoreAcquisitionHandler(segment, i).asHandler) + return true + } + // On CAS failure -- the cell must be either PERMIT or BROKEN + // If the cell already has PERMIT from tryResumeNextFromQueue, try to grab it + if (segment.cas(i, PERMIT, TAKEN)) { // took permit thus eliminating acquire/release pair cont.resume(Unit) - return@sc + return true } - cont.invokeOnCancellation(CancelSemaphoreAcquisitionHandler(this, segment, i).asHandler) + assert { segment.get(i) === BROKEN } // it must be broken in this case, no other way around it + return false // broken cell, need to retry on a different cell } @Suppress("UNCHECKED_CAST") - internal fun resumeNextFromQueue() { - try_again@ while (true) { - val curHead = this.head.value - val deqIdx = deqIdx.getAndIncrement() - val id = deqIdx / SEGMENT_SIZE - val segment = this.head.findSegmentAndMoveForward(id, startFrom = curHead, - createNewSegment = ::createSegment).run { segment } // cannot be closed - segment.cleanPrev() - if (segment.id > id) { - this.deqIdx.updateIfLower(segment.id * SEGMENT_SIZE) - continue@try_again + private fun tryResumeNextFromQueue(): Boolean { + val curHead = this.head.value + val deqIdx = deqIdx.getAndIncrement() + val id = deqIdx / SEGMENT_SIZE + val segment = this.head.findSegmentAndMoveForward(id, startFrom = curHead, + createNewSegment = ::createSegment).segment // cannot be closed + segment.cleanPrev() + if (segment.id > id) return false + val i = (deqIdx % SEGMENT_SIZE).toInt() + val cellState = segment.getAndSet(i, PERMIT) // set PERMIT and retrieve the prev cell state + when { + cellState === null -> { + // Acquire has not touched this cell yet, wait until it comes for a bounded time + // The cell state can only transition from PERMIT to TAKEN by addAcquireToQueue + repeat(MAX_SPIN_CYCLES) { + if (segment.get(i) === TAKEN) return true + } + // Try to break the slot in order not to wait + return !segment.cas(i, PERMIT, BROKEN) } - val i = (deqIdx % SEGMENT_SIZE).toInt() - val cont = segment.getAndSet(i, RESUMED) - if (cont === null) return // just resumed - if (cont === CANCELLED) continue@try_again - (cont as CancellableContinuation).resume(Unit) - return + cellState === CANCELLED -> return false // the acquire was already cancelled + else -> return (cellState as CancellableContinuation).tryResume() } } } -private inline fun AtomicLong.updateIfLower(value: Long): Unit = loop { cur -> - if (cur >= value || compareAndSet(cur, value)) return +private fun CancellableContinuation.tryResume(): Boolean { + val token = tryResume(Unit) ?: return false + completeResume(token) + return true } private class CancelSemaphoreAcquisitionHandler( - private val semaphore: SemaphoreImpl, private val segment: SemaphoreSegment, private val index: Int ) : CancelHandler() { override fun invoke(cause: Throwable?) { - val p = semaphore.incPermits() - if (p >= 0) return - if (segment.cancel(index)) return - semaphore.resumeNextFromQueue() + segment.cancel(index) } - override fun toString() = "CancelSemaphoreAcquisitionHandler[$semaphore, $segment, $index]" + override fun toString() = "CancelSemaphoreAcquisitionHandler[$segment, $index]" } private fun createSegment(id: Long, prev: SemaphoreSegment?) = SemaphoreSegment(id, prev, 0) @@ -202,6 +257,11 @@ private class SemaphoreSegment(id: Long, prev: SemaphoreSegment?, pointers: Int) @Suppress("NOTHING_TO_INLINE") inline fun get(index: Int): Any? = acquirers[index].value + @Suppress("NOTHING_TO_INLINE") + inline fun set(index: Int, value: Any?) { + acquirers[index].value = value + } + @Suppress("NOTHING_TO_INLINE") inline fun cas(index: Int, expected: Any?, value: Any?): Boolean = acquirers[index].compareAndSet(expected, value) @@ -210,19 +270,23 @@ private class SemaphoreSegment(id: Long, prev: SemaphoreSegment?, pointers: Int) // Cleans the acquirer slot located by the specified index // and removes this segment physically if all slots are cleaned. - fun cancel(index: Int): Boolean { - // Try to cancel the slot - val cancelled = getAndSet(index, CANCELLED) !== RESUMED + fun cancel(index: Int) { + // Clean the slot + set(index, CANCELLED) // Remove this segment if needed onSlotCleaned() - return cancelled } override fun toString() = "SemaphoreSegment[id=$id, hashCode=${hashCode()}]" } - @SharedImmutable -private val RESUMED = Symbol("RESUMED") +private val MAX_SPIN_CYCLES = systemProp("kotlinx.coroutines.semaphore.maxSpinCycles", 100) +@SharedImmutable +private val PERMIT = Symbol("PERMIT") +@SharedImmutable +private val TAKEN = Symbol("TAKEN") +@SharedImmutable +private val BROKEN = Symbol("BROKEN") @SharedImmutable private val CANCELLED = Symbol("CANCELLED") @SharedImmutable diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt index 3d1305c682..5e408b74ac 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt @@ -51,13 +51,15 @@ internal class SegmentBasedQueue { val segmentOrClosed = this.head.findSegmentAndMoveForward(id = deqIdx, startFrom = curHead, createNewSegment = ::createSegment) if (segmentOrClosed.isClosed) return null val s = segmentOrClosed.segment - s.cleanPrev() if (s.id > deqIdx) continue var el = s.element.value if (el === null) { if (s.element.compareAndSet(null, BROKEN)) continue else el = s.element.value } + // The link to the previous segment should be cleaned after retrieving the element; + // otherwise, `close()` cannot clean the slot. + s.cleanPrev() if (el === BROKEN) continue @Suppress("UNCHECKED_CAST") return el as T diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt new file mode 100644 index 0000000000..9542b5d8de --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("unused") +package kotlinx.coroutines.linearizability + +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.* +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.junit.* + +class MutexLCStressTest : VerifierState() { + private val mutex = Mutex() + + @Operation + fun tryLock() = mutex.tryLock() + + @Operation + suspend fun lock() = mutex.lock() + + @Operation(handleExceptionsAsResult = [IllegalStateException::class]) + fun unlock() = mutex.unlock() + + @Test + fun test() = LCStressOptionsDefault() + .actorsBefore(0) + .check(this::class) + + override fun extractState() = mutex.isLocked +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt new file mode 100644 index 0000000000..52902f4987 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("unused") +package kotlinx.coroutines.linearizability + +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.* +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.junit.* + +abstract class SemaphoreLCStressTestBase(permits: Int) : VerifierState() { + private val semaphore = Semaphore(permits) + + @Operation + fun tryAcquire() = semaphore.tryAcquire() + + @Operation + suspend fun acquire() = semaphore.acquire() + + @Operation(handleExceptionsAsResult = [IllegalStateException::class]) + fun release() = semaphore.release() + + @Test + fun test() = LCStressOptionsDefault() + .actorsBefore(0) + .check(this::class) + + override fun extractState() = semaphore.availablePermits +} + +class Semaphore1LCStressTest : SemaphoreLCStressTestBase(1) +class Semaphore2LCStressTest : SemaphoreLCStressTestBase(2) \ No newline at end of file From 70a7487d9e2116e9fa32f6a94923f4ead4f4f2a9 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 24 Apr 2020 14:59:28 +0300 Subject: [PATCH 020/257] Extract debugger (#1905) This is debug agent machinery rework in order to prepare for IDEA integration * Extract internal DebugProbesImpl to kotlinx-coroutines-core * Introduce AgentPremain that works without ByteBuddy to kotlinx-coroutines-core, so it now can be used as Java agent and all debug info can be extracted via reflection or JDWP * Reflective lookup of ByteBuddy attach to resolve cyclic dependency between core and debug modules * Reduce public API surface, introduce JDWP-specific API * Introduce a mechanism to produce a DebugProbesKt.bin and verify them against the golden value --- integration-testing/build.gradle | 25 ++++- .../src/coreAgentTest/kotlin/CoreAgentTest.kt | 22 ++++ .../debugAgentTest/kotlin/DebugAgentTest.kt | 5 +- .../src/debugAgentTest/kotlin/DebugProbes.kt | 14 +++ .../kotlin/PrecompiledDebugProbesTest.kt | 39 +++++++ .../api/kotlinx-coroutines-core.api | 4 + kotlinx-coroutines-core/build.gradle | 7 ++ .../jvm/resources/DebugProbesKt.bin | Bin 0 -> 1730 bytes .../jvm/src/debug/AgentPremain.kt | 69 +++++++++++++ .../src/debug/internal/DebugCoroutineInfo.kt | 87 ++++++++++++++++ .../src/debug}/internal/DebugProbesImpl.kt | 95 +++++++++--------- .../jvm/src/debug/internal/DebuggerInfo.kt | 26 +++++ .../api/kotlinx-coroutines-debug.api | 5 - kotlinx-coroutines-debug/src/AgentPremain.kt | 41 -------- kotlinx-coroutines-debug/src/CoroutineInfo.kt | 51 +++------- kotlinx-coroutines-debug/src/DebugProbes.kt | 18 ++-- .../src/internal/Attach.kt | 41 ++++++++ .../test/RunningThreadStackMergeTest.kt | 7 +- .../testdata/r8-test-rules.pro | 3 +- 19 files changed, 414 insertions(+), 145 deletions(-) create mode 100644 integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt create mode 100644 integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt create mode 100644 integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt create mode 100644 kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin create mode 100644 kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt rename {kotlinx-coroutines-debug/src => kotlinx-coroutines-core/jvm/src/debug}/internal/DebugProbesImpl.kt (87%) create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt delete mode 100644 kotlinx-coroutines-debug/src/AgentPremain.kt create mode 100644 kotlinx-coroutines-debug/src/internal/Attach.kt diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index f376f667e0..90f3365b09 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -25,6 +25,18 @@ sourceSets { compileClasspath += sourceSets.test.runtimeClasspath runtimeClasspath += sourceSets.test.runtimeClasspath } + + coreAgentTest { + kotlin + compileClasspath += sourceSets.test.runtimeClasspath + runtimeClasspath += sourceSets.test.runtimeClasspath + } +} + +compileDebugAgentTestKotlin { + kotlinOptions { + freeCompilerArgs += ["-Xallow-kotlin-package"] + } } task npmTest(type: Test) { @@ -62,6 +74,14 @@ task debugAgentTest(type: Test) { classpath = sourceSet.runtimeClasspath } +task coreAgentTest(type: Test) { + def sourceSet = sourceSets.coreAgentTest + dependsOn(project(':kotlinx-coroutines-core').jvmJar) + jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-core').jvmJar.outputs.files.getFiles()[0]) + testClassesDirs = sourceSet.output.classesDirs + classpath = sourceSet.runtimeClasspath +} + dependencies { testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" testCompile 'junit:junit:4.12' @@ -69,12 +89,13 @@ dependencies { npmTestCompile 'com.google.code.gson:gson:2.8.5' debugAgentTestCompile project(':kotlinx-coroutines-core') debugAgentTestCompile project(':kotlinx-coroutines-debug') + coreAgentTestCompile project(':kotlinx-coroutines-core') } compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } -test { - dependsOn([npmTest, mavenTest, debugAgentTest]) +check { + dependsOn([npmTest, mavenTest, debugAgentTest, coreAgentTest]) } diff --git a/integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt b/integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt new file mode 100644 index 0000000000..6d47323370 --- /dev/null +++ b/integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.junit.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.Test +import java.io.* + +class CoreAgentTest { + + @Test + fun testAgentDumpsCoroutines() = runBlocking { + val baos = ByteArrayOutputStream() + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + DebugProbesImpl.dumpCoroutines(PrintStream(baos)) + // if the agent works, then dumps should contain something, + // at least the fact that this test is running. + Assert.assertTrue(baos.toString().contains("testAgentDumpsCoroutines")) + } + +} diff --git a/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt b/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt index 925fe077ea..d6c4aa2fb4 100644 --- a/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt +++ b/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt @@ -4,17 +4,18 @@ import org.junit.* import kotlinx.coroutines.* import kotlinx.coroutines.debug.* +import org.junit.Test import java.io.* class DebugAgentTest { @Test - fun agentDumpsCoroutines() = runBlocking { + fun testAgentDumpsCoroutines() = runBlocking { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) // if the agent works, then dumps should contain something, // at least the fact that this test is running. - Assert.assertTrue(baos.toString().contains("agentDumpsCoroutines")) + Assert.assertTrue(baos.toString().contains("testAgentDumpsCoroutines")) } } diff --git a/integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt b/integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt new file mode 100644 index 0000000000..46a7634a8b --- /dev/null +++ b/integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package kotlin.coroutines.jvm.internal + +import kotlinx.coroutines.debug.internal.* +import kotlin.coroutines.* + +internal fun probeCoroutineCreated(completion: Continuation): Continuation = DebugProbesImpl.probeCoroutineCreated(completion) + +internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame) + +internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame) \ No newline at end of file diff --git a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt new file mode 100644 index 0000000000..5d799ee02f --- /dev/null +++ b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.junit.Test +import java.io.* +import kotlin.test.* + +/* + * This is intentionally put here instead of coreAgentTest to avoid accidental classpath replacing + * and ruining core agent test. + */ +class PrecompiledDebugProbesTest { + + private val overwrite = java.lang.Boolean.getBoolean("overwrite.probes") + + @Test + fun testClassFileContent() { + val clz = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val className: String = clz.getName() + val classFileResourcePath = className.replace(".", "/") + ".class" + val stream = clz.classLoader.getResourceAsStream(classFileResourcePath)!! + val array = stream.readBytes() + val binFile = clz.classLoader.getResourceAsStream("DebugProbesKt.bin")!! + val binContent = binFile.readBytes() + if (overwrite) { + val url = clz.classLoader.getResource("DebugProbesKt.bin")!! + val base = url.toExternalForm().toString().removePrefix("jar:file:").substringBefore("/build") + val probes = File(base, "jvm/resources/DebugProbesKt.bin") + FileOutputStream(probes).use { it.write(array) } + println("Content was successfully overwritten!") + } else { + assertTrue( + array.contentEquals(binContent), + "Compiled DebugProbesKt.class does not match the file shipped as a resource in kotlinx-coroutines-core. " + + "Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true and " + + "ensure that classfile has major version equal to 50 (Java 6 compliance)") + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 54e355ec37..08f46e5108 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -832,6 +832,10 @@ public final class kotlinx/coroutines/channels/ValueOrClosed { public final synthetic fun unbox-impl ()Ljava/lang/Object; } +public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile { + public fun (J)V +} + public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { public fun ()V public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 8e899016e7..451293c5ff 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -92,6 +92,13 @@ jvmTest { systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test } +jvmJar { + manifest { + attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain" + attributes "Can-Retransform-Classes": "true" + } +} + task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) { classpath = files { jvmTest.classpath } testClassesDirs = files { jvmTest.testClassesDirs } diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin new file mode 100644 index 0000000000000000000000000000000000000000..8a7160fbad39f25ba582998c532c6ff65c788d5f GIT binary patch literal 1730 zcmb7ETTc@~7(KHsy{r|MdV`{<1r<q5Irc6N!6{2U+E z2V;CN#7BRW@tdVcDPT3}cIW%%>^a|U=I5_(-vQ+CgyGhv>$hyjv|P^(eA^My+}LiL zw&M%W;Vtu-s0Ymz&#epj%8!FF7#n<>n=S4%&E@)ru*e%>IJc!R#eJ5dC%7*f3{R)Z zM-_^$Ls9|vZP&?9_X99kh1#QXuWWv?A9Z22R?9E;6)1!#hKaK4HO&p-*FA1K(&Ucg z`eBUHthjz9XtnYTy5+XFT0(`MK$2m+lis1Kl^owA3fq!#oMO0Oy%W~v3io(hP+d#1 zL&tDq>S$hFo~oj0=BL+EIE_IaDGV{p?DaE7DB3uN84{&RwYF3#K4)0y zl>9#^@+q9b*(B09$1ryYd4jN z;d(z*VTLAxfCFz;Hxd{>l1@kQ*OBmHHP8+ zBE1$qZ*ZT}*lF$U2%TA`{tPnfp$TQu>0_H}p^?8a&*1Lvr1jk$T^rGKEfLW*BYbDF z#_mogk&dRdm)ac5MiaX`hL%eikpt&TLYy_C8EtMLryH?MEKLM+Y|kA(#{J_%ETLp` z6hw_tfXJ%5qv?eQXVw>zPI~iAIG;;R;rKQ3FbsCxE4Mev5iPn6!7x;&E2t8*>%yz? zdW$^ivTO0y8ux5vcbv(p-E_Dgcmx<*4d^y$i;}Z#OPerD2j`YyvUm5-DLe$zt8U;~ z;)SgwM?2rF9pS?~G(@S?RCSDE05P)ell6f7lsyJ#()w2zpK*TX3r0VZN25Iuj?tsz zA+1w;NFrse_Bi}(K=v|;EP6dTBMlptqB-bNz`HxKY6QR0gQio*5yJRl)QcwJy z`g$+*;J;F9cpOG`4~z6(rd)1Nxu(_;DPcPN&EV#~b*qF~+%Cby+&Y+~D(BXblo*i6 SNGwRT9+S8uF)lG7arZY>jlv25 literal 0 HcmV?d00001 diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt new file mode 100644 index 0000000000..6493077593 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.debug.internal.DebugProbesImpl +import sun.misc.* +import java.lang.instrument.* +import java.lang.instrument.ClassFileTransformer +import java.security.* + +@Suppress("unused") +internal object AgentPremain { + + public var isInstalledStatically = false + + private val enableCreationStackTraces = + System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() + ?: DebugProbesImpl.enableCreationStackTraces + + @JvmStatic + public fun premain(args: String?, instrumentation: Instrumentation) { + isInstalledStatically = true + instrumentation.addTransformer(DebugProbesTransformer) + DebugProbesImpl.enableCreationStackTraces = enableCreationStackTraces + DebugProbesImpl.install() + installSignalHandler() + } + + internal object DebugProbesTransformer : ClassFileTransformer { + override fun transform( + loader: ClassLoader, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray? + ): ByteArray? { + if (className != "kotlin/coroutines/jvm/internal/DebugProbesKt") { + return null + } + /* + * DebugProbesKt.bin contains `kotlin.coroutines.jvm.internal.DebugProbesKt` class + * with method bodies that delegate all calls directly to their counterparts in + * kotlinx.coroutines.debug.DebugProbesImpl. This is done to avoid classfile patching + * on the fly (-> get rid of ASM dependency). + * You can verify its content either by using javap on it or looking at out integration test module. + */ + isInstalledStatically = true + return loader.getResourceAsStream("DebugProbesKt.bin").readBytes() + } + } + + private fun installSignalHandler() { + try { + Signal.handle(Signal("TRAP")) { // kill -5 + if (DebugProbesImpl.isInstalled) { + // Case with 'isInstalled' changed between this check-and-act is not considered + // a real debug probes use-case, thus is not guarded against. + DebugProbesImpl.dumpCoroutines(System.out) + } else { + println("Cannot perform coroutines dump, debug probes are disabled") + } + } + } catch (t: Throwable) { + System.err.println("Failed to install signal handler: $t") + } + } +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt new file mode 100644 index 0000000000..82e18eabe7 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* + +internal const val CREATED = "CREATED" +internal const val RUNNING = "RUNNING" +internal const val SUSPENDED = "SUSPENDED" + +internal class DebugCoroutineInfo( + public val context: CoroutineContext, + public val creationStackBottom: CoroutineStackFrame?, + @JvmField internal val sequenceNumber: Long +) { + + public val creationStackTrace: List get() = creationStackTrace() + + /** + * Last observed state of the coroutine. + * Can be CREATED, RUNNING, SUSPENDED. + */ + public val state: String get() = _state + private var _state: String = CREATED + + @JvmField + internal var lastObservedThread: Thread? = null + @JvmField + internal var lastObservedFrame: CoroutineStackFrame? = null + + public fun copy(): DebugCoroutineInfo = DebugCoroutineInfo( + context, + creationStackBottom, + sequenceNumber + ).also { + it._state = _state + it.lastObservedFrame = lastObservedFrame + it.lastObservedThread = lastObservedThread + } + + /** + * Last observed stacktrace of the coroutine captured on its suspension or resumption point. + * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and + * reflects stacktrace of the resumption point, not the actual current stacktrace. + */ + public fun lastObservedStackTrace(): List { + var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList() + val result = ArrayList() + while (frame != null) { + frame.getStackTraceElement()?.let { result.add(it) } + frame = frame.callerFrame + } + return result + } + + private fun creationStackTrace(): List { + val bottom = creationStackBottom ?: return emptyList() + // Skip "Coroutine creation stacktrace" frame + return sequence { yieldFrames(bottom.callerFrame) }.toList() + } + + private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) { + if (frame == null) return + frame.getStackTraceElement()?.let { yield(it) } + val caller = frame.callerFrame + if (caller != null) { + yieldFrames(caller) + } + } + + internal fun updateState(state: String, frame: Continuation<*>) { + // Propagate only duplicating transitions to running for KT-29997 + if (_state == state && state == SUSPENDED && lastObservedFrame != null) return + _state = state + lastObservedFrame = frame as? CoroutineStackFrame + lastObservedThread = if (state == RUNNING) { + Thread.currentThread() + } else { + null + } + } + + override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)" +} diff --git a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt similarity index 87% rename from kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt rename to kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 8b7d8e7998..6e8d19fb3f 100644 --- a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -7,9 +7,6 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.debug.* -import net.bytebuddy.* -import net.bytebuddy.agent.* -import net.bytebuddy.dynamic.loading.* import java.io.* import java.text.* import java.util.* @@ -21,11 +18,6 @@ import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.* import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround -/** - * Mirror of [DebugProbes] with actual implementation. - * [DebugProbes] are implemented with pimpl to simplify user-facing class and make it look simple and - * documented. - */ internal object DebugProbesImpl { private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace" private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") @@ -44,6 +36,23 @@ internal object DebugProbesImpl { */ private val coroutineStateLock = ReentrantReadWriteLock() + public var sanitizeStackTraces: Boolean = true + public var enableCreationStackTraces: Boolean = true + + /* + * Substitute for service loader, DI between core and debug modules. + * If the agent was installed via command line -javaagent parameter, do not use byte-byddy to avoud + */ + private val dynamicAttach = getDynamicAttach() + + @Suppress("UNCHECKED_CAST") + private fun getDynamicAttach(): Function1? = runCatching { + val clz = Class.forName("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach") + val ctor = clz.constructors[0] + ctor.newInstance() as Function1 + }.getOrNull() + + /* * This is an optimization in the face of KT-29997: * Consider suspending call stack a()->b()->c() and c() completes its execution and every call is @@ -52,36 +61,21 @@ internal object DebugProbesImpl { * Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth). * To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally. */ - private val callerInfoCache = ConcurrentHashMap() + private val callerInfoCache = ConcurrentHashMap() public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return - - ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE) - val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") - val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") - - ByteBuddy() - .redefine(cl2) - .name(cl.name) - .make() - .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + if (AgentPremain.isInstalledStatically) return + dynamicAttach?.invoke(true) // attach } public fun uninstall(): Unit = coroutineStateLock.write { check(isInstalled) { "Agent was not installed" } if (--installations != 0) return - capturedCoroutines.clear() callerInfoCache.clear() - val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") - val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") - - ByteBuddy() - .redefine(cl2) - .name(cl.name) - .make() - .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + if (AgentPremain.isInstalledStatically) return + dynamicAttach?.invoke(false) // detach } public fun hierarchyToString(job: Job): String = coroutineStateLock.write { @@ -94,7 +88,7 @@ internal object DebugProbesImpl { } } - private fun Job.build(map: Map, builder: StringBuilder, indent: String) { + private fun Job.build(map: Map, builder: StringBuilder, indent: String) { val info = map[this] val newIndent: String if (info == null) { // Append coroutine without stacktrace @@ -122,7 +116,7 @@ internal object DebugProbesImpl { @Suppress("DEPRECATION_ERROR") // JobSupport private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString() - public fun dumpCoroutinesInfo(): List = coroutineStateLock.write { + public fun dumpCoroutinesInfo(): List = coroutineStateLock.write { check(isInstalled) { "Debug probes are not installed" } return capturedCoroutines.asSequence() .map { it.info.copy() } // Copy as CoroutineInfo can be mutated concurrently by DebugProbes @@ -130,6 +124,12 @@ internal object DebugProbesImpl { .toList() } + /* + * Internal (JVM-public) method used by IDEA debugger. + * It is equivalent to dumpCoroutines, but returns serializable (and thus less typed) objects. + */ + public fun dumpDebuggerInfo() = dumpCoroutinesInfo().map { DebuggerInfo(it) } + public fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) { /* * This method synchronizes both on `out` and `this` for a reason: @@ -151,7 +151,7 @@ internal object DebugProbesImpl { val info = owner.info val observedStackTrace = info.lastObservedStackTrace() val enhancedStackTrace = enhanceStackTraceWithThreadDump(info, observedStackTrace) - val state = if (info.state == State.RUNNING && enhancedStackTrace === observedStackTrace) + val state = if (info.state == RUNNING && enhancedStackTrace === observedStackTrace) "${info.state} (Last suspension stacktrace, not an actual stacktrace)" else info.state.toString() @@ -173,17 +173,17 @@ internal object DebugProbesImpl { } /** - * Tries to enhance [coroutineTrace] (obtained by call to [CoroutineInfo.lastObservedStackTrace]) with - * thread dump of [CoroutineInfo.lastObservedThread]. + * Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfo.lastObservedStackTrace]) with + * thread dump of [DebugCoroutineInfo.lastObservedThread]. * * Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result. */ private fun enhanceStackTraceWithThreadDump( - info: CoroutineInfo, + info: DebugCoroutineInfo, coroutineTrace: List ): List { val thread = info.lastObservedThread - if (info.state != State.RUNNING || thread == null) return coroutineTrace + if (info.state != RUNNING || thread == null) return coroutineTrace // Avoid security manager issues val actualTrace = runCatching { thread.stackTrace }.getOrNull() ?: return coroutineTrace @@ -213,7 +213,8 @@ internal object DebugProbesImpl { val (continuationStartFrame, frameSkipped) = findContinuationStartIndex( indexOfResumeWith, actualTrace, - coroutineTrace) + coroutineTrace + ) if (continuationStartFrame == -1) return coroutineTrace @@ -266,13 +267,13 @@ internal object DebugProbesImpl { } } - internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, State.RUNNING) + internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, RUNNING) - internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED) + internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, SUSPENDED) - private fun updateState(frame: Continuation<*>, state: State) { + private fun updateState(frame: Continuation<*>, state: String) { // KT-29997 is here only since 1.3.30 - if (state == State.RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) { + if (state == RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) { val stackFrame = frame as? CoroutineStackFrame ?: return updateRunningState(stackFrame, state) return @@ -284,10 +285,10 @@ internal object DebugProbesImpl { } // See comment to callerInfoCache - private fun updateRunningState(frame: CoroutineStackFrame, state: State): Unit = coroutineStateLock.read { + private fun updateRunningState(frame: CoroutineStackFrame, state: String): Unit = coroutineStateLock.read { if (!isInstalled) return // Lookup coroutine info in cache or by traversing stack frame - val info: CoroutineInfo + val info: DebugCoroutineInfo val cached = callerInfoCache.remove(frame) if (cached != null) { info = cached @@ -309,7 +310,7 @@ internal object DebugProbesImpl { return if (caller.getStackTraceElement() != null) caller else caller.realCaller() } - private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: State) = coroutineStateLock.read { + private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: String) = coroutineStateLock.read { if (!isInstalled) return owner.info.updateState(state, frame) } @@ -335,7 +336,7 @@ internal object DebugProbesImpl { * and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls. */ - val frame = if (DebugProbes.enableCreationStackTraces) { + val frame = if (enableCreationStackTraces) { val stacktrace = sanitizeStackTrace(Exception()) stacktrace.foldRight(null) { frame, acc -> object : CoroutineStackFrame { @@ -352,7 +353,7 @@ internal object DebugProbesImpl { private fun createOwner(completion: Continuation, frame: CoroutineStackFrame?): Continuation { if (!isInstalled) return completion - val info = CoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet()) + val info = DebugCoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet()) val owner = CoroutineOwner(completion, info, frame) capturedCoroutines += owner if (!isInstalled) capturedCoroutines.clear() @@ -376,7 +377,7 @@ internal object DebugProbesImpl { */ private class CoroutineOwner( @JvmField val delegate: Continuation, - @JvmField val info: CoroutineInfo, + @JvmField val info: DebugCoroutineInfo, private val frame: CoroutineStackFrame? ) : Continuation by delegate, CoroutineStackFrame { @@ -398,7 +399,7 @@ internal object DebugProbesImpl { val size = stackTrace.size val probeIndex = stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" } - if (!DebugProbes.sanitizeStackTraces) { + if (!sanitizeStackTraces) { return List(size - probeIndex) { if (it == 0) createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex] } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt new file mode 100644 index 0000000000..4b95af986a --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UNUSED") + +package kotlinx.coroutines.debug.internal + +import java.io.Serializable +import kotlin.coroutines.* +import kotlinx.coroutines.* + +/* + * This class represents all the data required by IDEA debugger. + * It is serializable in order to speedup JDWP interactions + */ +internal class DebuggerInfo(source: DebugCoroutineInfo) : Serializable { + public val coroutineId: Long? = source.context[CoroutineId]?.id + public val dispatcher: String? = source.context[ContinuationInterceptor].toString() + public val name: String? = source.context[CoroutineName]?.name + public val state: String = source.state + public val lastObservedThreadState: String? = source.lastObservedThread?.state?.toString() + public val lastObservedThreadName = source.lastObservedThread?.name + public val lastObservedStackTrace: List = source.lastObservedStackTrace() + public val sequenceNumber: Long = source.sequenceNumber +} diff --git a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api index 749c94619e..b6056c410c 100644 --- a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api +++ b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api @@ -1,5 +1,4 @@ public final class kotlinx/coroutines/debug/CoroutineInfo { - public final fun copy ()Lkotlinx/coroutines/debug/CoroutineInfo; public final fun getContext ()Lkotlin/coroutines/CoroutineContext; public final fun getCreationStackTrace ()Ljava/util/List; public final fun getJob ()Lkotlinx/coroutines/Job; @@ -42,10 +41,6 @@ public final class kotlinx/coroutines/debug/State : java/lang/Enum { public static fun values ()[Lkotlinx/coroutines/debug/State; } -public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile { - public fun (J)V -} - public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit/rules/TestRule { public static final field Companion Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion; public fun (JZ)V diff --git a/kotlinx-coroutines-debug/src/AgentPremain.kt b/kotlinx-coroutines-debug/src/AgentPremain.kt deleted file mode 100644 index aa842ce327..0000000000 --- a/kotlinx-coroutines-debug/src/AgentPremain.kt +++ /dev/null @@ -1,41 +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.debug - -import net.bytebuddy.agent.* -import sun.misc.* -import java.lang.instrument.* - -@Suppress("unused") -internal object AgentPremain { - - private val enableCreationStackTraces = - System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() - ?: DebugProbes.enableCreationStackTraces - - @JvmStatic - public fun premain(args: String?, instrumentation: Instrumentation) { - Installer.premain(args, instrumentation) - DebugProbes.enableCreationStackTraces = enableCreationStackTraces - DebugProbes.install() - installSignalHandler() - } - - private fun installSignalHandler() { - try { - Signal.handle(Signal("TRAP")) { // kill -5 - if (DebugProbes.isInstalled) { - // Case with 'isInstalled' changed between this check-and-act is not considered - // a real debug probes use-case, thus is not guarded against. - DebugProbes.dumpCoroutines() - } else { - println("""Cannot perform coroutines dump, debug probes are disabled""") - } - } - } catch (t: Throwable) { - System.err.println("Failed to install signal handler: $t") - } - } -} diff --git a/kotlinx-coroutines-debug/src/CoroutineInfo.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt index c9517fe897..11224f53b3 100644 --- a/kotlinx-coroutines-debug/src/CoroutineInfo.kt +++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt @@ -1,12 +1,11 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - -@file:Suppress("PropertyName") - +@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "UNUSED") package kotlinx.coroutines.debug import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.* @@ -14,11 +13,16 @@ import kotlin.coroutines.jvm.internal.* * Class describing coroutine info such as its context, state and stacktrace. */ @ExperimentalCoroutinesApi -public class CoroutineInfo internal constructor( - public val context: CoroutineContext, - private val creationStackBottom: CoroutineStackFrame?, - @JvmField internal val sequenceNumber: Long -) { +public class CoroutineInfo internal constructor(delegate: DebugCoroutineInfo) { + /** + * [Coroutine context][coroutineContext] of the coroutine + */ + public val context: CoroutineContext = delegate.context + /** + * Last observed state of the coroutine + */ + public val state: State = State.valueOf(delegate.state) + private val creationStackBottom: CoroutineStackFrame? = delegate.creationStackBottom /** * [Job] associated with a current coroutine or null. @@ -32,24 +36,7 @@ public class CoroutineInfo internal constructor( */ public val creationStackTrace: List get() = creationStackTrace() - /** - * Last observed [state][State] of the coroutine. - */ - public val state: State get() = _state - - private var _state: State = State.CREATED - - @JvmField - internal var lastObservedThread: Thread? = null - - @JvmField - internal var lastObservedFrame: CoroutineStackFrame? = null - - public fun copy(): CoroutineInfo = CoroutineInfo(context, creationStackBottom, sequenceNumber).also { - it._state = _state - it.lastObservedFrame = lastObservedFrame - it.lastObservedThread = lastObservedThread - } + private val lastObservedFrame: CoroutineStackFrame? = delegate.lastObservedFrame /** * Last observed stacktrace of the coroutine captured on its suspension or resumption point. @@ -81,18 +68,6 @@ public class CoroutineInfo internal constructor( } } - internal fun updateState(state: State, frame: Continuation<*>) { - // Propagate only duplicating transitions to running for KT-29997 - if (_state == state && state == State.SUSPENDED && lastObservedFrame != null) return - _state = state - lastObservedFrame = frame as? CoroutineStackFrame - if (state == State.RUNNING) { - lastObservedThread = Thread.currentThread() - } else { - lastObservedThread = null - } - } - override fun toString(): String = "CoroutineInfo(state=$state,context=$context)" } diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt index 710300cb37..254385f942 100644 --- a/kotlinx-coroutines-debug/src/DebugProbes.kt +++ b/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -2,7 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("unused") +@file:Suppress("UNUSED", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package kotlinx.coroutines.debug @@ -10,7 +10,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.debug.internal.* import java.io.* import java.lang.management.* -import java.util.* import kotlin.coroutines.* /** @@ -41,7 +40,11 @@ public object DebugProbes { * Sanitization removes all frames from `kotlinx.coroutines` package except * the first one and the last one to simplify diagnostic. */ - public var sanitizeStackTraces: Boolean = true + public var sanitizeStackTraces: Boolean + get() = DebugProbesImpl.sanitizeStackTraces + set(value) { + DebugProbesImpl.sanitizeStackTraces = value + } /** * Whether coroutine creation stack traces should be captured. @@ -50,7 +53,11 @@ public object DebugProbes { * This option can be useful during local debug sessions, but is recommended * to be disabled in production environments to avoid stack trace dumping overhead. */ - public var enableCreationStackTraces: Boolean = true + public var enableCreationStackTraces: Boolean + get() = DebugProbesImpl.enableCreationStackTraces + set(value) { + DebugProbesImpl.enableCreationStackTraces = value + } /** * Determines whether debug probes were [installed][DebugProbes.install]. @@ -114,7 +121,7 @@ public object DebugProbes { * Returns all existing coroutines info. * The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation. */ - public fun dumpCoroutinesInfo(): List = DebugProbesImpl.dumpCoroutinesInfo() + public fun dumpCoroutinesInfo(): List = DebugProbesImpl.dumpCoroutinesInfo().map { CoroutineInfo(it) } /** * Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation. @@ -131,7 +138,6 @@ public object DebugProbes { * at MyClass.createIoRequest(MyClass.kt:142) * at MyClass.fetchData(MyClass.kt:154) * at MyClass.showData(MyClass.kt:31) - * * ... * ``` */ diff --git a/kotlinx-coroutines-debug/src/internal/Attach.kt b/kotlinx-coroutines-debug/src/internal/Attach.kt new file mode 100644 index 0000000000..cd4cc2a52e --- /dev/null +++ b/kotlinx-coroutines-debug/src/internal/Attach.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("unused") +package kotlinx.coroutines.debug.internal + +import net.bytebuddy.* +import net.bytebuddy.agent.* +import net.bytebuddy.dynamic.loading.* + +/* + * This class is used reflectively from kotlinx-coroutines-core when this module is present in the classpath. + * It is a substitute for service loading. + */ +internal class ByteBuddyDynamicAttach : Function1 { + override fun invoke(value: Boolean) { + if (value) attach() else detach() + } + + private fun attach() { + ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE) + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") + + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } + + private fun detach() { + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } +} diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index 85aa657be4..4c13f5e67c 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -1,9 +1,11 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package kotlinx.coroutines.debug import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* import org.junit.Test import java.util.concurrent.* import kotlin.test.* @@ -175,10 +177,9 @@ class RunningThreadStackMergeTest : DebugTestBase() { fun testActiveThread() = runBlocking { launchCoroutine() awaitCoroutineStarted() - val info = DebugProbes.dumpCoroutinesInfo().find { it.state == State.RUNNING } + val info = DebugProbesImpl.dumpDebuggerInfo().find { it.state == "RUNNING" } assertNotNull(info) - @Suppress("INVISIBLE_MEMBER") // IDEA bug - assertNotNull(info.lastObservedThread) + assertNotNull(info.lastObservedThreadName) coroutineBlocker.await() } } diff --git a/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro index dde8600854..63dc24ccd5 100644 --- a/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro +++ b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro @@ -6,9 +6,10 @@ -checkdiscard class kotlinx.coroutines.internal.FastServiceLoader -checkdiscard class kotlinx.coroutines.DebugKt -checkdiscard class kotlinx.coroutines.internal.StackTraceRecoveryKt +-checkdiscard class kotlinx.coroutines.debug.DebugProbesKt # Real android projects do not keep this class, but somehow it is kept in this test (R8 bug) # -checkdiscard class kotlinx.coroutines.internal.MissingMainCoroutineDispatcher # Should not keep this class, but it is still there (R8 bug) -#-checkdiscard class kotlinx.coroutines.CoroutineId \ No newline at end of file +#-checkdiscard class kotlinx.coroutines.CoroutineId From 64d23a0ad944677325dae5e9d7af5b14161e1526 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Fri, 24 Apr 2020 15:41:07 +0300 Subject: [PATCH 021/257] Kotlin DSL. Add version extension (#1939) --- buildSrc/build.gradle.kts | 11 +++++++++++ buildSrc/src/main/kotlin/Projects.kt | 4 ++++ 2 files changed, 15 insertions(+) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/Projects.kt diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000000..91b8bda92b --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() +} + +kotlinDslPluginOptions { + experimentalWarning.set(false) +} diff --git a/buildSrc/src/main/kotlin/Projects.kt b/buildSrc/src/main/kotlin/Projects.kt new file mode 100644 index 0000000000..109311e876 --- /dev/null +++ b/buildSrc/src/main/kotlin/Projects.kt @@ -0,0 +1,4 @@ +import org.gradle.api.Project + +fun Project.version(target: String): String = + property("${target}_version") as String From 3250e4743585c0a9011f21fc08e788a971bcd053 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 24 Apr 2020 19:32:55 +0300 Subject: [PATCH 022/257] Integration with RxJava3 (#1950) Fixes #1883 Co-authored-by: Zac Sweers --- README.md | 3 + docs/flow.md | 2 +- gradle.properties | 1 + reactive/README.md | 1 + reactive/kotlinx-coroutines-rx2/build.gradle | 2 +- reactive/kotlinx-coroutines-rx3/README.md | 84 +++++ .../api/kotlinx-coroutines-rx3.api | 70 ++++ reactive/kotlinx-coroutines-rx3/build.gradle | 45 +++ reactive/kotlinx-coroutines-rx3/package.list | 14 + .../kotlinx-coroutines-rx3/src/RxAwait.kt | 220 ++++++++++++ .../src/RxCancellable.kt | 25 ++ .../kotlinx-coroutines-rx3/src/RxChannel.kt | 86 +++++ .../src/RxCompletable.kt | 60 ++++ .../kotlinx-coroutines-rx3/src/RxConvert.kt | 126 +++++++ .../kotlinx-coroutines-rx3/src/RxFlowable.kt | 41 +++ .../kotlinx-coroutines-rx3/src/RxMaybe.kt | 61 ++++ .../src/RxObservable.kt | 195 +++++++++++ .../kotlinx-coroutines-rx3/src/RxScheduler.kt | 52 +++ .../kotlinx-coroutines-rx3/src/RxSingle.kt | 60 ++++ .../test/BackpressureTest.kt | 39 +++ reactive/kotlinx-coroutines-rx3/test/Check.kt | 76 +++++ .../test/CompletableTest.kt | 181 ++++++++++ .../test/ConvertTest.kt | 159 +++++++++ .../test/FlowAsObservableTest.kt | 142 ++++++++ .../test/FlowableContextTest.kt | 43 +++ .../test/FlowableExceptionHandlingTest.kt | 134 ++++++++ .../test/FlowableTest.kt | 126 +++++++ .../test/IntegrationTest.kt | 136 ++++++++ .../test/IterableFlowAsFlowableTckTest.kt | 37 ++ .../test/LeakedExceptionTest.kt | 107 ++++++ .../kotlinx-coroutines-rx3/test/MaybeTest.kt | 316 ++++++++++++++++++ .../test/ObservableAsFlowTest.kt | 185 ++++++++++ .../test/ObservableCompletionStressTest.kt | 36 ++ .../test/ObservableExceptionHandlingTest.kt | 134 ++++++++ .../test/ObservableMultiTest.kt | 91 +++++ .../test/ObservableSingleTest.kt | 212 ++++++++++++ .../test/ObservableSubscriptionSelectTest.kt | 51 +++ .../test/ObservableTest.kt | 164 +++++++++ .../test/SchedulerTest.kt | 34 ++ .../kotlinx-coroutines-rx3/test/SingleTest.kt | 266 +++++++++++++++ settings.gradle | 1 + 41 files changed, 3816 insertions(+), 2 deletions(-) create mode 100644 reactive/kotlinx-coroutines-rx3/README.md create mode 100644 reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api create mode 100644 reactive/kotlinx-coroutines-rx3/build.gradle create mode 100644 reactive/kotlinx-coroutines-rx3/package.list create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxAwait.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxCancellable.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxChannel.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxCompletable.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxConvert.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxFlowable.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxMaybe.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxObservable.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt create mode 100644 reactive/kotlinx-coroutines-rx3/src/RxSingle.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/BackpressureTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/Check.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/CompletableTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ConvertTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowableContextTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowableExceptionHandlingTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowableTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/IntegrationTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/IterableFlowAsFlowableTckTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/LeakedExceptionTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/MaybeTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableAsFlowTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableCompletionStressTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableExceptionHandlingTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableSingleTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableSubscriptionSelectTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/SingleTest.kt diff --git a/README.md b/README.md index 62a7dc85c7..d829dfeade 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ suspend fun main() = coroutineScope { * Reactive Streams ([Publisher.collect], [Publisher.awaitSingle], [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 * Project Reactor ([flux], [mono], etc). * [ui](ui/README.md) — modules that provide coroutine dispatchers for various single-threaded UI libraries: * Android, JavaFX, and Swing. @@ -288,6 +289,8 @@ The `develop` branch is pushed to `master` during release. [rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-flowable.html [rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html + + [flux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html diff --git a/docs/flow.md b/docs/flow.md index 705f338b20..37e491cf51 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -1786,7 +1786,7 @@ Indeed, its design was inspired by Reactive Streams and its various implementati be Kotlin and suspension friendly and respect structured concurrency. Achieving this goal would be impossible without reactive pioneers and their tremendous work. You can read the complete story in [Reactive Streams and Kotlin Flows](https://medium.com/@elizarov/reactive-streams-and-kotlin-flows-bfd12772cda4) article. While being different, conceptually, Flow *is* a reactive stream and it is possible to convert it to the reactive (spec and TCK compliant) Publisher and vice versa. -Such converters are provided by `kotlinx.coroutines` out-of-the-box and can be found in corresponding reactive modules (`kotlinx-coroutines-reactive` for Reactive Streams, `kotlinx-coroutines-reactor` for Project Reactor and `kotlinx-coroutines-rx2` for RxJava2). +Such converters are provided by `kotlinx.coroutines` out-of-the-box and can be found in corresponding reactive modules (`kotlinx-coroutines-reactive` for Reactive Streams, `kotlinx-coroutines-reactor` for Project Reactor and `kotlinx-coroutines-rx2`/`kotlinx-coroutines-rx3` for RxJava2/RxJava3). Integration modules include conversions from and to `Flow`, integration with Reactor's `Context` and suspension-friendly ways to work with various reactive entities. diff --git a/gradle.properties b/gradle.properties index 56a1a23c25..233ad3697f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ byte_buddy_version=1.10.9 reactor_vesion=3.2.5.RELEASE reactive_streams_version=1.0.2 rxjava2_version=2.2.8 +rxjava3_version=3.0.2 javafx_version=11.0.2 javafx_plugin_version=0.0.8 binary_compatibility_validator_version=0.2.2 diff --git a/reactive/README.md b/reactive/README.md index 8679a2b078..35706ac96e 100644 --- a/reactive/README.md +++ b/reactive/README.md @@ -8,3 +8,4 @@ Module name below corresponds to the artifact name in Maven/Gradle. * [kotlinx-coroutines-reactive](kotlinx-coroutines-reactive/README.md) -- utilities for [Reactive Streams](https://www.reactive-streams.org) * [kotlinx-coroutines-reactor](kotlinx-coroutines-reactor/README.md) -- utilities for [Reactor](https://projectreactor.io) * [kotlinx-coroutines-rx2](kotlinx-coroutines-rx2/README.md) -- utilities for [RxJava 2.x](https://github.com/ReactiveX/RxJava) +* [kotlinx-coroutines-rx3](kotlinx-coroutines-rx3/README.md) -- utilities for [RxJava 3.x](https://github.com/ReactiveX/RxJava) diff --git a/reactive/kotlinx-coroutines-rx2/build.gradle b/reactive/kotlinx-coroutines-rx2/build.gradle index b583025ce3..6d2c4abcc8 100644 --- a/reactive/kotlinx-coroutines-rx2/build.gradle +++ b/reactive/kotlinx-coroutines-rx2/build.gradle @@ -11,7 +11,7 @@ dependencies { tasks.withType(dokka.getClass()) { externalDocumentationLink { - url = new URL('http://reactivex.io/RxJava/javadoc/') + url = new URL('http://reactivex.io/RxJava/2.x/javadoc/') packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } diff --git a/reactive/kotlinx-coroutines-rx3/README.md b/reactive/kotlinx-coroutines-rx3/README.md new file mode 100644 index 0000000000..3aa73eb969 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/README.md @@ -0,0 +1,84 @@ +# Module kotlinx-coroutines-rx3 + +Utilities for [RxJava 3.x](https://github.com/ReactiveX/RxJava). + +Coroutine builders: + +| **Name** | **Result** | **Scope** | **Description** +| --------------- | --------------------------------------- | ---------------- | --------------- +| [rxCompletable] | `Completable` | [CoroutineScope] | Cold completable that starts coroutine on subscribe +| [rxMaybe] | `Maybe` | [CoroutineScope] | Cold maybe that starts coroutine on subscribe +| [rxSingle] | `Single` | [CoroutineScope] | Cold single that starts coroutine on subscribe +| [rxObservable] | `Observable` | [ProducerScope] | Cold observable that starts coroutine on subscribe +| [rxFlowable] | `Flowable` | [ProducerScope] | Cold observable that starts coroutine on subscribe with **backpressure** support + +Integration with [Flow]: + +| **Name** | **Result** | **Description** +| --------------- | -------------- | --------------- +| [Flow.asFlowable] | `Flowable` | Converts the given flow to a cold Flowable. +| [Flow.asObservable] | `Observable` | Converts the given flow to a cold Observable. +| [ObservableSource.asFlow] | `Flow` | Converts the given cold ObservableSource to flow + +Suspending extension functions and suspending iteration: + +| **Name** | **Description** +| -------- | --------------- +| [CompletableSource.await][io.reactivex.rxjava3.core.CompletableSource.await] | Awaits for completion of the completable value +| [MaybeSource.await][io.reactivex.rxjava3.core.MaybeSource.await] | Awaits for the value of the maybe and returns it or null +| [MaybeSource.awaitOrDefault][io.reactivex.rxjava3.core.MaybeSource.awaitOrDefault] | Awaits for the value of the maybe and returns it or default +| [SingleSource.await][io.reactivex.rxjava3.core.SingleSource.await] | Awaits for completion of the single value and returns it +| [ObservableSource.awaitFirst][io.reactivex.rxjava3.core.ObservableSource.awaitFirst] | Awaits for the first value from the given observable +| [ObservableSource.awaitFirstOrDefault][io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrDefault] | Awaits for the first value from the given observable or default +| [ObservableSource.awaitFirstOrElse][io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrElse] | Awaits for the first value from the given observable or default from a function +| [ObservableSource.awaitFirstOrNull][io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrNull] | Awaits for the first value from the given observable or null +| [ObservableSource.awaitLast][io.reactivex.rxjava3.core.ObservableSource.awaitFirst] | Awaits for the last value from the given observable +| [ObservableSource.awaitSingle][io.reactivex.rxjava3.core.ObservableSource.awaitSingle] | Awaits for the single value from the given observable + +Note that `Flowable` is a subclass of [Reactive Streams](https://www.reactive-streams.org) +`Publisher` and extensions for it are covered by +[kotlinx-coroutines-reactive](../kotlinx-coroutines-reactive) module. + +Conversion functions: + +| **Name** | **Description** +| -------- | --------------- +| [Job.asCompletable][kotlinx.coroutines.Job.asCompletable] | Converts job to hot completable +| [Deferred.asSingle][kotlinx.coroutines.Deferred.asSingle] | Converts deferred value to hot single +| [Scheduler.asCoroutineDispatcher][io.reactivex.rxjava3.core.Scheduler.asCoroutineDispatcher] | Converts scheduler to [CoroutineDispatcher] + + + +[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html +[CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html + +[ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html + +[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html + + +[rxCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-completable.html +[rxMaybe]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-maybe.html +[rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-single.html +[rxObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-observable.html +[rxFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-flowable.html +[Flow.asFlowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.flow.-flow/as-flowable.html +[Flow.asObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.flow.-flow/as-observable.html +[ObservableSource.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-observable-source/as-flow.html +[io.reactivex.rxjava3.core.CompletableSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-completable-source/await.html +[io.reactivex.rxjava3.core.MaybeSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-maybe-source/await.html +[io.reactivex.rxjava3.core.MaybeSource.awaitOrDefault]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-maybe-source/await-or-default.html +[io.reactivex.rxjava3.core.SingleSource.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-single-source/await.html +[io.reactivex.rxjava3.core.ObservableSource.awaitFirst]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-observable-source/await-first.html +[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrDefault]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-observable-source/await-first-or-default.html +[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrElse]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-observable-source/await-first-or-else.html +[io.reactivex.rxjava3.core.ObservableSource.awaitFirstOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-observable-source/await-first-or-null.html +[io.reactivex.rxjava3.core.ObservableSource.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-observable-source/await-single.html +[kotlinx.coroutines.Job.asCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.-job/as-completable.html +[kotlinx.coroutines.Deferred.asSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.-deferred/as-single.html +[io.reactivex.rxjava3.core.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-scheduler/as-coroutine-dispatcher.html + + +# Package kotlinx.coroutines.rx3 + +Utilities for [RxJava 3.x](https://github.com/ReactiveX/RxJava). diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api new file mode 100644 index 0000000000..27c3d3dfa0 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -0,0 +1,70 @@ +public final class kotlinx/coroutines/rx3/RxAwaitKt { + public static final fun await (Lio/reactivex/rxjava3/core/CompletableSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun await (Lio/reactivex/rxjava3/core/MaybeSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun await (Lio/reactivex/rxjava3/core/SingleSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirst (Lio/reactivex/rxjava3/core/ObservableSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirstOrDefault (Lio/reactivex/rxjava3/core/ObservableSource;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirstOrElse (Lio/reactivex/rxjava3/core/ObservableSource;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitFirstOrNull (Lio/reactivex/rxjava3/core/ObservableSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitLast (Lio/reactivex/rxjava3/core/ObservableSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitOrDefault (Lio/reactivex/rxjava3/core/MaybeSource;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingle (Lio/reactivex/rxjava3/core/ObservableSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class kotlinx/coroutines/rx3/RxChannelKt { + public static final fun collect (Lio/reactivex/rxjava3/core/MaybeSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun collect (Lio/reactivex/rxjava3/core/ObservableSource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun openSubscription (Lio/reactivex/rxjava3/core/MaybeSource;)Lkotlinx/coroutines/channels/ReceiveChannel; + public static final fun openSubscription (Lio/reactivex/rxjava3/core/ObservableSource;)Lkotlinx/coroutines/channels/ReceiveChannel; +} + +public final class kotlinx/coroutines/rx3/RxCompletableKt { + public static final fun rxCompletable (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lio/reactivex/rxjava3/core/Completable; + public static synthetic fun rxCompletable$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Completable; +} + +public final class kotlinx/coroutines/rx3/RxConvertKt { + public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Completable; + public static final fun asFlow (Lio/reactivex/rxjava3/core/ObservableSource;)Lkotlinx/coroutines/flow/Flow; + public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Maybe; + public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Single; + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; +} + +public final class kotlinx/coroutines/rx3/RxFlowableKt { + public static final fun rxFlowable (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lio/reactivex/rxjava3/core/Flowable; + public static synthetic fun rxFlowable$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; +} + +public final class kotlinx/coroutines/rx3/RxMaybeKt { + public static final fun rxMaybe (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lio/reactivex/rxjava3/core/Maybe; + public static synthetic fun rxMaybe$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Maybe; +} + +public final class kotlinx/coroutines/rx3/RxObservableKt { + public static final fun rxObservable (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lio/reactivex/rxjava3/core/Observable; + public static synthetic fun rxObservable$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; +} + +public final class kotlinx/coroutines/rx3/RxSchedulerKt { + public static final fun asCoroutineDispatcher (Lio/reactivex/rxjava3/core/Scheduler;)Lkotlinx/coroutines/rx3/SchedulerCoroutineDispatcher; +} + +public final class kotlinx/coroutines/rx3/RxSingleKt { + public static final fun rxSingle (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lio/reactivex/rxjava3/core/Single; + public static synthetic fun rxSingle$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Single; +} + +public final class kotlinx/coroutines/rx3/SchedulerCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay { + public fun (Lio/reactivex/rxjava3/core/Scheduler;)V + public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getScheduler ()Lio/reactivex/rxjava3/core/Scheduler; + public fun hashCode ()I + public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V + public fun toString ()Ljava/lang/String; +} + diff --git a/reactive/kotlinx-coroutines-rx3/build.gradle b/reactive/kotlinx-coroutines-rx3/build.gradle new file mode 100644 index 0000000000..ced694abd3 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/build.gradle @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +targetCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + compile project(':kotlinx-coroutines-reactive') + testCompile project(':kotlinx-coroutines-reactive').sourceSets.test.output + testCompile "org.reactivestreams:reactive-streams-tck:$reactive_streams_version" + compile "io.reactivex.rxjava3:rxjava:$rxjava3_version" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +tasks.withType(dokka.getClass()) { + externalDocumentationLink { + url = new URL('http://reactivex.io/RxJava/3.x/javadoc/') + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + } +} + +task testNG(type: Test) { + useTestNG() + reports.html.destination = file("$buildDir/reports/testng") + include '**/*ReactiveStreamTckTest.*' + // Skip testNG when tests are filtered with --tests, otherwise it simply fails + onlyIf { + filter.includePatterns.isEmpty() + } + doFirst { + // Classic gradle, nothing works without doFirst + println "TestNG tests: ($includes)" + } +} + +test { + dependsOn(testNG) + reports.html.destination = file("$buildDir/reports/junit") +} diff --git a/reactive/kotlinx-coroutines-rx3/package.list b/reactive/kotlinx-coroutines-rx3/package.list new file mode 100644 index 0000000000..889916d0db --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/package.list @@ -0,0 +1,14 @@ +io.reactivex.rxjava3.core +io.reactivex.rxjava3.annotations +io.reactivex.rxjava3.disposables +io.reactivex.rxjava3.exceptions +io.reactivex.rxjava3.flowables +io.reactivex.rxjava3.functions +io.reactivex.rxjava3.observables +io.reactivex.rxjava3.observers +io.reactivex.rxjava3.parallel +io.reactivex.rxjava3.plugins +io.reactivex.rxjava3.processors +io.reactivex.rxjava3.schedulers +io.reactivex.rxjava3.subjects +io.reactivex.rxjava3.subscribers diff --git a/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt b/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt new file mode 100644 index 0000000000..e52556e4e9 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxAwait.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.Disposable +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.* + +// ------------------------ CompletableSource ------------------------ + +/** + * Awaits for completion of this completable without blocking a thread. + * Returns `Unit` or throws the corresponding exception if this completable had produced error. + * + * This suspending function is cancellable. If the [Job] of the invoking coroutine is cancelled or completed while this + * suspending function is suspended, this function immediately resumes with [CancellationException]. + */ +public suspend fun CompletableSource.await(): Unit = suspendCancellableCoroutine { cont -> + subscribe(object : CompletableObserver { + override fun onSubscribe(d: Disposable) { cont.disposeOnCancellation(d) } + override fun onComplete() { cont.resume(Unit) } + override fun onError(e: Throwable) { cont.resumeWithException(e) } + }) +} + +// ------------------------ MaybeSource ------------------------ + +/** + * Awaits for completion of the maybe without blocking a thread. + * Returns the resulting value, null if no value was produced or throws the corresponding exception if this + * maybe had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +@Suppress("UNCHECKED_CAST") +public suspend fun MaybeSource.await(): T? = (this as MaybeSource).awaitOrDefault(null) + +/** + * Awaits for completion of the maybe without blocking a thread. + * Returns the resulting value, [default] if no value was produced or throws the corresponding exception if this + * maybe had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun MaybeSource.awaitOrDefault(default: T): T = suspendCancellableCoroutine { cont -> + subscribe(object : MaybeObserver { + override fun onSubscribe(d: Disposable) { cont.disposeOnCancellation(d) } + override fun onComplete() { cont.resume(default) } + override fun onSuccess(t: T) { cont.resume(t) } + override fun onError(error: Throwable) { cont.resumeWithException(error) } + }) +} + +// ------------------------ SingleSource ------------------------ + +/** + * Awaits for completion of the single value without blocking a thread. + * Returns the resulting value or throws the corresponding exception if this single had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun SingleSource.await(): T = suspendCancellableCoroutine { cont -> + subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) { cont.disposeOnCancellation(d) } + override fun onSuccess(t: T) { cont.resume(t) } + override fun onError(error: Throwable) { cont.resumeWithException(error) } + }) +} + +// ------------------------ ObservableSource ------------------------ + +/** + * Awaits for the first value from the given observable without blocking a thread. + * Returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if observable does not emit any value + */ +public suspend fun ObservableSource.awaitFirst(): T = awaitOne(Mode.FIRST) + +/** + * Awaits for the first value from the given observable or the [default] value if none is emitted without blocking a + * thread and returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun ObservableSource.awaitFirstOrDefault(default: T): T = awaitOne(Mode.FIRST_OR_DEFAULT, default) + +/** + * Awaits for the first value from the given observable or `null` value if none is emitted without blocking a + * thread and returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun ObservableSource.awaitFirstOrNull(): T? = awaitOne(Mode.FIRST_OR_DEFAULT) + +/** + * Awaits for the first value from the given observable or call [defaultValue] to get a value if none is emitted without blocking a + * thread and returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + */ +public suspend fun ObservableSource.awaitFirstOrElse(defaultValue: () -> T): T = awaitOne(Mode.FIRST_OR_DEFAULT) ?: defaultValue() + +/** + * Awaits for the last value from the given observable without blocking a thread. + * Returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if observable does not emit any value + */ +public suspend fun ObservableSource.awaitLast(): T = awaitOne(Mode.LAST) + +/** + * Awaits for the single value from the given observable without blocking a thread. + * Returns the resulting value or throws the corresponding exception if this observable had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if observable does not emit any value + * @throws IllegalArgumentException if observable emits more than one value + */ +public suspend fun ObservableSource.awaitSingle(): T = awaitOne(Mode.SINGLE) + +// ------------------------ private ------------------------ + +internal fun CancellableContinuation<*>.disposeOnCancellation(d: Disposable) = + invokeOnCancellation { d.dispose() } + +private enum class Mode(val s: String) { + FIRST("awaitFirst"), + FIRST_OR_DEFAULT("awaitFirstOrDefault"), + LAST("awaitLast"), + SINGLE("awaitSingle"); + override fun toString(): String = s +} + +private suspend fun ObservableSource.awaitOne( + mode: Mode, + default: T? = null +): T = suspendCancellableCoroutine { cont -> + subscribe(object : Observer { + private lateinit var subscription: Disposable + private var value: T? = null + private var seenValue = false + + override fun onSubscribe(sub: Disposable) { + subscription = sub + cont.invokeOnCancellation { sub.dispose() } + } + + override fun onNext(t: T) { + when (mode) { + Mode.FIRST, Mode.FIRST_OR_DEFAULT -> { + if (!seenValue) { + seenValue = true + cont.resume(t) + subscription.dispose() + } + } + Mode.LAST, Mode.SINGLE -> { + if (mode == Mode.SINGLE && seenValue) { + if (cont.isActive) + cont.resumeWithException(IllegalArgumentException("More than one onNext value for $mode")) + subscription.dispose() + } else { + value = t + seenValue = true + } + } + } + } + + @Suppress("UNCHECKED_CAST") + override fun onComplete() { + if (seenValue) { + if (cont.isActive) cont.resume(value as T) + return + } + when { + mode == Mode.FIRST_OR_DEFAULT -> { + cont.resume(default as T) + } + cont.isActive -> { + cont.resumeWithException(NoSuchElementException("No value received via onNext for $mode")) + } + } + } + + override fun onError(e: Throwable) { + cont.resumeWithException(e) + } + }) +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxCancellable.kt b/reactive/kotlinx-coroutines-rx3/src/RxCancellable.kt new file mode 100644 index 0000000000..0b57b8bbe4 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxCancellable.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.functions.* +import io.reactivex.rxjava3.plugins.* +import kotlinx.coroutines.* +import kotlin.coroutines.* + +internal class RxCancellable(private val job: Job) : Cancellable { + override fun cancel() { + job.cancel() + } +} + +internal fun handleUndeliverableException(cause: Throwable, context: CoroutineContext) { + if (cause is CancellationException) return // Async CE should be completely ignored + try { + RxJavaPlugins.onError(cause) + } catch (e: Throwable) { + handleCoroutineException(context, cause) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt new file mode 100644 index 0000000000..acb907b765 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import kotlinx.atomicfu.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.internal.* + +/** + * Subscribes to this [MaybeSource] and returns a channel to receive elements emitted by it. + * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this source. + * + * This API is internal in the favour of [Flow]. + * [MaybeSource] doesn't have a corresponding [Flow] adapter, so it should be transformed to [Observable] first. + */ +@PublishedApi +internal fun MaybeSource.openSubscription(): ReceiveChannel { + val channel = SubscriptionChannel() + subscribe(channel) + return channel +} + +/** + * Subscribes to this [ObservableSource] and returns a channel to receive elements emitted by it. + * The resulting channel shall be [cancelled][ReceiveChannel.cancel] to unsubscribe from this source. + * + * This API is internal in the favour of [Flow]. + * [ObservableSource] doesn't have a corresponding [Flow] adapter, so it should be transformed to [Observable] first. + */ +@PublishedApi +internal fun ObservableSource.openSubscription(): ReceiveChannel { + val channel = SubscriptionChannel() + subscribe(channel) + return channel +} + +/** + * Subscribes to this [MaybeSource] and performs the specified action for each received element. + * Cancels subscription if any exception happens during collect. + */ +public suspend inline fun MaybeSource.collect(action: (T) -> Unit): Unit = + openSubscription().consumeEach(action) + +/** + * Subscribes to this [ObservableSource] and performs the specified action for each received element. + * Cancels subscription if any exception happens during collect. + */ +public suspend inline fun ObservableSource.collect(action: (T) -> Unit): Unit = + openSubscription().consumeEach(action) + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +private class SubscriptionChannel : + LinkedListChannel(), Observer, MaybeObserver +{ + private val _subscription = atomic(null) + + @Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER") + override fun onClosedIdempotent(closed: LockFreeLinkedListNode) { + _subscription.getAndSet(null)?.dispose() // dispose exactly once + } + + // Observer overrider + override fun onSubscribe(sub: Disposable) { + _subscription.value = sub + } + + override fun onSuccess(t: T) { + offer(t) + } + + override fun onNext(t: T) { + offer(t) + } + + override fun onComplete() { + close(cause = null) + } + + override fun onError(e: Throwable) { + close(cause = e) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxCompletable.kt b/reactive/kotlinx-coroutines-rx3/src/RxCompletable.kt new file mode 100644 index 0000000000..54b412f171 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxCompletable.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlin.coroutines.* + +/** + * Creates cold [Completable] that runs a given [block] in a coroutine and emits its result. + * Every time the returned completable is subscribed, it starts a new coroutine. + * Unsubscribing cancels running coroutine. + * Coroutine context can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance. + */ +public fun rxCompletable( + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.() -> Unit +): Completable { + require(context[Job] === null) { "Completable context cannot contain job in it." + + "Its lifecycle should be managed via Disposable handle. Had $context" } + return rxCompletableInternal(GlobalScope, context, block) +} + +private fun rxCompletableInternal( + scope: CoroutineScope, // support for legacy rxCompletable in scope + context: CoroutineContext, + block: suspend CoroutineScope.() -> Unit +): Completable = Completable.create { subscriber -> + val newContext = scope.newCoroutineContext(context) + val coroutine = RxCompletableCoroutine(newContext, subscriber) + subscriber.setCancellable(RxCancellable(coroutine)) + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) +} + +private class RxCompletableCoroutine( + parentContext: CoroutineContext, + private val subscriber: CompletableEmitter +) : AbstractCoroutine(parentContext, true) { + override fun onCompleted(value: Unit) { + try { + subscriber.onComplete() + } catch (e: Throwable) { + handleUndeliverableException(e, context) + } + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + try { + if (!subscriber.tryOnError(cause)) { + handleUndeliverableException(cause, context) + } + } catch (e: Throwable) { + handleUndeliverableException(e, context) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt new file mode 100644 index 0000000000..f9e2e2158f --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.reactive.* +import java.util.concurrent.atomic.* +import kotlin.coroutines.* + +/** + * Converts this job to the hot reactive completable that signals + * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. + * + * Every subscriber gets the signal at the same time. + * Unsubscribing from the resulting completable **does not** affect the original job in any way. + * + * **Note: This is an experimental api.** Conversion of coroutines primitives to reactive entities may change + * in the future to account for the concept of structured concurrency. + * + * @param context -- the coroutine context from which the resulting completable is going to be signalled + */ +@ExperimentalCoroutinesApi +public fun Job.asCompletable(context: CoroutineContext): Completable = rxCompletable(context) { + this@asCompletable.join() +} + +/** + * Converts this deferred value to the hot reactive maybe that signals + * [onComplete][MaybeEmitter.onComplete], [onSuccess][MaybeEmitter.onSuccess] or [onError][MaybeEmitter.onError]. + * + * Every subscriber gets the same completion value. + * Unsubscribing from the resulting maybe **does not** affect the original deferred value in any way. + * + * **Note: This is an experimental api.** Conversion of coroutines primitives to reactive entities may change + * in the future to account for the concept of structured concurrency. + * + * @param context -- the coroutine context from which the resulting maybe is going to be signalled + */ +@ExperimentalCoroutinesApi +public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMaybe(context) { + this@asMaybe.await() +} + +/** + * Converts this deferred value to the hot reactive single that signals either + * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. + * + * Every subscriber gets the same completion value. + * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. + * + * **Note: This is an experimental api.** Conversion of coroutines primitives to reactive entities may change + * in the future to account for the concept of structured concurrency. + * + * @param context -- the coroutine context from which the resulting single is going to be signalled + */ +@ExperimentalCoroutinesApi +public fun Deferred.asSingle(context: CoroutineContext): Single = rxSingle(context) { + this@asSingle.await() +} + +/** + * Transforms given cold [ObservableSource] into cold [Flow]. + * + * The resulting flow is _cold_, which means that [ObservableSource.subscribe] is called every time a terminal operator + * is applied to the resulting flow. + * + * A channel with the [default][Channel.BUFFERED] buffer size is used. Use the [buffer] operator on the + * resulting flow to specify a user-defined value and to control what happens when data is produced faster + * than consumed, i.e. to control the back-pressure behavior. Check [callbackFlow] for more details. + */ +@ExperimentalCoroutinesApi +public fun ObservableSource.asFlow(): Flow = callbackFlow { + val disposableRef = AtomicReference() + val observer = object : Observer { + override fun onComplete() { close() } + override fun onSubscribe(d: Disposable) { if (!disposableRef.compareAndSet(null, d)) d.dispose() } + override fun onNext(t: T) { sendBlocking(t) } + override fun onError(e: Throwable) { close(e) } + } + + subscribe(observer) + awaitClose { disposableRef.getAndSet(Disposable.disposed())?.dispose() } +} + +/** + * Converts the given flow to a cold observable. + * The original flow is cancelled when the observable subscriber is disposed. + */ +@JvmName("from") +@ExperimentalCoroutinesApi +public fun Flow.asObservable() : Observable = Observable.create { emitter -> + /* + * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if + * asObservable is already invoked from unconfined + */ + val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { + try { + collect { value -> emitter.onNext(value) } + emitter.onComplete() + } catch (e: Throwable) { + // 'create' provides safe emitter, so we can unconditionally call on* here if exception occurs in `onComplete` + if (e !is CancellationException) { + if (!emitter.tryOnError(e)) { + handleUndeliverableException(e, coroutineContext) + } + } else { + emitter.onComplete() + } + } + } + emitter.setCancellable(RxCancellable(job)) +} + +/** + * Converts the given flow to a cold flowable. + * The original flow is cancelled when the flowable subscriber is disposed. + */ +@JvmName("from") +@ExperimentalCoroutinesApi +public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) diff --git a/reactive/kotlinx-coroutines-rx3/src/RxFlowable.kt b/reactive/kotlinx-coroutines-rx3/src/RxFlowable.kt new file mode 100644 index 0000000000..2de46a6a20 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxFlowable.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.reactive.* +import kotlin.coroutines.* + +/** + * Creates cold [flowable][Flowable] that will run a given [block] in a coroutine. + * Every time the returned flowable is subscribed, it starts a new coroutine. + * + * Coroutine emits ([ObservableEmitter.onNext]) values with `send`, completes ([ObservableEmitter.onComplete]) + * when the coroutine completes or channel is explicitly closed and emits error ([ObservableEmitter.onError]) + * if coroutine throws an exception or closes channel with a cause. + * Unsubscribing cancels running coroutine. + * + * Invocations of `send` are suspended appropriately when subscribers apply back-pressure and to ensure that + * `onNext` is not invoked concurrently. + * + * Coroutine context can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance. + * + * **Note: This is an experimental api.** Behaviour of publishers that work as children in a parent scope with respect + */ +@ExperimentalCoroutinesApi +public fun rxFlowable( + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference block: suspend ProducerScope.() -> Unit +): Flowable { + require(context[Job] === null) { "Flowable context cannot contain job in it." + + "Its lifecycle should be managed via Disposable handle. Had $context" } + return Flowable.fromPublisher(publishInternal(GlobalScope, context, RX_HANDLER, block)) +} + +private val RX_HANDLER: (Throwable, CoroutineContext) -> Unit = ::handleUndeliverableException diff --git a/reactive/kotlinx-coroutines-rx3/src/RxMaybe.kt b/reactive/kotlinx-coroutines-rx3/src/RxMaybe.kt new file mode 100644 index 0000000000..4d55ef5ffa --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxMaybe.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlin.coroutines.* + +/** + * Creates cold [maybe][Maybe] that will run a given [block] in a coroutine and emits its result. + * If [block] result is `null`, [onComplete][MaybeObserver.onComplete] is invoked without a value. + * Every time the returned observable is subscribed, it starts a new coroutine. + * Unsubscribing cancels running coroutine. + * Coroutine context can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance. + */ +public fun rxMaybe( + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.() -> T? +): Maybe { + require(context[Job] === null) { "Maybe context cannot contain job in it." + + "Its lifecycle should be managed via Disposable handle. Had $context" } + return rxMaybeInternal(GlobalScope, context, block) +} + +private fun rxMaybeInternal( + scope: CoroutineScope, // support for legacy rxMaybe in scope + context: CoroutineContext, + block: suspend CoroutineScope.() -> T? +): Maybe = Maybe.create { subscriber -> + val newContext = scope.newCoroutineContext(context) + val coroutine = RxMaybeCoroutine(newContext, subscriber) + subscriber.setCancellable(RxCancellable(coroutine)) + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) +} + +private class RxMaybeCoroutine( + parentContext: CoroutineContext, + private val subscriber: MaybeEmitter +) : AbstractCoroutine(parentContext, true) { + override fun onCompleted(value: T) { + try { + if (value == null) subscriber.onComplete() else subscriber.onSuccess(value) + } catch (e: Throwable) { + handleUndeliverableException(e, context) + } + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + try { + if (!subscriber.tryOnError(cause)) { + handleUndeliverableException(cause, context) + } + } catch (e: Throwable) { + handleUndeliverableException(e, context) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt new file mode 100644 index 0000000000..102d06ea60 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxObservable.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.exceptions.* +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.selects.* +import kotlinx.coroutines.sync.* +import kotlin.coroutines.* + +/** + * Creates cold [observable][Observable] that will run a given [block] in a coroutine. + * Every time the returned observable is subscribed, it starts a new coroutine. + * + * Coroutine emits ([ObservableEmitter.onNext]) values with `send`, completes ([ObservableEmitter.onComplete]) + * when the coroutine completes or channel is explicitly closed and emits error ([ObservableEmitter.onError]) + * if coroutine throws an exception or closes channel with a cause. + * Unsubscribing cancels running coroutine. + * + * Invocations of `send` are suspended appropriately to ensure that `onNext` is not invoked concurrently. + * Note that Rx 2.x [Observable] **does not support backpressure**. + * + * Coroutine context can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance. + */ +@ExperimentalCoroutinesApi +public fun rxObservable( + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference block: suspend ProducerScope.() -> Unit +): Observable { + require(context[Job] === null) { "Observable context cannot contain job in it." + + "Its lifecycle should be managed via Disposable handle. Had $context" } + return rxObservableInternal(GlobalScope, context, block) +} + +private fun rxObservableInternal( + scope: CoroutineScope, // support for legacy rxObservable in scope + context: CoroutineContext, + block: suspend ProducerScope.() -> Unit +): Observable = Observable.create { subscriber -> + val newContext = scope.newCoroutineContext(context) + val coroutine = RxObservableCoroutine(newContext, subscriber) + subscriber.setCancellable(RxCancellable(coroutine)) // do it first (before starting coroutine), to await unnecessary suspensions + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) +} + +private const val OPEN = 0 // open channel, still working +private const val CLOSED = -1 // closed, but have not signalled onCompleted/onError yet +private const val SIGNALLED = -2 // already signalled subscriber onCompleted/onError + +private class RxObservableCoroutine( + parentContext: CoroutineContext, + private val subscriber: ObservableEmitter +) : AbstractCoroutine(parentContext, true), ProducerScope, SelectClause2> { + override val channel: SendChannel get() = this + + // Mutex is locked when while subscriber.onXXX is being invoked + private val mutex = Mutex() + + private val _signal = atomic(OPEN) + + override val isClosedForSend: Boolean get() = isCompleted + override val isFull: Boolean = mutex.isLocked + override fun close(cause: Throwable?): Boolean = cancelCoroutine(cause) + override fun invokeOnClose(handler: (Throwable?) -> Unit) = + throw UnsupportedOperationException("RxObservableCoroutine doesn't support invokeOnClose") + + override fun offer(element: T): Boolean { + if (!mutex.tryLock()) return false + doLockedNext(element) + return true + } + + public override suspend fun send(element: T) { + // fast-path -- try send without suspension + if (offer(element)) return + // slow-path does suspend + return sendSuspend(element) + } + + private suspend fun sendSuspend(element: T) { + mutex.lock() + doLockedNext(element) + } + + override val onSend: SelectClause2> + get() = this + + // registerSelectSend + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun registerSelectClause2(select: SelectInstance, element: T, block: suspend (SendChannel) -> R) { + mutex.onLock.registerSelectClause2(select, null) { + doLockedNext(element) + block(this) + } + } + + // assert: mutex.isLocked() + private fun doLockedNext(elem: T) { + // check if already closed for send + if (!isActive) { + doLockedSignalCompleted(completionCause, completionCauseHandled) + throw getCancellationException() + } + // notify subscriber + try { + subscriber.onNext(elem) + } catch (e: Throwable) { + // If onNext fails with exception, then we cancel coroutine (with this exception) and then rethrow it + // to abort the corresponding send/offer invocation. From the standpoint of coroutines machinery, + // this failure is essentially equivalent to a failure of a child coroutine. + cancelCoroutine(e) + mutex.unlock() + throw e + } + /* + * There is no sense to check for `isActive` before doing `unlock`, because cancellation/completion might + * happen after this check and before `unlock` (see signalCompleted that does not do anything + * if it fails to acquire the lock that we are still holding). + * We have to recheck `isCompleted` after `unlock` anyway. + */ + unlockAndCheckCompleted() + } + + private fun unlockAndCheckCompleted() { + mutex.unlock() + // recheck isActive + if (!isActive && mutex.tryLock()) + doLockedSignalCompleted(completionCause, completionCauseHandled) + } + + // assert: mutex.isLocked() + private fun doLockedSignalCompleted(cause: Throwable?, handled: Boolean) { + // cancellation failures + try { + if (_signal.value >= CLOSED) { + _signal.value = SIGNALLED // we'll signal onError/onCompleted (that the final state -- no CAS needed) + try { + if (cause != null && cause !is CancellationException) { + /* + * Reactive frameworks have two types of exceptions: regular and fatal. + * Regular are passed to onError. + * Fatal can be passed to onError, but even the standard implementations **can just swallow it** (e.g. see #1297). + * Such behaviour is inconsistent, leads to silent failures and we can't possibly know whether + * the cause will be handled by onError (and moreover, it depends on whether a fatal exception was + * thrown by subscriber or upstream). + * To make behaviour consistent and least surprising, we always handle fatal exceptions + * by coroutines machinery, anyway, they should not be present in regular program flow, + * thus our goal here is just to expose it as soon as possible. + */ + subscriber.tryOnError(cause) + if (!handled && cause.isFatal()) { + handleUndeliverableException(cause, context) + } + } + else { + subscriber.onComplete() + } + } catch (e: Throwable) { + // Unhandled exception (cannot handle in other way, since we are already complete) + handleUndeliverableException(e, context) + } + } + } finally { + mutex.unlock() + } + } + + private fun signalCompleted(cause: Throwable?, handled: Boolean) { + if (!_signal.compareAndSet(OPEN, CLOSED)) return // abort, other thread invoked doLockedSignalCompleted + if (mutex.tryLock()) // if we can acquire the lock + doLockedSignalCompleted(cause, handled) + } + + override fun onCompleted(value: Unit) { + signalCompleted(null, false) + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + signalCompleted(cause, handled) + } +} + +internal fun Throwable.isFatal() = try { + Exceptions.throwIfFatal(this) // Rx-consistent behaviour without hardcode + false +} catch (e: Throwable) { + true +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt new file mode 100644 index 0000000000..6e91aeea85 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.Scheduler +import kotlinx.coroutines.* +import java.util.concurrent.TimeUnit +import kotlin.coroutines.CoroutineContext + +/** + * Converts an instance of [Scheduler] to an implementation of [CoroutineDispatcher] + * and provides native support of [delay] and [withTimeout]. + */ +public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = SchedulerCoroutineDispatcher(this) + +/** + * Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler]. + */ +public class SchedulerCoroutineDispatcher( + /** + * Underlying scheduler of current [CoroutineDispatcher]. + */ + public val scheduler: Scheduler +) : CoroutineDispatcher(), Delay { + /** @suppress */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + scheduler.scheduleDirect(block) + } + + /** @suppress */ + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + val disposable = scheduler.scheduleDirect({ + with(continuation) { resumeUndispatched(Unit) } + }, timeMillis, TimeUnit.MILLISECONDS) + continuation.disposeOnCancellation(disposable) + } + + /** @suppress */ + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) + return DisposableHandle { disposable.dispose() } + } + + /** @suppress */ + override fun toString(): String = scheduler.toString() + /** @suppress */ + override fun equals(other: Any?): Boolean = other is SchedulerCoroutineDispatcher && other.scheduler === scheduler + /** @suppress */ + override fun hashCode(): Int = System.identityHashCode(scheduler) +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxSingle.kt b/reactive/kotlinx-coroutines-rx3/src/RxSingle.kt new file mode 100644 index 0000000000..225df93ad1 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/src/RxSingle.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlin.coroutines.* + +/** + * Creates cold [single][Single] that will run a given [block] in a coroutine and emits its result. + * Every time the returned observable is subscribed, it starts a new coroutine. + * Unsubscribing cancels running coroutine. + * Coroutine context can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * Method throws [IllegalArgumentException] if provided [context] contains a [Job] instance. + */ +public fun rxSingle( + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.() -> T +): Single { + require(context[Job] === null) { "Single context cannot contain job in it." + + "Its lifecycle should be managed via Disposable handle. Had $context" } + return rxSingleInternal(GlobalScope, context, block) +} + +private fun rxSingleInternal( + scope: CoroutineScope, // support for legacy rxSingle in scope + context: CoroutineContext, + block: suspend CoroutineScope.() -> T +): Single = Single.create { subscriber -> + val newContext = scope.newCoroutineContext(context) + val coroutine = RxSingleCoroutine(newContext, subscriber) + subscriber.setCancellable(RxCancellable(coroutine)) + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) +} + +private class RxSingleCoroutine( + parentContext: CoroutineContext, + private val subscriber: SingleEmitter +) : AbstractCoroutine(parentContext, true) { + override fun onCompleted(value: T) { + try { + subscriber.onSuccess(value) + } catch (e: Throwable) { + handleUndeliverableException(e, context) + } + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + try { + if (!subscriber.tryOnError(cause)) { + handleUndeliverableException(cause, context) + } + } catch (e: Throwable) { + handleUndeliverableException(e, context) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/BackpressureTest.kt b/reactive/kotlinx-coroutines-rx3/test/BackpressureTest.kt new file mode 100644 index 0000000000..9cec4833a1 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/BackpressureTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.reactive.* +import org.junit.Test +import kotlin.test.* + +class BackpressureTest : TestBase() { + @Test + fun testBackpressureDropDirect() = runTest { + expect(1) + Flowable.fromArray(1) + .onBackpressureDrop() + .collect { + assertEquals(1, it) + expect(2) + } + finish(3) + } + + @Test + fun testBackpressureDropFlow() = runTest { + expect(1) + Flowable.fromArray(1) + .onBackpressureDrop() + .asFlow() + .collect { + assertEquals(1, it) + expect(2) + } + finish(3) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/Check.kt b/reactive/kotlinx-coroutines-rx3/test/Check.kt new file mode 100644 index 0000000000..3d4704f490 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/Check.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.plugins.* + +fun checkSingleValue( + observable: Observable, + checker: (T) -> Unit +) { + val singleValue = observable.blockingSingle() + checker(singleValue) +} + +fun checkErroneous( + observable: Observable<*>, + checker: (Throwable) -> Unit +) { + val singleNotification = observable.materialize().blockingSingle() + val error = singleNotification.error ?: error("Excepted error") + checker(error) +} + +fun checkSingleValue( + single: Single, + checker: (T) -> Unit +) { + val singleValue = single.blockingGet() + checker(singleValue) +} + +fun checkErroneous( + single: Single<*>, + checker: (Throwable) -> Unit +) { + try { + single.blockingGet() + error("Should have failed") + } catch (e: Throwable) { + checker(e) + } +} + +fun checkMaybeValue( + maybe: Maybe, + checker: (T?) -> Unit +) { + val maybeValue = maybe.toFlowable().blockingIterable().firstOrNull() + checker(maybeValue) +} + +@Suppress("UNCHECKED_CAST") +fun checkErroneous( + maybe: Maybe<*>, + checker: (Throwable) -> Unit +) { + try { + (maybe as Maybe).blockingGet() + error("Should have failed") + } catch (e: Throwable) { + checker(e) + } +} + +inline fun withExceptionHandler(noinline handler: (Throwable) -> Unit, block: () -> Unit) { + val original = RxJavaPlugins.getErrorHandler() + RxJavaPlugins.setErrorHandler { handler(it) } + try { + block() + } finally { + RxJavaPlugins.setErrorHandler(original) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/CompletableTest.kt b/reactive/kotlinx-coroutines-rx3/test/CompletableTest.kt new file mode 100644 index 0000000000..e5399d16d1 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/CompletableTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import io.reactivex.rxjava3.exceptions.* +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.* + +class CompletableTest : TestBase() { + @Test + fun testBasicSuccess() = runBlocking { + expect(1) + val completable = rxCompletable(currentDispatcher()) { + expect(4) + } + expect(2) + completable.subscribe { + expect(5) + } + expect(3) + yield() // to completable coroutine + finish(6) + } + + @Test + fun testBasicFailure() = runBlocking { + expect(1) + val completable = rxCompletable(currentDispatcher()) { + expect(4) + throw RuntimeException("OK") + } + expect(2) + completable.subscribe({ + expectUnreached() + }, { error -> + expect(5) + assertTrue(error is RuntimeException) + assertEquals("OK", error.message) + }) + expect(3) + yield() // to completable coroutine + finish(6) + } + + @Test + fun testBasicUnsubscribe() = runBlocking { + expect(1) + val completable = rxCompletable(currentDispatcher()) { + expect(4) + yield() // back to main, will get cancelled + expectUnreached() + } + expect(2) + // nothing is called on a disposed rx3 completable + val sub = completable.subscribe({ + expectUnreached() + }, { + expectUnreached() + }) + expect(3) + yield() // to started coroutine + expect(5) + sub.dispose() // will cancel coroutine + yield() + finish(6) + } + + @Test + fun testAwaitSuccess() = runBlocking { + expect(1) + val completable = rxCompletable(currentDispatcher()) { + expect(3) + } + expect(2) + completable.await() // shall launch coroutine + finish(4) + } + + @Test + fun testAwaitFailure() = runBlocking { + expect(1) + val completable = rxCompletable(currentDispatcher()) { + expect(3) + throw RuntimeException("OK") + } + expect(2) + try { + completable.await() // shall launch coroutine and throw exception + expectUnreached() + } catch (e: RuntimeException) { + finish(4) + assertEquals("OK", e.message) + } + } + + @Test + fun testSuppressedException() = runTest { + val completable = rxCompletable(currentDispatcher()) { + launch(start = CoroutineStart.ATOMIC) { + throw TestException() // child coroutine fails + } + try { + delay(Long.MAX_VALUE) + } finally { + throw TestException2() // but parent throws another exception while cleaning up + } + } + try { + completable.await() + expectUnreached() + } catch (e: TestException) { + assertTrue(e.suppressed[0] is TestException2) + } + } + + @Test + fun testUnhandledException() = runTest() { + expect(1) + var disposable: Disposable? = null + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is TestException) + expect(5) + } + val completable = rxCompletable(currentDispatcher()) { + expect(4) + disposable!!.dispose() // cancel our own subscription, so that delay will get cancelled + try { + delay(Long.MAX_VALUE) + } finally { + throw TestException() // would not be able to handle it since mono is disposed + } + } + withExceptionHandler(handler) { + completable.subscribe(object : CompletableObserver { + override fun onSubscribe(d: Disposable) { + expect(2) + disposable = d + } + + override fun onComplete() { + expectUnreached() + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + expect(3) + yield() // run coroutine + finish(6) + } + } + + @Test + fun testFatalExceptionInSubscribe() = runTest { + val handler: (Throwable) -> Unit = { e -> + assertTrue(e is UndeliverableException && e.cause is LinkageError); expect(2) + } + + withExceptionHandler(handler) { + rxCompletable(Dispatchers.Unconfined) { + expect(1) + 42 + }.subscribe({ throw LinkageError() }) + finish(3) + } + } + + @Test + fun testFatalExceptionInSingle() = runTest { + rxCompletable(Dispatchers.Unconfined) { + throw LinkageError() + }.subscribe({ expectUnreached() }, { expect(1); assertTrue(it is LinkageError) }) + finish(2) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ConvertTest.kt b/reactive/kotlinx-coroutines-rx3/test/ConvertTest.kt new file mode 100644 index 0000000000..e2fe533552 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ConvertTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.consumeAsFlow +import org.junit.Assert +import org.junit.Test +import kotlin.test.* + +class ConvertTest : TestBase() { + @Test + fun testToCompletableSuccess() = runBlocking { + expect(1) + val job = launch { + expect(3) + } + val completable = job.asCompletable(coroutineContext.minusKey(Job)) + completable.subscribe { + expect(4) + } + expect(2) + yield() + finish(5) + } + + @Test + fun testToCompletableFail() = runBlocking { + expect(1) + val job = async(NonCancellable) { // don't kill parent on exception + expect(3) + throw RuntimeException("OK") + } + val completable = job.asCompletable(coroutineContext.minusKey(Job)) + completable.subscribe { + expect(4) + } + expect(2) + yield() + finish(5) + } + + @Test + fun testToMaybe() { + val d = GlobalScope.async { + delay(50) + "OK" + } + val maybe1 = d.asMaybe(Dispatchers.Unconfined) + checkMaybeValue(maybe1) { + assertEquals("OK", it) + } + val maybe2 = d.asMaybe(Dispatchers.Unconfined) + checkMaybeValue(maybe2) { + assertEquals("OK", it) + } + } + + @Test + fun testToMaybeEmpty() { + val d = GlobalScope.async { + delay(50) + null + } + val maybe1 = d.asMaybe(Dispatchers.Unconfined) + checkMaybeValue(maybe1, Assert::assertNull) + val maybe2 = d.asMaybe(Dispatchers.Unconfined) + checkMaybeValue(maybe2, Assert::assertNull) + } + + @Test + fun testToMaybeFail() { + val d = GlobalScope.async { + delay(50) + throw TestRuntimeException("OK") + } + val maybe1 = d.asMaybe(Dispatchers.Unconfined) + checkErroneous(maybe1) { + check(it is TestRuntimeException && it.message == "OK") { "$it" } + } + val maybe2 = d.asMaybe(Dispatchers.Unconfined) + checkErroneous(maybe2) { + check(it is TestRuntimeException && it.message == "OK") { "$it" } + } + } + + @Test + fun testToSingle() { + val d = GlobalScope.async { + delay(50) + "OK" + } + val single1 = d.asSingle(Dispatchers.Unconfined) + checkSingleValue(single1) { + assertEquals("OK", it) + } + val single2 = d.asSingle(Dispatchers.Unconfined) + checkSingleValue(single2) { + assertEquals("OK", it) + } + } + + @Test + fun testToSingleFail() { + val d = GlobalScope.async { + delay(50) + throw TestRuntimeException("OK") + } + val single1 = d.asSingle(Dispatchers.Unconfined) + checkErroneous(single1) { + check(it is TestRuntimeException && it.message == "OK") { "$it" } + } + val single2 = d.asSingle(Dispatchers.Unconfined) + checkErroneous(single2) { + check(it is TestRuntimeException && it.message == "OK") { "$it" } + } + } + + @Test + fun testToObservable() { + val c = GlobalScope.produce { + delay(50) + send("O") + delay(50) + send("K") + } + val observable = c.consumeAsFlow().asObservable() + checkSingleValue(observable.reduce { t1, t2 -> t1 + t2 }.toSingle()) { + assertEquals("OK", it) + } + } + + @Test + fun testToObservableFail() { + val c = GlobalScope.produce { + delay(50) + send("O") + delay(50) + throw TestException("K") + } + val observable = c.consumeAsFlow().asObservable() + val single = rxSingle(Dispatchers.Unconfined) { + var result = "" + try { + observable.collect { result += it } + } catch(e: Throwable) { + check(e is TestException) + result += e.message + } + result + } + checkSingleValue(single) { + assertEquals("OK", it) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt new file mode 100644 index 0000000000..50c4ae7dad --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import kotlin.test.* + +class FlowAsObservableTest : TestBase() { + @Test + fun testBasicSuccess() = runTest { + expect(1) + val observable = flow { + expect(3) + emit("OK") + }.asObservable() + + expect(2) + observable.subscribe { value -> + expect(4) + assertEquals("OK", value) + } + + finish(5) + } + + @Test + fun testBasicFailure() = runTest { + expect(1) + val observable = flow { + expect(3) + throw RuntimeException("OK") + }.asObservable() + + expect(2) + observable.subscribe({ expectUnreached() }, { error -> + expect(4) + assertTrue(error is RuntimeException) + assertEquals("OK", error.message) + }) + finish(5) + } + + @Test + fun testBasicUnsubscribe() = runTest { + expect(1) + val observable = flow { + expect(3) + hang { + expect(4) + } + }.asObservable() + + expect(2) + val sub = observable.subscribe({ expectUnreached() }, { expectUnreached() }) + sub.dispose() // will cancel coroutine + finish(5) + } + + @Test + fun testNotifyOnceOnCancellation() = runTest { + val observable = + flow { + expect(3) + emit("OK") + hang { + expect(7) + } + }.asObservable() + .doOnNext { + expect(4) + assertEquals("OK", it) + } + .doOnDispose { + expect(6) // notified once! + } + + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + observable.collect { + expect(5) + assertEquals("OK", it) + } + } + + yield() + job.cancelAndJoin() + finish(8) + } + + @Test + fun testFailingConsumer() = runTest { + expect(1) + val observable = flow { + expect(2) + emit("OK") + hang { + expect(4) + } + + }.asObservable() + + try { + observable.collect { + expect(3) + throw TestException() + } + } catch (e: TestException) { + finish(5) + } + } + + @Test + fun testNonAtomicStart() = runTest { + withContext(Dispatchers.Unconfined) { + val observable = flow { + expect(1) + }.asObservable() + + val disposable = observable.subscribe({ expectUnreached() }, { expectUnreached() }, { expectUnreached() }) + disposable.dispose() + } + finish(2) + } + + @Test + fun testFlowCancelledFromWithin() = runTest { + val observable = flow { + expect(1) + emit(1) + kotlin.coroutines.coroutineContext.cancel() + kotlin.coroutines.coroutineContext.ensureActive() + expectUnreached() + }.asObservable() + + observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowableContextTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowableContextTest.kt new file mode 100644 index 0000000000..b70e0d5135 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/FlowableContextTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.reactive.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +class FlowableContextTest : TestBase() { + private val dispatcher = newSingleThreadContext("FlowableContextTest") + + @After + fun tearDown() { + dispatcher.close() + } + + @Test + fun testFlowableCreateAsFlowThread() = runTest { + expect(1) + val mainThread = Thread.currentThread() + val dispatcherThread = withContext(dispatcher) { Thread.currentThread() } + assertTrue(dispatcherThread != mainThread) + Flowable.create({ + assertEquals(dispatcherThread, Thread.currentThread()) + it.onNext("OK") + it.onComplete() + }, BackpressureStrategy.BUFFER) + .asFlow() + .flowOn(dispatcher) + .collect { + expect(2) + assertEquals("OK", it) + assertEquals(mainThread, Thread.currentThread()) + } + finish(3) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowableExceptionHandlingTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowableExceptionHandlingTest.kt new file mode 100644 index 0000000000..8cbd7ee89f --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/FlowableExceptionHandlingTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.exceptions.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +class FlowableExceptionHandlingTest : TestBase() { + + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + private inline fun handler(expect: Int) = { t: Throwable -> + assertTrue(t is UndeliverableException && t.cause is T) + expect(expect) + } + + private fun cehUnreached() = CoroutineExceptionHandler { _, _ -> expectUnreached() } + + @Test + fun testException() = withExceptionHandler({ expectUnreached() }) { + rxFlowable(Dispatchers.Unconfined + cehUnreached()) { + expect(1) + throw TestException() + }.subscribe({ + expectUnreached() + }, { + expect(2) // Reported to onError + }) + finish(3) + } + + @Test + fun testFatalException() = withExceptionHandler(handler(3)) { + rxFlowable(Dispatchers.Unconfined) { + expect(1) + throw LinkageError() + }.subscribe({ + expectUnreached() + }, { + expect(2) // Fatal exception is reported to both onError and CEH + }) + finish(4) + } + + @Test + fun testExceptionAsynchronous() = withExceptionHandler({ expectUnreached() }) { + rxFlowable(Dispatchers.Unconfined + cehUnreached()) { + expect(1) + throw TestException() + }.publish() + .refCount() + .subscribe({ + expectUnreached() + }, { + expect(2) // Reported to onError + }) + finish(3) + } + + @Test + fun testFatalExceptionAsynchronous() = withExceptionHandler(handler(3)) { + rxFlowable(Dispatchers.Unconfined) { + expect(1) + throw LinkageError() + }.publish() + .refCount() + .subscribe({ + expectUnreached() + }, { + expect(2) + }) + finish(4) + } + + @Test + fun testFatalExceptionFromSubscribe() = withExceptionHandler(handler(4)) { + rxFlowable(Dispatchers.Unconfined) { + expect(1) + send(Unit) + }.subscribe({ + expect(2) + throw LinkageError() + }, { expect(3) }) // Fatal exception is reported to both onError and CEH + finish(5) + } + + @Test + fun testExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { + rxFlowable(Dispatchers.Unconfined + cehUnreached()) { + expect(1) + send(Unit) + }.subscribe({ + expect(2) + throw TestException() + }, { expect(3) }) // not reported to onError because came from the subscribe itself + finish(4) + } + + @Test + fun testAsynchronousExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { + rxFlowable(Dispatchers.Unconfined + cehUnreached()) { + expect(1) + send(Unit) + }.publish() + .refCount() + .subscribe({ + expect(2) + throw RuntimeException() + }, { expect(3) }) + finish(4) + } + + @Test + fun testAsynchronousFatalExceptionFromSubscribe() = withExceptionHandler(handler(3)) { + rxFlowable(Dispatchers.Unconfined) { + expect(1) + send(Unit) + }.publish() + .refCount() + .subscribe({ + expect(2) + throw LinkageError() + }, { expectUnreached() }) + finish(4) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowableTest.kt new file mode 100644 index 0000000000..746d6e8b41 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/FlowableTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import kotlinx.coroutines.reactive.* +import org.junit.Test +import kotlin.test.* + +class FlowableTest : TestBase() { + @Test + fun testBasicSuccess() = runBlocking { + expect(1) + val observable = rxFlowable(currentDispatcher()) { + expect(4) + send("OK") + } + expect(2) + observable.subscribe { value -> + expect(5) + assertEquals("OK", value) + } + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicFailure() = runBlocking { + expect(1) + val observable = rxFlowable(currentDispatcher()) { + expect(4) + throw RuntimeException("OK") + } + expect(2) + observable.subscribe({ + expectUnreached() + }, { error -> + expect(5) + assertTrue(error is RuntimeException) + assertEquals("OK", error.message) + }) + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicUnsubscribe() = runBlocking { + expect(1) + val observable = rxFlowable(currentDispatcher()) { + expect(4) + yield() // back to main, will get cancelled + expectUnreached() + } + expect(2) + val sub = observable.subscribe({ + expectUnreached() + }, { + expectUnreached() + }) + expect(3) + yield() // to started coroutine + expect(5) + sub.dispose() // will cancel coroutine + yield() + finish(6) + } + + @Test + fun testNotifyOnceOnCancellation() = runTest { + expect(1) + val observable = + rxFlowable(currentDispatcher()) { + expect(5) + send("OK") + try { + delay(Long.MAX_VALUE) + } catch (e: CancellationException) { + expect(11) + } + } + .doOnNext { + expect(6) + assertEquals("OK", it) + } + .doOnCancel { + expect(10) // notified once! + } + expect(2) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(3) + observable.collect { + expect(8) + assertEquals("OK", it) + } + } + expect(4) + yield() // to observable code + expect(7) + yield() // to consuming coroutines + expect(9) + job.cancel() + job.join() + finish(12) + } + + @Test + fun testFailingConsumer() = runTest { + val pub = rxFlowable(currentDispatcher()) { + repeat(3) { + expect(it + 1) // expect(1), expect(2) *should* be invoked + send(it) + } + } + try { + pub.collect { + throw TestException() + } + } catch (e: TestException) { + finish(3) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-rx3/test/IntegrationTest.kt new file mode 100644 index 0000000000..395672cee9 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/IntegrationTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.consumeAsFlow +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import kotlin.coroutines.* +import kotlin.test.* + +@RunWith(Parameterized::class) +class IntegrationTest( + private val ctx: Ctx, + private val delay: Boolean +) : TestBase() { + + enum class Ctx { + MAIN { override fun invoke(context: CoroutineContext): CoroutineContext = context.minusKey(Job) }, + DEFAULT { override fun invoke(context: CoroutineContext): CoroutineContext = Dispatchers.Default }, + UNCONFINED { override fun invoke(context: CoroutineContext): CoroutineContext = Dispatchers.Unconfined }; + + abstract operator fun invoke(context: CoroutineContext): CoroutineContext + } + + companion object { + @Parameterized.Parameters(name = "ctx={0}, delay={1}") + @JvmStatic + fun params(): Collection> = Ctx.values().flatMap { ctx -> + listOf(false, true).map { delay -> + arrayOf(ctx, delay) + } + } + } + + @Test + fun testEmpty(): Unit = runBlocking { + val observable = rxObservable(ctx(coroutineContext)) { + if (delay) delay(1) + // does not send anything + } + assertFailsWith { observable.awaitFirst() } + assertEquals("OK", observable.awaitFirstOrDefault("OK")) + assertNull(observable.awaitFirstOrNull()) + assertEquals("ELSE", observable.awaitFirstOrElse { "ELSE" }) + assertFailsWith { observable.awaitLast() } + assertFailsWith { observable.awaitSingle() } + var cnt = 0 + observable.collect { + cnt++ + } + assertEquals(0, cnt) + } + + @Test + fun testSingle() = runBlocking { + val observable = rxObservable(ctx(coroutineContext)) { + if (delay) delay(1) + send("OK") + } + assertEquals("OK", observable.awaitFirst()) + assertEquals("OK", observable.awaitFirstOrDefault("OK")) + assertEquals("OK", observable.awaitFirstOrNull()) + assertEquals("OK", observable.awaitFirstOrElse { "ELSE" }) + assertEquals("OK", observable.awaitLast()) + assertEquals("OK", observable.awaitSingle()) + var cnt = 0 + observable.collect { + assertEquals("OK", it) + cnt++ + } + assertEquals(1, cnt) + } + + @Test + fun testNumbers() = runBlocking { + val n = 100 * stressTestMultiplier + val observable = rxObservable(ctx(coroutineContext)) { + for (i in 1..n) { + send(i) + if (delay) delay(1) + } + } + assertEquals(1, observable.awaitFirst()) + assertEquals(1, observable.awaitFirstOrDefault(0)) + assertEquals(1, observable.awaitFirstOrNull()) + assertEquals(1, observable.awaitFirstOrElse { 0 }) + assertEquals(n, observable.awaitLast()) + assertFailsWith { observable.awaitSingle() } + checkNumbers(n, observable) + val channel = observable.openSubscription() + ctx(coroutineContext) + checkNumbers(n, channel.consumeAsFlow().asObservable()) + channel.cancel() + } + + @Test + fun testCancelWithoutValue() = runTest { + val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { + rxObservable { + hang { } + }.awaitFirst() + } + + job.cancel() + job.join() + } + + @Test + fun testEmptySingle() = runTest(unhandled = listOf({e -> e is NoSuchElementException})) { + expect(1) + val job = launch(Job(), start = CoroutineStart.UNDISPATCHED) { + rxObservable { + yield() + expect(2) + // Nothing to emit + }.awaitFirst() + } + + job.join() + finish(3) + } + + private suspend fun checkNumbers(n: Int, observable: Observable) { + var last = 0 + observable.collect { + assertEquals(++last, it) + } + assertEquals(n, last) + } + +} diff --git a/reactive/kotlinx-coroutines-rx3/test/IterableFlowAsFlowableTckTest.kt b/reactive/kotlinx-coroutines-rx3/test/IterableFlowAsFlowableTckTest.kt new file mode 100644 index 0000000000..8e54922d4a --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/IterableFlowAsFlowableTckTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.flow.* +import org.junit.* +import org.reactivestreams.* +import org.reactivestreams.tck.* + +class IterableFlowAsFlowableTckTest : PublisherVerification(TestEnvironment()) { + + private fun generate(num: Long): Array { + return Array(if (num >= Integer.MAX_VALUE) 1000000 else num.toInt()) { it.toLong() } + } + + override fun createPublisher(elements: Long): Flowable { + return generate(elements).asIterable().asFlow().asFlowable() + } + + override fun createFailedPublisher(): Publisher? = null + + @Ignore + override fun required_spec309_requestZeroMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() { + } + + @Ignore + override fun required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() { + // + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/LeakedExceptionTest.kt b/reactive/kotlinx-coroutines-rx3/test/LeakedExceptionTest.kt new file mode 100644 index 0000000000..028ded0dd0 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/LeakedExceptionTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.exceptions.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.reactive.* +import org.junit.Test +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.test.* + +// Check that exception is not leaked to the global exception handler +class LeakedExceptionTest : TestBase() { + + private val handler: (Throwable) -> Unit = + { assertTrue { it is UndeliverableException && it.cause is TestException } } + + @Test + fun testSingle() = withExceptionHandler(handler) { + withFixedThreadPool(4) { dispatcher -> + val flow = rxSingle(dispatcher) { throw TestException() }.toFlowable().asFlow() + runBlocking { + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect {} + } + } + } + } + + @Test + fun testObservable() = withExceptionHandler(handler) { + withFixedThreadPool(4) { dispatcher -> + val flow = rxObservable(dispatcher) { throw TestException() } + .toFlowable(BackpressureStrategy.BUFFER) + .asFlow() + runBlocking { + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect {} + } + } + } + } + + @Test + fun testFlowable() = withExceptionHandler(handler) { + withFixedThreadPool(4) { dispatcher -> + val flow = rxFlowable(dispatcher) { throw TestException() }.asFlow() + runBlocking { + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect {} + } + } + } + } + + /** + * This test doesn't test much and was added to display a problem with straighforward use of + * [withExceptionHandler]. + * + * If one was to remove `dispatcher` and launch `rxFlowable` with an empty coroutine context, + * this test would fail fairly often, while other tests were also vulnerable, but the problem is + * much more difficult to reproduce. Thus, this test is a justification for adding `dispatcher` + * to other tests. + * + * See the commit that introduced this test for a better explanation. + */ + @Test + fun testResettingExceptionHandler() = withExceptionHandler(handler) { + withFixedThreadPool(4) { dispatcher -> + val flow = rxFlowable(dispatcher) { + if ((0..1).random() == 0) { + Thread.sleep(100) + } + throw TestException() + }.asFlow() + runBlocking { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect {} + } + } + } + + /** + * Run in a thread pool, then wait for all the tasks to finish. + */ + private fun withFixedThreadPool(numberOfThreads: Int, block: (CoroutineDispatcher) -> Unit) { + val pool = Executors.newFixedThreadPool(numberOfThreads) + val dispatcher = pool.asCoroutineDispatcher() + block(dispatcher) + pool.shutdown() + while (!pool.awaitTermination(10, TimeUnit.SECONDS)) { + /* deliberately empty */ + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/MaybeTest.kt b/reactive/kotlinx-coroutines-rx3/test/MaybeTest.kt new file mode 100644 index 0000000000..e0cec748f8 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/MaybeTest.kt @@ -0,0 +1,316 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import io.reactivex.rxjava3.exceptions.* +import io.reactivex.rxjava3.functions.* +import io.reactivex.rxjava3.internal.functions.Functions.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import java.util.concurrent.CancellationException +import kotlin.test.* + +class MaybeTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testBasicSuccess() = runBlocking { + expect(1) + val maybe = rxMaybe(currentDispatcher()) { + expect(4) + "OK" + } + expect(2) + maybe.subscribe { value -> + expect(5) + assertEquals("OK", value) + } + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicEmpty() = runBlocking { + expect(1) + val maybe = rxMaybe(currentDispatcher()) { + expect(4) + null + } + expect(2) + maybe.subscribe (emptyConsumer(), ON_ERROR_MISSING, Action { + expect(5) + }) + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicFailure() = runBlocking { + expect(1) + val maybe = rxMaybe(currentDispatcher()) { + expect(4) + throw RuntimeException("OK") + } + expect(2) + maybe.subscribe({ + expectUnreached() + }, { error -> + expect(5) + assertTrue(error is RuntimeException) + assertEquals("OK", error.message) + }) + expect(3) + yield() // to started coroutine + finish(6) + } + + + @Test + fun testBasicUnsubscribe() = runBlocking { + expect(1) + val maybe = rxMaybe(currentDispatcher()) { + expect(4) + yield() // back to main, will get cancelled + expectUnreached() + } + expect(2) + // nothing is called on a disposed rx3 maybe + val sub = maybe.subscribe({ + expectUnreached() + }, { + expectUnreached() + }) + expect(3) + yield() // to started coroutine + expect(5) + sub.dispose() // will cancel coroutine + yield() + finish(6) + } + + @Test + fun testMaybeNoWait() { + val maybe = rxMaybe { + "OK" + } + + checkMaybeValue(maybe) { + assertEquals("OK", it) + } + } + + @Test + fun testMaybeAwait() = runBlocking { + assertEquals("OK", Maybe.just("O").await() + "K") + } + + @Test + fun testMaybeAwaitForNull() = runBlocking { + assertNull(Maybe.empty().await()) + } + + @Test + fun testMaybeEmitAndAwait() { + val maybe = rxMaybe { + Maybe.just("O").await() + "K" + } + + checkMaybeValue(maybe) { + assertEquals("OK", it) + } + } + + @Test + fun testMaybeWithDelay() { + val maybe = rxMaybe { + Observable.timer(50, TimeUnit.MILLISECONDS).map { "O" }.awaitSingle() + "K" + } + + checkMaybeValue(maybe) { + assertEquals("OK", it) + } + } + + @Test + fun testMaybeException() { + val maybe = rxMaybe { + Observable.just("O", "K").awaitSingle() + "K" + } + + checkErroneous(maybe) { + assert(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitFirst() { + val maybe = rxMaybe { + Observable.just("O", "#").awaitFirst() + "K" + } + + checkMaybeValue(maybe) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitLast() { + val maybe = rxMaybe { + Observable.just("#", "O").awaitLast() + "K" + } + + checkMaybeValue(maybe) { + assertEquals("OK", it) + } + } + + @Test + fun testExceptionFromObservable() { + val maybe = rxMaybe { + try { + Observable.error(RuntimeException("O")).awaitFirst() + } catch (e: RuntimeException) { + Observable.just(e.message!!).awaitLast() + "K" + } + } + + checkMaybeValue(maybe) { + assertEquals("OK", it) + } + } + + @Test + fun testExceptionFromCoroutine() { + val maybe = rxMaybe { + throw IllegalStateException(Observable.just("O").awaitSingle() + "K") + } + + checkErroneous(maybe) { + assert(it is IllegalStateException) + assertEquals("OK", it.message) + } + } + + @Test + fun testCancelledConsumer() = runTest { + expect(1) + val maybe = rxMaybe(currentDispatcher()) { + expect(4) + try { + delay(Long.MAX_VALUE) + } catch (e: CancellationException) { + expect(6) + } + 42 + } + expect(2) + val timeout = withTimeoutOrNull(100) { + expect(3) + maybe.collect { + expectUnreached() + } + expectUnreached() + } + assertNull(timeout) + expect(5) + yield() // must cancel code inside maybe!!! + finish(7) + } + + @Test + fun testSuppressedException() = runTest { + val maybe = rxMaybe(currentDispatcher()) { + launch(start = CoroutineStart.ATOMIC) { + throw TestException() // child coroutine fails + } + try { + delay(Long.MAX_VALUE) + } finally { + throw TestException2() // but parent throws another exception while cleaning up + } + } + try { + maybe.await() + expectUnreached() + } catch (e: TestException) { + assertTrue(e.suppressed[0] is TestException2) + } + } + + @Test + fun testUnhandledException() = runTest { + expect(1) + var disposable: Disposable? = null + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is TestException) + expect(5) + } + val maybe = rxMaybe(currentDispatcher()) { + expect(4) + disposable!!.dispose() // cancel our own subscription, so that delay will get cancelled + try { + delay(Long.MAX_VALUE) + } finally { + throw TestException() // would not be able to handle it since mono is disposed + } + } + withExceptionHandler(handler) { + maybe.subscribe(object : MaybeObserver { + override fun onSubscribe(d: Disposable) { + expect(2) + disposable = d + } + + override fun onComplete() { + expectUnreached() + } + + override fun onSuccess(t: Unit) { + expectUnreached() + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + expect(3) + yield() // run coroutine + finish(6) + } + } + + @Test + fun testFatalExceptionInSubscribe() = runTest { + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is LinkageError) + expect(2) + } + + withExceptionHandler(handler) { + rxMaybe(Dispatchers.Unconfined) { + expect(1) + 42 + }.subscribe({ throw LinkageError() }) + finish(3) + } + } + + @Test + fun testFatalExceptionInSingle() = runTest { + rxMaybe(Dispatchers.Unconfined) { + throw LinkageError() + }.subscribe({ expectUnreached() }, { expect(1); assertTrue(it is LinkageError) }) + finish(2) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableAsFlowTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableAsFlowTest.kt new file mode 100644 index 0000000000..262da9ac1b --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableAsFlowTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableSource +import io.reactivex.rxjava3.core.Observer +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.subjects.PublishSubject +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class ObservableAsFlowTest : TestBase() { + @Test + fun testCancellation() = runTest { + var onNext = 0 + var onCancelled = 0 + var onError = 0 + + val source = rxObservable(currentDispatcher()) { + coroutineContext[Job]?.invokeOnCompletion { + if (it is CancellationException) ++onCancelled + } + + repeat(100) { + send(it) + } + } + + source.asFlow().launchIn(CoroutineScope(Dispatchers.Unconfined)) { + onEach { + ++onNext + throw RuntimeException() + } + catch { + ++onError + } + }.join() + + + assertEquals(1, onNext) + assertEquals(1, onError) + assertEquals(1, onCancelled) + } + + @Test + fun testImmediateCollection() { + val source = PublishSubject.create() + val flow = source.asFlow() + GlobalScope.launch(Dispatchers.Unconfined) { + expect(1) + flow.collect { expect(it) } + expect(6) + } + expect(2) + source.onNext(3) + expect(4) + source.onNext(5) + source.onComplete() + finish(7) + } + + @Test + fun testOnErrorCancellation() { + val source = PublishSubject.create() + val flow = source.asFlow() + val exception = RuntimeException() + GlobalScope.launch(Dispatchers.Unconfined) { + try { + expect(1) + flow.collect { expect(it) } + expectUnreached() + } + catch (e: Exception) { + assertSame(exception, e.cause) + expect(5) + } + expect(6) + } + expect(2) + source.onNext(3) + expect(4) + source.onError(exception) + finish(7) + } + + @Test + fun testUnsubscribeOnCollectionException() { + val source = PublishSubject.create() + val flow = source.asFlow() + val exception = RuntimeException() + GlobalScope.launch(Dispatchers.Unconfined) { + try { + expect(1) + flow.collect { + expect(it) + if (it == 3) throw exception + } + expectUnreached() + } + catch (e: Exception) { + assertSame(exception, e.cause) + expect(4) + } + expect(5) + } + expect(2) + assertTrue(source.hasObservers()) + source.onNext(3) + assertFalse(source.hasObservers()) + finish(6) + } + + @Test + fun testLateOnSubscribe() { + var observer: Observer? = null + val source = ObservableSource { observer = it } + val flow = source.asFlow() + assertNull(observer) + val job = GlobalScope.launch(Dispatchers.Unconfined) { + expect(1) + flow.collect { expectUnreached() } + expectUnreached() + } + expect(2) + assertNotNull(observer) + job.cancel() + val disposable = Disposable.empty() + observer!!.onSubscribe(disposable) + assertTrue(disposable.isDisposed) + finish(3) + } + + @Test + fun testBufferUnlimited() = runTest { + val source = rxObservable(currentDispatcher()) { + expect(1); send(10) + expect(2); send(11) + expect(3); send(12) + expect(4); send(13) + expect(5); send(14) + expect(6); send(15) + expect(7); send(16) + expect(8); send(17) + expect(9) + } + source.asFlow().buffer(Channel.UNLIMITED).collect { expect(it) } + finish(18) + } + + @Test + fun testConflated() = runTest { + val source = Observable.range(1, 5) + val list = source.asFlow().conflate().toList() + assertEquals(listOf(1, 5), list) + } + + @Test + fun testLongRange() = runTest { + val source = Observable.range(1, 10_000) + val count = source.asFlow().count() + assertEquals(10_000, count) + } + + @Test + fun testProduce() = runTest { + val source = Observable.range(0, 10) + val flow = source.asFlow() + check((0..9).toList(), flow.produceIn(this)) + check((0..9).toList(), flow.buffer(Channel.UNLIMITED).produceIn(this)) + check((0..9).toList(), flow.buffer(2).produceIn(this)) + check((0..9).toList(), flow.buffer(0).produceIn(this)) + check(listOf(0, 9), flow.conflate().produceIn(this)) + } + + private suspend fun check(expected: List, channel: ReceiveChannel) { + val result = ArrayList(10) + channel.consumeEach { result.add(it) } + assertEquals(expected, result) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableCompletionStressTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableCompletionStressTest.kt new file mode 100644 index 0000000000..c1d25bcb51 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableCompletionStressTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import org.junit.* +import java.util.* +import kotlin.coroutines.* + +class ObservableCompletionStressTest : TestBase() { + private val N_REPEATS = 10_000 * stressTestMultiplier + + private fun CoroutineScope.range(context: CoroutineContext, start: Int, count: Int) = rxObservable(context) { + for (x in start until start + count) send(x) + } + + @Test + fun testCompletion() { + val rnd = Random() + repeat(N_REPEATS) { + val count = rnd.nextInt(5) + runBlocking { + withTimeout(5000) { + var received = 0 + range(Dispatchers.Default, 1, count).collect { x -> + received++ + if (x != received) error("$x != $received") + } + if (received != count) error("$received != $count") + } + } + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableExceptionHandlingTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableExceptionHandlingTest.kt new file mode 100644 index 0000000000..1183b2ae21 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableExceptionHandlingTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.exceptions.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +class ObservableExceptionHandlingTest : TestBase() { + + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + private inline fun handler(expect: Int) = { t: Throwable -> + assertTrue(t is UndeliverableException && t.cause is T) + expect(expect) + } + + private fun cehUnreached() = CoroutineExceptionHandler { _, _ -> expectUnreached() } + + @Test + fun testException() = withExceptionHandler({ expectUnreached() }) { + rxObservable(Dispatchers.Unconfined + cehUnreached()) { + expect(1) + throw TestException() + }.subscribe({ + expectUnreached() + }, { + expect(2) // Reported to onError + }) + finish(3) + } + + @Test + fun testFatalException() = withExceptionHandler(handler(3)) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + throw LinkageError() + }.subscribe({ + expectUnreached() + }, { + expect(2) + }) + finish(4) + } + + @Test + fun testExceptionAsynchronous() = withExceptionHandler({ expectUnreached() }) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + throw TestException() + }.publish() + .refCount() + .subscribe({ + expectUnreached() + }, { + expect(2) // Reported to onError + }) + finish(3) + } + + @Test + fun testFatalExceptionAsynchronous() = withExceptionHandler(handler(3)) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + throw LinkageError() + }.publish() + .refCount() + .subscribe({ + expectUnreached() + }, { + expect(2) // Fatal exception is not reported in onError + }) + finish(4) + } + + @Test + fun testFatalExceptionFromSubscribe() = withExceptionHandler(handler(4)) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + send(Unit) + }.subscribe({ + expect(2) + throw LinkageError() + }, { expect(3) }) // Unreached because fatal errors are rethrown + finish(5) + } + + @Test + fun testExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + send(Unit) + }.subscribe({ + expect(2) + throw TestException() + }, { expect(3) }) // not reported to onError because came from the subscribe itself + finish(4) + } + + @Test + fun testAsynchronousExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + send(Unit) + }.publish() + .refCount() + .subscribe({ + expect(2) + throw RuntimeException() + }, { expect(3) }) + finish(4) + } + + @Test + fun testAsynchronousFatalExceptionFromSubscribe() = withExceptionHandler(handler(4)) { + rxObservable(Dispatchers.Unconfined) { + expect(1) + send(Unit) + }.publish() + .refCount() + .subscribe({ + expect(2) + throw LinkageError() + }, { expect(3) }) + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt new file mode 100644 index 0000000000..b4adf7af27 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableMultiTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import org.junit.Test +import java.io.* +import kotlin.test.* + +/** + * Test emitting multiple values with [rxObservable]. + */ +class ObservableMultiTest : TestBase() { + @Test + fun testNumbers() { + val n = 100 * stressTestMultiplier + val observable = rxObservable { + repeat(n) { send(it) } + } + checkSingleValue(observable.toList()) { list -> + assertEquals((0 until n).toList(), list) + } + } + + + @Test + fun testConcurrentStress() { + val n = 10_000 * stressTestMultiplier + val observable = rxObservable { + newCoroutineContext(coroutineContext) + // concurrent emitters (many coroutines) + val jobs = List(n) { + // launch + launch { + val i = it + send(i) + } + } + jobs.forEach { it.join() } + } + checkSingleValue(observable.toList()) { list -> + assertEquals(n, list.size) + assertEquals((0 until n).toList(), list.sorted()) + } + } + + @Test + fun testIteratorResendUnconfined() { + val n = 10_000 * stressTestMultiplier + val observable = rxObservable(Dispatchers.Unconfined) { + Observable.range(0, n).collect { send(it) } + } + checkSingleValue(observable.toList()) { list -> + assertEquals((0 until n).toList(), list) + } + } + + @Test + fun testIteratorResendPool() { + val n = 10_000 * stressTestMultiplier + val observable = rxObservable { + Observable.range(0, n).collect { send(it) } + } + checkSingleValue(observable.toList()) { list -> + assertEquals((0 until n).toList(), list) + } + } + + @Test + fun testSendAndCrash() { + val observable = rxObservable { + send("O") + throw IOException("K") + } + val single = rxSingle { + var result = "" + try { + observable.collect { result += it } + } catch(e: IOException) { + result += e.message + } + result + } + checkSingleValue(single) { + assertEquals("OK", it) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableSingleTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableSingleTest.kt new file mode 100644 index 0000000000..2a3ce04638 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableSingleTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class ObservableSingleTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testSingleNoWait() { + val observable = rxObservable { + send("OK") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testSingleAwait() = runBlocking { + assertEquals("OK", Observable.just("O").awaitSingle() + "K") + } + + @Test + fun testSingleEmitAndAwait() { + val observable = rxObservable { + send(Observable.just("O").awaitSingle() + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testSingleWithDelay() { + val observable = rxObservable { + send(Observable.timer(50, TimeUnit.MILLISECONDS).map { "O" }.awaitSingle() + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testSingleException() { + val observable = rxObservable { + send(Observable.just("O", "K").awaitSingle() + "K") + } + + checkErroneous(observable) { + assertTrue(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitFirst() { + val observable = rxObservable { + send(Observable.just("O", "#").awaitFirst() + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitFirstOrDefault() { + val observable = rxObservable { + send(Observable.empty().awaitFirstOrDefault("O") + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitFirstOrDefaultWithValues() { + val observable = rxObservable { + send(Observable.just("O", "#").awaitFirstOrDefault("!") + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitFirstOrNull() { + val observable = rxObservable { + send(Observable.empty().awaitFirstOrNull() ?: "OK") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitFirstOrNullWithValues() { + val observable = rxObservable { + send((Observable.just("O", "#").awaitFirstOrNull() ?: "!") + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitFirstOrElse() { + val observable = rxObservable { + send(Observable.empty().awaitFirstOrElse { "O" } + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitFirstOrElseWithValues() { + val observable = rxObservable { + send(Observable.just("O", "#").awaitFirstOrElse { "!" } + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitLast() { + val observable = rxObservable { + send(Observable.just("#", "O").awaitLast() + "K") + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testExceptionFromObservable() { + val observable = rxObservable { + try { + send(Observable.error(RuntimeException("O")).awaitFirst()) + } catch (e: RuntimeException) { + send(Observable.just(e.message!!).awaitLast() + "K") + } + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testExceptionFromCoroutine() { + val observable = rxObservable { + throw IllegalStateException(Observable.just("O").awaitSingle() + "K") + } + + checkErroneous(observable) { + assertTrue(it is IllegalStateException) + assertEquals("OK", it.message) + } + } + + @Test + fun testObservableIteration() { + val observable = rxObservable { + var result = "" + Observable.just("O", "K").collect { result += it } + send(result) + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } + + @Test + fun testObservableIterationFailure() { + val observable = rxObservable { + try { + Observable.error(RuntimeException("OK")).collect { fail("Should not be here") } + send("Fail") + } catch (e: RuntimeException) { + send(e.message!!) + } + } + + checkSingleValue(observable) { + assertEquals("OK", it) + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableSubscriptionSelectTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableSubscriptionSelectTest.kt new file mode 100644 index 0000000000..2f04316159 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableSubscriptionSelectTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import org.junit.Test +import kotlin.test.* + +class ObservableSubscriptionSelectTest : TestBase() { + @Test + fun testSelect() = runTest { + // source with n ints + val n = 1000 * stressTestMultiplier + val source = rxObservable { repeat(n) { send(it) } } + var a = 0 + var b = 0 + // open two subs + val channelA = source.openSubscription() + val channelB = source.openSubscription() + loop@ while (true) { + val done: Int = select { + channelA.onReceiveOrNull { + if (it != null) assertEquals(a++, it) + if (it == null) 0 else 1 + } + channelB.onReceiveOrNull { + if (it != null) assertEquals(b++, it) + if (it == null) 0 else 2 + } + } + when (done) { + 0 -> break@loop + 1 -> { + val r = channelB.receiveOrNull() + if (r != null) assertEquals(b++, r) + } + 2 -> { + val r = channelA.receiveOrNull() + if (r != null) assertEquals(a++, r) + } + } + } + channelA.cancel() + channelB.cancel() + // should receive one of them fully + assertTrue(a == n || b == n) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableTest.kt new file mode 100644 index 0000000000..c6a6be56e3 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.plugins.* +import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class ObservableTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testBasicSuccess() = runBlocking { + expect(1) + val observable = rxObservable(currentDispatcher()) { + expect(4) + send("OK") + } + expect(2) + observable.subscribe { value -> + expect(5) + assertEquals("OK", value) + } + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicFailure() = runBlocking { + expect(1) + val observable = rxObservable(currentDispatcher()) { + expect(4) + throw RuntimeException("OK") + } + expect(2) + observable.subscribe({ + expectUnreached() + }, { error -> + expect(5) + assertTrue(error is RuntimeException) + assertEquals("OK", error.message) + }) + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicUnsubscribe() = runBlocking { + expect(1) + val observable = rxObservable(currentDispatcher()) { + expect(4) + yield() // back to main, will get cancelled + expectUnreached() + } + expect(2) + val sub = observable.subscribe({ + expectUnreached() + }, { + expectUnreached() + }) + expect(3) + yield() // to started coroutine + expect(5) + sub.dispose() // will cancel coroutine + yield() + finish(6) + } + + @Test + fun testNotifyOnceOnCancellation() = runTest { + expect(1) + val observable = + rxObservable(currentDispatcher()) { + expect(5) + send("OK") + try { + delay(Long.MAX_VALUE) + } catch (e: CancellationException) { + expect(11) + } + } + .doOnNext { + expect(6) + assertEquals("OK", it) + } + .doOnDispose { + expect(10) // notified once! + } + expect(2) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(3) + observable.collect { + expect(8) + assertEquals("OK", it) + } + } + expect(4) + yield() // to observable code + expect(7) + yield() // to consuming coroutines + expect(9) + job.cancel() + job.join() + finish(12) + } + + @Test + fun testFailingConsumer() = runTest { + expect(1) + val pub = rxObservable(currentDispatcher()) { + expect(2) + send("OK") + try { + delay(Long.MAX_VALUE) + } catch (e: CancellationException) { + finish(5) + } + } + try { + pub.collect { + expect(3) + throw TestException() + } + } catch (e: TestException) { + expect(4) + } + } + + @Test + fun testExceptionAfterCancellation() { + // Test that no exceptions were reported to the global EH (it will fail the test if so) + val handler = { e: Throwable -> + assertFalse(e is CancellationException) + } + withExceptionHandler(handler) { + RxJavaPlugins.setErrorHandler { + require(it !is CancellationException) + } + Observable + .interval(1, TimeUnit.MILLISECONDS) + .take(1000) + .switchMapSingle { + rxSingle { + timeBomb().await() + } + } + .blockingSubscribe({}, {}) + } + } + + private fun timeBomb() = Single.timer(1, TimeUnit.MILLISECONDS).doOnSuccess { throw TestException() } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt b/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt new file mode 100644 index 0000000000..9e95c213d0 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/SchedulerTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.* +import org.junit.Before +import org.junit.Test +import kotlin.test.* + +class SchedulerTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxCachedThreadScheduler-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testIoScheduler(): Unit = runBlocking { + expect(1) + val mainThread = Thread.currentThread() + withContext(Schedulers.io().asCoroutineDispatcher()) { + val t1 = Thread.currentThread() + assertNotSame(t1, mainThread) + expect(2) + delay(100) + val t2 = Thread.currentThread() + assertNotSame(t2, mainThread) + expect(3) + } + finish(4) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/SingleTest.kt b/reactive/kotlinx-coroutines-rx3/test/SingleTest.kt new file mode 100644 index 0000000000..46bcaf84dc --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/SingleTest.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* +import io.reactivex.rxjava3.exceptions.* +import io.reactivex.rxjava3.functions.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class SingleTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testBasicSuccess() = runBlocking { + expect(1) + val single = rxSingle(currentDispatcher()) { + expect(4) + "OK" + } + expect(2) + single.subscribe { value -> + expect(5) + assertEquals("OK", value) + } + expect(3) + yield() // to started coroutine + finish(6) + } + + @Test + fun testBasicFailure() = runBlocking { + expect(1) + val single = rxSingle(currentDispatcher()) { + expect(4) + throw RuntimeException("OK") + } + expect(2) + single.subscribe({ + expectUnreached() + }, { error -> + expect(5) + assertTrue(error is RuntimeException) + assertEquals("OK", error.message) + }) + expect(3) + yield() // to started coroutine + finish(6) + } + + + @Test + fun testBasicUnsubscribe() = runBlocking { + expect(1) + val single = rxSingle(currentDispatcher()) { + expect(4) + yield() // back to main, will get cancelled + expectUnreached() + + } + expect(2) + // nothing is called on a disposed rx3 single + val sub = single.subscribe({ + expectUnreached() + }, { + expectUnreached() + }) + expect(3) + yield() // to started coroutine + expect(5) + sub.dispose() // will cancel coroutine + yield() + finish(6) + } + + @Test + fun testSingleNoWait() { + val single = rxSingle { + "OK" + } + + checkSingleValue(single) { + assertEquals("OK", it) + } + } + + @Test + fun testSingleAwait() = runBlocking { + assertEquals("OK", Single.just("O").await() + "K") + } + + @Test + fun testSingleEmitAndAwait() { + val single = rxSingle { + Single.just("O").await() + "K" + } + + checkSingleValue(single) { + assertEquals("OK", it) + } + } + + @Test + fun testSingleWithDelay() { + val single = rxSingle { + Observable.timer(50, TimeUnit.MILLISECONDS).map { "O" }.awaitSingle() + "K" + } + + checkSingleValue(single) { + assertEquals("OK", it) + } + } + + @Test + fun testSingleException() { + val single = rxSingle { + Observable.just("O", "K").awaitSingle() + "K" + } + + checkErroneous(single) { + assert(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitFirst() { + val single = rxSingle { + Observable.just("O", "#").awaitFirst() + "K" + } + + checkSingleValue(single) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitLast() { + val single = rxSingle { + Observable.just("#", "O").awaitLast() + "K" + } + + checkSingleValue(single) { + assertEquals("OK", it) + } + } + + @Test + fun testExceptionFromObservable() { + val single = rxSingle { + try { + Observable.error(RuntimeException("O")).awaitFirst() + } catch (e: RuntimeException) { + Observable.just(e.message!!).awaitLast() + "K" + } + } + + checkSingleValue(single) { + assertEquals("OK", it) + } + } + + @Test + fun testExceptionFromCoroutine() { + val single = rxSingle { + throw IllegalStateException(Observable.just("O").awaitSingle() + "K") + } + + checkErroneous(single) { + assert(it is IllegalStateException) + assertEquals("OK", it.message) + } + } + + @Test + fun testSuppressedException() = runTest { + val single = rxSingle(currentDispatcher()) { + launch(start = CoroutineStart.ATOMIC) { + throw TestException() // child coroutine fails + } + try { + delay(Long.MAX_VALUE) + } finally { + throw TestException2() // but parent throws another exception while cleaning up + } + } + try { + single.await() + expectUnreached() + } catch (e: TestException) { + assertTrue(e.suppressed[0] is TestException2) + } + } + + @Test + fun testFatalExceptionInSubscribe() = runTest { + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is LinkageError) + expect(2) + } + withExceptionHandler(handler) { + rxSingle(Dispatchers.Unconfined) { + expect(1) + 42 + }.subscribe(Consumer { + throw LinkageError() + }) + finish(3) + } + } + + @Test + fun testFatalExceptionInSingle() = runTest { + rxSingle(Dispatchers.Unconfined) { + throw LinkageError() + }.subscribe({ _, e -> assertTrue(e is LinkageError); expect(1) }) + + finish(2) + } + + @Test + fun testUnhandledException() = runTest { + expect(1) + var disposable: Disposable? = null + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is TestException) + expect(5) + } + val single = rxSingle(currentDispatcher()) { + expect(4) + disposable!!.dispose() // cancel our own subscription, so that delay will get cancelled + try { + delay(Long.MAX_VALUE) + } finally { + throw TestException() // would not be able to handle it since mono is disposed + } + } + withExceptionHandler(handler) { + single.subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) { + expect(2) + disposable = d + } + + override fun onSuccess(t: Unit) { + expectUnreached() + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + expect(3) + yield() // run coroutine + finish(6) + } + } +} diff --git a/settings.gradle b/settings.gradle index 95fcd7cb2d..759628f134 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,7 @@ module('reactive/kotlinx-coroutines-reactive') module('reactive/kotlinx-coroutines-reactor') module('reactive/kotlinx-coroutines-jdk9') module('reactive/kotlinx-coroutines-rx2') +module('reactive/kotlinx-coroutines-rx3') module('ui/kotlinx-coroutines-android') module('ui/kotlinx-coroutines-android/android-unit-tests') module('ui/kotlinx-coroutines-javafx') From 5073704efa5818f70e09fd35d90bf84e22da6320 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Mon, 27 Apr 2020 14:53:24 +0300 Subject: [PATCH 023/257] Remove unused Gradle wrapper (#1958) --- .../gradle/wrapper/gradle-wrapper.jar | 0 .../gradle/wrapper/gradle-wrapper.properties | 6 - ui/kotlinx-coroutines-android/gradlew | 172 ------------------ ui/kotlinx-coroutines-android/gradlew.bat | 84 --------- 4 files changed, 262 deletions(-) delete mode 100644 ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 ui/kotlinx-coroutines-android/gradlew delete mode 100644 ui/kotlinx-coroutines-android/gradlew.bat diff --git a/ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.jar b/ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.properties b/ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d5aeee10af..0000000000 --- a/ui/kotlinx-coroutines-android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sun Apr 08 03:07:42 CEST 2018 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/ui/kotlinx-coroutines-android/gradlew b/ui/kotlinx-coroutines-android/gradlew deleted file mode 100644 index cccdd3d517..0000000000 --- a/ui/kotlinx-coroutines-android/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/ui/kotlinx-coroutines-android/gradlew.bat b/ui/kotlinx-coroutines-android/gradlew.bat deleted file mode 100644 index f9553162f1..0000000000 --- a/ui/kotlinx-coroutines-android/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega From eb4e7d32dadb6962372e625cb684e84857650169 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 27 Apr 2020 15:04:00 +0300 Subject: [PATCH 024/257] Breaking change: Experimental Flow.onCompletion contract for cause (#1732) Flow.onCompletion now reports all failures and cancellation in its cause just like invokeOnCompletion. A null cause is reported if and only if flow had completed successfully (no failure, no cancellation). Emission of additional elements after the end of the flow is only possible from inside of onCompletion block in case of successful completion. Also fixed a bug where onCompletion implementation was trying to add exception to its own list of suppressed exceptions, which is not allowed. Fixes #1693 --- coroutines-guide.md | 2 +- docs/flow.md | 12 ++--- .../common/src/flow/operators/Emitters.kt | 52 ++++++++++--------- .../test/flow/operators/OnCompletionTest.kt | 26 +++++++--- .../jvm/test/guide/test/FlowGuideTest.kt | 2 +- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/coroutines-guide.md b/coroutines-guide.md index bebd0d0e3c..0b7b842acb 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -76,7 +76,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Flow completion](docs/flow.md#flow-completion) * [Imperative finally block](docs/flow.md#imperative-finally-block) * [Declarative handling](docs/flow.md#declarative-handling) - * [Upstream exceptions only](docs/flow.md#upstream-exceptions-only) + * [Successful completion](docs/flow.md#successful-completion) * [Imperative versus declarative](docs/flow.md#imperative-versus-declarative) * [Launching flow](docs/flow.md#launching-flow) * [Flow and Reactive Streams](docs/flow.md#flow-and-reactive-streams) diff --git a/docs/flow.md b/docs/flow.md index 37e491cf51..3f14b10417 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -39,7 +39,7 @@ * [Flow completion](#flow-completion) * [Imperative finally block](#imperative-finally-block) * [Declarative handling](#declarative-handling) - * [Upstream exceptions only](#upstream-exceptions-only) + * [Successful completion](#successful-completion) * [Imperative versus declarative](#imperative-versus-declarative) * [Launching flow](#launching-flow) * [Flow and Reactive Streams](#flow-and-reactive-streams) @@ -1635,10 +1635,10 @@ The [onCompletion] operator, unlike [catch], does not handle the exception. As w example code, the exception still flows downstream. It will be delivered to further `onCompletion` operators and can be handled with a `catch` operator. -#### Upstream exceptions only +#### Successful completion -Just like the [catch] operator, [onCompletion] only sees exceptions coming from upstream and does not -see downstream exceptions. For example, run the following code: +Another difference with [catch] operator is that [onCompletion] sees all exceptions and receives +a `null` exception only on successful completion of the upstream flow (without cancellation or failure).
@@ -1664,11 +1664,11 @@ fun main() = runBlocking { > You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-34.kt). -We can see the completion cause is null, yet collection failed with exception: +We can see the completion cause is not null, because the flow was aborted due to downstream exception: ```text 1 -Flow completed with null +Flow completed with java.lang.IllegalStateException: Collected 2 Exception in thread "main" java.lang.IllegalStateException: Collected 2 ``` diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index cbed2df929..93530234eb 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -81,8 +81,8 @@ public fun Flow.onStart( } /** - * Invokes the given [action] when the given flow is completed or cancelled, using - * the exception from the upstream (if any) as cause parameter of [action]. + * Invokes the given [action] when the given 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, * for example the following imperative snippet: @@ -106,53 +106,55 @@ public fun Flow.onStart( * .collect() * ``` * - * This operator is *transparent* to exceptions that occur in downstream flow - * and does not observe exceptions that are thrown to cancel the flow, - * while any exception from the [action] will be thrown downstream. - * This behaviour can be demonstrated by the following example: + * Unlike [catch], this operator reports exception that occur both upstream and downstream + * and observe exceptions that are thrown to cancel the flow. Exception is empty if and only if + * the flow had fully completed successfully. Conceptually, the following code: * * ``` - * flow { emitData() } - * .map { computeOne(it) } - * .onCompletion { println(it) } // Can print exceptions from emitData and computeOne - * .map { computeTwo(it) } - * .onCompletion { println(it) } // Can print exceptions from emitData, computeOne, onCompletion and computeTwo + * myFlow.collect { value -> + * println(value) + * } + * println("Completed successfully") + * ``` + * + * can be replaced with: + * + * ``` + * myFlow + * .onEach { println(it) } + * .onCompletion { if (it == null) println("Completed successfully") } * .collect() * ``` * * The receiver of the [action] is [FlowCollector] and this operator can be used to emit additional - * elements at the end of the collection. For example: + * elements at the end if it completed successfully. For example: * * ``` * flowOf("a", "b", "c") * .onCompletion { emit("Done") } * .collect { println(it) } // prints a, b, c, Done * ``` + * + * In case of failure or cancellation, any attempt to emit additional elements throws the corresponding exception. + * Use [catch] if you need to suppress failure and replace it with emission of elements. */ @ExperimentalCoroutinesApi public fun Flow.onCompletion( action: suspend FlowCollector.(cause: Throwable?) -> Unit ): Flow = unsafeFlow { // Note: unsafe flow is used here, but safe collector is used to invoke completion action - val exception = try { - catchImpl(this) + try { + collect(this) } catch (e: Throwable) { /* - * Exception from the downstream. * Use throwing collector to prevent any emissions from the * completion sequence when downstream has failed, otherwise it may * lead to a non-sequential behaviour impossible with `finally` */ - ThrowingCollector(e).invokeSafely(action, null) + ThrowingCollector(e).invokeSafely(action, e) throw e } - // Exception from the upstream or normal completion - val safeCollector = SafeCollector(this, coroutineContext) - try { - safeCollector.invokeSafely(action, exception) - } finally { - safeCollector.releaseIntercepted() - } - exception?.let { throw it } + // Normal completion + SafeCollector(this, coroutineContext).invokeSafely(action, null) } /** @@ -205,7 +207,7 @@ private suspend fun FlowCollector.invokeSafely( try { action(cause) } catch (e: Throwable) { - if (cause !== null) e.addSuppressedThrowable(cause) + if (cause !== null && cause !== e) e.addSuppressedThrowable(cause) throw e } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt index f56632d5fd..7f0c548ca6 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt @@ -50,7 +50,7 @@ class OnCompletionTest : TestBase() { }.onEach { expect(2) }.onCompletion { - assertNull(it) + assertTrue(it is TestException) // flow fails because of this exception expect(4) }.onEach { expect(3) @@ -65,7 +65,7 @@ class OnCompletionTest : TestBase() { @Test fun testMultipleOnCompletions() = runTest { flowOf(1).onCompletion { - assertNull(it) + assertTrue(it is TestException) expect(2) }.onEach { expect(1) @@ -145,12 +145,12 @@ class OnCompletionTest : TestBase() { .onCompletion { e -> expect(8) assertTrue(e is TestException) - emit(TestData.Done(e)) + emit(TestData.Done(e)) // will fail }.collect { collected += it } } - val expected = (1..5).map { TestData.Value(it) } + TestData.Done(TestException("OK")) + val expected: List = (1..5).map { TestData.Value(it) } assertEquals(expected, collected) finish(9) } @@ -171,7 +171,7 @@ class OnCompletionTest : TestBase() { } .onCompletion { e -> expect(8) - assertNull(e) + assertTrue(e is CancellationException) try { emit(TestData.Done(e)) expectUnreached() @@ -197,7 +197,7 @@ class OnCompletionTest : TestBase() { emit(TestData.Value(2)) expectUnreached() }.onCompletion { - assertNull(it) + assertSame(cause, it) // flow failed because of the exception in downstream expect(3) try { emit(TestData.Done(it)) @@ -216,7 +216,7 @@ class OnCompletionTest : TestBase() { @Test fun testFirst() = runTest { val value = flowOf(239).onCompletion { - assertNull(it) + assertNotNull(it) // the flow did not complete normally expect(1) try { emit(42) @@ -278,4 +278,16 @@ class OnCompletionTest : TestBase() { assertNull(flow.singleOrNull()) finish(4) } + + @Test + fun testTakeOnCompletion() = runTest { + // even though it uses "take" from the outside it completes normally + val flow = (1..10).asFlow().take(5) + val result = flow.onCompletion { cause -> + assertNull(cause) + emit(-1) + }.toList() + val expected = (1..5).toList() + (-1) + assertEquals(expected, result) + } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index 5d320f2124..39a7853b5f 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -357,7 +357,7 @@ class FlowGuideTest { fun testExampleFlow34() { test("ExampleFlow34") { kotlinx.coroutines.guide.exampleFlow34.main() }.verifyExceptions( "1", - "Flow completed with null", + "Flow completed with java.lang.IllegalStateException: Collected 2", "Exception in thread \"main\" java.lang.IllegalStateException: Collected 2" ) } From cd5ab49effc50cc593ae487cae8dd3d47f6a596b Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 27 Apr 2020 21:12:33 +0300 Subject: [PATCH 025/257] Setup cancellation eagerly in suspendCancellableCoroutine to properly integrate with APIs that may block the current thread, but react on cancellation (#1680) Fixes #1671 --- .../api/kotlinx-coroutines-core.api | 4 +- .../common/src/CancellableContinuation.kt | 27 ++++----- .../common/src/CancellableContinuationImpl.kt | 10 ++-- .../common/test/CancellableResumeTest.kt | 21 ++++++- .../test/CancellableContinuationJvmTest.kt | 59 +++++++++++++++++++ 5 files changed, 98 insertions(+), 23 deletions(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 08f46e5108..f908f964d4 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -37,7 +37,7 @@ public final class kotlinx/coroutines/BuildersKt { public abstract interface class kotlinx/coroutines/CancellableContinuation : kotlin/coroutines/Continuation { public abstract fun cancel (Ljava/lang/Throwable;)Z public abstract fun completeResume (Ljava/lang/Object;)V - public abstract synthetic fun initCancellability ()V + public abstract fun initCancellability ()V public abstract fun invokeOnCancellation (Lkotlin/jvm/functions/Function1;)V public abstract fun isActive ()Z public abstract fun isCancelled ()Z @@ -63,7 +63,7 @@ public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/ public fun getContinuationCancellationCause (Lkotlinx/coroutines/Job;)Ljava/lang/Throwable; public final fun getResult ()Ljava/lang/Object; public fun getStackTraceElement ()Ljava/lang/StackTraceElement; - public synthetic fun initCancellability ()V + public fun initCancellability ()V public fun invokeOnCancellation (Lkotlin/jvm/functions/Function1;)V public fun isActive ()Z public fun isCancelled ()Z diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index e01dc82521..0d3fe847dc 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -100,14 +100,8 @@ public interface CancellableContinuation : Continuation { * Legacy function that turned on cancellation behavior in [suspendCancellableCoroutine] before kotlinx.coroutines 1.1.0. * This function does nothing and is left only for binary compatibility with old compiled code. * - * @suppress **Deprecated**: This function is no longer used. - * It is left for binary compatibility with code compiled before kotlinx.coroutines 1.1.0. + * @suppress **This is unstable API and it is subject to change.** */ - @Deprecated( - level = DeprecationLevel.HIDDEN, - message = "This function is no longer used. " + - "It is left for binary compatibility with code compiled before kotlinx.coroutines 1.1.0. " - ) @InternalCoroutinesApi public fun initCancellability() @@ -183,7 +177,7 @@ public interface CancellableContinuation : Continuation { * It can be invoked concurrently with the surrounding code. * There is no guarantee on the execution context of its invocation. */ - @ExperimentalCoroutinesApi // since 1.2.0, tentatively graduates in 1.3.0 + @ExperimentalCoroutinesApi // since 1.2.0 public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) } @@ -196,9 +190,12 @@ public suspend inline fun suspendCancellableCoroutine( ): T = suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE) - // NOTE: Before version 1.1.0 the following invocation was inlined here, so invocation of this - // method indicates that the code was compiled by kotlinx.coroutines < 1.1.0 - // cancellable.initCancellability() + /* + * For non-atomic cancellation we setup parent-child relationship immediately + * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but + * properly supports cancellation. + */ + cancellable.initCancellability() block(cancellable) cancellable.getResult() } @@ -229,10 +226,10 @@ public suspend inline fun suspendAtomicCancellableCoroutine( internal suspend inline fun suspendAtomicCancellableCoroutineReusable( crossinline block: (CancellableContinuation) -> Unit ): T = suspendCoroutineUninterceptedOrReturn { uCont -> - val cancellable = getOrCreateCancellableContinuation(uCont.intercepted()) - block(cancellable) - cancellable.getResult() - } + val cancellable = getOrCreateCancellableContinuation(uCont.intercepted()) + block(cancellable) + cancellable.getResult() +} internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl { // If used outside of our dispatcher diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 1f67dd3c6c..e25ebd3a37 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -32,8 +32,8 @@ internal open class CancellableContinuationImpl( /* * Implementation notes * - * AbstractContinuation is a subset of Job with following limitations: - * 1) It can have only cancellation listeners + * CancellableContinuationImpl is a subset of Job with following limitations: + * 1) It can have only cancellation listener (no "on cancelling") * 2) It always invokes cancellation listener if it's cancelled (no 'invokeImmediately') * 3) It can have at most one cancellation listener * 4) Its cancellation listeners cannot be deregistered @@ -82,7 +82,7 @@ internal open class CancellableContinuationImpl( public override val isCancelled: Boolean get() = state is CancelledContinuation public override fun initCancellability() { - // This method does nothing. Leftover for binary compatibility with old compiled code + setupCancellation() } private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this) @@ -107,7 +107,8 @@ internal open class CancellableContinuationImpl( /** * Setups parent cancellation and checks for postponed cancellation in the case of reusable continuations. - * It is only invoked from an internal [getResult] function. + * It is only invoked from an internal [getResult] function for reusable continuations + * and from [suspendCancellableCoroutine] to establish a cancellation before registering CC anywhere. */ private fun setupCancellation() { if (checkCompleted()) return @@ -453,4 +454,3 @@ private class CompletedWithCancellation( ) { override fun toString(): String = "CompletedWithCancellation[$result]" } - diff --git a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt index b2cde6b978..035e2fb55d 100644 --- a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt @@ -30,7 +30,7 @@ class CancellableResumeTest : TestBase() { expected = { it is TestException } ) { expect(1) - val ok = suspendCancellableCoroutine { cont -> + suspendCancellableCoroutine { cont -> expect(2) cont.invokeOnCancellation { expect(3) } cont.cancel(TestException("FAIL")) @@ -44,6 +44,25 @@ class CancellableResumeTest : TestBase() { expectUnreached() } + @Test + fun testResumeImmediateAfterIndirectCancel() = runTest( + expected = { it is CancellationException } + ) { + expect(1) + val ctx = coroutineContext + suspendCancellableCoroutine { cont -> + expect(2) + cont.invokeOnCancellation { expect(3) } + ctx.cancel() + expect(4) + cont.resume("OK") { cause -> + expect(5) + } + finish(6) + } + expectUnreached() + } + @Test fun testResumeLaterNormally() = runTest { expect(1) diff --git a/kotlinx-coroutines-core/jvm/test/CancellableContinuationJvmTest.kt b/kotlinx-coroutines-core/jvm/test/CancellableContinuationJvmTest.kt index 4e25da96f5..f1a957adca 100644 --- a/kotlinx-coroutines-core/jvm/test/CancellableContinuationJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/CancellableContinuationJvmTest.kt @@ -20,4 +20,63 @@ class CancellableContinuationJvmTest : TestBase() { } suspend {}() // Eliminate tail-call optimization } + + @Test + fun testExceptionIsNotReported() = runTest({ it is CancellationException }) { + val ctx = coroutineContext + suspendCancellableCoroutine { + ctx.cancel() + it.resumeWith(Result.failure(TestException())) + } + } + + @Test + fun testBlockingIntegration() = runTest { + val source = BlockingSource() + val job = launch(Dispatchers.Default) { + source.await() + } + source.cancelAndJoin(job) + } + + @Test + fun testBlockingIntegrationAlreadyCancelled() = runTest { + val source = BlockingSource() + val job = launch(Dispatchers.Default) { + cancel() + source.await() + } + source.cancelAndJoin(job) + } + + private suspend fun BlockingSource.cancelAndJoin(job: Job) { + while (!hasSubscriber) { + Thread.sleep(10) + } + job.cancelAndJoin() + } + + private suspend fun BlockingSource.await() = suspendCancellableCoroutine { + it.invokeOnCancellation { this.cancel() } + subscribe() + } + + private class BlockingSource { + @Volatile + private var isCancelled = false + + @Volatile + public var hasSubscriber = false + + public fun subscribe() { + hasSubscriber = true + while (!isCancelled) { + Thread.sleep(10) + } + } + + public fun cancel() { + isCancelled = true + } + } } From 515e86aabb90864d373faf5aee4515eb2ac237f1 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Tue, 28 Apr 2020 11:30:04 +0300 Subject: [PATCH 026/257] Add a lincheck test for `remove` in the segment list algorithm (#1964) --- gradle.properties | 2 +- .../jvm/test/internal/SegmentBasedQueue.kt | 24 ++++++++-- .../SegmentListRemoveLCStressTest.kt | 45 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt diff --git a/gradle.properties b/gradle.properties index 233ad3697f..6545d79292 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junit_version=4.12 atomicfu_version=0.14.2 knit_version=0.1.3 html_version=0.6.8 -lincheck_version=2.6 +lincheck_version=2.7.1 dokka_version=0.9.16-rdev-2-mpp-hacks byte_buddy_version=1.10.9 reactor_vesion=3.2.5.RELEASE diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt index 5e408b74ac..a125bec25c 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentBasedQueue.kt @@ -87,8 +87,24 @@ internal class SegmentBasedQueue { } fun checkHeadPrevIsCleaned() { - check(head.value.prev === null) + check(head.value.prev === null) { "head.prev is not null"} } + + fun checkAllSegmentsAreNotLogicallyRemoved() { + var prev: OneElementSegment? = null + var cur = head.value + while (true) { + check(!cur.logicallyRemoved || cur.isTail) { + "This queue contains removed segments, memory leak detected" + } + check(cur.prev === prev) { + "Two neighbour segments are incorrectly linked: S.next.prev != S" + } + prev = cur + cur = cur.next ?: return + } + } + } private fun createSegment(id: Long, prev: OneElementSegment?) = OneElementSegment(id, prev, 0) @@ -98,9 +114,11 @@ internal class OneElementSegment(id: Long, prev: OneElementSegment?, point override val maxSlots get() = 1 + val logicallyRemoved get() = element.value === BROKEN + fun removeSegment() { - element.value = BROKEN - onSlotCleaned() + val old = element.getAndSet(BROKEN) + if (old !== BROKEN) onSlotCleaned() } } diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt new file mode 100644 index 0000000000..5daed99829 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused") + +package kotlinx.coroutines.linearizability + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import org.jetbrains.kotlinx.lincheck.annotations.* +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.paramgen.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.junit.* + + +class SegmentListRemoveLCStressTest : VerifierState() { + private val q = SegmentBasedQueue() + private val segments: Array> + + init { + segments = (0..5).map { q.enqueue(it)!! }.toTypedArray() + q.enqueue(6) + } + + @Operation + fun removeSegment(@Param(gen = IntGen::class, conf = "1:5") index: Int) { + segments[index].removeSegment() + } + + override fun extractState() = segments.map { it.logicallyRemoved } + + @Validate + fun checkAllRemoved() { + q.checkHeadPrevIsCleaned() + q.checkAllSegmentsAreNotLogicallyRemoved() + } + + @Test + fun test() = LCStressOptionsDefault() + .actorsBefore(0) + .actorsAfter(0) + .check(this::class) +} \ No newline at end of file From 5b00e48e3571ed0f882cd431d555bed11918cbcd Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 28 Apr 2020 12:13:53 +0300 Subject: [PATCH 027/257] Remove experimental status from Flow API (#1963) * Remove experimental annotation from Flow terminal and Channel operators * Remove experimental annotation from Flow count* and reduce* operators * Remove experimental annotation from Flow operations, including buffer and flowOn * Remove experimental annotation from combine and zip --- .../common/src/flow/Channels.kt | 3 --- .../common/src/flow/operators/Context.kt | 3 --- .../common/src/flow/operators/Distinct.kt | 3 --- .../common/src/flow/operators/Emitters.kt | 1 - .../common/src/flow/operators/Errors.kt | 3 --- .../common/src/flow/operators/Limit.kt | 5 ----- .../common/src/flow/operators/Transform.kt | 1 - .../common/src/flow/operators/Zip.kt | 15 --------------- .../common/src/flow/terminal/Collect.kt | 4 ---- .../common/src/flow/terminal/Count.kt | 2 -- .../common/src/flow/terminal/Reduce.kt | 2 -- 11 files changed, 42 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 130ffc72dc..2d3ef95aa1 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * This function provides a more efficient shorthand for `channel.consumeEach { value -> emit(value) }`. * See [consumeEach][ReceiveChannel.consumeEach]. */ -@ExperimentalCoroutinesApi // since version 1.3.0 public suspend fun FlowCollector.emitAll(channel: ReceiveChannel): Unit = emitAllImpl(channel, consume = true) @@ -84,7 +83,6 @@ private suspend fun FlowCollector.emitAllImpl(channel: ReceiveChannel, * In particular, [produceIn] returns the original channel. * Calls to [flowOn] have generally no effect, unless [buffer] is used to explicitly request buffering. */ -@ExperimentalCoroutinesApi // since version 1.4.0 public fun ReceiveChannel.receiveAsFlow(): Flow = ChannelAsFlow(this, consume = false) /** @@ -106,7 +104,6 @@ public fun ReceiveChannel.receiveAsFlow(): Flow = ChannelAsFlow(this, * In particular, [produceIn] returns the original channel (but throws [IllegalStateException] on repeated calls). * Calls to [flowOn] have generally no effect, unless [buffer] is used to explicitly request buffering. */ -@ExperimentalCoroutinesApi // since version 1.3.0 public fun ReceiveChannel.consumeAsFlow(): Flow = ChannelAsFlow(this, consume = true) /** diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 4e6167f0cd..5f184e8feb 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -104,7 +104,6 @@ import kotlin.jvm.* * [RENDEZVOUS][Channel.RENDEZVOUS], [UNLIMITED][Channel.UNLIMITED] or a non-negative value indicating * an explicitly requested size. */ -@ExperimentalCoroutinesApi public fun Flow.buffer(capacity: Int = BUFFERED): Flow { require(capacity >= 0 || capacity == BUFFERED || capacity == CONFLATED) { "Buffer size should be non-negative, BUFFERED, or CONFLATED, but was $capacity" @@ -147,7 +146,6 @@ public fun Flow.buffer(capacity: Int = BUFFERED): Flow { * always fused so that only one properly configured channel is used for execution. * **Conflation takes precedence over `buffer()` calls with any other capacity.** */ -@ExperimentalCoroutinesApi public fun Flow.conflate(): Flow = buffer(CONFLATED) /** @@ -194,7 +192,6 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * * @throws [IllegalArgumentException] if provided context contains [Job] instance. */ -@ExperimentalCoroutinesApi public fun Flow.flowOn(context: CoroutineContext): Flow { checkFlowContext(context) return when { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt index 53c4d6d2aa..6b8c61c1af 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -15,14 +15,12 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** * Returns flow where all subsequent repetitions of the same value are filtered out. */ -@ExperimentalCoroutinesApi public fun Flow.distinctUntilChanged(): Flow = distinctUntilChangedBy { it } /** * Returns flow where all subsequent repetitions of the same value are filtered out, when compared * with each other via the provided [areEquivalent] function. */ -@FlowPreview public fun Flow.distinctUntilChanged(areEquivalent: (old: T, new: T) -> Boolean): Flow = distinctUntilChangedBy(keySelector = { it }, areEquivalent = areEquivalent) @@ -30,7 +28,6 @@ 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. */ -@FlowPreview public fun Flow.distinctUntilChangedBy(keySelector: (T) -> K): Flow = distinctUntilChangedBy(keySelector = keySelector, areEquivalent = { old, new -> old == new }) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 93530234eb..d296517bdb 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -34,7 +34,6 @@ import kotlin.jvm.* * } * ``` */ -@ExperimentalCoroutinesApi public inline fun Flow.transform( @BuilderInference crossinline transform: suspend FlowCollector.(value: T) -> Unit ): Flow = flow { // Note: safe flow is used here, because collector is exposed to transform on each operation diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt index ad77ba9b14..c73ded9eea 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Errors.kt @@ -54,7 +54,6 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * retry an original flow use [retryWhen] operator that can retry the flow multiple times without * introducing ever-growing stack of suspending calls. */ -@ExperimentalCoroutinesApi // tentatively stable in 1.3.0 public fun Flow.catch(action: suspend FlowCollector.(cause: Throwable) -> Unit): Flow = flow { val exception = catchImpl(this) @@ -117,7 +116,6 @@ public fun Flow.onErrorCollect( * * @throws IllegalArgumentException when [retries] is not positive. */ -@ExperimentalCoroutinesApi // tentatively stable in 1.3.0 public fun Flow.retry( retries: Long = Long.MAX_VALUE, predicate: suspend (cause: Throwable) -> Boolean = { true } @@ -169,7 +167,6 @@ public fun Flow.retry( * * See [catch] for more details. */ -@ExperimentalCoroutinesApi // tentatively stable in 1.3.0 public fun Flow.retryWhen(predicate: suspend FlowCollector.(cause: Throwable, attempt: Long) -> Boolean): Flow = flow { var attempt = 0L diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index 988c22f489..d30a2db206 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.flow -import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* import kotlinx.coroutines.flow.internal.unsafeFlow as flow @@ -16,7 +15,6 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * Returns a flow that ignores first [count] elements. * Throws [IllegalArgumentException] if [count] is negative. */ -@ExperimentalCoroutinesApi public fun Flow.drop(count: Int): Flow { require(count >= 0) { "Drop count should be non-negative, but had $count" } return flow { @@ -30,7 +28,6 @@ public fun Flow.drop(count: Int): Flow { /** * Returns a flow containing all elements except first elements that satisfy the given predicate. */ -@ExperimentalCoroutinesApi public fun Flow.dropWhile(predicate: suspend (T) -> Boolean): Flow = flow { var matched = false collect { value -> @@ -48,7 +45,6 @@ public fun Flow.dropWhile(predicate: suspend (T) -> Boolean): Flow = f * When [count] elements are consumed, the original flow is cancelled. * Throws [IllegalArgumentException] if [count] is not positive. */ -@ExperimentalCoroutinesApi public fun Flow.take(count: Int): Flow { require(count > 0) { "Requested element count $count should be positive" } return flow { @@ -75,7 +71,6 @@ private suspend fun FlowCollector.emitAbort(value: T) { /** * Returns a flow that contains first elements satisfying the given [predicate]. */ -@ExperimentalCoroutinesApi public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = flow { try { collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index f446c80c23..fefa42f251 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -59,7 +59,6 @@ public inline fun Flow.mapNotNull(crossinline transform: suspend /** * Returns a flow that wraps each element into [IndexedValue], containing value and its index (starting from zero). */ -@ExperimentalCoroutinesApi public fun Flow.withIndex(): Flow> = flow { var index = 0 collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt index 03bddf8330..76f61b127d 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * This function is a shorthand for `flow.combineTransform(flow2) { a, b -> emit(transform(a, b)) } */ @JvmName("flowCombine") -@ExperimentalCoroutinesApi public fun Flow.combine(flow: Flow, transform: suspend (a: T1, b: T2) -> R): Flow = flow { combineTransformInternal(this@combine, flow) { a, b -> emit(transform(a, b)) @@ -52,7 +51,6 @@ public fun Flow.combine(flow: Flow, transform: suspend (a: T * * This function is a shorthand for `combineTransform(flow, flow2) { a, b -> emit(transform(a, b)) } */ -@ExperimentalCoroutinesApi public fun combine(flow: Flow, flow2: Flow, transform: suspend (a: T1, b: T2) -> R): Flow = flow.combine(flow2, transform) @@ -74,7 +72,6 @@ public fun combine(flow: Flow, flow2: Flow, transform: suspe * ``` */ @JvmName("flowCombineTransform") -@ExperimentalCoroutinesApi public fun Flow.combineTransform( flow: Flow, @BuilderInference transform: suspend FlowCollector.(a: T1, b: T2) -> Unit @@ -101,7 +98,6 @@ public fun Flow.combineTransform( * } * ``` */ -@ExperimentalCoroutinesApi public fun combineTransform( flow: Flow, flow2: Flow, @@ -117,7 +113,6 @@ public fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@ExperimentalCoroutinesApi public inline fun combine( flow: Flow, flow2: Flow, @@ -137,7 +132,6 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -@ExperimentalCoroutinesApi public inline fun combineTransform( flow: Flow, flow2: Flow, @@ -155,7 +149,6 @@ public inline fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@ExperimentalCoroutinesApi public inline fun combine( flow: Flow, flow2: Flow, @@ -177,7 +170,6 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -@ExperimentalCoroutinesApi public inline fun combineTransform( flow: Flow, flow2: Flow, @@ -197,7 +189,6 @@ public inline fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@ExperimentalCoroutinesApi public inline fun combine( flow: Flow, flow2: Flow, @@ -221,7 +212,6 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -@ExperimentalCoroutinesApi public inline fun combineTransform( flow: Flow, flow2: Flow, @@ -243,7 +233,6 @@ public inline fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@ExperimentalCoroutinesApi public inline fun combine( vararg flows: Flow, crossinline transform: suspend (Array) -> R @@ -257,7 +246,6 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -@ExperimentalCoroutinesApi public inline fun combineTransform( vararg flows: Flow, @BuilderInference crossinline transform: suspend FlowCollector.(Array) -> Unit @@ -269,7 +257,6 @@ public inline fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -@ExperimentalCoroutinesApi public inline fun combine( flows: Iterable>, crossinline transform: suspend (Array) -> R @@ -289,7 +276,6 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -@ExperimentalCoroutinesApi public inline fun combineTransform( flows: Iterable>, @BuilderInference crossinline transform: suspend FlowCollector.(Array) -> Unit @@ -313,5 +299,4 @@ public inline fun combineTransform( * } * ``` */ -@ExperimentalCoroutinesApi public fun Flow.zip(other: Flow, transform: suspend (T1, T2) -> R): Flow = zipImpl(this, other, transform) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index 9d463df073..e8f2a9a3b6 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -46,7 +46,6 @@ public suspend fun Flow<*>.collect(): Unit = collect(NopCollector) * * Note that resulting value of [launchIn] is not used the provided scope takes care of cancellation. */ -@ExperimentalCoroutinesApi // tentatively stable in 1.3.0 public fun Flow.launchIn(scope: CoroutineScope): Job = scope.launch { collect() // tail-call } @@ -80,7 +79,6 @@ public suspend inline fun Flow.collect(crossinline action: suspend (value * * See also [collect] and [withIndex]. */ -@ExperimentalCoroutinesApi public suspend inline fun Flow.collectIndexed(crossinline action: suspend (index: Int, value: T) -> Unit): Unit = collect(object : FlowCollector { private var index = 0 @@ -108,7 +106,6 @@ public suspend inline fun Flow.collectIndexed(crossinline action: suspend * * prints "Collecting 1, Collecting 2, 2 collected" */ -@ExperimentalCoroutinesApi public suspend fun Flow.collectLatest(action: suspend (value: T) -> Unit) { /* * Implementation note: @@ -131,5 +128,4 @@ public suspend fun Flow.collectLatest(action: suspend (value: T) -> Unit) * It is a shorthand for `flow.collect { value -> emit(value) }`. */ @BuilderInference -@ExperimentalCoroutinesApi public suspend inline fun FlowCollector.emitAll(flow: Flow): Unit = flow.collect(this) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt index 63cf52434b..d50c027268 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Count.kt @@ -13,7 +13,6 @@ import kotlin.jvm.* /** * Returns the number of elements in this flow. */ -@ExperimentalCoroutinesApi public suspend fun Flow.count(): Int { var i = 0 collect { @@ -26,7 +25,6 @@ public suspend fun Flow.count(): Int { /** * Returns the number of elements matching the given predicate. */ -@ExperimentalCoroutinesApi public suspend fun Flow.count(predicate: suspend (T) -> Boolean): Int { var i = 0 collect { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index 674f8322f2..98e665f601 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -17,7 +17,6 @@ import kotlin.jvm.* * Accumulates value starting with the first element and applying [operation] to current accumulator value and each element. * Throws [NoSuchElementException] if flow was empty. */ -@ExperimentalCoroutinesApi public suspend fun Flow.reduce(operation: suspend (accumulator: S, value: T) -> S): S { var accumulator: Any? = NULL @@ -38,7 +37,6 @@ public suspend fun Flow.reduce(operation: suspend (accumulator: S, /** * Accumulates value starting with [initial] value and applying [operation] current accumulator value and each element */ -@ExperimentalCoroutinesApi public suspend inline fun Flow.fold( initial: R, crossinline operation: suspend (acc: R, value: T) -> R From a8f4b6465fe6f4fb853f09acd238bd9f96f0efab Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Tue, 28 Apr 2020 14:30:02 +0300 Subject: [PATCH 028/257] Common java version method (#1965) --- build.gradle | 9 +++------ buildSrc/src/main/kotlin/JavaVersion.kt | 7 +++++++ ui/kotlinx-coroutines-javafx/build.gradle | 10 ++-------- 3 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 buildSrc/src/main/kotlin/JavaVersion.kt diff --git a/build.gradle b/build.gradle index c679109ca3..08a241a411 100644 --- a/build.gradle +++ b/build.gradle @@ -227,7 +227,7 @@ if (build_snapshot_train) { * but publishing plugin does not re-read artifact names for kotlin-jvm projects, so renaming is not applied in pom files * for JVM-only projects. * - * We artificially replace "project" dependency with "module" one to have proper names in pom files, but then substitute it + * We artificially replace "project" dependency with "module" one to have proper names in pom files, but then substitute it * to have out "project" dependency back. */ configure(subprojects.findAll { it.name != coreModule && it.name != rootModule }) { @@ -278,11 +278,8 @@ println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompi // --------------- Publish only from under JDK11+ --------------- task checkJdkForPublish { doFirst { - String javaVersion = System.properties["java.version"] - int i = javaVersion.indexOf('.') - int javaVersionMajor = (i < 0 ? javaVersion : javaVersion.substring(0, i)).toInteger() - if (javaVersionMajor < 11) { - throw new GradleException("Project can be build for publishing only under JDK 11+, but found ${javaVersion}") + if (JavaVersionKt.javaVersionMajor < 11) { + throw new GradleException("Project can be build for publishing only under JDK 11+, but found ${JavaVersionKt.javaVersion}") } } } diff --git a/buildSrc/src/main/kotlin/JavaVersion.kt b/buildSrc/src/main/kotlin/JavaVersion.kt new file mode 100644 index 0000000000..2fbefce5e3 --- /dev/null +++ b/buildSrc/src/main/kotlin/JavaVersion.kt @@ -0,0 +1,7 @@ +val javaVersion: String + get() = System.getProperty("java.version")!! + +val javaVersionMajor: Int + get() = javaVersion + .substringBefore(".") + .toInt() diff --git a/ui/kotlinx-coroutines-javafx/build.gradle b/ui/kotlinx-coroutines-javafx/build.gradle index 9d1c128239..daabda40ff 100644 --- a/ui/kotlinx-coroutines-javafx/build.gradle +++ b/ui/kotlinx-coroutines-javafx/build.gradle @@ -2,16 +2,10 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -static int javaVersionMajor() { - String javaVersion = System.properties["java.version"] - int i = javaVersion.indexOf('.') - return (i < 0 ? javaVersion : javaVersion.substring(0, i)).toInteger() -} - // JDK11+ does not bundle JavaFx and the plugin for JavaFx support is compiled with class file version 55.0 (JDK 11) -if (javaVersionMajor() >= 11) { +if (JavaVersionKt.javaVersionMajor >= 11) { apply plugin: 'org.openjfx.javafxplugin' - + javafx { version = javafx_version modules = ['javafx.controls'] From 76e6440eb42d3166267f1672b14eff0583806766 Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Fri, 17 Apr 2020 04:57:08 +0300 Subject: [PATCH 029/257] Gradle 6.3 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7068436015..23082f9a23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -6,4 +6,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip From 52135fbdc1379e5b9f9f53c2a3f6a45629ee5d2c Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Tue, 28 Apr 2020 21:40:55 +0300 Subject: [PATCH 030/257] Fix JS example warnings (#1968) --- js/example-frontend-js/src/ExampleMain.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt index f3e8c081b1..cbb6d07893 100644 --- a/js/example-frontend-js/src/ExampleMain.kt +++ b/js/example-frontend-js/src/ExampleMain.kt @@ -12,7 +12,7 @@ import kotlin.browser.* import kotlin.coroutines.* import kotlin.math.* -fun main(args: Array) { +fun main() { println("Starting example application...") document.addEventListener("DOMContentLoaded", { Application().start() @@ -89,7 +89,7 @@ class Application : CoroutineScope { val speed = 0.3 val rs = 20.0 val turnAfter = 5000.0 // seconds - var maxX = sw - rs + val maxX = sw - rs val maxY = sh - rs animation("rect", rs) { rect -> println("Started new 'rect' coroutine #$index") From d5766f34a6deb76ba12aaed476dd7e2ce3c3c8f5 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 29 Apr 2020 10:21:48 +0300 Subject: [PATCH 031/257] Revert "Gradle 6.3" This reverts commit 76e6440e --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23082f9a23..7068436015 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -6,4 +6,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip From be854550f781973b82df98f1381deb28c6e3fc87 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 29 Apr 2020 11:42:46 +0300 Subject: [PATCH 032/257] Fix class cast exception during undispatched resume of cancellable continuation with onCancellation block (#1967) Fixes #1966 --- .../common/src/internal/DispatchedTask.kt | 2 +- .../common/test/CancellableResumeTest.kt | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 9588d22b17..32258ba101 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -117,7 +117,7 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In // slow-path - use delegate val state = takeState() val exception = getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } - val result = if (exception != null) Result.failure(exception) else Result.success(state as T) + val result = if (exception != null) Result.failure(exception) else Result.success(getSuccessfulResult(state)) when (useMode) { MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) diff --git a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt index 035e2fb55d..39176a9a94 100644 --- a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt @@ -138,4 +138,16 @@ class CancellableResumeTest : TestBase() { yield() // to coroutine -- throws cancellation exception finish(9) } + + + @Test + fun testResumeUnconfined() = runTest { + val outerScope = this + withContext(Dispatchers.Unconfined) { + val result = suspendCancellableCoroutine { + outerScope.launch { it.resume("OK", {}) } + } + assertEquals("OK", result) + } + } } \ No newline at end of file From 8d6e464a3af47ae0b45a5c07acc4358ad8e3fd1f Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 29 Apr 2020 19:24:33 +0300 Subject: [PATCH 033/257] Support thread interrupting blocking functions (#1972) * Support thread interrupting blocking functions (#1947) This is the implementation of issue #1947 Signed-off-by: Trol Co-authored-by: Trol --- .../api/kotlinx-coroutines-core.api | 5 + .../jvm/src/Interruptible.kt | 164 ++++++++++++++++++ .../jvm/test/RunInterruptibleStressTest.kt | 58 +++++++ .../jvm/test/RunInterruptibleTest.kt | 63 +++++++ 4 files changed, 290 insertions(+) create mode 100644 kotlinx-coroutines-core/jvm/src/Interruptible.kt create mode 100644 kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/RunInterruptibleTest.kt diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index f908f964d4..0368cf1cc0 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -328,6 +328,11 @@ public final class kotlinx/coroutines/GlobalScope : kotlinx/coroutines/Coroutine public abstract interface annotation class kotlinx/coroutines/InternalCoroutinesApi : java/lang/annotation/Annotation { } +public final class kotlinx/coroutines/InterruptibleKt { + public static final fun runInterruptible (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun runInterruptible$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + public abstract interface class kotlinx/coroutines/Job : kotlin/coroutines/CoroutineContext$Element { public static final field Key Lkotlinx/coroutines/Job$Key; public abstract fun attachChild (Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; diff --git a/kotlinx-coroutines-core/jvm/src/Interruptible.kt b/kotlinx-coroutines-core/jvm/src/Interruptible.kt new file mode 100644 index 0000000000..a4144d87d6 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/Interruptible.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.atomicfu.* +import kotlin.coroutines.* + +/** + * Calls the specified [block] with a given coroutine context in a interruptible manner. + * The blocking code block will be interrupted and this function will throw [CancellationException] + * if the coroutine is cancelled. + * The specified [coroutineContext] directly translates into [withContext] argument. + * + * Example: + * ``` + * val blockingJob = launch { + * // This function will throw CancellationException + * runInterruptible(Dispatchers.IO) { + * // This blocking procedure will be interrupted when this coroutine is canceled + * doSomethingElseUsefulInterruptible() + * } + * } + * + * delay(500L) + * blockingJob.cancel() // Interrupt blocking call + * } + * ``` + * + * There is also an optional context parameter to this function to enable single-call conversion of + * interruptible Java methods into suspending functions like this: + * ``` + * // With one call here we are moving the call to Dispatchers.IO and supporting interruption. + * suspend fun BlockingQueue.awaitTake(): T = + * runInterruptible(Dispatchers.IO) { queue.take() } + * ``` + */ +public suspend fun runInterruptible( + context: CoroutineContext = EmptyCoroutineContext, + block: () -> T +): T = withContext(context) { + runInterruptibleInExpectedContext(block) +} + +private suspend fun runInterruptibleInExpectedContext(block: () -> T): T { + try { + // No job -> no cancellation + val job = coroutineContext[Job] ?: return block() + val threadState = ThreadState(job) + try { + return block() + } finally { + threadState.clearInterrupt() + } + } catch (e: InterruptedException) { + throw CancellationException("Blocking call was interrupted due to parent cancellation").initCause(e) + } +} + +private const val WORKING = 0 +private const val FINISHED = 1 +private const val INTERRUPTING = 2 +private const val INTERRUPTED = 3 + +private class ThreadState : CompletionHandler { + /* + === States === + + WORKING: running normally + FINISH: complete normally + INTERRUPTING: canceled, going to interrupt this thread + INTERRUPTED: this thread is interrupted + + === Possible Transitions === + + +----------------+ register job +-------------------------+ + | WORKING | cancellation listener | WORKING | + | (thread, null) | -------------------------> | (thread, cancel handle) | + +----------------+ +-------------------------+ + | | | + | cancel cancel | | complete + | | | + V | | + +---------------+ | | + | INTERRUPTING | <--------------------------------------+ | + +---------------+ | + | | + | interrupt | + | | + V V + +---------------+ +-------------------------+ + | INTERRUPTED | | FINISHED | + +---------------+ +-------------------------+ + */ + private val state: AtomicRef = atomic(State(WORKING, null)) + private val targetThread = Thread.currentThread() + + private data class State(@JvmField val state: Int, @JvmField val cancelHandle: DisposableHandle?) + + // We're using a non-primary constructor instead of init block of a primary constructor here, because + // we need to `return`. + constructor(job: Job) { + // Register cancellation handler + val cancelHandle = + job.invokeOnCompletion(onCancelling = true, invokeImmediately = true, handler = this) + // Either we successfully stored it or it was immediately cancelled + state.loop { s -> + when (s.state) { + // Happy-path, move forward + WORKING -> if (state.compareAndSet(s, State(WORKING, cancelHandle))) return + // Immediately cancelled, just continue + INTERRUPTING, INTERRUPTED -> return + else -> throw IllegalStateException("Illegal state $s") + } + } + } + + fun clearInterrupt() { + /* + * Do not allow to untriggered interrupt to leak + */ + state.loop { s -> + when (s.state) { + WORKING -> if (state.compareAndSet(s, State(FINISHED, null))) { + s.cancelHandle?.dispose() + return + } + INTERRUPTING -> { + /* + * Spin, cancellation mechanism is interrupting our thread right now + * and we have to wait it and then clear interrupt status + */ + } + INTERRUPTED -> { + // Clear it and bail out + Thread.interrupted(); + return + } + else -> error("Illegal state: $s") + } + } + } + + // Cancellation handler + override fun invoke(cause: Throwable?) { + state.loop { s -> + when (s.state) { + // Working -> try to transite state and interrupt the thread + WORKING -> { + if (state.compareAndSet(s, State(INTERRUPTING, null))) { + targetThread.interrupt() + state.value = State(INTERRUPTED, null) + return + } + } + // Finished -- runInterruptible is already complete + FINISHED -> return + INTERRUPTING, INTERRUPTED -> return + else -> error("Illegal state: $s") + } + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt b/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt new file mode 100644 index 0000000000..03c7c6ecb8 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import java.util.concurrent.atomic.* +import kotlin.test.* + +class RunInterruptibleStressTest : TestBase() { + + private val dispatcher = Dispatchers.IO + private val REPEAT_TIMES = 1000 * stressTestMultiplier + + @Test + fun testStress() = runBlocking { + val interruptLeak = AtomicBoolean(false) + val enterCount = AtomicInteger(0) + val interruptedCount = AtomicInteger(0) + val otherExceptionCount = AtomicInteger(0) + + repeat(REPEAT_TIMES) { repeat -> + val job = launch(dispatcher, start = CoroutineStart.LAZY) { + try { + runInterruptible { + enterCount.incrementAndGet() + try { + Thread.sleep(Long.MAX_VALUE) + } catch (e: InterruptedException) { + interruptedCount.incrementAndGet() + throw e + } + } + } catch (e: CancellationException) { + } catch (e: Throwable) { + otherExceptionCount.incrementAndGet() + } finally { + interruptLeak.set(interruptLeak.get() || Thread.currentThread().isInterrupted) + } + } + + val cancelJob = launch(dispatcher, start = CoroutineStart.LAZY) { + job.cancel() + } + + job.start() + val canceller = launch(dispatcher) { + cancelJob.start() + } + + joinAll(job, cancelJob, canceller) + } + + assertFalse(interruptLeak.get()) + assertEquals(enterCount.get(), interruptedCount.get()) + assertEquals(0, otherExceptionCount.get()) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/RunInterruptibleTest.kt b/kotlinx-coroutines-core/jvm/test/RunInterruptibleTest.kt new file mode 100644 index 0000000000..e755b17d91 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/RunInterruptibleTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.channels.* +import java.io.* +import java.util.concurrent.* +import java.util.concurrent.atomic.* +import kotlin.test.* + +class RunInterruptibleTest : TestBase() { + + @Test + fun testNormalRun() = runTest { + val result = runInterruptible { + val x = 1 + val y = 2 + Thread.sleep(1) + x + y + } + assertEquals(3, result) + } + + @Test + fun testExceptionalRun() = runTest { + try { + runInterruptible { + expect(1) + throw TestException() + } + } catch (e: TestException) { + finish(2) + } + } + + @Test + fun testInterrupt() = runTest { + val latch = Channel(1) + val job = launch { + runInterruptible(Dispatchers.IO) { + expect(2) + latch.offer(Unit) + try { + Thread.sleep(10_000L) + expectUnreached() + } catch (e: InterruptedException) { + expect(4) + assertFalse { Thread.currentThread().isInterrupted } + } + } + } + + launch(start = CoroutineStart.UNDISPATCHED) { + expect(1) + latch.receive() + expect(3) + job.cancelAndJoin() + }.join() + finish(5) + } +} From 0f15812be017fac06e017a4008ba1cae0152c509 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 29 Apr 2020 23:35:41 +0300 Subject: [PATCH 034/257] Kts transition (#1951) * Kotlin DSL - 'android:example-app' (#1940) * 'android:example-app'. Gradle 5.6.1 - sync with root project * Kotlin DSL - 'android:example-app' * Kotlin DSL - 'android:animation-app' (#1941) * 'android:animation-app'. Gradle 5.6.1 - sync with root project * Kotlin DSL - 'android:animation-app' * Kotlin DSL - 'swing' (#1943) * Kotlin DSL - 'integration:jdk8' (#1944) * Kotlin DSL - 'test' (#1945) * Kotlin DSL - 'reactive:reactive' (#1946) * Kotlin DSL - 'android-unit-tests' * Use relative paths * Kotlin DSL - 'js-stub' (#1938) (#1959) * Kotlin DSL - 'android' (#1938) (#1956) * Kotlin DSL - 'stdlib-stubs' (#1938) (#1955) * Kotlin DSL - 'site' (#1938) (#1954) * Kotlin DSL - 'maven-central' (#1938) (#1953) * Kotlin DSL - 'benchmarks' (#1938) (#1952) Co-authored-by: Victor Turansky --- benchmarks/build.gradle | 79 ------------- benchmarks/build.gradle.kts | 85 ++++++++++++++ buildSrc/src/main/kotlin/MavenCentral.kt | 42 +++++++ gradle/maven-central.gradle | 37 ------ gradle/publish-bintray.gradle | 4 +- .../{build.gradle => build.gradle.kts} | 0 .../js-stub/build.gradle.kts | 4 +- kotlinx-coroutines-test/build.gradle | 3 - kotlinx-coroutines-test/build.gradle.kts | 3 + .../kotlinx-coroutines-reactive/build.gradle | 34 ------ .../build.gradle.kts | 43 +++++++ site/build.gradle | 43 ------- site/build.gradle.kts | 62 ++++++++++ .../build.gradle.kts | 4 +- .../android-unit-tests/build.gradle | 10 -- .../android-unit-tests/build.gradle.kts | 10 ++ .../animation-app/app/build.gradle | 33 ------ .../animation-app/app/build.gradle.kts | 35 ++++++ .../build.gradle.kts} | 8 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58694 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 +- .../animation-app/gradlew | 51 ++++---- .../animation-app/gradlew.bat | 21 +++- .../{settings.gradle => settings.gradle.kts} | 2 +- ui/kotlinx-coroutines-android/build.gradle | 106 ----------------- .../build.gradle.kts | 108 +++++++++++++++++ .../example-app/app/build.gradle | 32 ----- .../example-app/app/build.gradle.kts | 34 ++++++ .../build.gradle.kts} | 8 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58694 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 +- .../example-app/gradlew | 109 +++++++++++------- .../example-app/gradlew.bat | 33 ++++-- .../example-app/settings.gradle | 1 - .../example-app/settings.gradle.kts | 1 + .../{build.gradle => build.gradle.kts} | 2 +- 36 files changed, 582 insertions(+), 477 deletions(-) delete mode 100644 benchmarks/build.gradle create mode 100644 benchmarks/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/MavenCentral.kt delete mode 100644 gradle/maven-central.gradle rename integration/kotlinx-coroutines-jdk8/{build.gradle => build.gradle.kts} (100%) rename stdlib-stubs/build.gradle => js/js-stub/build.gradle.kts (65%) delete mode 100644 kotlinx-coroutines-test/build.gradle create mode 100644 kotlinx-coroutines-test/build.gradle.kts delete mode 100644 reactive/kotlinx-coroutines-reactive/build.gradle create mode 100644 reactive/kotlinx-coroutines-reactive/build.gradle.kts delete mode 100644 site/build.gradle create mode 100644 site/build.gradle.kts rename js/js-stub/build.gradle => stdlib-stubs/build.gradle.kts (65%) delete mode 100644 ui/kotlinx-coroutines-android/android-unit-tests/build.gradle create mode 100644 ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/build.gradle create mode 100644 ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts rename ui/kotlinx-coroutines-android/{example-app/build.gradle => animation-app/build.gradle.kts} (72%) mode change 100644 => 100755 ui/kotlinx-coroutines-android/animation-app/gradlew rename ui/kotlinx-coroutines-android/animation-app/{settings.gradle => settings.gradle.kts} (87%) delete mode 100644 ui/kotlinx-coroutines-android/build.gradle create mode 100644 ui/kotlinx-coroutines-android/build.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/build.gradle create mode 100644 ui/kotlinx-coroutines-android/example-app/app/build.gradle.kts rename ui/kotlinx-coroutines-android/{animation-app/build.gradle => example-app/build.gradle.kts} (72%) mode change 100644 => 100755 ui/kotlinx-coroutines-android/example-app/gradlew delete mode 100644 ui/kotlinx-coroutines-android/example-app/settings.gradle create mode 100644 ui/kotlinx-coroutines-android/example-app/settings.gradle.kts rename ui/kotlinx-coroutines-swing/{build.gradle => build.gradle.kts} (70%) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle deleted file mode 100644 index a192f2795e..0000000000 --- a/benchmarks/build.gradle +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -apply plugin: "net.ltgt.apt" -apply plugin: "com.github.johnrengelman.shadow" -apply plugin: "me.champeau.gradle.jmh" - -repositories { - maven { url "https://repo.typesafe.com/typesafe/releases/" } -} - -compileJmhKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs += ['-Xjvm-default=enable'] - } -} - -/* - * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, - * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. - */ -task removeRedundantFiles(type: Delete) { - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$2\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$2\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$2\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$2\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble//SaneFlowPlaysScrabble\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class" - - // Primes - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$2\$1.class" - delete "$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$1.class" -} - -jmhRunBytecodeGenerator.dependsOn(removeRedundantFiles) - -// It is better to use the following to run benchmarks, otherwise you may get unexpected errors: -// ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark" -jmh { - jmhVersion = '1.21' - duplicateClassesStrategy DuplicatesStrategy.INCLUDE - failOnError = true - resultFormat = 'CSV' - if (project.hasProperty('jmh')) { - include = ".*" + project.jmh + ".*" - } -// includeTests = false -} - -jmhJar { - baseName 'benchmarks' - classifier = null - version = null - destinationDir = file("$rootDir") -} - -dependencies { - compile "org.openjdk.jmh:jmh-core:1.21" - compile "io.projectreactor:reactor-core:$reactor_vesion" - compile 'io.reactivex.rxjava2:rxjava:2.1.9' - compile "com.github.akarnokd:rxjava2-extensions:0.20.8" - - compile "org.openjdk.jmh:jmh-core:1.21" - compile 'com.typesafe.akka:akka-actor_2.12:2.5.0' - compile project(':kotlinx-coroutines-core') - // add jmh dependency on main - jmh sourceSets.main.runtimeClasspath -} diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts new file mode 100644 index 0000000000..0841129148 --- /dev/null +++ b/benchmarks/build.gradle.kts @@ -0,0 +1,85 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("net.ltgt.apt") + id("com.github.johnrengelman.shadow") + id("me.champeau.gradle.jmh") +} + +repositories { + maven("https://repo.typesafe.com/typesafe/releases/") +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = "1.8" +} + +tasks.compileJmhKotlin { + kotlinOptions.freeCompilerArgs += "-Xjvm-default=enable" +} + +/* + * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, + * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. + */ +val removeRedundantFiles = tasks.register("removeRedundantFiles") { + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$2\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$2\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$2\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$2\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble//SaneFlowPlaysScrabble\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") + + // Primes + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$2\$1.class") + delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$1.class") +} + +tasks.jmhRunBytecodeGenerator { + dependsOn(removeRedundantFiles) +} + +// It is better to use the following to run benchmarks, otherwise you may get unexpected errors: +// ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark" +jmh { + jmhVersion = "1.21" + duplicateClassesStrategy = DuplicatesStrategy.INCLUDE + failOnError = true + resultFormat = "CSV" + project.findProperty("jmh")?.also { + include = listOf(".*$it.*") + } +// includeTests = false +} + +tasks.jmhJar { + baseName = "benchmarks" + classifier = null + version = null + destinationDir = file("$rootDir") +} + +dependencies { + compile("org.openjdk.jmh:jmh-core:1.21") + compile("io.projectreactor:reactor-core:${property("reactor_vesion")}") + compile("io.reactivex.rxjava2:rxjava:2.1.9") + compile("com.github.akarnokd:rxjava2-extensions:0.20.8") + + compile("org.openjdk.jmh:jmh-core:1.21") + compile("com.typesafe.akka:akka-actor_2.12:2.5.0") + compile(project(":kotlinx-coroutines-core")) + + // add jmh dependency on main + jmhImplementation(sourceSets.main.get().runtimeClasspath) +} diff --git a/buildSrc/src/main/kotlin/MavenCentral.kt b/buildSrc/src/main/kotlin/MavenCentral.kt new file mode 100644 index 0000000000..0d7e18cf15 --- /dev/null +++ b/buildSrc/src/main/kotlin/MavenCentral.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. + */ + +@file:Suppress("UnstableApiUsage") + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.publish.maven.MavenPom + +// --------------- pom configuration --------------- + +fun MavenPom.configureMavenCentralMetadata(project: Project) { + name by project.name + description by "Coroutines support libraries for Kotlin" + url by "https://github.com/Kotlin/kotlinx.coroutines" + + licenses { + license { + name by "The Apache Software License, Version 2.0" + url by "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution by "repo" + } + } + + developers { + developer { + id by "JetBrains" + name by "JetBrains Team" + organization by "JetBrains" + organizationUrl by "https://www.jetbrains.com" + } + } + + scm { + url by "https://github.com/Kotlin/kotlinx.coroutines" + } +} + +private infix fun Property.by(value: T) { + set(value) +} diff --git a/gradle/maven-central.gradle b/gradle/maven-central.gradle deleted file mode 100644 index eef7993921..0000000000 --- a/gradle/maven-central.gradle +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// --------------- pom configuration --------------- - -def pomConfig = { - licenses { - license { - name "The Apache Software License, Version 2.0" - url "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution "repo" - } - } - developers { - developer { - id "JetBrains" - name "JetBrains Team" - organization "JetBrains" - organizationUrl "https://www.jetbrains.com" - } - } - - scm { - url "https://github.com/Kotlin/kotlinx.coroutines" - } -} - -project.ext.configureMavenCentralMetadata = { - def root = it.asNode() - // NOTE: Don't try to move top-level things (especially "description") to the pomConfig block - // because they would resolve incorrectly to top-level project properties in Gradle/Groovy - root.appendNode('name', project.name) - root.appendNode('description', 'Coroutines support libraries for Kotlin') - root.appendNode('url', 'https://github.com/Kotlin/kotlinx.coroutines') - root.children().last() + pomConfig -} diff --git a/gradle/publish-bintray.gradle b/gradle/publish-bintray.gradle index d4bbe69b64..ee9337f8c8 100644 --- a/gradle/publish-bintray.gradle +++ b/gradle/publish-bintray.gradle @@ -7,8 +7,6 @@ apply plugin: 'maven' apply plugin: 'maven-publish' -apply from: project.rootProject.file('gradle/maven-central.gradle') - // ------------- tasks def isMultiplatform = project.name == "kotlinx-coroutines-core" @@ -64,7 +62,7 @@ publishing { } publications.all { - pom.withXml(configureMavenCentralMetadata) + MavenCentralKt.configureMavenCentralMetadata(pom, project) // add empty javadocs (no need for MPP root publication which publishes only pom file) if (it.name != 'kotlinMultiplatform' && !isBom) { diff --git a/integration/kotlinx-coroutines-jdk8/build.gradle b/integration/kotlinx-coroutines-jdk8/build.gradle.kts similarity index 100% rename from integration/kotlinx-coroutines-jdk8/build.gradle rename to integration/kotlinx-coroutines-jdk8/build.gradle.kts diff --git a/stdlib-stubs/build.gradle b/js/js-stub/build.gradle.kts similarity index 65% rename from stdlib-stubs/build.gradle rename to js/js-stub/build.gradle.kts index b2ca0398d9..6b9d65555a 100644 --- a/stdlib-stubs/build.gradle +++ b/js/js-stub/build.gradle.kts @@ -2,7 +2,9 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -compileKotlin { +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +tasks.named("compileKotlin") { kotlinOptions { freeCompilerArgs += "-Xallow-kotlin-package" } diff --git a/kotlinx-coroutines-test/build.gradle b/kotlinx-coroutines-test/build.gradle deleted file mode 100644 index e13946fbb4..0000000000 --- a/kotlinx-coroutines-test/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -dependencies { - implementation project(":kotlinx-coroutines-debug") -} diff --git a/kotlinx-coroutines-test/build.gradle.kts b/kotlinx-coroutines-test/build.gradle.kts new file mode 100644 index 0000000000..825c95f4c8 --- /dev/null +++ b/kotlinx-coroutines-test/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + implementation(project(":kotlinx-coroutines-debug")) +} diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle b/reactive/kotlinx-coroutines-reactive/build.gradle deleted file mode 100644 index ad97c63f45..0000000000 --- a/reactive/kotlinx-coroutines-reactive/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile "org.reactivestreams:reactive-streams:$reactive_streams_version" - testCompile "org.reactivestreams:reactive-streams-tck:$reactive_streams_version" -} - -task testNG(type: Test) { - useTestNG() - reports.html.destination = file("$buildDir/reports/testng") - include '**/*ReactiveStreamTckTest.*' - // Skip testNG when tests are filtered with --tests, otherwise it simply fails - onlyIf { - filter.includePatterns.isEmpty() - } - doFirst { - // Classic gradle, nothing works without doFirst - println "TestNG tests: ($includes)" - } -} - -test { - dependsOn(testNG) - reports.html.destination = file("$buildDir/reports/junit") -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://www.reactive-streams.org/reactive-streams-$reactive_streams_version-javadoc/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts new file mode 100644 index 0000000000..c69148fecf --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import org.jetbrains.dokka.gradle.DokkaTask +import java.net.URL + +val reactiveStreamsVersion = property("reactive_streams_version") + +dependencies { + compile("org.reactivestreams:reactive-streams:$reactiveStreamsVersion") + testCompile("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion") +} + +tasks { + val testNG = register("testNG") { + useTestNG() + reports.html.destination = file("$buildDir/reports/testng") + include("**/*ReactiveStreamTckTest.*") + // Skip testNG when tests are filtered with --tests, otherwise it simply fails + onlyIf { + filter.includePatterns.isEmpty() + } + doFirst { + // Classic gradle, nothing works without doFirst + println("TestNG tests: ($includes)") + } + } + + named("test") { + reports.html.destination = file("$buildDir/reports/junit") + + dependsOn(testNG) + } + + withType().configureEach { + externalDocumentationLink(delegateClosureOf { + url = URL("https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + }) + } +} diff --git a/site/build.gradle b/site/build.gradle deleted file mode 100644 index 6b2c5b7916..0000000000 --- a/site/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -def buildDocsDir = "$buildDir/docs" -def jekyllDockerImage = "jekyll/jekyll:$jekyll_version" - -task copyDocs(type: Copy, dependsOn: rootProject.getTasksByName("dokka", true)) { - from (rootProject.getTasksByName("dokka", true).collect { "$it.project.buildDir/dokka" }) { - include "**/*.md" - include "**/package-list" - } - from "docs" - into buildDocsDir -} - -task copyExampleFrontendJs(type: Copy, dependsOn: ':example-frontend-js:bundle') { - def srcBuildDir = project(':example-frontend-js').buildDir - from "$srcBuildDir/dist" - into "$buildDocsDir/example-frontend-js" -} - -task site(type: Exec, description: 'Generate github pages', dependsOn: [copyDocs, copyExampleFrontendJs]) { - inputs.files(fileTree(buildDocsDir)) - outputs.dir("$buildDir/dist") - workingDir file(buildDocsDir) - commandLine 'docker', 'run', '--rm', "--volume=$buildDir:/srv/jekyll", - '-t', jekyllDockerImage, - '/bin/bash', '-c', 'cd docs; jekyll build' -} - -// A useful task for local debugging -- serves a site locally -task serve(type: Exec, dependsOn: [copyDocs, copyExampleFrontendJs]) { - commandLine 'docker', 'run', '--rm', "--volume=$buildDir:/srv/jekyll", - '-p', '8080:8080', - '-t', jekyllDockerImage, - '/bin/bash', '-c', 'cd docs; jekyll serve --host 0.0.0.0 --port 8080' -} - -task clean(type: Delete) { - delete buildDir -} - diff --git a/site/build.gradle.kts b/site/build.gradle.kts new file mode 100644 index 0000000000..450589729e --- /dev/null +++ b/site/build.gradle.kts @@ -0,0 +1,62 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +val buildDocsDir = "$buildDir/docs" +val jekyllDockerImage = "jekyll/jekyll:${version("jekyll")}" + +val copyDocs = tasks.register("copyDocs") { + val dokkaTasks = rootProject.getTasksByName("dokka", true) + + from(dokkaTasks.map { "${it.project.buildDir}/dokka" }) { + include("**/*.md") + include("**/package-list") + } + from("docs") + into(buildDocsDir) + + dependsOn(dokkaTasks) +} + +val copyExampleFrontendJs = tasks.register("copyExampleFrontendJs") { + val srcBuildDir = project(":example-frontend-js").buildDir + from("$srcBuildDir/dist") + into("$buildDocsDir/example-frontend-js") + + dependsOn(":example-frontend-js:bundle") +} + +tasks.register("site") { + description = "Generate github pages" + + inputs.files(fileTree(buildDocsDir)) + outputs.dir("$buildDir/dist") + workingDir = file(buildDocsDir) + + commandLine( + "docker", "run", "--rm", "--volume=$buildDir:/srv/jekyll", + "-t", jekyllDockerImage, + "/bin/bash", "-c", "cd docs; jekyll build" + ) + + dependsOn(copyDocs) + dependsOn(copyExampleFrontendJs) +} + +// A useful task for local debugging -- serves a site locally +tasks.register("serve") { + commandLine( + "docker", "run", "--rm", "--volume=$buildDir:/srv/jekyll", + "-p", "8080:8080", + "-t", jekyllDockerImage, + "/bin/bash", "-c", "cd docs; jekyll serve --host 0.0.0.0 --port 8080" + ) + + dependsOn(copyDocs) + dependsOn(copyExampleFrontendJs) +} + +tasks.register("clean") { + delete(buildDir) +} + diff --git a/js/js-stub/build.gradle b/stdlib-stubs/build.gradle.kts similarity index 65% rename from js/js-stub/build.gradle rename to stdlib-stubs/build.gradle.kts index b2ca0398d9..6b9d65555a 100644 --- a/js/js-stub/build.gradle +++ b/stdlib-stubs/build.gradle.kts @@ -2,7 +2,9 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -compileKotlin { +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +tasks.named("compileKotlin") { kotlinOptions { freeCompilerArgs += "-Xallow-kotlin-package" } diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle deleted file mode 100644 index 99759b254c..0000000000 --- a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - testImplementation "com.google.android:android:$android_version" - testImplementation "org.robolectric:robolectric:$robolectric_version" - testImplementation project(":kotlinx-coroutines-test") - testImplementation project(":kotlinx-coroutines-android") -} diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts new file mode 100644 index 0000000000..e4b0dbf840 --- /dev/null +++ b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + testImplementation("com.google.android:android:${property("android_version")}") + testImplementation("org.robolectric:robolectric:${property("robolectric_version")}") + testImplementation(project(":kotlinx-coroutines-test")) + testImplementation(project(":kotlinx-coroutines-android")) +} diff --git a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle b/ui/kotlinx-coroutines-android/animation-app/app/build.gradle deleted file mode 100644 index 44f4a5f589..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' - -android { - compileSdkVersion 29 - defaultConfig { - applicationId "org.jetbrains.kotlinx.animation" - minSdkVersion 14 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.google.android.material:material:1.0.0' - implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} diff --git a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts b/ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts new file mode 100644 index 0000000000..517f1f6341 --- /dev/null +++ b/ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("com.android.application") + kotlin("android") + kotlin("android.extensions") +} + +android { + compileSdkVersion = "29" + defaultConfig { + applicationId = "org.jetbrains.kotlinx.animation" + minSdkVersion(14) + targetSdkVersion(29) + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.0.2") + implementation("androidx.constraintlayout:constraintlayout:1.1.3") + implementation("com.google.android.material:material:1.0.0") + implementation("androidx.lifecycle:lifecycle-extensions:2.0.0") + + implementation(kotlin("stdlib-jdk7")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${property("coroutines_version")}") + + testImplementation("junit:junit:4.12") + androidTestImplementation("androidx.test:runner:1.2.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0") +} diff --git a/ui/kotlinx-coroutines-android/example-app/build.gradle b/ui/kotlinx-coroutines-android/animation-app/build.gradle.kts similarity index 72% rename from ui/kotlinx-coroutines-android/example-app/build.gradle rename to ui/kotlinx-coroutines-android/animation-app/build.gradle.kts index d98ab8cfe7..9cd0c592df 100644 --- a/ui/kotlinx-coroutines-android/example-app/build.gradle +++ b/ui/kotlinx-coroutines-android/animation-app/build.gradle.kts @@ -10,8 +10,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("com.android.tools.build:gradle:3.5.0") + classpath(kotlin("gradle-plugin", property("kotlin_version") as String)) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -25,6 +25,6 @@ allprojects { } } -task clean(type: Delete) { - delete rootProject.buildDir +task("clean") { + delete(rootProject.buildDir) } diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.jar b/ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.jar index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch literal 58694 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 literal 0 HcmV?d00001 diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.properties b/ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.properties index dfd98a959a..dbe85eef28 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,5 @@ -# -# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. -# - distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/ui/kotlinx-coroutines-android/animation-app/gradlew b/ui/kotlinx-coroutines-android/animation-app/gradlew old mode 100644 new mode 100755 index cccdd3d517..2fe81a7d95 --- a/ui/kotlinx-coroutines-android/animation-app/gradlew +++ b/ui/kotlinx-coroutines-android/animation-app/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -138,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/ui/kotlinx-coroutines-android/animation-app/gradlew.bat b/ui/kotlinx-coroutines-android/animation-app/gradlew.bat index f9553162f1..62bd9b9cce 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradlew.bat +++ b/ui/kotlinx-coroutines-android/animation-app/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome diff --git a/ui/kotlinx-coroutines-android/animation-app/settings.gradle b/ui/kotlinx-coroutines-android/animation-app/settings.gradle.kts similarity index 87% rename from ui/kotlinx-coroutines-android/animation-app/settings.gradle rename to ui/kotlinx-coroutines-android/animation-app/settings.gradle.kts index 0087705ce7..b05d810bc0 100644 --- a/ui/kotlinx-coroutines-android/animation-app/settings.gradle +++ b/ui/kotlinx-coroutines-android/animation-app/settings.gradle.kts @@ -2,4 +2,4 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -include ':app' +include(":app") diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle deleted file mode 100644 index 6f66e6c5d9..0000000000 --- a/ui/kotlinx-coroutines-android/build.gradle +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -repositories { - google() -} - -configurations { - r8 -} - -dependencies { - compileOnly "com.google.android:android:$android_version" - compileOnly "androidx.annotation:annotation:$androidx_annotation_version" - - testImplementation "com.google.android:android:$android_version" - testImplementation "org.robolectric:robolectric:$robolectric_version" - testImplementation "org.smali:baksmali:$baksmali_version" - - r8 'com.android.tools.build:builder:4.0.0-alpha06' // Contains r8-2.0.4-dev -} - -class RunR8Task extends JavaExec { - - @OutputDirectory - File outputDex - - @InputFile - File inputConfig - - @InputFile - final File inputConfigCommon = new File('testdata/r8-test-common.pro') - - @InputFiles - final File jarFile = project.jar.archivePath - - @Override - Task configure(Closure closure) { - super.configure(closure) - classpath = project.configurations.r8 - main = 'com.android.tools.r8.R8' - return this - } - - @Override - void exec() { - // Resolve classpath only during execution - def arguments = [ - '--release', - '--no-desugaring', - '--output', outputDex.absolutePath, - '--pg-conf', inputConfig.absolutePath - ] - arguments.addAll(project.configurations.runtimeClasspath.files.collect { it.absolutePath }) - arguments.addAll(jarFile.absolutePath) - - args = arguments - - if (outputDex.exists()) { - outputDex.deleteDir() - } - outputDex.mkdirs() - - super.exec() - } -} - -def optimizedDexDir = new File(buildDir, "dex-optim/") -def unOptimizedDexDir = new File(buildDir, "dex-unoptim/") - -def optimizedDexFile = new File(optimizedDexDir, "classes.dex") -def unOptimizedDexFile = new File(unOptimizedDexDir, "classes.dex") - -task runR8(type: RunR8Task, dependsOn: 'jar'){ - outputDex = optimizedDexDir - inputConfig = file('testdata/r8-test-rules.pro') -} - -task runR8NoOptim(type: RunR8Task, dependsOn: 'jar') { - outputDex = unOptimizedDexDir - inputConfig = file('testdata/r8-test-rules-no-optim.pro') -} - -test { - // Ensure the R8-processed dex is built and supply its path as a property to the test. - dependsOn(runR8) - dependsOn(runR8NoOptim) - - inputs.files(optimizedDexFile, unOptimizedDexFile) - - systemProperty 'dexPath', optimizedDexFile.absolutePath - systemProperty 'noOptimDexPath', unOptimizedDexFile.absolutePath - - // Output custom metric with the size of the optimized dex - doLast { - println("##teamcity[buildStatisticValue key='optimizedDexSize' value='${optimizedDexFile.length()}']") - } -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://developer.android.com/reference/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts new file mode 100644 index 0000000000..4be32fc5c6 --- /dev/null +++ b/ui/kotlinx-coroutines-android/build.gradle.kts @@ -0,0 +1,108 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import org.jetbrains.dokka.gradle.DokkaTask +import java.net.URL + +repositories { + google() +} + +configurations { + create("r8") +} + +dependencies { + compileOnly("com.google.android:android:${version("android")}") + compileOnly("androidx.annotation:annotation:${version("androidx_annotation")}") + + testImplementation("com.google.android:android:${version("android")}") + testImplementation("org.robolectric:robolectric:${version("robolectric")}") + testImplementation("org.smali:baksmali:${version("baksmali")}") + + "r8"("com.android.tools.build:builder:4.0.0-alpha06") // Contains r8-2.0.4-dev +} + +open class RunR8Task : JavaExec() { + + @OutputDirectory + lateinit var outputDex: File + + @InputFile + lateinit var inputConfig: File + + @InputFile + val inputConfigCommon: File = File("testdata/r8-test-common.pro") + + @InputFiles + val jarFile: File = project.tasks.named("jar").get().archivePath + + init { + classpath = project.configurations["r8"] + main = "com.android.tools.r8.R8" + } + + override fun exec() { + // Resolve classpath only during execution + val arguments = mutableListOf( + "--release", + "--no-desugaring", + "--output", outputDex.absolutePath, + "--pg-conf", inputConfig.absolutePath + ) + arguments.addAll(project.configurations.runtimeClasspath.files.map { it.absolutePath }) + arguments.add(jarFile.absolutePath) + + args = arguments + + project.delete(outputDex) + outputDex.mkdirs() + + super.exec() + } +} + +val optimizedDexDir = File(buildDir, "dex-optim/") +val unOptimizedDexDir = File(buildDir, "dex-unoptim/") + +val optimizedDexFile = File(optimizedDexDir, "classes.dex") +val unOptimizedDexFile = File(unOptimizedDexDir, "classes.dex") + +val runR8 = tasks.register("runR8") { + outputDex = optimizedDexDir + inputConfig = file("testdata/r8-test-rules.pro") + + dependsOn("jar") +} + +val runR8NoOptim = tasks.register("runR8NoOptim") { + outputDex = unOptimizedDexDir + inputConfig = file("testdata/r8-test-rules-no-optim.pro") + + dependsOn("jar") +} + +tasks.test { + // Ensure the R8-processed dex is built and supply its path as a property to the test. + dependsOn(runR8) + dependsOn(runR8NoOptim) + + inputs.files(optimizedDexFile, unOptimizedDexFile) + + systemProperty("dexPath", optimizedDexFile.absolutePath) + systemProperty("noOptimDexPath", unOptimizedDexFile.absolutePath) + + // Output custom metric with the size of the optimized dex + doLast { + println("##teamcity[buildStatisticValue key='optimizedDexSize' value='${optimizedDexFile.length()}']") + } +} + +tasks.withType().configureEach { + externalDocumentationLink(delegateClosureOf { + url = URL("https://developer.android.com/reference/") + packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() + }) +} diff --git a/ui/kotlinx-coroutines-android/example-app/app/build.gradle b/ui/kotlinx-coroutines-android/example-app/app/build.gradle deleted file mode 100644 index 3f013247d7..0000000000 --- a/ui/kotlinx-coroutines-android/example-app/app/build.gradle +++ /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. - */ - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' - -android { - compileSdkVersion 29 - defaultConfig { - applicationId "com.example.app" - minSdkVersion 14 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.google.android.material:material:1.0.0' - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} diff --git a/ui/kotlinx-coroutines-android/example-app/app/build.gradle.kts b/ui/kotlinx-coroutines-android/example-app/app/build.gradle.kts new file mode 100644 index 0000000000..39bba5bf30 --- /dev/null +++ b/ui/kotlinx-coroutines-android/example-app/app/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("com.android.application") + kotlin("android") + kotlin("android.extensions") +} + +android { + compileSdkVersion = "29" + defaultConfig { + applicationId = "com.example.app" + minSdkVersion(14) + targetSdkVersion(29) + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.0.2") + implementation("androidx.constraintlayout:constraintlayout:1.1.3") + implementation("com.google.android.material:material:1.0.0") + + implementation(kotlin("stdlib-jdk7")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${property("coroutines_version")}") + + testImplementation("junit:junit:4.12") + androidTestImplementation("androidx.test:runner:1.2.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0") +} diff --git a/ui/kotlinx-coroutines-android/animation-app/build.gradle b/ui/kotlinx-coroutines-android/example-app/build.gradle.kts similarity index 72% rename from ui/kotlinx-coroutines-android/animation-app/build.gradle rename to ui/kotlinx-coroutines-android/example-app/build.gradle.kts index d98ab8cfe7..9cd0c592df 100644 --- a/ui/kotlinx-coroutines-android/animation-app/build.gradle +++ b/ui/kotlinx-coroutines-android/example-app/build.gradle.kts @@ -10,8 +10,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("com.android.tools.build:gradle:3.5.0") + classpath(kotlin("gradle-plugin", property("kotlin_version") as String)) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -25,6 +25,6 @@ allprojects { } } -task clean(type: Delete) { - delete rootProject.buildDir +task("clean") { + delete(rootProject.buildDir) } diff --git a/ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.jar b/ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.jar index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch literal 58694 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 literal 0 HcmV?d00001 diff --git a/ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.properties b/ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.properties index dfd98a959a..dbe85eef28 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,5 @@ -# -# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. -# - distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/ui/kotlinx-coroutines-android/example-app/gradlew b/ui/kotlinx-coroutines-android/example-app/gradlew old mode 100644 new mode 100755 index 9d82f78915..2fe81a7d95 --- a/ui/kotlinx-coroutines-android/example-app/gradlew +++ b/ui/kotlinx-coroutines-android/example-app/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -105,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -134,27 +154,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/ui/kotlinx-coroutines-android/example-app/gradlew.bat b/ui/kotlinx-coroutines-android/example-app/gradlew.bat index 8a0b282aa6..62bd9b9cce 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradlew.bat +++ b/ui/kotlinx-coroutines-android/example-app/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,17 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +65,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +78,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/ui/kotlinx-coroutines-android/example-app/settings.gradle b/ui/kotlinx-coroutines-android/example-app/settings.gradle deleted file mode 100644 index e7b4def49c..0000000000 --- a/ui/kotlinx-coroutines-android/example-app/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app' diff --git a/ui/kotlinx-coroutines-android/example-app/settings.gradle.kts b/ui/kotlinx-coroutines-android/example-app/settings.gradle.kts new file mode 100644 index 0000000000..15a801b10a --- /dev/null +++ b/ui/kotlinx-coroutines-android/example-app/settings.gradle.kts @@ -0,0 +1 @@ +include(":app") diff --git a/ui/kotlinx-coroutines-swing/build.gradle b/ui/kotlinx-coroutines-swing/build.gradle.kts similarity index 70% rename from ui/kotlinx-coroutines-swing/build.gradle rename to ui/kotlinx-coroutines-swing/build.gradle.kts index ad8bef0e2f..b834f29fb7 100644 --- a/ui/kotlinx-coroutines-swing/build.gradle +++ b/ui/kotlinx-coroutines-swing/build.gradle.kts @@ -3,5 +3,5 @@ */ dependencies { - testCompile project(':kotlinx-coroutines-jdk8') + testCompile(project(":kotlinx-coroutines-jdk8")) } From 998297954ead303a7bc01bd043082c464c96bf8f Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Sat, 2 May 2020 14:22:08 +0300 Subject: [PATCH 035/257] Plugin versions in 'plugins' block (#1977) --- build.gradle | 2 -- settings.gradle | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 08a241a411..af33d38aa0 100644 --- a/build.gradle +++ b/build.gradle @@ -83,8 +83,6 @@ buildscript { // JMH plugins classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" - classpath "me.champeau.gradle:jmh-gradle-plugin:0.5.0-rc-2" - classpath "net.ltgt.gradle:gradle-apt-plugin:0.21" } } diff --git a/settings.gradle b/settings.gradle index 759628f134..2f0e661b4c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,13 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +pluginManagement { + plugins { + id "net.ltgt.apt" version "0.21" + id "me.champeau.gradle.jmh" version "0.5.0-rc-2" + } +} + rootProject.name = 'kotlinx.coroutines' enableFeaturePreview('GRADLE_METADATA') From 1beebf10049077e6d183c564db963e7718f1691f Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Sat, 2 May 2020 14:22:19 +0300 Subject: [PATCH 036/257] 'reactor_vesion' -> 'reactor_veRsion' (#1976) --- benchmarks/build.gradle.kts | 2 +- gradle.properties | 2 +- reactive/kotlinx-coroutines-reactor/build.gradle | 4 ++-- .../android-unit-tests/build.gradle.kts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 0841129148..fa651346ce 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -72,7 +72,7 @@ tasks.jmhJar { dependencies { compile("org.openjdk.jmh:jmh-core:1.21") - compile("io.projectreactor:reactor-core:${property("reactor_vesion")}") + compile("io.projectreactor:reactor-core:${version("reactor")}") compile("io.reactivex.rxjava2:rxjava:2.1.9") compile("com.github.akarnokd:rxjava2-extensions:0.20.8") diff --git a/gradle.properties b/gradle.properties index 6545d79292..49fa106490 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ html_version=0.6.8 lincheck_version=2.7.1 dokka_version=0.9.16-rdev-2-mpp-hacks byte_buddy_version=1.10.9 -reactor_vesion=3.2.5.RELEASE +reactor_version=3.2.5.RELEASE reactive_streams_version=1.0.2 rxjava2_version=2.2.8 rxjava3_version=3.0.2 diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle b/reactive/kotlinx-coroutines-reactor/build.gradle index a33d3e687a..3b640bd5cc 100644 --- a/reactive/kotlinx-coroutines-reactor/build.gradle +++ b/reactive/kotlinx-coroutines-reactor/build.gradle @@ -3,13 +3,13 @@ */ dependencies { - compile "io.projectreactor:reactor-core:$reactor_vesion" + compile "io.projectreactor:reactor-core:$reactor_version" compile project(':kotlinx-coroutines-reactive') } tasks.withType(dokka.getClass()) { externalDocumentationLink { - url = new URL("https://projectreactor.io/docs/core/$reactor_vesion/api/") + url = new URL("https://projectreactor.io/docs/core/$reactor_version/api/") packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts index e4b0dbf840..2acc058d73 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle.kts @@ -3,8 +3,8 @@ */ dependencies { - testImplementation("com.google.android:android:${property("android_version")}") - testImplementation("org.robolectric:robolectric:${property("robolectric_version")}") + testImplementation("com.google.android:android:${version("android")}") + testImplementation("org.robolectric:robolectric:${version("robolectric")}") testImplementation(project(":kotlinx-coroutines-test")) testImplementation(project(":kotlinx-coroutines-android")) } From de38890abb4300206d25d8620b84eeffe30f289a Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Sat, 2 May 2020 14:22:30 +0300 Subject: [PATCH 037/257] Platform util in 'buildSrc' (#1969) --- build.gradle | 11 +---------- buildSrc/src/main/kotlin/Platform.kt | 8 ++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 buildSrc/src/main/kotlin/Platform.kt diff --git a/build.gradle b/build.gradle index af33d38aa0..032c4e798b 100644 --- a/build.gradle +++ b/build.gradle @@ -13,15 +13,6 @@ def internal = ['kotlinx.coroutines', 'site', 'benchmarks', 'js-stub', 'stdlib-s // Not published def unpublished = internal + ['example-frontend-js', 'android-unit-tests'] -static def platformOf(project) { - def name = project.name - if (name.endsWith("-js")) return "js" - if (name.endsWith("-common") || name.endsWith("-native")) { - throw IllegalStateException("$name platform is not supported") - } - return "jvm" -} - buildscript { /* * These property group is used to build kotlinx.coroutines against Kotlin compiler snapshot. @@ -164,7 +155,7 @@ allprojects { // Add dependency to core source sets. Core is configured in kx-core/build.gradle configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != coreModule }) { evaluationDependsOn(":$coreModule") - def platform = platformOf(it) + def platform = PlatformKt.platformOf(it) apply from: rootProject.file("gradle/compile-${platform}.gradle") dependencies { // See comment below for rationale, it will be replaced with "project" dependency diff --git a/buildSrc/src/main/kotlin/Platform.kt b/buildSrc/src/main/kotlin/Platform.kt new file mode 100644 index 0000000000..4cacd9e026 --- /dev/null +++ b/buildSrc/src/main/kotlin/Platform.kt @@ -0,0 +1,8 @@ +import org.gradle.api.Project + +fun platformOf(project: Project): String = + when (project.name.substringAfterLast("-")) { + "js" -> "js" + "common", "native" -> throw IllegalStateException("${project.name} platform is not supported") + else -> "jvm" + } From 18f68ad7b8ba5239843215c3a7d34576a2762a86 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Wed, 6 May 2020 15:27:00 +0300 Subject: [PATCH 038/257] Use standard random API (#1988) * Use standard random API * Inline 'random' method --- js/example-frontend-js/src/ExampleMain.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt index cbb6d07893..25a73c61c0 100644 --- a/js/example-frontend-js/src/ExampleMain.kt +++ b/js/example-frontend-js/src/ExampleMain.kt @@ -11,6 +11,7 @@ import org.w3c.dom.* import kotlin.browser.* import kotlin.coroutines.* import kotlin.math.* +import kotlin.random.Random fun main() { println("Starting example application...") @@ -35,9 +36,6 @@ private fun HTMLElement.setPosition(x: Double, y: Double) { } } -@Suppress("DEPRECATION") -private fun random() = kotlin.js.Math.random() - class Application : CoroutineScope { private val body get() = document.body!! private val scene get() = document.getElementById("scene") as HTMLElement @@ -125,7 +123,7 @@ class Application : CoroutineScope { timer.delay(1000) // pause a bit // flip direction val t = vx - if (random() > 0.5) { + if (Random.nextDouble() > 0.5) { vx = vy vy = -t } else { @@ -150,11 +148,11 @@ class Application : CoroutineScope { animation("circle", radius) { circle -> println("Started new 'circle' coroutine #$index") val timer = AnimationTimer() - val initialAngle = random() * 2 * PI + val initialAngle = Random.nextDouble() * 2 * PI var vx = sin(initialAngle) * initialSpeed var vy = cos(initialAngle) * initialSpeed - var x = (random() * initialRange + (1 - initialRange) / 2) * sw - var y = (random() * initialRange + (1 - initialRange) / 2) * sh + var x = (Random.nextDouble() * initialRange + (1 - initialRange) / 2) * sw + var y = (Random.nextDouble() * initialRange + (1 - initialRange) / 2) * sh while (true) { val dt = timer.await() val dx = sw / 2 - x From 5ecebe133822412df37a2e68d2025dbd1d2628df Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 7 May 2020 16:57:16 +0300 Subject: [PATCH 039/257] Fix logic that determines future removal for scheduled executors (#1995) I've failed to write a reliable test here. See also #571 Fixes #1992 --- kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt index 171748ff68..75b668a335 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt @@ -29,10 +29,10 @@ private val REMOVE_FUTURE_ON_CANCEL: Method? = try { @Suppress("NAME_SHADOWING") internal fun removeFutureOnCancel(executor: Executor): Boolean { try { - val executor = executor as? ScheduledExecutorService ?: return false + val executor = executor as? ScheduledThreadPoolExecutor ?: return false (REMOVE_FUTURE_ON_CANCEL ?: return false).invoke(executor, true) return true } catch (e: Throwable) { - return true + return false // failed to setRemoveOnCancelPolicy, assume it does not removes future on cancel } } From 134a4bcfefdb9225b59f013e5afd2ad6e454c6d0 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 7 May 2020 16:58:05 +0300 Subject: [PATCH 040/257] Improve docs & code style in runInterruptibe (#1994) * Fix code formatting in doc example and make it more concise (both vertically and horizontally). * Don't promote custom `withTimeout` logic in examples. Use the actual `withTimeout` function. * Logical introduction of `context` parameter in docs (first use without it, then explain how it helps), consistent doc references. * Improved implementation in various places: * runInterruptibleInExpectedContext does not have to be suspend * There is always Job in the context * ThreadState should not do complex init in constructor (that's bad style) * ThreadState does not need inner State class, atomic int is enough * Consistent project-wide variable naming: state -> _state * Consistent use of `error` function to throw IllegalStateException --- .../jvm/src/Interruptible.kt | 83 +++++++++---------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/Interruptible.kt b/kotlinx-coroutines-core/jvm/src/Interruptible.kt index a4144d87d6..f50e0936d5 100644 --- a/kotlinx-coroutines-core/jvm/src/Interruptible.kt +++ b/kotlinx-coroutines-core/jvm/src/Interruptible.kt @@ -11,27 +11,22 @@ import kotlin.coroutines.* * Calls the specified [block] with a given coroutine context in a interruptible manner. * The blocking code block will be interrupted and this function will throw [CancellationException] * if the coroutine is cancelled. - * The specified [coroutineContext] directly translates into [withContext] argument. * * Example: + * * ``` - * val blockingJob = launch { - * // This function will throw CancellationException - * runInterruptible(Dispatchers.IO) { - * // This blocking procedure will be interrupted when this coroutine is canceled - * doSomethingElseUsefulInterruptible() + * withTimeout(500L) { // Cancels coroutine on timeout + * runInterruptible { // Throws CancellationException if interrupted + * doSomethingBlocking() // Interrupted on coroutines cancellation * } * } - * - * delay(500L) - * blockingJob.cancel() // Interrupt blocking call - * } * ``` * - * There is also an optional context parameter to this function to enable single-call conversion of - * interruptible Java methods into suspending functions like this: + * There is an optional [context] parameter to this function working just like [withContext]. + * It enables single-call conversion of interruptible Java methods into suspending functions. + * With one call here we are moving the call to [Dispatchers.IO] and supporting interruption: + * * ``` - * // With one call here we are moving the call to Dispatchers.IO and supporting interruption. * suspend fun BlockingQueue.awaitTake(): T = * runInterruptible(Dispatchers.IO) { queue.take() } * ``` @@ -40,14 +35,14 @@ public suspend fun runInterruptible( context: CoroutineContext = EmptyCoroutineContext, block: () -> T ): T = withContext(context) { - runInterruptibleInExpectedContext(block) + runInterruptibleInExpectedContext(coroutineContext, block) } -private suspend fun runInterruptibleInExpectedContext(block: () -> T): T { +private fun runInterruptibleInExpectedContext(coroutineContext: CoroutineContext, block: () -> T): T { try { - // No job -> no cancellation - val job = coroutineContext[Job] ?: return block() + val job = coroutineContext[Job]!! // withContext always creates a job val threadState = ThreadState(job) + threadState.setup() try { return block() } finally { @@ -63,7 +58,7 @@ private const val FINISHED = 1 private const val INTERRUPTING = 2 private const val INTERRUPTED = 3 -private class ThreadState : CompletionHandler { +private class ThreadState(private val job: Job) : CompletionHandler { /* === States === @@ -90,28 +85,25 @@ private class ThreadState : CompletionHandler { | | V V +---------------+ +-------------------------+ - | INTERRUPTED | | FINISHED | + | INTERRUPTED | | FINISHED | +---------------+ +-------------------------+ */ - private val state: AtomicRef = atomic(State(WORKING, null)) + private val _state = atomic(WORKING) private val targetThread = Thread.currentThread() - private data class State(@JvmField val state: Int, @JvmField val cancelHandle: DisposableHandle?) + // Registered cancellation handler + private var cancelHandle: DisposableHandle? = null - // We're using a non-primary constructor instead of init block of a primary constructor here, because - // we need to `return`. - constructor(job: Job) { - // Register cancellation handler - val cancelHandle = - job.invokeOnCompletion(onCancelling = true, invokeImmediately = true, handler = this) + fun setup() { + cancelHandle = job.invokeOnCompletion(onCancelling = true, invokeImmediately = true, handler = this) // Either we successfully stored it or it was immediately cancelled - state.loop { s -> - when (s.state) { + _state.loop { state -> + when (state) { // Happy-path, move forward - WORKING -> if (state.compareAndSet(s, State(WORKING, cancelHandle))) return + WORKING -> if (_state.compareAndSet(state, WORKING)) return // Immediately cancelled, just continue INTERRUPTING, INTERRUPTED -> return - else -> throw IllegalStateException("Illegal state $s") + else -> invalidState(state) } } } @@ -120,10 +112,10 @@ private class ThreadState : CompletionHandler { /* * Do not allow to untriggered interrupt to leak */ - state.loop { s -> - when (s.state) { - WORKING -> if (state.compareAndSet(s, State(FINISHED, null))) { - s.cancelHandle?.dispose() + _state.loop { state -> + when (state) { + WORKING -> if (_state.compareAndSet(state, FINISHED)) { + cancelHandle?.dispose() return } INTERRUPTING -> { @@ -134,31 +126,32 @@ private class ThreadState : CompletionHandler { } INTERRUPTED -> { // Clear it and bail out - Thread.interrupted(); + Thread.interrupted() return } - else -> error("Illegal state: $s") + else -> invalidState(state) } } } // Cancellation handler override fun invoke(cause: Throwable?) { - state.loop { s -> - when (s.state) { + _state.loop { state -> + when (state) { // Working -> try to transite state and interrupt the thread WORKING -> { - if (state.compareAndSet(s, State(INTERRUPTING, null))) { + if (_state.compareAndSet(state, INTERRUPTING)) { targetThread.interrupt() - state.value = State(INTERRUPTED, null) + _state.value = INTERRUPTED return } } - // Finished -- runInterruptible is already complete - FINISHED -> return - INTERRUPTING, INTERRUPTED -> return - else -> error("Illegal state: $s") + // Finished -- runInterruptible is already complete, INTERRUPTING - ignore + FINISHED, INTERRUPTING, INTERRUPTED -> return + else -> invalidState(state) } } } + + private fun invalidState(state: Int): Nothing = error("Illegal state $state") } From 171fcc171ed7ec5e13b5ca1541989c03a3650712 Mon Sep 17 00:00:00 2001 From: Vadim Semenov Date: Sun, 3 May 2020 18:20:09 +0100 Subject: [PATCH 041/257] Fix typos in comments to ticker channels (#1987) --- docs/channels.md | 4 ++-- kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/channels.md b/docs/channels.md index 6769b8dc52..9f08768415 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -634,9 +634,9 @@ import kotlinx.coroutines.channels.* fun main() = runBlocking { val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) // create ticker channel var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() } - println("Initial element is available immediately: $nextElement") // initial delay hasn't passed yet + println("Initial element is available immediately: $nextElement") // no initial delay - nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // all subsequent elements has 100ms delay + nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // all subsequent elements have 100ms delay println("Next element is not ready in 50 ms: $nextElement") nextElement = withTimeoutOrNull(60) { tickerChannel.receive() } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt b/kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt index ce0424471a..1f48b02e4c 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-channel-10.kt @@ -11,9 +11,9 @@ import kotlinx.coroutines.channels.* fun main() = runBlocking { val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) // create ticker channel var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() } - println("Initial element is available immediately: $nextElement") // initial delay hasn't passed yet + println("Initial element is available immediately: $nextElement") // no initial delay - nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // all subsequent elements has 100ms delay + nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // all subsequent elements have 100ms delay println("Next element is not ready in 50 ms: $nextElement") nextElement = withTimeoutOrNull(60) { tickerChannel.receive() } From 9291b00ec39da40c83e47b7f749c140b9ee6e23b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 7 May 2020 17:53:09 +0300 Subject: [PATCH 042/257] Additional VCS configuration options Spell-check & subject/body separation inspection --- .idea/vcs.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 9ea55e627a..fd137b62c9 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,11 @@ + + + + + +
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-basic-09.kt). +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-basic-09.kt). You can run and see that it prints three lines and terminates: diff --git a/docs/cancellation-and-timeouts.md b/docs/cancellation-and-timeouts.md index b51c45c941..d8d5b7bad4 100644 --- a/docs/cancellation-and-timeouts.md +++ b/docs/cancellation-and-timeouts.md @@ -49,7 +49,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-01.kt). +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-01.kt). It produces the following output: @@ -104,7 +104,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-02.kt). +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-02.kt). Run it to see that it continues to print "I'm sleeping" even after cancellation until the job completes by itself after five iterations. @@ -156,7 +156,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt). +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt). As you can see, now this loop is cancelled. [isActive] is an extension property available inside the coroutine via the [CoroutineScope] object. @@ -203,7 +203,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt). +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt). Both [join][Job.join] and [cancelAndJoin] wait for all finalization actions to complete, so the example above produces the following output: @@ -259,7 +259,7 @@ fun main() = runBlocking {
-> You can get full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt). +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt). ### Scope builder -In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using -[coroutineScope] builder. It creates a coroutine scope and does not complete until all launched children -complete. -[runBlocking] and [coroutineScope] may look similar because they both wait for its body and all its children to complete. -The main difference between these two is that the [runBlocking] method _blocks_ the current thread for waiting, +In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using the +[coroutineScope] builder. It creates a coroutine scope and does not complete until all launched children complete. + +[runBlocking] and [coroutineScope] may look similar because they both wait for their body and all its children to complete. +The main difference is that the [runBlocking] method _blocks_ the current thread for waiting, while [coroutineScope] just suspends, releasing the underlying thread for other usages. Because of that difference, [runBlocking] is a regular function and [coroutineScope] is a suspending function. @@ -280,16 +280,16 @@ Task from nested launch Coroutine scope is over --> -Note that right after "Task from coroutine scope" message, while waiting for nested launch, - "Task from runBlocking" is executed and printed, though coroutineScope is not completed yet. +Note that right after the "Task from coroutine scope" message (while waiting for nested launch) + "Task from runBlocking" is executed and printed — even though the [coroutineScope] is not completed yet. ### Extract function refactoring Let's extract the block of code inside `launch { ... }` into a separate function. When you -perform "Extract function" refactoring on this code you get a new function with `suspend` modifier. -That is your first _suspending function_. Suspending functions can be used inside coroutines +perform "Extract function" refactoring on this code, you get a new function with the `suspend` modifier. +This is your first _suspending function_. Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, -use other suspending functions, like `delay` in this example, to _suspend_ execution of a coroutine. +use other suspending functions (like `delay` in this example) to _suspend_ execution of a coroutine.
@@ -319,11 +319,11 @@ World! But what if the extracted function contains a coroutine builder which is invoked on the current scope? -In this case `suspend` modifier on the extracted function is not enough. Making `doWorld` an extension -method on `CoroutineScope` is one of the solutions, but it may not always be applicable as it does not make API clearer. +In this case, the `suspend` modifier on the extracted function is not enough. Making `doWorld` an extension +method on `CoroutineScope` is one of the solutions, but it may not always be applicable as it does not make the API clearer. The idiomatic solution is to have either an explicit `CoroutineScope` as a field in a class containing the target function or an implicit one when the outer class implements `CoroutineScope`. -As a last resort, [CoroutineScope(coroutineContext)][CoroutineScope()] can be used, but such approach is structurally unsafe +As a last resort, [CoroutineScope(coroutineContext)][CoroutineScope()] can be used, but such an approach is structurally unsafe because you no longer have control on the scope of execution of this method. Only private APIs can use this builder. ### Coroutines ARE light-weight @@ -352,6 +352,7 @@ fun main() = runBlocking { It launches 100K coroutines and, after a second, each coroutine prints a dot. + Now, try that with threads. What would happen? (Most likely your code will produce some sort of out-of-memory error) ### Global coroutines are like daemon threads From 9bbb6693e4f5b0fe10130f67b596ccfc5038452e Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 3 Jun 2020 12:02:17 +0300 Subject: [PATCH 068/257] Improve the docs and guide on flow cancellation (#2043) * Improve the docs and guide on flow cancellation * Remove outdated phrase on "flow infrastructure does not introduce additional cancellation points". They were introduced by #2028 * Add a section on "Flow cancellation check" with examples on `Flow.cancellable()`` operator. * Add a bit more detail in `flow` and `cancellable` docs with links to `ensureActive()`. --- coroutines-guide.md | 4 +- docs/flow.md | 139 +++++++++++++++++- .../common/src/flow/Builders.kt | 7 +- .../common/src/flow/operators/Context.kt | 3 + .../jvm/test/guide/example-flow-37.kt | 23 +++ .../jvm/test/guide/example-flow-38.kt | 16 ++ .../jvm/test/guide/example-flow-39.kt | 16 ++ .../jvm/test/guide/test/FlowGuideTest.kt | 36 +++++ 8 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-flow-37.kt create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-flow-38.kt create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-flow-39.kt diff --git a/coroutines-guide.md b/coroutines-guide.md index 0b7b842acb..4b3c09c40f 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -47,7 +47,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Suspending functions](docs/flow.md#suspending-functions) * [Flows](docs/flow.md#flows) * [Flows are cold](docs/flow.md#flows-are-cold) - * [Flow cancellation](docs/flow.md#flow-cancellation) + * [Flow cancellation basics](docs/flow.md#flow-cancellation-basics) * [Flow builders](docs/flow.md#flow-builders) * [Intermediate flow operators](docs/flow.md#intermediate-flow-operators) * [Transform operator](docs/flow.md#transform-operator) @@ -79,6 +79,8 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Successful completion](docs/flow.md#successful-completion) * [Imperative versus declarative](docs/flow.md#imperative-versus-declarative) * [Launching flow](docs/flow.md#launching-flow) + * [Flow cancellation checks](docs/flow.md#flow-cancellation-checks) + * [Making busy flow cancellable](docs/flow.md#making-busy-flow-cancellable) * [Flow and Reactive Streams](docs/flow.md#flow-and-reactive-streams) * [Channels](docs/channels.md#channels) diff --git a/docs/flow.md b/docs/flow.md index 3f14b10417..9b331588c3 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -10,7 +10,7 @@ * [Suspending functions](#suspending-functions) * [Flows](#flows) * [Flows are cold](#flows-are-cold) - * [Flow cancellation](#flow-cancellation) + * [Flow cancellation basics](#flow-cancellation-basics) * [Flow builders](#flow-builders) * [Intermediate flow operators](#intermediate-flow-operators) * [Transform operator](#transform-operator) @@ -42,6 +42,8 @@ * [Successful completion](#successful-completion) * [Imperative versus declarative](#imperative-versus-declarative) * [Launching flow](#launching-flow) + * [Flow cancellation checks](#flow-cancellation-checks) + * [Making busy flow cancellable](#making-busy-flow-cancellable) * [Flow and Reactive Streams](#flow-and-reactive-streams) @@ -267,12 +269,10 @@ This is a key reason the `foo()` function (which returns a flow) is not marked w By itself, `foo()` returns quickly and does not wait for anything. The flow starts every time it is collected, that is why we see "Flow started" when we call `collect` again. -### Flow cancellation - -Flow adheres to the general cooperative cancellation of coroutines. However, flow infrastructure does not introduce -additional cancellation points. It is fully transparent for cancellation. As usual, flow collection can be -cancelled when the flow is suspended in a cancellable suspending function (like [delay]), and cannot be cancelled otherwise. +### Flow cancellation basics +Flow adheres to the general cooperative cancellation of coroutines. As usual, flow collection can be +cancelled when the flow is suspended in a cancellable suspending function (like [delay]). The following example shows how the flow gets cancelled on a timeout when running in a [withTimeoutOrNull] block and stops executing its code: @@ -316,6 +316,8 @@ Done +See [Flow cancellation checks](#flow-cancellation-checks) section for more details. + ### Flow builders The `flow { ... }` builder from the previous examples is the most basic one. There are other builders for @@ -1777,6 +1779,127 @@ as cancellation and structured concurrency serve this purpose. Note that [launchIn] also returns a [Job], which can be used to [cancel][Job.cancel] the corresponding flow collection coroutine only without cancelling the whole scope or to [join][Job.join] it. +### Flow cancellation checks + +For convenience, the [flow] builder performs additional [ensureActive] checks for cancellation on each emitted value. +It means that a busy loop emitting from a `flow { ... }` is cancellable: + +
+ +```kotlin +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +//sampleStart +fun foo(): Flow = flow { + for (i in 1..5) { + println("Emitting $i") + emit(i) + } +} + +fun main() = runBlocking { + foo().collect { value -> + if (value == 3) cancel() + println(value) + } +} +//sampleEnd +``` + +
+ +> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-37.kt). + +We get only numbers up to 3 and a [CancellationException] after trying to emit number 4: + +```text +Emitting 1 +1 +Emitting 2 +2 +Emitting 3 +3 +Emitting 4 +Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@6d7b4f4c +``` + + + +However, most other flow operators do not do additional cancellation checks on their own for performance reasons. +For example, if you use [IntRange.asFlow] extension to write the same busy loop and don't suspend anywhere, +then there are no checks for cancellation: + +
+ +```kotlin +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +//sampleStart +fun main() = runBlocking { + (1..5).asFlow().collect { value -> + if (value == 3) cancel() + println(value) + } +} +//sampleEnd +``` + +
+ +> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-38.kt). + +All numbers from 1 to 5 are collected and cancellation gets detected only before return from `runBlocking`: + +```text +1 +2 +3 +4 +5 +Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@3327bd23 +``` + + + +#### Making busy flow cancellable + +In the case where you have a busy loop with coroutines you must explicitly check for cancellation. +You can add `.onEach { currentCoroutineContext().ensureActive() }`, but there is a ready-to-use +[cancellable] operator provided to do that: + +
+ +```kotlin +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +//sampleStart +fun main() = runBlocking { + (1..5).asFlow().cancellable().collect { value -> + if (value == 3) cancel() + println(value) + } +} +//sampleEnd +``` + +
+ +> You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-39.kt). + +With the `cancellable` operator only the numbers from 1 to 3 are collected: + +```text +1 +2 +3 +Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@5ec0a365 +``` + + + ### Flow and Reactive Streams For those who are familiar with [Reactive Streams](https://www.reactive-streams.org/) or reactive frameworks such as RxJava and project Reactor, @@ -1813,6 +1936,8 @@ Integration modules include conversions from and to `Flow`, integration with Rea [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html [Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html +[ensureActive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html +[CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html [flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html @@ -1845,4 +1970,6 @@ Integration modules include conversions from and to `Flow`, integration with Rea [catch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/catch.html [onCompletion]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-completion.html [launchIn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.html +[IntRange.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/kotlin.ranges.-int-range/as-flow.html +[cancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/cancellable.html diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 74a0ad2583..8fd9ae76a4 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * Creates a flow from the given suspendable [block]. * * Example of usage: + * * ``` * fun fibonacci(): Flow = flow { * var x = BigInteger.ZERO @@ -33,10 +34,13 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * * fibonacci().take(100).collect { println(it) } * ``` - * Emissions from [flow] builder are [cancellable] by default. + * + * Emissions from [flow] builder are [cancellable] by default — each call to [emit][FlowCollector.emit] + * also calls [ensureActive][CoroutineContext.ensureActive]. * * `emit` should happen strictly in the dispatchers of the [block] in order to preserve the flow context. * For example, the following code will result in an [IllegalStateException]: + * * ``` * flow { * emit(1) // Ok @@ -45,6 +49,7 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * } * } * ``` + * * If you want to switch the context of execution of a flow, use the [flowOn] operator. */ public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow = SafeFlow(block) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index afdcd9ed18..010d781c02 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -212,6 +212,9 @@ 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. + * + * 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 diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-37.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-37.kt new file mode 100644 index 0000000000..229e55b130 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-37.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from flow.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleFlow37 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun foo(): Flow = flow { + for (i in 1..5) { + println("Emitting $i") + emit(i) + } +} + +fun main() = runBlocking { + foo().collect { value -> + if (value == 3) cancel() + println(value) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-38.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-38.kt new file mode 100644 index 0000000000..fcbbb24ef7 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-38.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from flow.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleFlow38 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + (1..5).asFlow().collect { value -> + if (value == 3) cancel() + println(value) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-39.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-39.kt new file mode 100644 index 0000000000..86275c7c4a --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-39.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from flow.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleFlow39 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + (1..5).asFlow().cancellable().collect { value -> + if (value == 3) cancel() + println(value) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index 39a7853b5f..15c9e1e4c6 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -381,4 +381,40 @@ class FlowGuideTest { "Event: 3" ) } + + @Test + fun testExampleFlow37() { + test("ExampleFlow37") { kotlinx.coroutines.guide.exampleFlow37.main() }.verifyExceptions( + "Emitting 1", + "1", + "Emitting 2", + "2", + "Emitting 3", + "3", + "Emitting 4", + "Exception in thread \"main\" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=\"coroutine#1\":BlockingCoroutine{Cancelled}@6d7b4f4c" + ) + } + + @Test + fun testExampleFlow38() { + test("ExampleFlow38") { kotlinx.coroutines.guide.exampleFlow38.main() }.verifyExceptions( + "1", + "2", + "3", + "4", + "5", + "Exception in thread \"main\" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=\"coroutine#1\":BlockingCoroutine{Cancelled}@3327bd23" + ) + } + + @Test + fun testExampleFlow39() { + test("ExampleFlow39") { kotlinx.coroutines.guide.exampleFlow39.main() }.verifyExceptions( + "1", + "2", + "3", + "Exception in thread \"main\" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=\"coroutine#1\":BlockingCoroutine{Cancelled}@5ec0a365" + ) + } } From 4b16e1b0311ce456b338e6b6563a1097780dc9c6 Mon Sep 17 00:00:00 2001 From: Stanislav Erokhin Date: Tue, 9 Jun 2020 09:38:51 +0300 Subject: [PATCH 069/257] Ignore API change in kotlinx-coroutines-core in train build Ignored change in interface ReceiveChannel interface is: - public abstract fun receiveOrClosed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun receiveOrClosed-ZYPwvRU (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; This change is planned in 1.4.0 compiler. --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 4dc0481af4..a758393b6c 100644 --- a/build.gradle +++ b/build.gradle @@ -132,6 +132,7 @@ apiValidation { if (build_snapshot_train) { ignoredProjects.remove("site") ignoredProjects.remove("example-frontend-js") + ignoredProjects.add("kotlinx-coroutines-core") } ignoredPackages += "kotlinx.coroutines.internal" } From 68360e02e679e3457e9f60048eb7d81c6710f978 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 16 Jun 2020 11:31:45 -0700 Subject: [PATCH 070/257] Fix missing unbox state in native runBlocking (#2091) --- kotlinx-coroutines-core/native/src/Builders.kt | 2 +- .../native/test/RunBlockingTest.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 kotlinx-coroutines-core/native/test/RunBlockingTest.kt diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index 975fc98db7..3554dc604e 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -73,7 +73,7 @@ private class BlockingCoroutine( eventLoop?.decrementUseCount() } // now return result - val state = state + val state = state.unboxState() (state as? CompletedExceptionally)?.let { throw it.cause } state as T } diff --git a/kotlinx-coroutines-core/native/test/RunBlockingTest.kt b/kotlinx-coroutines-core/native/test/RunBlockingTest.kt new file mode 100644 index 0000000000..c5d08af5f3 --- /dev/null +++ b/kotlinx-coroutines-core/native/test/RunBlockingTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines +import kotlin.test.* + +class RunBlockingTest : TestBase() { + + @Test + fun testIncompleteState() { + val handle = runBlocking { + coroutineContext[Job]!!.invokeOnCompletion { } + } + + handle.dispose() + } +} \ No newline at end of file From ad542c49c5e799c6dd4eb54cd8556b84678b3137 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 17 Jun 2020 04:43:28 -0700 Subject: [PATCH 071/257] Minor improvements of debugging experience (#2093) * Recognizable toString implementation in default dispatchers * Fast-path for disabled debug probes --- docs/flow.md | 2 +- .../common/src/Unconfined.kt | 2 +- .../jvm/src/debug/internal/DebugProbesImpl.kt | 1 + .../jvm/src/scheduling/Dispatcher.kt | 22 ++++++++++++------- .../jvm/src/scheduling/Tasks.kt | 3 +++ .../jvm/test/guide/test/FlowGuideTest.kt | 2 +- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/flow.md b/docs/flow.md index 9b331588c3..f0f570d65a 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -697,7 +697,7 @@ This code produces the following exception: ```text Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated: Flow was collected in [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323], - but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, DefaultDispatcher]. + but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default]. Please refer to 'flow' documentation or use 'flowOn' instead at ... ``` diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt index ce03a28765..a0997a51d3 100644 --- a/kotlinx-coroutines-core/common/src/Unconfined.kt +++ b/kotlinx-coroutines-core/common/src/Unconfined.kt @@ -26,7 +26,7 @@ internal object Unconfined : CoroutineDispatcher() { "isDispatchNeeded and dispatch calls.") } - override fun toString(): String = "Unconfined" + override fun toString(): String = "Dispatchers.Unconfined" } /** diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 6e8d19fb3f..6ff51f3da5 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -272,6 +272,7 @@ internal object DebugProbesImpl { internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, SUSPENDED) private fun updateState(frame: Continuation<*>, state: String) { + if (!isInstalled) return // KT-29997 is here only since 1.3.30 if (state == RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) { val stackFrame = frame as? CoroutineStackFrame ?: return diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt index 35ebf92d1b..e0890eff66 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt @@ -14,13 +14,18 @@ import kotlin.coroutines.* * Default instance of coroutine dispatcher. */ internal object DefaultScheduler : ExperimentalCoroutineDispatcher() { - val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS))) + val IO: CoroutineDispatcher = LimitingDispatcher( + this, + systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)), + "Dispatchers.IO", + TASK_PROBABLY_BLOCKING + ) override fun close() { - throw UnsupportedOperationException("$DEFAULT_SCHEDULER_NAME cannot be closed") + throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed") } - override fun toString(): String = DEFAULT_SCHEDULER_NAME + override fun toString(): String = DEFAULT_DISPATCHER_NAME @InternalCoroutinesApi @Suppress("UNUSED") @@ -85,7 +90,7 @@ public open class ExperimentalCoroutineDispatcher( */ public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher { require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" } - return LimitingDispatcher(this, parallelism, TASK_PROBABLY_BLOCKING) + return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING) } /** @@ -98,7 +103,7 @@ public open class ExperimentalCoroutineDispatcher( public fun limited(parallelism: Int): CoroutineDispatcher { require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" } require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" } - return LimitingDispatcher(this, parallelism, TASK_NON_BLOCKING) + return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING) } internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) { @@ -130,8 +135,9 @@ public open class ExperimentalCoroutineDispatcher( } private class LimitingDispatcher( - val dispatcher: ExperimentalCoroutineDispatcher, - val parallelism: Int, + private val dispatcher: ExperimentalCoroutineDispatcher, + private val parallelism: Int, + private val name: String?, override val taskMode: Int ) : ExecutorCoroutineDispatcher(), TaskContext, Executor { @@ -190,7 +196,7 @@ private class LimitingDispatcher( } override fun toString(): String { - return "${super.toString()}[dispatcher = $dispatcher]" + return name ?: "${super.toString()}[dispatcher = $dispatcher]" } /** diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt index 247615d777..a317b9754c 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt @@ -11,6 +11,9 @@ import java.util.concurrent.* // TODO most of these fields will be moved to 'object ExperimentalDispatcher' +// User-visible name +internal const val DEFAULT_DISPATCHER_NAME = "Dispatchers.Default" +// Internal debuggability name + thread name prefixes internal const val DEFAULT_SCHEDULER_NAME = "DefaultDispatcher" // 100us as default diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index 15c9e1e4c6..1d7f952500 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -151,7 +151,7 @@ class FlowGuideTest { test("ExampleFlow14") { kotlinx.coroutines.guide.exampleFlow14.main() }.verifyExceptions( "Exception in thread \"main\" java.lang.IllegalStateException: Flow invariant is violated:", "\t\tFlow was collected in [CoroutineId(1), \"coroutine#1\":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323],", - "\t\tbut emission happened in [CoroutineId(1), \"coroutine#1\":DispatchedCoroutine{Active}@2dae0000, DefaultDispatcher].", + "\t\tbut emission happened in [CoroutineId(1), \"coroutine#1\":DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default].", "\t\tPlease refer to 'flow' documentation or use 'flowOn' instead", "\tat ..." ) From de61a74537eb3232b5d06ea1f852b17d6eb46a3b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 26 Jun 2020 16:53:46 +0300 Subject: [PATCH 072/257] Fixed invalid self-links to index.md in docs Fixes #2107 --- site/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/build.gradle.kts b/site/build.gradle.kts index 450589729e..a18062a371 100644 --- a/site/build.gradle.kts +++ b/site/build.gradle.kts @@ -1,3 +1,5 @@ +import groovy.lang.* + /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @@ -14,6 +16,7 @@ val copyDocs = tasks.register("copyDocs") { } from("docs") into(buildDocsDir) + filter { it.replace("/index.md\"", "/index.html\"") } dependsOn(dokkaTasks) } From ff4b2ceca355708a6a17371ed30287e83cf2b4af Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 2 Jul 2020 01:44:00 -0700 Subject: [PATCH 073/257] Do not print error message to the console when we've failed to install signal handler. (#2118) Otherwise, it may lead to obscure error messages in the console when users are on Windows and are not aware about signals. --- kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index c4d6f530b7..df10501bac 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -69,7 +69,7 @@ internal object AgentPremain { } } } catch (t: Throwable) { - System.err.println("Failed to install signal handler: $t") + // Do nothing, signal cannot be installed, e.g. because we are on Windows } } } From 755d76b93fda84620d4903f448fdfa5b5c53b67d Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 2 Jul 2020 21:13:01 +0300 Subject: [PATCH 074/257] Fix ensureAction to work in the empty context case (#2119) Fixes #2044 --- kotlinx-coroutines-core/common/src/Job.kt | 7 +++---- .../common/test/EmptyContext.kt | 16 ++++++++++++++++ .../common/test/EnsureActiveTest.kt | 7 +++++++ .../common/test/flow/FlowInvariantsTest.kt | 11 +---------- .../jvm/test/flow/FlowCancellationTest.kt | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/EmptyContext.kt diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index e10b138e08..754fa43ece 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -587,10 +587,10 @@ public fun Job.ensureActive(): Unit { /** * Ensures that job in the current context is [active][Job.isActive]. - * Throws [IllegalStateException] if the context does not have a job in it. * * If the job is no longer active, throws [CancellationException]. * If the job was cancelled, thrown exception contains the original cancellation cause. + * This function does not do anything if there is no [Job] in the context, since such a coroutine cannot be cancelled. * * This method is a drop-in replacement for the following code, but with more precise exception: * ``` @@ -599,9 +599,8 @@ public fun Job.ensureActive(): Unit { * } * ``` */ -public fun CoroutineContext.ensureActive(): Unit { - val job = get(Job) ?: error("Context cannot be checked for liveness because it does not have a job: $this") - job.ensureActive() +public fun CoroutineContext.ensureActive() { + get(Job)?.ensureActive() } /** diff --git a/kotlinx-coroutines-core/common/test/EmptyContext.kt b/kotlinx-coroutines-core/common/test/EmptyContext.kt new file mode 100644 index 0000000000..ad78429d2b --- /dev/null +++ b/kotlinx-coroutines-core/common/test/EmptyContext.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.intrinsics.* +import kotlin.coroutines.* + +suspend fun withEmptyContext(block: suspend () -> T): T { + val baseline = Result.failure(IllegalStateException("Block was suspended")) + var result: Result = baseline + block.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) { result = it }) + while (result == baseline) yield() + return result.getOrThrow() +} diff --git a/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt b/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt index 716be629e8..89e749cae0 100644 --- a/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt +++ b/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt @@ -68,6 +68,13 @@ class EnsureActiveTest : TestBase() { finish(4) } + @Test + fun testEnsureActiveWithEmptyContext() = runTest { + withEmptyContext { + ensureActive() // should not do anything + } + } + private inline fun checkException(block: () -> Unit) { val result = runCatching(block) val exception = result.exceptionOrNull() ?: fail() diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt index ce93f1fdb2..d02a6097d2 100644 --- a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* -import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* import kotlin.reflect.* import kotlin.test.* @@ -243,16 +242,8 @@ class FlowInvariantsTest : TestBase() { return result } - val result = runSuspendFun { collector() } + val result = withEmptyContext { collector() } assertEquals(2, result) finish(3) } - - private suspend fun runSuspendFun(block: suspend () -> Int): Int { - val baseline = Result.failure(IllegalStateException("Block was suspended")) - var result: Result = baseline - block.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) { result = it }) - while (result == baseline) yield() - return result.getOrThrow() - } } diff --git a/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt b/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt index b6ba3514f6..269805f9b6 100644 --- a/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/flow/FlowCancellationTest.kt @@ -43,4 +43,20 @@ class FlowCancellationTest : TestBase() { job.cancelAndJoin() finish(3) } + + @Test + fun testFlowWithEmptyContext() = runTest { + expect(1) + withEmptyContext { + val flow = flow { + expect(2) + emit("OK") + } + flow.collect { + expect(3) + assertEquals("OK", it) + } + } + finish(4) + } } \ No newline at end of file From f4fb204b220b224d5ffa9b7c00ff077b681213e9 Mon Sep 17 00:00:00 2001 From: Yanis Batura Date: Sun, 21 Jun 2020 20:03:40 +0700 Subject: [PATCH 075/257] Fix typos in docs (#2098) Co-authored-by: Louis CAD --- docs/coroutine-context-and-dispatchers.md | 2 +- docs/exception-handling.md | 76 +++++++++---------- .../src/future/Future.kt | 4 +- .../jvm/test/guide/example-supervision-01.kt | 10 +-- .../jvm/test/guide/example-supervision-02.kt | 8 +- .../jvm/test/guide/example-supervision-03.kt | 6 +- .../test/guide/test/ExceptionsGuideTest.kt | 22 +++--- .../kotlinx-coroutines-reactive/README.md | 6 +- reactive/kotlinx-coroutines-reactor/README.md | 20 ++--- 9 files changed, 77 insertions(+), 77 deletions(-) diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index 5883dda13f..4909206e17 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -625,7 +625,7 @@ fun main() = runBlocking { In this example we launch a new coroutine in a background thread pool using [Dispatchers.Default], so it works on a different thread from the thread pool, but it still has the value of the thread local variable that we specified using `threadLocal.asContextElement(value = "launch")`, -no matter on what thread the coroutine is executed. +no matter which thread the coroutine is executed on. Thus, the output (with [debug](#debugging-coroutines-and-threads)) is: ```text diff --git a/docs/exception-handling.md b/docs/exception-handling.md index ca78200e0f..5618cafbdc 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -28,7 +28,7 @@ coroutine throw an exception. Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or exposing them to users ([async] and [produce]). When these builders are used to create a _root_ coroutine, that is not a _child_ of another coroutine, -the former builder treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`, +the former builders treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`, while the latter are relying on the user to consume the final exception, for example via [await][Deferred.await] or [receive][ReceiveChannel.receive] ([produce] and [receive][ReceiveChannel.receive] are covered later in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section). @@ -246,7 +246,7 @@ CoroutineExceptionHandler got java.lang.ArithmeticException ### Exceptions aggregation -When multiple children of a coroutine fail with an exception the +When multiple children of a coroutine fail with an exception, the general rule is "the first exception wins", so the first exception gets handled. All additional exceptions that happen after the first one are attached to the first exception as suppressed ones. @@ -296,8 +296,8 @@ CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.Ari -> Note, this mechanism currently works only on Java version 1.7+. -Limitation on JS and Native is temporary and will be fixed in the future. +> Note that this mechanism currently only works on Java version 1.7+. +The JS and Native restrictions are temporary and will be lifted in the future. Cancellation exceptions are transparent and are unwrapped by default: @@ -353,14 +353,14 @@ A good example of such a requirement is a UI component with the job defined in i have failed, it is not always necessary to cancel (effectively kill) the whole UI component, but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer needed. -Another example is a server process that spawns several children jobs and needs to _supervise_ -their execution, tracking their failures and restarting just those children jobs that had failed. +Another example is a server process that spawns multiple child jobs and needs to _supervise_ +their execution, tracking their failures and only restarting the failed ones. #### Supervision job -For these purposes [SupervisorJob][SupervisorJob()] can be used. +The [SupervisorJob][SupervisorJob()] can be used for these purposes. It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated -only downwards. It is easy to demonstrate with an example: +only downwards. This can easily be demonstrated using the following example:
@@ -372,24 +372,24 @@ fun main() = runBlocking { with(CoroutineScope(coroutineContext + supervisor)) { // launch the first child -- its exception is ignored for this example (don't do this in practice!) val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) { - println("First child is failing") - throw AssertionError("First child is cancelled") + println("The first child is failing") + throw AssertionError("The first child is cancelled") } // launch the second child val secondChild = launch { firstChild.join() // Cancellation of the first child is not propagated to the second child - println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active") + println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active") try { delay(Long.MAX_VALUE) } finally { // But cancellation of the supervisor is propagated - println("Second child is cancelled because supervisor is cancelled") + println("The second child is cancelled because the supervisor was cancelled") } } // wait until the first child fails & completes firstChild.join() - println("Cancelling supervisor") + println("Cancelling the supervisor") supervisor.cancel() secondChild.join() } @@ -403,18 +403,18 @@ fun main() = runBlocking { The output of this code is: ```text -First child is failing -First child is cancelled: true, but second one is still active -Cancelling supervisor -Second child is cancelled because supervisor is cancelled +The first child is failing +The first child is cancelled: true, but the second one is still active +Cancelling the supervisor +The second child is cancelled because the supervisor was cancelled ``` #### Supervision scope -For _scoped_ concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation -in one direction only and cancels all children only if it has failed itself. It also waits for all children before completion +Instead of [coroutineScope], we can use [supervisorScope] for _scoped_ concurrency. It propagates the cancellation +in one direction only and cancels all its children only if it failed itself. It also waits for all children before completion just like [coroutineScope] does.
@@ -428,19 +428,19 @@ fun main() = runBlocking { supervisorScope { val child = launch { try { - println("Child is sleeping") + println("The child is sleeping") delay(Long.MAX_VALUE) } finally { - println("Child is cancelled") + println("The child is cancelled") } } // Give our child a chance to execute and print using yield yield() - println("Throwing exception from scope") + println("Throwing an exception from the scope") throw AssertionError() } } catch(e: AssertionError) { - println("Caught assertion error") + println("Caught an assertion error") } } ``` @@ -452,21 +452,21 @@ fun main() = runBlocking { The output of this code is: ```text -Child is sleeping -Throwing exception from scope -Child is cancelled -Caught assertion error +The child is sleeping +Throwing an exception from the scope +The child is cancelled +Caught an assertion error ``` #### Exceptions in supervised coroutines Another crucial difference between regular and supervisor jobs is exception handling. -Every child should handle its exceptions by itself via exception handling mechanism. -This difference comes from the fact that child's failure is not propagated to the parent. -It means that coroutines launched directly inside [supervisorScope] _do_ use the [CoroutineExceptionHandler] +Every child should handle its exceptions by itself via the exception handling mechanism. +This difference comes from the fact that child's failure does not propagate to the parent. +It means that coroutines launched directly inside the [supervisorScope] _do_ use the [CoroutineExceptionHandler] that is installed in their scope in the same way as root coroutines do -(see [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details). +(see the [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details).
@@ -480,12 +480,12 @@ fun main() = runBlocking { } supervisorScope { val child = launch(handler) { - println("Child throws an exception") + println("The child throws an exception") throw AssertionError() } - println("Scope is completing") + println("The scope is completing") } - println("Scope is completed") + println("The scope is completed") } ``` @@ -496,10 +496,10 @@ fun main() = runBlocking { The output of this code is: ```text -Scope is completing -Child throws an exception +The scope is completing +The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError -Scope is completed +The scope is completed ``` @@ -517,8 +517,8 @@ Scope is completed [runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html [Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job.html -[supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html [coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html +[supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html [actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html [produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html diff --git a/integration/kotlinx-coroutines-jdk8/src/future/Future.kt b/integration/kotlinx-coroutines-jdk8/src/future/Future.kt index 3cd848f2cf..f7fdba5f69 100644 --- a/integration/kotlinx-coroutines-jdk8/src/future/Future.kt +++ b/integration/kotlinx-coroutines-jdk8/src/future/Future.kt @@ -11,10 +11,10 @@ import java.util.function.* import kotlin.coroutines.* /** - * Starts new coroutine and returns its result as an implementation of [CompletableFuture]. + * Starts a new coroutine and returns its result as an implementation of [CompletableFuture]. * The running coroutine is cancelled when the resulting future is cancelled or otherwise completed. * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. + * The coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with the [context] argument. * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. * The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden * with corresponding [context] element. diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt index ffd56a8f85..db819e4fc6 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-01.kt @@ -12,24 +12,24 @@ fun main() = runBlocking { with(CoroutineScope(coroutineContext + supervisor)) { // launch the first child -- its exception is ignored for this example (don't do this in practice!) val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) { - println("First child is failing") - throw AssertionError("First child is cancelled") + println("The first child is failing") + throw AssertionError("The first child is cancelled") } // launch the second child val secondChild = launch { firstChild.join() // Cancellation of the first child is not propagated to the second child - println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active") + println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active") try { delay(Long.MAX_VALUE) } finally { // But cancellation of the supervisor is propagated - println("Second child is cancelled because supervisor is cancelled") + println("The second child is cancelled because the supervisor was cancelled") } } // wait until the first child fails & completes firstChild.join() - println("Cancelling supervisor") + println("Cancelling the supervisor") supervisor.cancel() secondChild.join() } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt index dc3a0f2844..50deff3d0b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-02.kt @@ -13,18 +13,18 @@ fun main() = runBlocking { supervisorScope { val child = launch { try { - println("Child is sleeping") + println("The child is sleeping") delay(Long.MAX_VALUE) } finally { - println("Child is cancelled") + println("The child is cancelled") } } // Give our child a chance to execute and print using yield yield() - println("Throwing exception from scope") + println("Throwing an exception from the scope") throw AssertionError() } } catch(e: AssertionError) { - println("Caught assertion error") + println("Caught an assertion error") } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt index 5efbe69146..943c64f6ee 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt @@ -14,10 +14,10 @@ fun main() = runBlocking { } supervisorScope { val child = launch(handler) { - println("Child throws an exception") + println("The child throws an exception") throw AssertionError() } - println("Scope is completing") + println("The scope is completing") } - println("Scope is completed") + println("The scope is completed") } diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt index 21d2c73b1b..c42ba31d3a 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt @@ -63,30 +63,30 @@ class ExceptionsGuideTest { @Test fun testExampleSupervision01() { test("ExampleSupervision01") { kotlinx.coroutines.guide.exampleSupervision01.main() }.verifyLines( - "First child is failing", - "First child is cancelled: true, but second one is still active", - "Cancelling supervisor", - "Second child is cancelled because supervisor is cancelled" + "The first child is failing", + "The first child is cancelled: true, but the second one is still active", + "Cancelling the supervisor", + "The second child is cancelled because the supervisor was cancelled" ) } @Test fun testExampleSupervision02() { test("ExampleSupervision02") { kotlinx.coroutines.guide.exampleSupervision02.main() }.verifyLines( - "Child is sleeping", - "Throwing exception from scope", - "Child is cancelled", - "Caught assertion error" + "The child is sleeping", + "Throwing an exception from the scope", + "The child is cancelled", + "Caught an assertion error" ) } @Test fun testExampleSupervision03() { test("ExampleSupervision03") { kotlinx.coroutines.guide.exampleSupervision03.main() }.verifyLines( - "Scope is completing", - "Child throws an exception", + "The scope is completing", + "The child throws an exception", "CoroutineExceptionHandler got java.lang.AssertionError", - "Scope is completed" + "The scope is completed" ) } } diff --git a/reactive/kotlinx-coroutines-reactive/README.md b/reactive/kotlinx-coroutines-reactive/README.md index b38202bbdf..0a59b2c251 100644 --- a/reactive/kotlinx-coroutines-reactive/README.md +++ b/reactive/kotlinx-coroutines-reactive/README.md @@ -6,14 +6,14 @@ Coroutine builders: | **Name** | **Result** | **Scope** | **Description** | --------------- | ----------------------------- | ---------------- | --------------- -| [publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts coroutine on subscribe +| [publish] | `Publisher` | [ProducerScope] | Cold reactive publisher that starts the coroutine on subscribe Integration with [Flow]: | **Name** | **Result** | **Description** | --------------- | -------------- | --------------- -| [Publisher.asFlow] | `Flow` | Converts the given publisher to flow -| [Flow.asPublisher] | `Publisher` | Converts the given flow to the TCK-compliant publisher +| [Publisher.asFlow] | `Flow` | Converts the given publisher to a flow +| [Flow.asPublisher] | `Publisher` | Converts the given flow to a TCK-compliant publisher If these adapters are used along with `kotlinx-coroutines-reactor` in the classpath, then Reactor's `Context` is properly propagated as coroutine context element (`ReactorContext`) and vice versa. diff --git a/reactive/kotlinx-coroutines-reactor/README.md b/reactive/kotlinx-coroutines-reactor/README.md index 50e1602a3a..cd4a42a34c 100644 --- a/reactive/kotlinx-coroutines-reactor/README.md +++ b/reactive/kotlinx-coroutines-reactor/README.md @@ -6,29 +6,29 @@ Coroutine builders: | **Name** | **Result** | **Scope** | **Description** | --------------- | ------------| ---------------- | --------------- -| [mono] | `Mono` | [CoroutineScope] | Cold mono that starts coroutine on subscribe -| [flux] | `Flux` | [CoroutineScope] | Cold flux that starts coroutine on subscribe +| [mono] | `Mono` | [CoroutineScope] | A cold Mono that starts the coroutine on subscription +| [flux] | `Flux` | [CoroutineScope] | A cold Flux that starts the coroutine on subscription -Note that `Mono` and `Flux` are a subclass of [Reactive Streams](https://www.reactive-streams.org) -`Publisher` and extensions for it are covered by +Note that `Mono` and `Flux` are subclasses of [Reactive Streams](https://www.reactive-streams.org)' +`Publisher` and extensions for it are covered by the [kotlinx-coroutines-reactive](../kotlinx-coroutines-reactive) module. Integration with [Flow]: | **Name** | **Result** | **Description** | --------------- | -------------- | --------------- -| [Flow.asFlux] | `Flux` | Converts the given flow to the TCK-compliant Flux. +| [Flow.asFlux] | `Flux` | Converts the given flow to a TCK-compliant Flux. -This adapter is integrated with Reactor's `Context` and coroutines [ReactorContext]. +This adapter is integrated with Reactor's `Context` and coroutines' [ReactorContext]. Conversion functions: | **Name** | **Description** | -------- | --------------- -| [Job.asMono][kotlinx.coroutines.Job.asMono] | Converts job to hot mono -| [Deferred.asMono][kotlinx.coroutines.Deferred.asMono] | Converts deferred value to hot mono -| [ReceiveChannel.asFlux][kotlinx.coroutines.channels.ReceiveChannel.asFlux] | Converts streaming channel to hot flux -| [Scheduler.asCoroutineDispatcher][reactor.core.scheduler.Scheduler.asCoroutineDispatcher] | Converts scheduler to [CoroutineDispatcher] +| [Job.asMono][kotlinx.coroutines.Job.asMono] | Converts a job to a hot Mono +| [Deferred.asMono][kotlinx.coroutines.Deferred.asMono] | Converts a deferred value to a hot Mono +| [ReceiveChannel.asFlux][kotlinx.coroutines.channels.ReceiveChannel.asFlux] | Converts a streaming channel to a hot Flux +| [Scheduler.asCoroutineDispatcher][reactor.core.scheduler.Scheduler.asCoroutineDispatcher] | Converts a scheduler to a [CoroutineDispatcher] From 75dede3ac6d4e687e18e174798a7a9993674fa3c Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 8 Jul 2020 10:38:14 +0300 Subject: [PATCH 076/257] Removed unused NativeThreadLocal declaration --- kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt b/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt index db62371c1f..09f501a4f5 100644 --- a/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt +++ b/kotlinx-coroutines-core/native/src/internal/ThreadLocal.kt @@ -4,9 +4,6 @@ package kotlinx.coroutines.internal -@Suppress("ACTUAL_WITHOUT_EXPECT") -internal actual typealias NativeThreadLocal = kotlin.native.concurrent.ThreadLocal - internal actual class CommonThreadLocal actual constructor() { private var value: T? = null @Suppress("UNCHECKED_CAST") From 5183b62a6472aaab1b592e82f50ac5d228dcf36f Mon Sep 17 00:00:00 2001 From: Thibault Seisel Date: Thu, 9 Jul 2020 09:03:23 +0200 Subject: [PATCH 077/257] Replace references to deprecated delayEach operator (#2127) Co-authored-by: Thibault SEISEL --- .../common/src/flow/operators/Zip.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt index 76f61b127d..ec661819f2 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt @@ -20,8 +20,8 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * * It can be demonstrated with the following example: * ``` - * val flow = flowOf(1, 2).delayEach(10) - * val flow2 = flowOf("a", "b", "c").delayEach(15) + * val flow = flowOf(1, 2).onEach { delay(10) } + * val flow2 = flowOf("a", "b", "c").onEach { delay(15) } * flow.combine(flow2) { i, s -> i.toString() + s }.collect { * println(it) // Will print "1a 2a 2b 2c" * } @@ -42,8 +42,8 @@ public fun Flow.combine(flow: Flow, transform: suspend (a: T * * It can be demonstrated with the following example: * ``` - * val flow = flowOf(1, 2).delayEach(10) - * val flow2 = flowOf("a", "b", "c").delayEach(15) + * val flow = flowOf(1, 2).onEach { delay(10) } + * val flow2 = flowOf("a", "b", "c").onEach { delay(15) } * combine(flow, flow2) { i, s -> i.toString() + s }.collect { * println(it) // Will print "1a 2a 2b 2c" * } @@ -292,8 +292,8 @@ public inline fun combineTransform( * * It can be demonstrated with the following example: * ``` - * val flow = flowOf(1, 2, 3).delayEach(10) - * val flow2 = flowOf("a", "b", "c", "d").delayEach(15) + * val flow = flowOf(1, 2, 3).onEach { delay(10) } + * val flow2 = flowOf("a", "b", "c", "d").onEach { delay(15) } * flow.zip(flow2) { i, s -> i.toString() + s }.collect { * println(it) // Will print "1a 2b 3c" * } From b42f9861b2a902376e8b2e7bb016d8bdaf4f91a0 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 17:46:06 +0300 Subject: [PATCH 078/257] Flow.transformWhile operator (#2066) Also, most flow-truncating operators are refactored via a common internal collectWhile operator that properly uses AbortFlowException and checks for its ownership, so that we don't have to look for bugs in interactions between all those operators (and zip, too, which is also flow-truncating). But `take` operator still users a custom highly-tuned implementation. Fixes #2065 Co-authored-by: EdwarDDay <4127904+EdwarDDay@users.noreply.github.com> Co-authored-by: Louis CAD --- .../benchmarks/flow/TakeWhileBenchmark.kt | 68 ++++++++++++++++++ .../api/kotlinx-coroutines-core.api | 1 + .../common/src/flow/operators/Emitters.kt | 7 +- .../common/src/flow/operators/Limit.kt | 69 ++++++++++++++++-- .../common/src/flow/terminal/Reduce.kt | 35 +++------- .../common/test/flow/FlowInvariantsTest.kt | 34 ++++++++- .../test/flow/operators/TransformWhileTest.kt | 70 +++++++++++++++++++ 7 files changed, 249 insertions(+), 35 deletions(-) create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/TransformWhileTest.kt diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt new file mode 100644 index 0000000000..fd3d3cdb96 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/TakeWhileBenchmark.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package benchmarks.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.internal.* +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +open class TakeWhileBenchmark { + @Param("1", "10", "100", "1000") + private var size: Int = 0 + + private suspend inline fun Flow.consume() = + filter { it % 2L != 0L } + .map { it * it }.count() + + @Benchmark + fun baseline() = runBlocking { + (0L until size).asFlow().consume() + } + + @Benchmark + fun takeWhileDirect() = runBlocking { + (0L..Long.MAX_VALUE).asFlow().takeWhileDirect { it < size }.consume() + } + + @Benchmark + fun takeWhileViaCollectWhile() = runBlocking { + (0L..Long.MAX_VALUE).asFlow().takeWhileViaCollectWhile { it < size }.consume() + } + + // Direct implementation by checking predicate and throwing AbortFlowException + private fun Flow.takeWhileDirect(predicate: suspend (T) -> Boolean): Flow = unsafeFlow { + try { + collect { value -> + if (predicate(value)) emit(value) + else throw AbortFlowException(this) + } + } catch (e: AbortFlowException) { + e.checkOwnership(owner = this) + } + } + + // Essentially the same code, but reusing the logic via collectWhile function + private fun Flow.takeWhileViaCollectWhile(predicate: suspend (T) -> Boolean): Flow = unsafeFlow { + // This return is needed to work around a bug in JS BE: KT-39227 + return@unsafeFlow collectWhile { value -> + if (predicate(value)) { + emit(value) + true + } else { + false + } + } + } +} diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 6a24b6a23a..9fc49ca798 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -995,6 +995,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static synthetic fun toSet$default (Lkotlinx/coroutines/flow/Flow;Ljava/util/Set;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun transform (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun transformLatest (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; + public static final fun transformWhile (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun unsafeTransform (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun withIndex (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun zip (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 1ffbf94a98..fb37da3a83 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -19,10 +19,11 @@ import kotlin.jvm.* /** * Applies [transform] function to each value of the given flow. * - * The receiver of the [transform] is [FlowCollector] and thus `transform` is a - * generic function that may transform emitted element, skip it or emit it multiple times. + * The receiver of the `transform` is [FlowCollector] and thus `transform` is a + * flexible function that may transform emitted element, skip it or emit it multiple times. * - * This operator can be used as a building block for other operators, for example: + * This operator generalizes [filter] and [map] operators and + * can be used as a building block for other operators, for example: * * ``` * fun Flow.skipOddAndDuplicateEven(): Flow = transform { value -> diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index d30a2db206..1d7ffd1db6 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -7,8 +7,10 @@ package kotlinx.coroutines.flow +import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* +import kotlinx.coroutines.flow.flow as safeFlow import kotlinx.coroutines.flow.internal.unsafeFlow as flow /** @@ -51,6 +53,10 @@ public fun Flow.take(count: Int): Flow { var consumed = 0 try { collect { value -> + // Note: this for take is not written via collectWhile on purpose. + // It checks condition first and then makes a tail-call to either emit or emitAbort. + // This way normal execution does not require a state machine, only a termination (emitAbort). + // See "TakeBenchmark" for comparision of different approaches. if (++consumed < count) { return@collect emit(value) } else { @@ -70,14 +76,67 @@ private suspend fun FlowCollector.emitAbort(value: T) { /** * Returns a flow that contains first elements satisfying the given [predicate]. + * + * Note, that the resulting flow does not contain the element on which the [predicate] returned `false`. + * See [transformWhile] for a more flexible operator. */ public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = flow { - try { - collect { value -> - if (predicate(value)) emit(value) - else throw AbortFlowException(this) + // This return is needed to work around a bug in JS BE: KT-39227 + return@flow collectWhile { value -> + if (predicate(value)) { + emit(value) + true + } else { + false } + } +} + +/** + * Applies [transform] function to each value of the given flow while this + * function returns `true`. + * + * The receiver of the `transformWhile` is [FlowCollector] and thus `transformWhile` is a + * flexible function that may transform emitted element, skip it or emit it multiple times. + * + * This operator generalizes [takeWhile] and can be used as a building block for other operators. + * For example, a flow of download progress messages can be completed when the + * download is done but emit this last message (unlike `takeWhile`): + * + * ``` + * fun Flow.completeWhenDone(): Flow = + * transformWhile { progress -> + * emit(progress) // always emit progress + * !progress.isDone() // continue while download is not done + * } + * } + * ``` + */ +@ExperimentalCoroutinesApi +public fun Flow.transformWhile( + @BuilderInference transform: suspend FlowCollector.(value: T) -> Boolean +): Flow = + safeFlow { // Note: safe flow is used here, because collector is exposed to transform on each operation + // This return is needed to work around a bug in JS BE: KT-39227 + return@safeFlow collectWhile { value -> + transform(value) + } + } + +// Internal building block for non-tailcalling flow-truncating operators +internal suspend inline fun Flow.collectWhile(crossinline predicate: suspend (value: T) -> Boolean) { + val collector = object : FlowCollector { + override suspend fun emit(value: T) { + // Note: we are checking predicate first, then throw. If the predicate does suspend (calls emit, for example) + // the the resulting code is never tail-suspending and produces a state-machine + if (!predicate(value)) { + throw AbortFlowException(this) + } + } + } + try { + collect(collector) } catch (e: AbortFlowException) { - e.checkOwnership(owner = this) + e.checkOwnership(collector) } } diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index d99ae52c7d..d36e1bbf7b 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -82,9 +82,9 @@ public suspend fun Flow.singleOrNull(): T? { */ public suspend fun Flow.first(): T { var result: Any? = NULL - collectUntil { + collectWhile { result = it - true + false } if (result === NULL) throw NoSuchElementException("Expected at least one element") return result as T @@ -96,12 +96,12 @@ public suspend fun Flow.first(): T { */ public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { var result: Any? = NULL - collectUntil { + collectWhile { if (predicate(it)) { result = it - true - } else { false + } else { + true } } if (result === NULL) throw NoSuchElementException("Expected at least one element matching the predicate $predicate") @@ -114,9 +114,9 @@ public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { */ public suspend fun Flow.firstOrNull(): T? { var result: T? = null - collectUntil { + collectWhile { result = it - true + false } return result } @@ -127,28 +127,13 @@ public suspend fun Flow.firstOrNull(): T? { */ public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { var result: T? = null - collectUntil { + collectWhile { if (predicate(it)) { result = it - true - } else { false + } else { + true } } return result } - -internal suspend inline fun Flow.collectUntil(crossinline block: suspend (value: T) -> Boolean) { - val collector = object : FlowCollector { - override suspend fun emit(value: T) { - if (block(value)) { - throw AbortFlowException(this) - } - } - } - try { - collect(collector) - } catch (e: AbortFlowException) { - e.checkOwnership(collector) - } -} diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt index d02a6097d2..b5f1bf7bb8 100644 --- a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt @@ -192,7 +192,7 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testEmptyCoroutineContext() = runTest { + fun testEmptyCoroutineContextMap() = runTest { emptyContextTest { map { expect(it) @@ -212,7 +212,18 @@ class FlowInvariantsTest : TestBase() { } @Test - fun testEmptyCoroutineContextViolation() = runTest { + fun testEmptyCoroutineContextTransformWhile() = runTest { + emptyContextTest { + transformWhile { + expect(it) + emit(it + 1) + true + } + } + } + + @Test + fun testEmptyCoroutineContextViolationTransform() = runTest { try { emptyContextTest { transform { @@ -229,6 +240,25 @@ class FlowInvariantsTest : TestBase() { } } + @Test + fun testEmptyCoroutineContextViolationTransformWhile() = runTest { + try { + emptyContextTest { + transformWhile { + expect(it) + withContext(Dispatchers.Unconfined) { + emit(it + 1) + } + true + } + } + expectUnreached() + } catch (e: IllegalStateException) { + assertTrue(e.message!!.contains("Flow invariant is violated")) + finish(2) + } + } + private suspend fun emptyContextTest(block: Flow.() -> Flow) { suspend fun collector(): Int { var result: Int = -1 diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TransformWhileTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TransformWhileTest.kt new file mode 100644 index 0000000000..df660103c3 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/TransformWhileTest.kt @@ -0,0 +1,70 @@ +/* + * 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 TransformWhileTest : TestBase() { + @Test + fun testSimple() = runTest { + val flow = (0..10).asFlow() + val expected = listOf("A", "B", "C", "D") + val actual = flow.transformWhile { value -> + when(value) { + 0 -> { emit("A"); true } + 1 -> true + 2 -> { emit("B"); emit("C"); true } + 3 -> { emit("D"); false } + else -> { expectUnreached(); false } + } + }.toList() + assertEquals(expected, actual) + } + + @Test + fun testCancelUpstream() = runTest { + var cancelled = false + val flow = flow { + coroutineScope { + launch(start = CoroutineStart.ATOMIC) { + hang { cancelled = true } + } + emit(1) + emit(2) + emit(3) + } + } + val transformed = flow.transformWhile { + emit(it) + it < 2 + } + assertEquals(listOf(1, 2), transformed.toList()) + assertTrue(cancelled) + } + + @Test + fun testExample() = runTest { + val source = listOf( + DownloadProgress(0), + DownloadProgress(50), + DownloadProgress(100), + DownloadProgress(147) + ) + val expected = source.subList(0, 3) + val actual = source.asFlow().completeWhenDone().toList() + assertEquals(expected, actual) + } + + private fun Flow.completeWhenDone(): Flow = + transformWhile { progress -> + emit(progress) // always emit progress + !progress.isDone() // continue while download is not done + } + + private data class DownloadProgress(val percent: Int) { + fun isDone() = percent >= 100 + } +} \ No newline at end of file From 5e91dc45044b28f8b10d76e231e886b0b7da42f1 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 16 Jul 2020 08:19:53 -0700 Subject: [PATCH 079/257] Replace scanReduce with runningReduce to be consistent with Kotlin standard library. (#2139) foldReduce is not introduced to see how it goes in the standard library first. For the full rationale of renaming, please refer to KT-38060 --- .../api/kotlinx-coroutines-core.api | 1 + kotlinx-coroutines-core/common/src/flow/Migration.kt | 8 ++++++++ .../common/src/flow/operators/Transform.kt | 4 ++-- .../common/test/flow/operators/ScanTest.kt | 10 +++++----- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 9fc49ca798..8fcd14dc13 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -971,6 +971,7 @@ public final class kotlinx/coroutines/flow/FlowKt { public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun retryWhen (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function4;)Lkotlinx/coroutines/flow/Flow; + public static final fun runningReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun sample (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; public static final fun sample-8GFy2Ro (Lkotlinx/coroutines/flow/Flow;D)Lkotlinx/coroutines/flow/Flow; public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index fc34f85b4c..bb2f584474 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -430,3 +430,11 @@ public fun Flow.delayEach(timeMillis: Long): Flow = onEach { delay(tim replaceWith = ReplaceWith("this.flatMapLatest(transform)") ) public fun Flow.switchMap(transform: suspend (value: T) -> Flow): Flow = flatMapLatest(transform) + +@Deprecated( + level = DeprecationLevel.WARNING, // Since 1.3.8, was experimental when deprecated + message = "'scanReduce' was renamed to 'runningReduce' to be consistent with Kotlin standard library", + replaceWith = ReplaceWith("runningReduce(operation)") +) +@ExperimentalCoroutinesApi +public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = runningReduce(operation) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt index fefa42f251..520311ee5d 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Transform.kt @@ -100,12 +100,12 @@ public fun Flow.scan(initial: R, @BuilderInference operation: suspend * * For example: * ``` - * flowOf(1, 2, 3, 4).scanReduce { (v1, v2) -> v1 + v2 }.toList() + * flowOf(1, 2, 3, 4).runningReduce { (v1, v2) -> v1 + v2 }.toList() * ``` * will produce `[1, 3, 6, 10]` */ @ExperimentalCoroutinesApi -public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = flow { +public fun Flow.runningReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = flow { var accumulator: Any? = NULL collect { value -> accumulator = if (accumulator === NULL) { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt index 0108ea174e..20e07873bb 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ScanTest.kt @@ -12,7 +12,7 @@ class ScanTest : TestBase() { @Test fun testScan() = runTest { val flow = flowOf(1, 2, 3, 4, 5) - val result = flow.scanReduce { acc, v -> acc + v }.toList() + val result = flow.runningReduce { acc, v -> acc + v }.toList() assertEquals(listOf(1, 3, 6, 10, 15), result) } @@ -26,13 +26,13 @@ class ScanTest : TestBase() { @Test fun testNulls() = runTest { val flow = flowOf(null, 2, null, null, null, 5) - val result = flow.scanReduce { acc, v -> if (v == null) acc else (if (acc == null) v else acc + v) }.toList() + val result = flow.runningReduce { acc, v -> if (v == null) acc else (if (acc == null) v else acc + v) }.toList() assertEquals(listOf(null, 2, 2, 2, 2, 7), result) } @Test fun testEmptyFlow() = runTest { - val result = emptyFlow().scanReduce { _, _ -> 1 }.toList() + val result = emptyFlow().runningReduce { _, _ -> 1 }.toList() assertTrue(result.isEmpty()) } @@ -49,7 +49,7 @@ class ScanTest : TestBase() { emit(1) emit(2) } - }.scanReduce { _, value -> + }.runningReduce { _, value -> expect(value) // 2 latch.receive() throw TestException() @@ -59,7 +59,7 @@ class ScanTest : TestBase() { finish(4) } - public operator fun Collection.plus(element: T): List { + private operator fun Collection.plus(element: T): List { val result = ArrayList(size + 1) result.addAll(this) result.add(element) From ccdc56320ab60eb15501740a9558c26931c0a09a Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 18:30:32 +0300 Subject: [PATCH 080/257] Rename 'foo' function to 'simple' in flow docs (#2078) The name 'foo' might be hard to get for novice developers. It is really hard to find a "meaningful" name for all examples in the docs, so this is simply a consistent solution. The alternative is to something like "simpleFlow", but adding a "Flow" suffix to the name implicitly condones Hungarian notation, which is not aligned with Kotlin coding style. --- docs/flow.md | 128 +++++++++--------- .../jvm/test/guide/example-flow-01.kt | 4 +- .../jvm/test/guide/example-flow-02.kt | 4 +- .../jvm/test/guide/example-flow-03.kt | 4 +- .../jvm/test/guide/example-flow-04.kt | 4 +- .../jvm/test/guide/example-flow-05.kt | 6 +- .../jvm/test/guide/example-flow-06.kt | 4 +- .../jvm/test/guide/example-flow-13.kt | 6 +- .../jvm/test/guide/example-flow-14.kt | 4 +- .../jvm/test/guide/example-flow-15.kt | 4 +- .../jvm/test/guide/example-flow-16.kt | 4 +- .../jvm/test/guide/example-flow-17.kt | 4 +- .../jvm/test/guide/example-flow-18.kt | 4 +- .../jvm/test/guide/example-flow-19.kt | 4 +- .../jvm/test/guide/example-flow-26.kt | 4 +- .../jvm/test/guide/example-flow-27.kt | 4 +- .../jvm/test/guide/example-flow-28.kt | 4 +- .../jvm/test/guide/example-flow-29.kt | 4 +- .../jvm/test/guide/example-flow-30.kt | 4 +- .../jvm/test/guide/example-flow-31.kt | 4 +- .../jvm/test/guide/example-flow-32.kt | 4 +- .../jvm/test/guide/example-flow-33.kt | 4 +- .../jvm/test/guide/example-flow-34.kt | 4 +- .../jvm/test/guide/test/FlowGuideTest.kt | 4 +- 24 files changed, 112 insertions(+), 112 deletions(-) diff --git a/docs/flow.md b/docs/flow.md index bb35b2ecec..0405e80df4 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -54,16 +54,16 @@ multiple asynchronously computed values? This is where Kotlin Flows come in. ### Representing multiple values Multiple values can be represented in Kotlin using [collections]. -For example, we can have a function `foo()` that returns a [List] +For example, we can have a `simple` function that returns a [List] of three numbers and then print them all using [forEach]:
```kotlin -fun foo(): List = listOf(1, 2, 3) +fun simple(): List = listOf(1, 2, 3) fun main() { - foo().forEach { value -> println(value) } + simple().forEach { value -> println(value) } } ``` @@ -89,7 +89,7 @@ If we are computing the numbers with some CPU-consuming blocking code
```kotlin -fun foo(): Sequence = sequence { // sequence builder +fun simple(): Sequence = sequence { // sequence builder for (i in 1..3) { Thread.sleep(100) // pretend we are computing it yield(i) // yield next value @@ -97,7 +97,7 @@ fun foo(): Sequence = sequence { // sequence builder } fun main() { - foo().forEach { value -> println(value) } + simple().forEach { value -> println(value) } } ``` @@ -116,7 +116,7 @@ This code outputs the same numbers, but it waits 100ms before printing each one. #### Suspending functions However, this computation blocks the main thread that is running the code. -When these values are computed by asynchronous code we can mark the function `foo` with a `suspend` modifier, +When these values are computed by asynchronous code we can mark the `simple` function with a `suspend` modifier, so that it can perform its work without blocking and return the result as a list:
@@ -125,13 +125,13 @@ so that it can perform its work without blocking and return the result as a list import kotlinx.coroutines.* //sampleStart -suspend fun foo(): List { +suspend fun simple(): List { delay(1000) // pretend we are doing something asynchronous here return listOf(1, 2, 3) } fun main() = runBlocking { - foo().forEach { value -> println(value) } + simple().forEach { value -> println(value) } } //sampleEnd ``` @@ -160,7 +160,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { // flow builder +fun simple(): Flow = flow { // flow builder for (i in 1..3) { delay(100) // pretend we are doing something useful here emit(i) // emit next value @@ -176,7 +176,7 @@ fun main() = runBlocking { } } // Collect the flow - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } //sampleEnd ``` @@ -203,11 +203,11 @@ Notice the following differences in the code with the [Flow] from the earlier ex * A builder function for [Flow] type is called [flow]. * Code inside the `flow { ... }` builder block can suspend. -* The function `foo()` is no longer marked with `suspend` modifier. +* The `simple` function is no longer marked with `suspend` modifier. * Values are _emitted_ from the flow using [emit][FlowCollector.emit] function. * Values are _collected_ from the flow using [collect][collect] function. -> We can replace [delay] with `Thread.sleep` in the body of `foo`'s `flow { ... }` and see that the main +> We can replace [delay] with `Thread.sleep` in the body of `simple`'s `flow { ... }` and see that the main thread is blocked in this case. ### Flows are cold @@ -222,7 +222,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { println("Flow started") for (i in 1..3) { delay(100) @@ -231,8 +231,8 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - println("Calling foo...") - val flow = foo() + println("Calling simple function...") + val flow = simple() println("Calling collect...") flow.collect { value -> println(value) } println("Calling collect again...") @@ -248,7 +248,7 @@ fun main() = runBlocking { Which prints: ```text -Calling foo... +Calling simple function... Calling collect... Flow started 1 @@ -263,8 +263,8 @@ Flow started -This is a key reason the `foo()` function (which returns a flow) is not marked with `suspend` modifier. -By itself, `foo()` returns quickly and does not wait for anything. The flow starts every time it is collected, +This is a key reason the `simple` function (which returns a flow) is not marked with `suspend` modifier. +By itself, `simple()` call returns quickly and does not wait for anything. The flow starts every time it is collected, that is why we see "Flow started" when we call `collect` again. ### Flow cancellation @@ -283,7 +283,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) println("Emitting $i") @@ -293,7 +293,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { withTimeoutOrNull(250) { // Timeout after 250ms - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } println("Done") } @@ -304,7 +304,7 @@ fun main() = runBlocking { > You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-06.kt). -Notice how only two numbers get emitted by the flow in `foo()` function, producing the following output: +Notice how only two numbers get emitted by the flow in the `simple` function, producing the following output: ```text Emitting 1 @@ -590,14 +590,14 @@ Filter 5 ### Flow context Collection of a flow always happens in the context of the calling coroutine. For example, if there is -a `foo` flow, then the following code runs in the context specified -by the author of this code, regardless of the implementation details of the `foo` flow: +a `simple` flow, then the following code runs in the context specified +by the author of this code, regardless of the implementation details of the `simple` flow:
```kotlin withContext(context) { - foo.collect { value -> + simple().collect { value -> println(value) // run in the specified context } } @@ -610,7 +610,7 @@ withContext(context) { This property of a flow is called _context preservation_. So, by default, code in the `flow { ... }` builder runs in the context that is provided by a collector -of the corresponding flow. For example, consider the implementation of `foo` that prints the thread +of the corresponding flow. For example, consider the implementation of a `simple` function that prints the thread it is called on and emits three numbers:
@@ -622,15 +622,15 @@ import kotlinx.coroutines.flow.* fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") //sampleStart -fun foo(): Flow = flow { - log("Started foo flow") +fun simple(): Flow = flow { + log("Started simple flow") for (i in 1..3) { emit(i) } } fun main() = runBlocking { - foo().collect { value -> log("Collected $value") } + simple().collect { value -> log("Collected $value") } } //sampleEnd ``` @@ -642,7 +642,7 @@ fun main() = runBlocking { Running this code produces: ```text -[main @coroutine#1] Started foo flow +[main @coroutine#1] Started simple flow [main @coroutine#1] Collected 1 [main @coroutine#1] Collected 2 [main @coroutine#1] Collected 3 @@ -650,7 +650,7 @@ Running this code produces: -Since `foo().collect` is called from the main thread, the body of `foo`'s flow is also called in the main thread. +Since `simple().collect` is called from the main thread, the body of `simple`'s flow is also called in the main thread. This is the perfect default for fast-running or asynchronous code that does not care about the execution context and does not block the caller. @@ -670,7 +670,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { // The WRONG way to change context for CPU-consuming code in flow builder kotlinx.coroutines.withContext(Dispatchers.Default) { for (i in 1..3) { @@ -681,7 +681,7 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } //sampleEnd ``` @@ -717,7 +717,7 @@ import kotlinx.coroutines.flow.* fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { Thread.sleep(100) // pretend we are computing it in CPU-consuming way log("Emitting $i") @@ -726,7 +726,7 @@ fun foo(): Flow = flow { }.flowOn(Dispatchers.Default) // RIGHT way to change context for CPU-consuming code in flow builder fun main() = runBlocking { - foo().collect { value -> + simple().collect { value -> log("Collected $value") } } @@ -757,7 +757,7 @@ creates another coroutine for an upstream flow when it has to change the [Corout Running different parts of a flow in different coroutines can be helpful from the standpoint of the overall time it takes to collect the flow, especially when long-running asynchronous operations are involved. For example, consider a case when -the emission by `foo()` flow is slow, taking 100 ms to produce an element; and collector is also slow, +the emission by a `simple` flow is slow, taking 100 ms to produce an element; and collector is also slow, taking 300 ms to process an element. Let's see how long it takes to collect such a flow with three numbers:
@@ -768,7 +768,7 @@ import kotlinx.coroutines.flow.* import kotlin.system.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -777,7 +777,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { val time = measureTimeMillis { - foo().collect { value -> + simple().collect { value -> delay(300) // pretend we are processing it for 300 ms println(value) } @@ -802,7 +802,7 @@ Collected in 1220 ms -We can use a [buffer] operator on a flow to run emitting code of `foo()` concurrently with collecting code, +We can use a [buffer] operator on a flow to run emitting code of the `simple` flow concurrently with collecting code, as opposed to running them sequentially:
@@ -812,7 +812,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -822,7 +822,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { //sampleStart val time = measureTimeMillis { - foo() + simple() .buffer() // buffer emissions, don't wait .collect { value -> delay(300) // pretend we are processing it for 300 ms @@ -867,7 +867,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -877,7 +877,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { //sampleStart val time = measureTimeMillis { - foo() + simple() .conflate() // conflate emissions, don't process each one .collect { value -> delay(300) // pretend we are processing it for 300 ms @@ -918,7 +918,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -928,7 +928,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { //sampleStart val time = measureTimeMillis { - foo() + simple() .collectLatest { value -> // cancel & restart on the latest value println("Collecting $value") delay(300) // pretend we are processing it for 300 ms @@ -1279,7 +1279,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") emit(i) // emit next value @@ -1288,7 +1288,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { try { - foo().collect { value -> + simple().collect { value -> println(value) check(value <= 1) { "Collected $value" } } @@ -1329,7 +1329,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") @@ -1343,7 +1343,7 @@ fun foo(): Flow = fun main() = runBlocking { try { - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } catch (e: Throwable) { println("Caught $e") } @@ -1390,7 +1390,7 @@ For example, let us emit the text on catching an exception: import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") @@ -1404,7 +1404,7 @@ fun foo(): Flow = fun main() = runBlocking { //sampleStart - foo() + simple() .catch { e -> emit("Caught $e") } // emit on exception .collect { value -> println(value) } //sampleEnd @@ -1437,7 +1437,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") emit(i) @@ -1445,7 +1445,7 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - foo() + simple() .catch { e -> println("Caught $e") } // does not catch downstream exceptions .collect { value -> check(value <= 1) { "Collected $value" } @@ -1481,7 +1481,7 @@ be triggered by a call to `collect()` without parameters: import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") emit(i) @@ -1490,7 +1490,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { //sampleStart - foo() + simple() .onEach { value -> check(value <= 1) { "Collected $value" } println(value) @@ -1532,11 +1532,11 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = (1..3).asFlow() +fun simple(): Flow = (1..3).asFlow() fun main() = runBlocking { try { - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } finally { println("Done") } @@ -1548,7 +1548,7 @@ fun main() = runBlocking { > You can get the full code from [here](../kotlinx-coroutines-core/jvm/test/guide/example-flow-31.kt). -This code prints three numbers produced by the `foo()` flow followed by a "Done" string: +This code prints three numbers produced by the `simple` flow followed by a "Done" string: ```text 1 @@ -1572,11 +1572,11 @@ The previous example can be rewritten using an [onCompletion] operator and produ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = (1..3).asFlow() +fun simple(): Flow = (1..3).asFlow() fun main() = runBlocking { //sampleStart - foo() + simple() .onCompletion { println("Done") } .collect { value -> println(value) } //sampleEnd @@ -1595,7 +1595,7 @@ Done The key advantage of [onCompletion] is a nullable `Throwable` parameter of the lambda that can be used to determine whether the flow collection was completed normally or exceptionally. In the following -example the `foo()` flow throws an exception after emitting the number 1: +example the `simple` flow throws an exception after emitting the number 1:
@@ -1604,13 +1604,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = flow { +fun simple(): Flow = flow { emit(1) throw RuntimeException() } fun main() = runBlocking { - foo() + simple() .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") } .catch { cause -> println("Caught exception") } .collect { value -> println(value) } @@ -1647,10 +1647,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart -fun foo(): Flow = (1..3).asFlow() +fun simple(): Flow = (1..3).asFlow() fun main() = runBlocking { - foo() + simple() .onCompletion { cause -> println("Flow completed with $cause") } .collect { value -> check(value <= 1) { "Collected $value" } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-01.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-01.kt index df14603db0..7c6c1578f8 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-01.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-01.kt @@ -5,8 +5,8 @@ // This file was automatically generated from flow.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.exampleFlow01 -fun foo(): List = listOf(1, 2, 3) +fun simple(): List = listOf(1, 2, 3) fun main() { - foo().forEach { value -> println(value) } + simple().forEach { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-02.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-02.kt index fcb61b9d1d..e3fabe37f8 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-02.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-02.kt @@ -5,7 +5,7 @@ // This file was automatically generated from flow.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.exampleFlow02 -fun foo(): Sequence = sequence { // sequence builder +fun simple(): Sequence = sequence { // sequence builder for (i in 1..3) { Thread.sleep(100) // pretend we are computing it yield(i) // yield next value @@ -13,5 +13,5 @@ fun foo(): Sequence = sequence { // sequence builder } fun main() { - foo().forEach { value -> println(value) } + simple().forEach { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-03.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-03.kt index ba94b2f8f6..61601dd94d 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-03.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-03.kt @@ -7,11 +7,11 @@ package kotlinx.coroutines.guide.exampleFlow03 import kotlinx.coroutines.* -suspend fun foo(): List { +suspend fun simple(): List { delay(1000) // pretend we are doing something asynchronous here return listOf(1, 2, 3) } fun main() = runBlocking { - foo().forEach { value -> println(value) } + simple().forEach { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-04.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-04.kt index 3e3aea0f55..c91f70403e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-04.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-04.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow04 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { // flow builder +fun simple(): Flow = flow { // flow builder for (i in 1..3) { delay(100) // pretend we are doing something useful here emit(i) // emit next value @@ -24,5 +24,5 @@ fun main() = runBlocking { } } // Collect the flow - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-05.kt index 6d0e451923..788d941d53 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-05.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-05.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow05 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { println("Flow started") for (i in 1..3) { delay(100) @@ -17,8 +17,8 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - println("Calling foo...") - val flow = foo() + println("Calling simple function...") + val flow = simple() println("Calling collect...") flow.collect { value -> println(value) } println("Calling collect again...") diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-06.kt index 9d9348ea5c..bd4058e757 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-06.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-06.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow06 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) println("Emitting $i") @@ -18,7 +18,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { withTimeoutOrNull(250) { // Timeout after 250ms - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } println("Done") } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-13.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-13.kt index 4feacc6d25..945ce8954d 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-13.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-13.kt @@ -10,13 +10,13 @@ import kotlinx.coroutines.flow.* fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") -fun foo(): Flow = flow { - log("Started foo flow") +fun simple(): Flow = flow { + log("Started simple flow") for (i in 1..3) { emit(i) } } fun main() = runBlocking { - foo().collect { value -> log("Collected $value") } + simple().collect { value -> log("Collected $value") } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-14.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-14.kt index c0f2320490..b5fc35e24e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-14.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-14.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow14 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { // The WRONG way to change context for CPU-consuming code in flow builder kotlinx.coroutines.withContext(Dispatchers.Default) { for (i in 1..3) { @@ -19,5 +19,5 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-15.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-15.kt index 8f0e395ce4..0218e99532 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-15.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-15.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.* fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { Thread.sleep(100) // pretend we are computing it in CPU-consuming way log("Emitting $i") @@ -19,7 +19,7 @@ fun foo(): Flow = flow { }.flowOn(Dispatchers.Default) // RIGHT way to change context for CPU-consuming code in flow builder fun main() = runBlocking { - foo().collect { value -> + simple().collect { value -> log("Collected $value") } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-16.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-16.kt index d2f41ff6cc..7f3414fff4 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-16.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-16.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -18,7 +18,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { val time = measureTimeMillis { - foo().collect { value -> + simple().collect { value -> delay(300) // pretend we are processing it for 300 ms println(value) } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-17.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-17.kt index 5db79df185..ed7161783f 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-17.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-17.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -18,7 +18,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { val time = measureTimeMillis { - foo() + simple() .buffer() // buffer emissions, don't wait .collect { value -> delay(300) // pretend we are processing it for 300 ms diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-18.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-18.kt index 3c1a8a1b7c..fc7bdef561 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-18.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-18.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -18,7 +18,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { val time = measureTimeMillis { - foo() + simple() .conflate() // conflate emissions, don't process each one .collect { value -> delay(300) // pretend we are processing it for 300 ms diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-19.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-19.kt index 1725276bf8..f4ee2193c0 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-19.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-19.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.system.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are asynchronously waiting 100 ms emit(i) // emit next value @@ -18,7 +18,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { val time = measureTimeMillis { - foo() + simple() .collectLatest { value -> // cancel & restart on the latest value println("Collecting $value") delay(300) // pretend we are processing it for 300 ms diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-26.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-26.kt index e489c3f2ed..95f9a113b5 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-26.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-26.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow26 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") emit(i) // emit next value @@ -17,7 +17,7 @@ fun foo(): Flow = flow { fun main() = runBlocking { try { - foo().collect { value -> + simple().collect { value -> println(value) check(value <= 1) { "Collected $value" } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-27.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-27.kt index f9ef9793cf..3f34a7674f 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-27.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-27.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow27 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") @@ -22,7 +22,7 @@ fun foo(): Flow = fun main() = runBlocking { try { - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } catch (e: Throwable) { println("Caught $e") } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-28.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-28.kt index 84fc69fd7b..02b231e985 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-28.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-28.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow28 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") @@ -21,7 +21,7 @@ fun foo(): Flow = } fun main() = runBlocking { - foo() + simple() .catch { e -> emit("Caught $e") } // emit on exception .collect { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-29.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-29.kt index 6c60c5d9d2..5ec37afd1e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-29.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-29.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow29 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") emit(i) @@ -16,7 +16,7 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - foo() + simple() .catch { e -> println("Caught $e") } // does not catch downstream exceptions .collect { value -> check(value <= 1) { "Collected $value" } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-30.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-30.kt index e21c77fcf3..e787ca39ae 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-30.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-30.kt @@ -8,7 +8,7 @@ package kotlinx.coroutines.guide.exampleFlow30 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { for (i in 1..3) { println("Emitting $i") emit(i) @@ -16,7 +16,7 @@ fun foo(): Flow = flow { } fun main() = runBlocking { - foo() + simple() .onEach { value -> check(value <= 1) { "Collected $value" } println(value) diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-31.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-31.kt index 9b2855ef09..19fcb1c626 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-31.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-31.kt @@ -8,11 +8,11 @@ package kotlinx.coroutines.guide.exampleFlow31 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = (1..3).asFlow() +fun simple(): Flow = (1..3).asFlow() fun main() = runBlocking { try { - foo().collect { value -> println(value) } + simple().collect { value -> println(value) } } finally { println("Done") } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-32.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-32.kt index 3ad74ae5f1..984895753d 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-32.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-32.kt @@ -8,10 +8,10 @@ package kotlinx.coroutines.guide.exampleFlow32 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = (1..3).asFlow() +fun simple(): Flow = (1..3).asFlow() fun main() = runBlocking { - foo() + simple() .onCompletion { println("Done") } .collect { value -> println(value) } } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-33.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-33.kt index c0e0ab3d5b..9f86765afd 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-33.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-33.kt @@ -8,13 +8,13 @@ package kotlinx.coroutines.guide.exampleFlow33 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = flow { +fun simple(): Flow = flow { emit(1) throw RuntimeException() } fun main() = runBlocking { - foo() + simple() .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") } .catch { cause -> println("Caught exception") } .collect { value -> println(value) } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-flow-34.kt b/kotlinx-coroutines-core/jvm/test/guide/example-flow-34.kt index 4b79a73683..b2cee7f9fb 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-flow-34.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-flow-34.kt @@ -8,10 +8,10 @@ package kotlinx.coroutines.guide.exampleFlow34 import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -fun foo(): Flow = (1..3).asFlow() +fun simple(): Flow = (1..3).asFlow() fun main() = runBlocking { - foo() + simple() .onCompletion { cause -> println("Flow completed with $cause") } .collect { value -> check(value <= 1) { "Collected $value" } diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index 39a7853b5f..b3574db008 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -50,7 +50,7 @@ class FlowGuideTest { @Test fun testExampleFlow05() { test("ExampleFlow05") { kotlinx.coroutines.guide.exampleFlow05.main() }.verifyLines( - "Calling foo...", + "Calling simple function...", "Calling collect...", "Flow started", "1", @@ -139,7 +139,7 @@ class FlowGuideTest { @Test fun testExampleFlow13() { test("ExampleFlow13") { kotlinx.coroutines.guide.exampleFlow13.main() }.verifyLinesFlexibleThread( - "[main @coroutine#1] Started foo flow", + "[main @coroutine#1] Started simple flow", "[main @coroutine#1] Collected 1", "[main @coroutine#1] Collected 2", "[main @coroutine#1] Collected 3" From d7189701495c24fe99f394a721033cfe9f7a2436 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 18:33:31 +0300 Subject: [PATCH 081/257] Fix race in Flow.asPublisher (#2124) The race was leading to emitting more items via onNext than requested, the corresponding stress-test was added, too Fixes #2109 --- .../test/FlowAsPublisherTest.kt | 9 +- .../src/ReactiveFlow.kt | 46 +++--- .../test/FlowAsPublisherTest.kt | 9 +- .../test/PublisherRequestStressTest.kt | 141 ++++++++++++++++++ 4 files changed, 170 insertions(+), 35 deletions(-) create mode 100644 reactive/kotlinx-coroutines-reactive/test/PublisherRequestStressTest.kt diff --git a/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt index 8017ee5b4f..488695dea2 100644 --- a/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt +++ b/reactive/kotlinx-coroutines-jdk9/test/FlowAsPublisherTest.kt @@ -16,10 +16,10 @@ class FlowAsPublisherTest : TestBase() { fun testErrorOnCancellationIsReported() { expect(1) flow { - emit(2) try { - hang { expect(3) } + emit(2) } finally { + expect(3) throw TestException() } }.asPublisher().subscribe(object : JFlow.Subscriber { @@ -52,12 +52,11 @@ class FlowAsPublisherTest : TestBase() { expect(1) flow { emit(2) - hang { expect(3) } }.asPublisher().subscribe(object : JFlow.Subscriber { private lateinit var subscription: JFlow.Subscription override fun onComplete() { - expect(4) + expect(3) } override fun onSubscribe(s: JFlow.Subscription?) { @@ -74,6 +73,6 @@ class FlowAsPublisherTest : TestBase() { expectUnreached() } }) - finish(5) + finish(4) } } diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index 96ae6287c1..efa9c9c9f1 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -166,11 +166,12 @@ private class FlowAsPublisher(private val flow: Flow) : Publisher public class FlowSubscription( @JvmField public val flow: Flow, @JvmField public val subscriber: Subscriber -) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, false) { +) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, true) { private val requested = atomic(0L) - private val producer = atomic?>(null) + private val producer = atomic?>(createInitialContinuation()) - override fun onStart() { + // This code wraps startCoroutineCancellable into continuation + private fun createInitialContinuation(): Continuation = Continuation(coroutineContext) { ::flowProcessing.startCoroutineCancellable(this) } @@ -197,19 +198,17 @@ public class FlowSubscription( */ private suspend fun consumeFlow() { flow.collect { value -> - /* - * Flow is scopeless, thus if it's not active, its subscription was cancelled. - * No intermediate "child failed, but flow coroutine is not" states are allowed. - */ - coroutineContext.ensureActive() - if (requested.value <= 0L) { + // Emit the value + subscriber.onNext(value) + // Suspend if needed before requesting the next value + if (requested.decrementAndGet() <= 0) { suspendCancellableCoroutine { producer.value = it - if (requested.value != 0L) it.resumeSafely() } + } else { + // check for cancellation if we don't suspend + coroutineContext.ensureActive() } - requested.decrementAndGet() - subscriber.onNext(value) } } @@ -218,22 +217,19 @@ public class FlowSubscription( } override fun request(n: Long) { - if (n <= 0) { - return - } - start() - requested.update { value -> + if (n <= 0) return + val old = requested.getAndUpdate { value -> val newValue = value + n if (newValue <= 0L) Long.MAX_VALUE else newValue } - val producer = producer.getAndSet(null) ?: return - producer.resumeSafely() - } - - private fun CancellableContinuation.resumeSafely() { - val token = tryResume(Unit) - if (token != null) { - completeResume(token) + if (old <= 0L) { + assert(old == 0L) + // Emitter is not started yet or has suspended -- spin on race with suspendCancellableCoroutine + while(true) { + val producer = producer.getAndSet(null) ?: continue // spin if not set yet + producer.resume(Unit) + break + } } } } diff --git a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt index 8633492810..c044d92725 100644 --- a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt @@ -16,10 +16,10 @@ class FlowAsPublisherTest : TestBase() { fun testErrorOnCancellationIsReported() { expect(1) flow { - emit(2) try { - hang { expect(3) } + emit(2) } finally { + expect(3) throw TestException() } }.asPublisher().subscribe(object : Subscriber { @@ -52,12 +52,11 @@ class FlowAsPublisherTest : TestBase() { expect(1) flow { emit(2) - hang { expect(3) } }.asPublisher().subscribe(object : Subscriber { private lateinit var subscription: Subscription override fun onComplete() { - expect(4) + expect(3) } override fun onSubscribe(s: Subscription?) { @@ -74,6 +73,6 @@ class FlowAsPublisherTest : TestBase() { expectUnreached() } }) - finish(5) + finish(4) } } diff --git a/reactive/kotlinx-coroutines-reactive/test/PublisherRequestStressTest.kt b/reactive/kotlinx-coroutines-reactive/test/PublisherRequestStressTest.kt new file mode 100644 index 0000000000..736a66404f --- /dev/null +++ b/reactive/kotlinx-coroutines-reactive/test/PublisherRequestStressTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.reactive + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import org.junit.* +import org.reactivestreams.* +import java.util.concurrent.* +import java.util.concurrent.atomic.* +import kotlin.coroutines.* +import kotlin.random.* + +/** + * This stress-test is self-contained reproducer for the race in [Flow.asPublisher] extension + * that was originally reported in the issue + * [#2109](https://github.com/Kotlin/kotlinx.coroutines/issues/2109). + * The original reproducer used a flow that loads a file using AsynchronousFileChannel + * (that issues completion callbacks from multiple threads) + * and uploads it to S3 via Amazon SDK, which internally uses netty for I/O + * (which uses a single thread for connection-related callbacks). + * + * This stress-test essentially mimics the logic in multiple interacting threads: several emitter threads that form + * the flow and a single requesting thread works on the subscriber's side to periodically request more + * values when the number of items requested drops below the threshold. + */ +@Suppress("ReactiveStreamsSubscriberImplementation") +class PublisherRequestStressTest : TestBase() { + private val testDurationSec = 3 * stressTestMultiplier + + // Original code in Amazon SDK uses 4 and 16 as low/high watermarks. + // There constants were chosen so that problem reproduces asap with particular this code. + private val minDemand = 8L + private val maxDemand = 16L + + private val nEmitThreads = 4 + + private val emitThreadNo = AtomicInteger() + + private val emitPool = Executors.newFixedThreadPool(nEmitThreads) { r -> + Thread(r, "PublisherRequestStressTest-emit-${emitThreadNo.incrementAndGet()}") + } + + private val reqPool = Executors.newSingleThreadExecutor { r -> + Thread(r, "PublisherRequestStressTest-req") + } + + private val nextValue = AtomicLong(0) + + @After + fun tearDown() { + emitPool.shutdown() + reqPool.shutdown() + emitPool.awaitTermination(10, TimeUnit.SECONDS) + reqPool.awaitTermination(10, TimeUnit.SECONDS) + } + + private lateinit var subscription: Subscription + + @Test + fun testRequestStress() { + val expectedValue = AtomicLong(0) + val requestedTill = AtomicLong(0) + val completionLatch = CountDownLatch(1) + val callingOnNext = AtomicInteger() + + val publisher = mtFlow().asPublisher() + var error = false + + publisher.subscribe(object : Subscriber { + private var demand = 0L // only updated from reqPool + + override fun onComplete() { + completionLatch.countDown() + } + + override fun onSubscribe(sub: Subscription) { + subscription = sub + maybeRequestMore() + } + + private fun maybeRequestMore() { + if (demand >= minDemand) return + val nextDemand = Random.nextLong(minDemand + 1..maxDemand) + val more = nextDemand - demand + demand = nextDemand + requestedTill.addAndGet(more) + subscription.request(more) + } + + override fun onNext(value: Long) { + check(callingOnNext.getAndIncrement() == 0) // make sure it is not concurrent + // check for expected value + check(value == expectedValue.get()) + // check that it does not exceed requested values + check(value < requestedTill.get()) + val nextExpected = value + 1 + expectedValue.set(nextExpected) + // send more requests from request thread + reqPool.execute { + demand-- // processed an item + maybeRequestMore() + } + callingOnNext.decrementAndGet() + } + + override fun onError(ex: Throwable?) { + error = true + error("Failed", ex) + } + }) + var prevExpected = -1L + for (second in 1..testDurationSec) { + if (error) break + Thread.sleep(1000) + val expected = expectedValue.get() + println("$second: expectedValue = $expected") + check(expected > prevExpected) // should have progress + prevExpected = expected + } + if (!error) { + subscription.cancel() + completionLatch.await() + } + } + + private fun mtFlow(): Flow = flow { + while (currentCoroutineContext().isActive) { + emit(aWait()) + } + } + + private suspend fun aWait(): Long = suspendCancellableCoroutine { cont -> + emitPool.execute(Runnable { + cont.resume(nextValue.getAndIncrement()) + }) + } +} \ No newline at end of file From b7d651811f9bc7f3c7ad21aa20eafef281c926ba Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 19:31:13 +0300 Subject: [PATCH 082/257] Consistent toString for MainCoroutineDispatcher implementations (#2131) So that is shows as "Dispatchers.Main" and "Dispatchers.Main.immediate". Also remove hardcoded "Main" name in places of code where it is not needed anymore. --- .../api/kotlinx-coroutines-core.api | 2 ++ .../common/src/MainCoroutineDispatcher.kt | 23 +++++++++++++++++++ kotlinx-coroutines-core/js/src/Dispatchers.kt | 2 +- .../jvm/src/internal/MainDispatchers.kt | 2 +- .../jvm/test/DispatchersToStringTest.kt | 18 +++++++++++++++ .../native/src/Dispatchers.kt | 2 +- .../src/HandlerDispatcher.kt | 14 +++++------ .../test/HandlerDispatcherTest.kt | 10 ++++++-- .../src/JavaFxDispatcher.kt | 4 ++-- .../test/JavaFxDispatcherTest.kt | 8 +++++++ .../src/SwingDispatcher.kt | 4 ++-- ui/kotlinx-coroutines-swing/test/SwingTest.kt | 6 +++++ 12 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/DispatchersToStringTest.kt diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 8fcd14dc13..2f1fd1517a 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -446,6 +446,8 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public abstract class kotlinx/coroutines/MainCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher { public fun ()V public abstract fun getImmediate ()Lkotlinx/coroutines/MainCoroutineDispatcher; + public fun toString ()Ljava/lang/String; + protected final fun toStringInternalImpl ()Ljava/lang/String; } public final class kotlinx/coroutines/NonCancellable : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/Job { diff --git a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt index 3f2ddcd69f..daba38f0fd 100644 --- a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt @@ -43,4 +43,27 @@ public abstract class MainCoroutineDispatcher : CoroutineDispatcher() { * [Dispatchers.Main] supports immediate execution for Android, JavaFx and Swing platforms. */ public abstract val immediate: MainCoroutineDispatcher + + /** + * Returns a name of this main dispatcher for debugging purposes. This implementation returns + * `Dispatchers.Main` or `Dispatchers.Main.immediate` if it is the same as the corresponding + * reference in [Dispatchers] or a short class-name representation with address otherwise. + */ + override fun toString(): String = toStringInternalImpl() ?: "$classSimpleName@$hexAddress" + + /** + * Internal method for more specific [toString] implementations. It returns non-null + * string if this dispatcher is set in the platform as the main one. + * @suppress + */ + @InternalCoroutinesApi + protected fun toStringInternalImpl(): String? { + val main = Dispatchers.Main + if (this === main) return "Dispatchers.Main" + val immediate = + try { main.immediate } + catch (e: UnsupportedOperationException) { null } + if (this === immediate) return "Dispatchers.Main.immediate" + return null + } } diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 995801ea0d..033b39c7e0 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -26,5 +26,5 @@ private class JsMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutin override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = delegate.toString() + override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index b3e49752e0..ddfcdbb142 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -114,7 +114,7 @@ private class MissingMainCoroutineDispatcher( } } - override fun toString(): String = "Main[missing${if (cause != null) ", cause=$cause" else ""}]" + override fun toString(): String = "Dispatchers.Main[missing${if (cause != null) ", cause=$cause" else ""}]" } /** diff --git a/kotlinx-coroutines-core/jvm/test/DispatchersToStringTest.kt b/kotlinx-coroutines-core/jvm/test/DispatchersToStringTest.kt new file mode 100644 index 0000000000..7adaa2a4b6 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/DispatchersToStringTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class DispatchersToStringTest { + @Test + fun testStrings() { + assertEquals("Dispatchers.Unconfined", Dispatchers.Unconfined.toString()) + assertEquals("Dispatchers.Default", Dispatchers.Default.toString()) + assertEquals("Dispatchers.IO", Dispatchers.IO.toString()) + assertEquals("Dispatchers.Main[missing]", Dispatchers.Main.toString()) + assertEquals("Dispatchers.Main[missing]", Dispatchers.Main.immediate.toString()) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt index 6c650046a0..aca1cc0693 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt @@ -26,5 +26,5 @@ private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoro override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = delegate.toString() + override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index af1ab4331b..1693409875 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -52,7 +52,7 @@ public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay { internal class AndroidDispatcherFactory : MainDispatcherFactory { override fun createDispatcher(allFactories: List) = - HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main") + HandlerContext(Looper.getMainLooper().asHandler(async = true)) override fun hintOnError(): String? = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used" @@ -97,7 +97,7 @@ internal fun Looper.asHandler(async: Boolean): Handler { @JvmField @Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN) -internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main") }.getOrNull() +internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler(async = true)) }.getOrNull() /** * Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler]. @@ -149,12 +149,10 @@ internal class HandlerContext private constructor( } } - override fun toString(): String = - if (name != null) { - if (invokeImmediately) "$name [immediate]" else name - } else { - handler.toString() - } + override fun toString(): String = toStringInternalImpl() ?: run { + val str = name ?: handler.toString() + if (invokeImmediately) "$str.immediate" else str + } override fun equals(other: Any?): Boolean = other is HandlerContext && other.handler === handler override fun hashCode(): Int = System.identityHashCode(handler) diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt index 1501639e5d..55decde61b 100644 --- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt @@ -123,8 +123,8 @@ class HandlerDispatcherTest : TestBase() { ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28) val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName") assertEquals("testName", main.toString()) - assertEquals("testName [immediate]", main.immediate.toString()) - assertEquals("testName [immediate]", main.immediate.immediate.toString()) + assertEquals("testName.immediate", main.immediate.toString()) + assertEquals("testName.immediate", main.immediate.immediate.toString()) } private suspend fun Job.join(mainLooper: ShadowLooper) { @@ -155,4 +155,10 @@ class HandlerDispatcherTest : TestBase() { yield() // yield back finish(5) } + + @Test + fun testMainDispatcherToString() { + assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) + assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) + } } diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index 5f39bf758c..a13a68368e 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -70,7 +70,7 @@ private object ImmediateJavaFxDispatcher : JavaFxDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = !Platform.isFxApplicationThread() - override fun toString() = "JavaFx [immediate]" + override fun toString() = toStringInternalImpl() ?: "JavaFx.immediate" } /** @@ -85,7 +85,7 @@ internal object JavaFx : JavaFxDispatcher() { override val immediate: MainCoroutineDispatcher get() = ImmediateJavaFxDispatcher - override fun toString() = "JavaFx" + override fun toString() = toStringInternalImpl() ?: "JavaFx" } private val pulseTimer by lazy { diff --git a/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt b/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt index 724be6d77b..24c5c132fd 100644 --- a/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt +++ b/ui/kotlinx-coroutines-javafx/test/JavaFxDispatcherTest.kt @@ -7,6 +7,8 @@ package kotlinx.coroutines.javafx import javafx.application.* import kotlinx.coroutines.* import org.junit.* +import org.junit.Test +import kotlin.test.* class JavaFxDispatcherTest : TestBase() { @Before @@ -56,4 +58,10 @@ class JavaFxDispatcherTest : TestBase() { finish(5) } } + + @Test + fun testMainDispatcherToString() { + assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) + assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) + } } \ No newline at end of file diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt index 50efe47126..77f109df91 100644 --- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt +++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt @@ -67,7 +67,7 @@ private object ImmediateSwingDispatcher : SwingDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = !SwingUtilities.isEventDispatchThread() - override fun toString() = "Swing [immediate]" + override fun toString() = toStringInternalImpl() ?: "Swing.immediate" } /** @@ -77,5 +77,5 @@ internal object Swing : SwingDispatcher() { override val immediate: MainCoroutineDispatcher get() = ImmediateSwingDispatcher - override fun toString() = "Swing" + override fun toString() = toStringInternalImpl() ?: "Swing" } diff --git a/ui/kotlinx-coroutines-swing/test/SwingTest.kt b/ui/kotlinx-coroutines-swing/test/SwingTest.kt index cbed5bf1e9..7e53e57b17 100644 --- a/ui/kotlinx-coroutines-swing/test/SwingTest.kt +++ b/ui/kotlinx-coroutines-swing/test/SwingTest.kt @@ -97,4 +97,10 @@ class SwingTest : TestBase() { yield() // yield back finish(5) } + + @Test + fun testMainDispatcherToString() { + assertEquals("Dispatchers.Main", Dispatchers.Main.toString()) + assertEquals("Dispatchers.Main.immediate", Dispatchers.Main.immediate.toString()) + } } \ No newline at end of file From 570537226fed886fb257b532b7c8361fe5254c76 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 19:56:13 +0300 Subject: [PATCH 083/257] Increase timeout in the example to make sure it fails with threads (#2141) Fixes #2135 --- docs/basics.md | 4 ++-- kotlinx-coroutines-core/jvm/test/guide/example-basic-08.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index f171c2c29e..cb64328676 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -338,7 +338,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines launch { - delay(1000L) + delay(5000L) print(".") } } @@ -351,7 +351,7 @@ fun main() = runBlocking { -It launches 100K coroutines and, after a second, each coroutine prints a dot. +It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot. Now, try that with threads. What would happen? (Most likely your code will produce some sort of out-of-memory error) diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-basic-08.kt b/kotlinx-coroutines-core/jvm/test/guide/example-basic-08.kt index ff11eb70d7..bb7786f28a 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-basic-08.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-basic-08.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines launch { - delay(1000L) + delay(5000L) print(".") } } From c05de8807e4bb25fcc70efc74df0bcd3722f2808 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 23:34:40 +0300 Subject: [PATCH 084/257] Coroutines debugger should keep weak references to running coroutines (#2129) It should not prevent garbage-collection of coroutines that were otherwise lost, which included the following practically-useful cases: * Synchronous coroutines (iterator/sequence). * Lazy coroutines that were not started. * Abandoned coroutines that suspend forever without strong references to them in GlobalScope. Two kinds of tests cover this functionality: * A test via FieldWalker ensures that debugger impl does not keep a strong reference. This tests works fast and provides good diagnostics if anything goes wrong, but it is fragile, as futures changes to debugger my introduce static references to running coroutines elsewhere. * A stress-test that ensures that no OOM indeed happens when you run a lot of such lost coroutines. Longer-running, more stable to code change, but fragile in a difference sense as it may accidentally start passing in the future if lots of memory get allocated for tests. Fixes #2117 --- .../api/kotlinx-coroutines-core.api | 24 ++ .../src/debug/internal/ConcurrentWeakMap.kt | 284 ++++++++++++++++++ .../src/debug/internal/DebugCoroutineInfo.kt | 95 ++---- .../debug/internal/DebugCoroutineInfoImpl.kt | 100 ++++++ .../jvm/src/debug/internal/DebugProbesImpl.kt | 140 ++++++--- .../jvm/src/debug/internal/DebuggerInfo.kt | 12 +- .../jvm/src/debug/internal/StackTraceFrame.kt | 17 ++ .../jvm/test/FieldWalker.kt | 41 ++- .../ReusableCancellableContinuationTest.kt | 2 +- .../ConcurrentWeakMapCollectionStressTest.kt | 32 ++ .../ConcurrentWeakMapOperationStressTest.kt | 73 +++++ .../test/internal/ConcurrentWeakMapTest.kt | 46 +++ kotlinx-coroutines-debug/src/CoroutineInfo.kt | 2 + .../test/DebugLeaksStressTest.kt | 56 ++++ .../test/DebugLeaksTest.kt | 57 ++++ .../test/DumpWithoutCreationStackTraceTest.kt | 10 +- .../test/RunningThreadStackMergeTest.kt | 18 +- ...StracktraceUtils.kt => StacktraceUtils.kt} | 0 18 files changed, 856 insertions(+), 153 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/StackTraceFrame.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapCollectionStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt create mode 100644 kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt create mode 100644 kotlinx-coroutines-debug/test/DebugLeaksTest.kt rename kotlinx-coroutines-debug/test/{StracktraceUtils.kt => StacktraceUtils.kt} (100%) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 2f1fd1517a..4609c68e36 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -840,10 +840,34 @@ public final class kotlinx/coroutines/channels/ValueOrClosed { public final synthetic fun unbox-impl ()Ljava/lang/Object; } +public final class kotlinx/coroutines/debug/internal/DebugCoroutineInfo { + public fun (Lkotlinx/coroutines/debug/internal/DebugCoroutineInfoImpl;Lkotlin/coroutines/CoroutineContext;)V + public final fun getContext ()Lkotlin/coroutines/CoroutineContext; + public final fun getCreationStackBottom ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; + public final fun getCreationStackTrace ()Ljava/util/List; + public final fun getLastObservedFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; + public final fun getLastObservedThread ()Ljava/lang/Thread; + public final fun getSequenceNumber ()J + public final fun getState ()Ljava/lang/String; + public final fun lastObservedStackTrace ()Ljava/util/List; +} + public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile { public fun (J)V } +public final class kotlinx/coroutines/debug/internal/DebuggerInfo : java/io/Serializable { + public fun (Lkotlinx/coroutines/debug/internal/DebugCoroutineInfoImpl;Lkotlin/coroutines/CoroutineContext;)V + public final fun getCoroutineId ()Ljava/lang/Long; + public final fun getDispatcher ()Ljava/lang/String; + public final fun getLastObservedStackTrace ()Ljava/util/List; + public final fun getLastObservedThreadName ()Ljava/lang/String; + public final fun getLastObservedThreadState ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getSequenceNumber ()J + public final fun getState ()Ljava/lang/String; +} + public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { public fun ()V public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt new file mode 100644 index 0000000000..79f024cc93 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/ConcurrentWeakMap.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +import kotlinx.atomicfu.* +import kotlinx.coroutines.internal.* +import java.lang.ref.* + +// This is very limited implementation, not suitable as a generic map replacement. +// It has lock-free get and put with synchronized rehash for simplicity (and better CPU usage on contention) +@OptIn(ExperimentalStdlibApi::class) +@Suppress("UNCHECKED_CAST") +internal class ConcurrentWeakMap( + /** + * Weak reference queue is needed when a small key is mapped to a large value and we need to promptly release a + * reference to the value when the key was already disposed. + */ + weakRefQueue: Boolean = false +) : AbstractMutableMap() { + private val _size = atomic(0) + private val core = atomic(Core(MIN_CAPACITY)) + private val weakRefQueue: ReferenceQueue? = if (weakRefQueue) ReferenceQueue() else null + + override val size: Int + get() = _size.value + + private fun decrementSize() { _size.decrementAndGet() } + + override fun get(key: K): V? = core.value.getImpl(key) + + override fun put(key: K, value: V): V? { + var oldValue = core.value.putImpl(key, value) + if (oldValue === REHASH) oldValue = putSynchronized(key, value) + if (oldValue == null) _size.incrementAndGet() + return oldValue as V? + } + + override fun remove(key: K): V? { + var oldValue = core.value.putImpl(key, null) + if (oldValue === REHASH) oldValue = putSynchronized(key, null) + if (oldValue != null) _size.decrementAndGet() + return oldValue as V? + } + + @Synchronized + private fun putSynchronized(key: K, value: V?): V? { + // Note: concurrent put leaves chance that we fail to put even after rehash, we retry until successful + var curCore = core.value + while (true) { + val oldValue = curCore.putImpl(key, value) + if (oldValue !== REHASH) return oldValue as V? + curCore = curCore.rehash() + core.value = curCore + } + } + + override val keys: MutableSet + get() = KeyValueSet { k, _ -> k } + + override val entries: MutableSet> + get() = KeyValueSet { k, v -> Entry(k, v) } + + // We don't care much about clear's efficiency + override fun clear() { + for (k in keys) remove(k) + } + + fun runWeakRefQueueCleaningLoopUntilInterrupted() { + check(weakRefQueue != null) { "Must be created with weakRefQueue = true" } + try { + while (true) { + cleanWeakRef(weakRefQueue.remove() as HashedWeakRef<*>) + } + } catch(e: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + private fun cleanWeakRef(w: HashedWeakRef<*>) { + core.value.cleanWeakRef(w) + } + + @Suppress("UNCHECKED_CAST") + private inner class Core(private val allocated: Int) { + private val shift = allocated.countLeadingZeroBits() + 1 + private val threshold = 2 * allocated / 3 // max fill factor at 66% to ensure speedy lookups + private val load = atomic(0) // counts how many slots are occupied in this core + private val keys = atomicArrayOfNulls?>(allocated) + private val values = atomicArrayOfNulls(allocated) + + private fun index(hash: Int) = (hash * MAGIC) ushr shift + + // get is always lock-free, unwraps the value that was marked by concurrent rehash + fun getImpl(key: K): V? { + var index = index(key.hashCode()) + while (true) { + val w = keys[index].value ?: return null // not found + val k = w.get() + if (key == k) { + val value = values[index].value + return (if (value is Marked) value.ref else value) as V? + } + if (k == null) removeCleanedAt(index) // weak ref was here, but collected + if (index == 0) index = allocated + index-- + } + } + + private fun removeCleanedAt(index: Int) { + while (true) { + val oldValue = values[index].value ?: return // return when already removed + if (oldValue is Marked) return // cannot remove marked (rehash is working on it, will not copy) + if (values[index].compareAndSet(oldValue, null)) { // removed + decrementSize() + return + } + } + } + + // returns REHASH when rehash is needed (the value was not put) + fun putImpl(key: K, value: V?, weakKey0: HashedWeakRef? = null): Any? { + var index = index(key.hashCode()) + var loadIncremented = false + var weakKey: HashedWeakRef? = weakKey0 + while (true) { + val w = keys[index].value + if (w == null) { // slot empty => not found => try reserving slot + if (value == null) return null // removing missing value, nothing to do here + if (!loadIncremented) { + // We must increment load before we even try to occupy a slot to avoid overfill during concurrent put + load.update { n -> + if (n >= threshold) return REHASH // the load is already too big -- rehash + n + 1 // otherwise increment + } + loadIncremented = true + } + if (weakKey == null) weakKey = HashedWeakRef(key, weakRefQueue) + if (keys[index].compareAndSet(null, weakKey)) break // slot reserved !!! + continue // retry at this slot on CAS failure (somebody already reserved this slot) + } + val k = w.get() + if (key == k) { // found already reserved slot at index + if (loadIncremented) load.decrementAndGet() // undo increment, because found a slot + break + } + if (k == null) removeCleanedAt(index) // weak ref was here, but collected + if (index == 0) index = allocated + index-- + } + // update value + var oldValue: Any? + while (true) { + oldValue = values[index].value + if (oldValue is Marked) return REHASH // rehash started, cannot work here + if (values[index].compareAndSet(oldValue, value)) break + } + return oldValue as V? + } + + // only one thread can rehash, but may have concurrent puts/gets + fun rehash(): Core { + // use size to approximate new required capacity to have at least 25-50% fill factor, + // may fail due to concurrent modification, will retry + retry@while (true) { + val newCapacity = size.coerceAtLeast(MIN_CAPACITY / 4).takeHighestOneBit() * 4 + val newCore = Core(newCapacity) + for (index in 0 until allocated) { + // load the key + val w = keys[index].value + val k = w?.get() + if (w != null && k == null) removeCleanedAt(index) // weak ref was here, but collected + // mark value so that it cannot be changed while we rehash to new core + var value: Any? + while (true) { + value = values[index].value + if (value is Marked) { // already marked -- good + value = value.ref + break + } + // try mark + if (values[index].compareAndSet(value, value.mark())) break + } + if (k != null && value != null) { + val oldValue = newCore.putImpl(k, value as V, w) + if (oldValue === REHASH) continue@retry // retry if we underestimated capacity + assert(oldValue == null) + } + } + return newCore // rehashed everything successfully + } + } + + fun cleanWeakRef(weakRef: HashedWeakRef<*>) { + var index = index(weakRef.hash) + while (true) { + val w = keys[index].value ?: return // return when slots are over + if (w === weakRef) { // found + removeCleanedAt(index) + return + } + if (index == 0) index = allocated + index-- + } + } + + fun keyValueIterator(factory: (K, V) -> E): MutableIterator = KeyValueIterator(factory) + + private inner class KeyValueIterator(private val factory: (K, V) -> E) : MutableIterator { + private var index = -1 + private lateinit var key: K + private lateinit var value: V + + init { findNext() } + + private fun findNext() { + while (++index < allocated) { + key = keys[index].value?.get() ?: continue + var value = values[index].value + if (value is Marked) value = value.ref + if (value != null) { + this.value = value as V + return + } + } + } + + override fun hasNext(): Boolean = index < allocated + + override fun next(): E { + if (index >= allocated) throw NoSuchElementException() + return factory(key, value).also { findNext() } + } + + override fun remove() = noImpl() + } + } + + private class Entry(override val key: K, override val value: V) : MutableMap.MutableEntry { + override fun setValue(newValue: V): V = noImpl() + } + + private inner class KeyValueSet( + private val factory: (K, V) -> E + ) : AbstractMutableSet() { + override val size: Int get() = this@ConcurrentWeakMap.size + override fun add(element: E): Boolean = noImpl() + override fun iterator(): MutableIterator = core.value.keyValueIterator(factory) + } +} + +private const val MAGIC = 2654435769L.toInt() // golden ratio +private const val MIN_CAPACITY = 16 +private val REHASH = Symbol("REHASH") +private val MARKED_NULL = Marked(null) +private val MARKED_TRUE = Marked(true) // When using map as set "true" used as value, optimize its mark allocation + +/** + * Weak reference that stores the original hash code so that we can use reference queue to promptly clean them up + * from the hashtable even in the absence of ongoing modifications. + */ +internal class HashedWeakRef( + ref: T, queue: ReferenceQueue? +) : WeakReference(ref, queue) { + @JvmField + val hash = ref.hashCode() +} + +/** + * Marked values cannot be modified. The marking is performed when rehash has started to ensure that concurrent + * modifications (that are lock-free) cannot perform any changes and are forced to synchronize with ongoing rehash. + */ +private class Marked(@JvmField val ref: Any?) + +private fun Any?.mark(): Marked = when(this) { + null -> MARKED_NULL + true -> MARKED_TRUE + else -> Marked(this) +} + +private fun noImpl(): Nothing { + throw UnsupportedOperationException("not implemented") +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt index 82e18eabe7..9d9fa3fbb2 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt @@ -7,81 +7,24 @@ package kotlinx.coroutines.debug.internal import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.* -internal const val CREATED = "CREATED" -internal const val RUNNING = "RUNNING" -internal const val SUSPENDED = "SUSPENDED" - +/** + * This class represents the data required by IDEA debugger. + * IDEA debugger either directly reads data from the corresponding JVM fields of this class or calls the getters, + * so we keep both for maximal flexibility for now. + * **DO NOT MAKE BINARY-INCOMPATIBLE CHANGES TO THIS CLASS**. + */ +@Suppress("unused") +@PublishedApi internal class DebugCoroutineInfo( - public val context: CoroutineContext, - public val creationStackBottom: CoroutineStackFrame?, - @JvmField internal val sequenceNumber: Long + source: DebugCoroutineInfoImpl, + public val context: CoroutineContext // field is used as of 1.4-M3 ) { - - public val creationStackTrace: List get() = creationStackTrace() - - /** - * Last observed state of the coroutine. - * Can be CREATED, RUNNING, SUSPENDED. - */ - public val state: String get() = _state - private var _state: String = CREATED - - @JvmField - internal var lastObservedThread: Thread? = null - @JvmField - internal var lastObservedFrame: CoroutineStackFrame? = null - - public fun copy(): DebugCoroutineInfo = DebugCoroutineInfo( - context, - creationStackBottom, - sequenceNumber - ).also { - it._state = _state - it.lastObservedFrame = lastObservedFrame - it.lastObservedThread = lastObservedThread - } - - /** - * Last observed stacktrace of the coroutine captured on its suspension or resumption point. - * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and - * reflects stacktrace of the resumption point, not the actual current stacktrace. - */ - public fun lastObservedStackTrace(): List { - var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList() - val result = ArrayList() - while (frame != null) { - frame.getStackTraceElement()?.let { result.add(it) } - frame = frame.callerFrame - } - return result - } - - private fun creationStackTrace(): List { - val bottom = creationStackBottom ?: return emptyList() - // Skip "Coroutine creation stacktrace" frame - return sequence { yieldFrames(bottom.callerFrame) }.toList() - } - - private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) { - if (frame == null) return - frame.getStackTraceElement()?.let { yield(it) } - val caller = frame.callerFrame - if (caller != null) { - yieldFrames(caller) - } - } - - internal fun updateState(state: String, frame: Continuation<*>) { - // Propagate only duplicating transitions to running for KT-29997 - if (_state == state && state == SUSPENDED && lastObservedFrame != null) return - _state = state - lastObservedFrame = frame as? CoroutineStackFrame - lastObservedThread = if (state == RUNNING) { - Thread.currentThread() - } else { - null - } - } - - override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)" -} + public val creationStackBottom: CoroutineStackFrame? = source.creationStackBottom // field is used as of 1.4-M3 + public val sequenceNumber: Long = source.sequenceNumber // field is used as of 1.4-M3 + public val creationStackTrace = source.creationStackTrace // getter is used as of 1.4-M3 + public val state: String = source.state // getter is used as of 1.4-M3 + public val lastObservedThread: Thread? = source.lastObservedThread // field is used as of 1.4-M3 + public val lastObservedFrame: CoroutineStackFrame? = source.lastObservedFrame // field is used as of 1.4-M3 + @get:JvmName("lastObservedStackTrace") // method with this name is used as of 1.4-M3 + public val lastObservedStackTrace: List = source.lastObservedStackTrace() +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt new file mode 100644 index 0000000000..cf007bb978 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +import java.lang.ref.* +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* + +internal const val CREATED = "CREATED" +internal const val RUNNING = "RUNNING" +internal const val SUSPENDED = "SUSPENDED" + +/** + * Internal implementation class where debugger tracks details it knows about each coroutine. + */ +internal class DebugCoroutineInfoImpl( + context: CoroutineContext?, + /** + * A reference to a stack-trace that is converted to a [StackTraceFrame] which implements [CoroutineStackFrame]. + * The actual reference to the coroutine is not stored here, so we keep a strong reference. + */ + public val creationStackBottom: StackTraceFrame?, + @JvmField internal val sequenceNumber: Long +) { + /** + * We cannot keep a strong reference to the context, because with the [Job] in the context it will indirectly + * keep a reference to the last frame of an abandoned coroutine which the debugger should not be preventing + * garbage-collection of. The reference to context will not disappear as long as the coroutine itself is not lost. + */ + private val _context = WeakReference(context) + public val context: CoroutineContext? // can be null when the coroutine was already garbage-collected + get() = _context.get() + + public val creationStackTrace: List get() = creationStackTrace() + + /** + * Last observed state of the coroutine. + * Can be CREATED, RUNNING, SUSPENDED. + */ + public val state: String get() = _state + private var _state: String = CREATED + + @JvmField + internal var lastObservedThread: Thread? = null + + /** + * We cannot keep a strong reference to the last observed frame of the coroutine, because this will + * prevent garbage-collection of a coroutine that was lost. + */ + private var _lastObservedFrame: WeakReference? = null + internal var lastObservedFrame: CoroutineStackFrame? + get() = _lastObservedFrame?.get() + set(value) { _lastObservedFrame = value?.let { WeakReference(it) } } + + /** + * Last observed stacktrace of the coroutine captured on its suspension or resumption point. + * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and + * reflects stacktrace of the resumption point, not the actual current stacktrace. + */ + public fun lastObservedStackTrace(): List { + var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList() + val result = ArrayList() + while (frame != null) { + frame.getStackTraceElement()?.let { result.add(it) } + frame = frame.callerFrame + } + return result + } + + private fun creationStackTrace(): List { + val bottom = creationStackBottom ?: return emptyList() + // Skip "Coroutine creation stacktrace" frame + return sequence { yieldFrames(bottom.callerFrame) }.toList() + } + + private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) { + if (frame == null) return + frame.getStackTraceElement()?.let { yield(it) } + val caller = frame.callerFrame + if (caller != null) { + yieldFrames(caller) + } + } + + internal fun updateState(state: String, frame: Continuation<*>) { + // Propagate only duplicating transitions to running for KT-29997 + if (_state == state && state == SUSPENDED && lastObservedFrame != null) return + _state = state + lastObservedFrame = frame as? CoroutineStackFrame + lastObservedThread = if (state == RUNNING) { + Thread.currentThread() + } else { + null + } + } + + override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)" +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 6ff51f3da5..9dd6c5a548 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -7,24 +7,38 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.debug.* +import kotlinx.coroutines.internal.* +import kotlinx.coroutines.internal.ScopeCoroutine import java.io.* +import java.lang.StackTraceElement import java.text.* -import java.util.* -import java.util.concurrent.* import java.util.concurrent.locks.* import kotlin.collections.ArrayList import kotlin.concurrent.* import kotlin.coroutines.* -import kotlin.coroutines.jvm.internal.* +import kotlin.coroutines.jvm.internal.CoroutineStackFrame +import kotlin.synchronized import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround internal object DebugProbesImpl { private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace" private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") - private val capturedCoroutines = Collections.newSetFromMap(ConcurrentHashMap, Boolean>()) + + private var weakRefCleanerThread: Thread? = null + + // Values are boolean, so this map does not need to use a weak reference queue + private val capturedCoroutinesMap = ConcurrentWeakMap, Boolean>() + private val capturedCoroutines: Set> get() = capturedCoroutinesMap.keys + @Volatile private var installations = 0 + + /** + * This internal method is used by IDEA debugger under the JVM name of + * "isInstalled$kotlinx_coroutines_debug". + */ internal val isInstalled: Boolean get() = installations > 0 + // To sort coroutines by creation order, used as unique id private val sequenceNumber = atomic(0L) /* @@ -52,7 +66,6 @@ internal object DebugProbesImpl { ctor.newInstance() as Function1 }.getOrNull() - /* * This is an optimization in the face of KT-29997: * Consider suspending call stack a()->b()->c() and c() completes its execution and every call is @@ -60,11 +73,15 @@ internal object DebugProbesImpl { * * Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth). * To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally. + * + * [DebugCoroutineInfoImpl] keeps a lot of auxiliary information about a coroutine, so we use a weak reference queue + * to promptly release the corresponding memory when the reference to the coroutine itself was already collected. */ - private val callerInfoCache = ConcurrentHashMap() + private val callerInfoCache = ConcurrentWeakMap(weakRefQueue = true) public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return + startWeakRefCleanerThread() if (AgentPremain.isInstalledStatically) return dynamicAttach?.invoke(true) // attach } @@ -72,12 +89,24 @@ internal object DebugProbesImpl { public fun uninstall(): Unit = coroutineStateLock.write { check(isInstalled) { "Agent was not installed" } if (--installations != 0) return - capturedCoroutines.clear() + stopWeakRefCleanerThread() + capturedCoroutinesMap.clear() callerInfoCache.clear() if (AgentPremain.isInstalledStatically) return dynamicAttach?.invoke(false) // detach } + private fun startWeakRefCleanerThread() { + weakRefCleanerThread = thread(isDaemon = true, name = "Coroutines Debugger Cleaner") { + callerInfoCache.runWeakRefQueueCleaningLoopUntilInterrupted() + } + } + + private fun stopWeakRefCleanerThread() { + weakRefCleanerThread?.interrupt() + weakRefCleanerThread = null + } + public fun hierarchyToString(job: Job): String = coroutineStateLock.write { check(isInstalled) { "Debug probes are not installed" } val jobToStack = capturedCoroutines @@ -88,13 +117,13 @@ internal object DebugProbesImpl { } } - private fun Job.build(map: Map, builder: StringBuilder, indent: String) { + private fun Job.build(map: Map, builder: StringBuilder, indent: String) { val info = map[this] val newIndent: String if (info == null) { // Append coroutine without stacktrace // Do not print scoped coroutines and do not increase indentation level @Suppress("INVISIBLE_REFERENCE") - if (this !is kotlinx.coroutines.internal.ScopeCoroutine<*>) { + if (this !is ScopeCoroutine<*>) { builder.append("$indent$debugString\n") newIndent = indent + "\t" } else { @@ -116,19 +145,32 @@ internal object DebugProbesImpl { @Suppress("DEPRECATION_ERROR") // JobSupport private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString() - public fun dumpCoroutinesInfo(): List = coroutineStateLock.write { - check(isInstalled) { "Debug probes are not installed" } - return capturedCoroutines.asSequence() - .map { it.info.copy() } // Copy as CoroutineInfo can be mutated concurrently by DebugProbes - .sortedBy { it.sequenceNumber } - .toList() - } + /** + * Private method that dumps coroutines so that different public-facing method can use + * to produce different result types. + */ + private inline fun dumpCoroutinesInfoImpl(create: (CoroutineOwner<*>, CoroutineContext) -> R): List = + coroutineStateLock.write { + check(isInstalled) { "Debug probes are not installed" } + capturedCoroutines + // Stable ordering of coroutines by their sequence number + .sortedBy { it.info.sequenceNumber } + // Leave in the dump only the coroutines that were not collected while we were dumping them + .mapNotNull { owner -> owner.info.context?.let { context -> create(owner, context) } } + } /* - * Internal (JVM-public) method used by IDEA debugger. - * It is equivalent to dumpCoroutines, but returns serializable (and thus less typed) objects. + * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3. */ - public fun dumpDebuggerInfo() = dumpCoroutinesInfo().map { DebuggerInfo(it) } + public fun dumpCoroutinesInfo(): List = + dumpCoroutinesInfoImpl { owner, context -> DebugCoroutineInfo(owner.info, context) } + + /* + * Internal (JVM-public) method to be used by IDEA debugger in the future (not used as of 1.4-M3). + * It is equivalent to [dumpCoroutinesInfo], but returns serializable (and thus less typed) objects. + */ + public fun dumpDebuggerInfo(): List = + dumpCoroutinesInfoImpl { owner, context -> DebuggerInfo(owner.info, context) } public fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) { /* @@ -145,17 +187,15 @@ internal object DebugProbesImpl { check(isInstalled) { "Debug probes are not installed" } out.print("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}") capturedCoroutines - .asSequence() .sortedBy { it.info.sequenceNumber } .forEach { owner -> val info = owner.info val observedStackTrace = info.lastObservedStackTrace() - val enhancedStackTrace = enhanceStackTraceWithThreadDump(info, observedStackTrace) + val enhancedStackTrace = enhanceStackTraceWithThreadDumpImpl(info.state, info.lastObservedThread, observedStackTrace) val state = if (info.state == RUNNING && enhancedStackTrace === observedStackTrace) "${info.state} (Last suspension stacktrace, not an actual stacktrace)" else - info.state.toString() - + info.state out.print("\n\nCoroutine ${owner.delegate}, state: $state") if (observedStackTrace.isEmpty()) { out.print("\n\tat ${createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE)}") @@ -172,18 +212,29 @@ internal object DebugProbesImpl { } } + /* + * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3. + * It is similar to [enhanceStackTraceWithThreadDumpImpl], but uses debugger-facing [DebugCoroutineInfo] type. + */ + @Suppress("unused") + public fun enhanceStackTraceWithThreadDump( + info: DebugCoroutineInfo, + coroutineTrace: List + ): List = + enhanceStackTraceWithThreadDumpImpl(info.state, info.lastObservedThread, coroutineTrace) + /** - * Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfo.lastObservedStackTrace]) with - * thread dump of [DebugCoroutineInfo.lastObservedThread]. + * Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfoImpl.lastObservedStackTrace]) with + * thread dump of [DebugCoroutineInfoImpl.lastObservedThread]. * * Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result. */ - private fun enhanceStackTraceWithThreadDump( - info: DebugCoroutineInfo, + private fun enhanceStackTraceWithThreadDumpImpl( + state: String, + thread: Thread?, coroutineTrace: List ): List { - val thread = info.lastObservedThread - if (info.state != RUNNING || thread == null) return coroutineTrace + if (state != RUNNING || thread == null) return coroutineTrace // Avoid security manager issues val actualTrace = runCatching { thread.stackTrace }.getOrNull() ?: return coroutineTrace @@ -289,7 +340,7 @@ internal object DebugProbesImpl { private fun updateRunningState(frame: CoroutineStackFrame, state: String): Unit = coroutineStateLock.read { if (!isInstalled) return // Lookup coroutine info in cache or by traversing stack frame - val info: DebugCoroutineInfo + val info: DebugCoroutineInfoImpl val cached = callerInfoCache.remove(frame) if (cached != null) { info = cached @@ -331,39 +382,36 @@ internal object DebugProbesImpl { val owner = completion.owner() if (owner != null) return completion /* - * Here we replace completion with a sequence of CoroutineStackFrame objects + * Here we replace completion with a sequence of StackTraceFrame objects * which represents creation stacktrace, thus making stacktrace recovery mechanism * even more verbose (it will attach coroutine creation stacktrace to all exceptions), * and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls. */ - val frame = if (enableCreationStackTraces) { - val stacktrace = sanitizeStackTrace(Exception()) - stacktrace.foldRight(null) { frame, acc -> - object : CoroutineStackFrame { - override val callerFrame: CoroutineStackFrame? = acc - override fun getStackTraceElement(): StackTraceElement = frame - } - } + sanitizeStackTrace(Exception()).toStackTraceFrame() } else { null } - return createOwner(completion, frame) } - private fun createOwner(completion: Continuation, frame: CoroutineStackFrame?): Continuation { + private fun List.toStackTraceFrame(): StackTraceFrame? = + foldRight(null) { frame, acc -> + StackTraceFrame(acc, frame) + } + + private fun createOwner(completion: Continuation, frame: StackTraceFrame?): Continuation { if (!isInstalled) return completion - val info = DebugCoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet()) + val info = DebugCoroutineInfoImpl(completion.context, frame, sequenceNumber.incrementAndGet()) val owner = CoroutineOwner(completion, info, frame) - capturedCoroutines += owner - if (!isInstalled) capturedCoroutines.clear() + capturedCoroutinesMap[owner] = true + if (!isInstalled) capturedCoroutinesMap.clear() return owner } // Not guarded by the lock at all, does not really affect consistency private fun probeCoroutineCompleted(owner: CoroutineOwner<*>) { - capturedCoroutines.remove(owner) + capturedCoroutinesMap.remove(owner) /* * This removal is a guard against improperly implemented CoroutineStackFrame * and bugs in the compiler. @@ -378,7 +426,7 @@ internal object DebugProbesImpl { */ private class CoroutineOwner( @JvmField val delegate: Continuation, - @JvmField val info: DebugCoroutineInfo, + @JvmField val info: DebugCoroutineInfoImpl, private val frame: CoroutineStackFrame? ) : Continuation by delegate, CoroutineStackFrame { diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt index 4b95af986a..3e9533b09e 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt @@ -12,12 +12,14 @@ import kotlinx.coroutines.* /* * This class represents all the data required by IDEA debugger. - * It is serializable in order to speedup JDWP interactions + * It is serializable in order to speedup JDWP interactions. + * **DO NOT MAKE BINARY-INCOMPATIBLE CHANGES TO THIS CLASS**. */ -internal class DebuggerInfo(source: DebugCoroutineInfo) : Serializable { - public val coroutineId: Long? = source.context[CoroutineId]?.id - public val dispatcher: String? = source.context[ContinuationInterceptor].toString() - public val name: String? = source.context[CoroutineName]?.name +@PublishedApi +internal class DebuggerInfo(source: DebugCoroutineInfoImpl, context: CoroutineContext) : Serializable { + public val coroutineId: Long? = context[CoroutineId]?.id + public val dispatcher: String? = context[ContinuationInterceptor]?.toString() + public val name: String? = context[CoroutineName]?.name public val state: String = source.state public val lastObservedThreadState: String? = source.lastObservedThread?.state?.toString() public val lastObservedThreadName = source.lastObservedThread?.name diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/StackTraceFrame.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/StackTraceFrame.kt new file mode 100644 index 0000000000..37c60eeb56 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/StackTraceFrame.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +import kotlin.coroutines.jvm.internal.* + +/** + * A stack-trace represented as [CoroutineStackFrame]. + */ +internal class StackTraceFrame( + override val callerFrame: CoroutineStackFrame?, + private val stackTraceElement: StackTraceElement +) : CoroutineStackFrame { + override fun getStackTraceElement(): StackTraceElement = stackTraceElement +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt index 12fd4dea04..e8079ebdfa 100644 --- a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt +++ b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt @@ -4,10 +4,13 @@ package kotlinx.coroutines +import java.lang.ref.* import java.lang.reflect.* +import java.text.* import java.util.* import java.util.Collections.* import java.util.concurrent.atomic.* +import java.util.concurrent.locks.* import kotlin.collections.ArrayList import kotlin.test.* @@ -22,7 +25,11 @@ object FieldWalker { init { // excluded/terminal classes (don't walk them) - fieldsCache += listOf(Any::class, String::class, Thread::class, Throwable::class) + fieldsCache += listOf( + Any::class, String::class, Thread::class, Throwable::class, StackTraceElement::class, + WeakReference::class, ReferenceQueue::class, AbstractMap::class, + ReentrantReadWriteLock::class, SimpleDateFormat::class + ) .map { it.java } .associateWith { emptyList() } } @@ -31,10 +38,10 @@ object FieldWalker { * Reflectively starts to walk through object graph and returns identity set of all reachable objects. * Use [walkRefs] if you need a path from root for debugging. */ - public fun walk(root: Any?): Set = walkRefs(root).keys + public fun walk(root: Any?): Set = walkRefs(root, false).keys - public fun assertReachableCount(expected: Int, root: Any?, predicate: (Any) -> Boolean) { - val visited = walkRefs(root) + public fun assertReachableCount(expected: Int, root: Any?, rootStatics: Boolean = false, predicate: (Any) -> Boolean) { + val visited = walkRefs(root, rootStatics) val actual = visited.keys.filter(predicate) if (actual.size != expected) { val textDump = actual.joinToString("") { "\n\t" + showPath(it, visited) } @@ -49,16 +56,18 @@ object FieldWalker { * Reflectively starts to walk through object graph and map to all the reached object to their path * in from root. Use [showPath] do display a path if needed. */ - private fun walkRefs(root: Any?): Map { + private fun walkRefs(root: Any?, rootStatics: Boolean): Map { val visited = IdentityHashMap() if (root == null) return visited visited[root] = Ref.RootRef val stack = ArrayDeque() stack.addLast(root) + var statics = rootStatics while (stack.isNotEmpty()) { val element = stack.removeLast() try { - visit(element, visited, stack) + visit(element, visited, stack, statics) + statics = false // only scan root static when asked } catch (e: Exception) { error("Failed to visit element ${showPath(element, visited)}: $e") } @@ -75,7 +84,7 @@ object FieldWalker { when (ref) { is Ref.FieldRef -> { cur = ref.parent - path += ".${ref.name}" + path += "|${ref.parent.javaClass.simpleName}::${ref.name}" } is Ref.ArrayRef -> { cur = ref.parent @@ -87,7 +96,7 @@ object FieldWalker { return path.joinToString("") } - private fun visit(element: Any, visited: IdentityHashMap, stack: ArrayDeque) { + private fun visit(element: Any, visited: IdentityHashMap, stack: ArrayDeque, statics: Boolean) { val type = element.javaClass when { // Special code for arrays @@ -111,8 +120,16 @@ object FieldWalker { element is AtomicReference<*> -> { push(element.get(), visited, stack) { Ref.FieldRef(element, "value") } } + element is AtomicReferenceArray<*> -> { + for (index in 0 until element.length()) { + push(element[index], visited, stack) { Ref.ArrayRef(element, index) } + } + } + element is AtomicLongFieldUpdater<*> -> { + /* filter it out here to suppress its subclasses too */ + } // All the other classes are reflectively scanned - else -> fields(type).forEach { field -> + else -> fields(type, statics).forEach { field -> push(field.get(element), visited, stack) { Ref.FieldRef(element, field.name) } // special case to scan Throwable cause (cannot get it reflectively) if (element is Throwable) { @@ -129,19 +146,21 @@ object FieldWalker { } } - private fun fields(type0: Class<*>): List { + private fun fields(type0: Class<*>, rootStatics: Boolean): List { fieldsCache[type0]?.let { return it } val result = ArrayList() var type = type0 + var statics = rootStatics while (true) { val fields = type.declaredFields.filter { !it.type.isPrimitive - && !Modifier.isStatic(it.modifiers) + && (statics || !Modifier.isStatic(it.modifiers)) && !(it.type.isArray && it.type.componentType.isPrimitive) } fields.forEach { it.isAccessible = true } // make them all accessible result.addAll(fields) type = type.superclass + statics = false val superFields = fieldsCache[type] // will stop at Any anyway if (superFields != null) { result.addAll(superFields) diff --git a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt index 5f5620c632..892a2a62d4 100644 --- a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt @@ -203,6 +203,6 @@ class ReusableCancellableContinuationTest : TestBase() { for (value in channel) { delay(1) } - FieldWalker.assertReachableCount(1, coroutineContext[Job], { it is ChildContinuation }) + FieldWalker.assertReachableCount(1, coroutineContext[Job]) { it is ChildContinuation } } } diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapCollectionStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapCollectionStressTest.kt new file mode 100644 index 0000000000..d9a2a96d81 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapCollectionStressTest.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.internal + +import junit.framework.Assert.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.* +import kotlin.concurrent.* + +class ConcurrentWeakMapCollectionStressTest : TestBase() { + private data class Key(val i: Int) + private val nElements = 100_000 * stressTestMultiplier + private val size = 100_000 + + @Test + fun testCollected() { + // use very big arrays as values, we'll need a queue and a cleaner thread to handle them + val m = ConcurrentWeakMap(weakRefQueue = true) + val cleaner = thread(name = "ConcurrentWeakMapCollectionStressTest-Cleaner") { + m.runWeakRefQueueCleaningLoopUntilInterrupted() + } + for (i in 1..nElements) { + m.put(Key(i), ByteArray(size)) + } + assertTrue(m.size < nElements) // some of it was collected for sure + cleaner.interrupt() + cleaner.join() + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt new file mode 100644 index 0000000000..49e6cccd77 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapOperationStressTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.Test +import kotlin.concurrent.* +import kotlin.test.* + +/** + * Concurrent test for [ConcurrentWeakMap] that tests put/get/remove from concurrent threads and is + * arranged so that concurrent rehashing is also happening. + */ +class ConcurrentWeakMapOperationStressTest : TestBase() { + private val nThreads = 10 + private val batchSize = 1000 + private val nSeconds = 3 * stressTestMultiplier + + private val count = atomic(0L) + private val stop = atomic(false) + + private data class Key(val i: Long) + + @Test + fun testOperations() { + // We don't create queue here, because concurrent operations are enough to make it clean itself + val m = ConcurrentWeakMap() + val threads = Array(nThreads) { index -> + thread(start = false, name = "ConcurrentWeakMapOperationStressTest-$index") { + var generationOffset = 0L + while (!stop.value) { + val kvs = (generationOffset + batchSize * index until generationOffset + batchSize * (index + 1)) + .associateBy({ Key(it) }, { it * it }) + generationOffset += batchSize * nThreads + for ((k, v) in kvs) { + assertEquals(null, m.put(k, v)) + } + for ((k, v) in kvs) { + assertEquals(v, m[k]) + } + for ((k, v) in kvs) { + assertEquals(v, m.remove(k)) + } + for ((k, v) in kvs) { + assertEquals(null, m.get(k)) + } + count.incrementAndGet() + } + } + } + val uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, ex -> + ex.printStackTrace() + error("Error in thread $t", ex) + } + threads.forEach { it.uncaughtExceptionHandler = uncaughtExceptionHandler } + threads.forEach { it.start() } + var lastCount = -1L + for (sec in 1..nSeconds) { + Thread.sleep(1000) + val count = count.value + println("$sec: done $count batches") + assertTrue(count > lastCount) // ensure progress + lastCount = count + } + stop.value = true + threads.forEach { it.join() } + assertEquals(0, m.size) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt new file mode 100644 index 0000000000..ae4b5fce30 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import junit.framework.Assert.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.* + +class ConcurrentWeakMapTest : TestBase() { + @Test + fun testSimple() { + val expect = (1..1000).associateWith { it.toString() } + val m = ConcurrentWeakMap() + // repeat adding/removing a few times + repeat(5) { + assertEquals(0, m.size) + assertEquals(emptySet(), m.keys) + assertEquals(emptyList(), m.values.toList()) + assertEquals(emptySet>(), m.entries) + for ((k, v) in expect) { + assertNull(m.put(k, v)) + } + assertEquals(expect.size, m.size) + assertEquals(expect.keys, m.keys) + assertEquals(expect.entries, m.entries) + for ((k, v) in expect) { + assertEquals(v, m.get(k)) + } + assertEquals(expect.size, m.size) + if (it % 2 == 0) { + for ((k, v) in expect) { + assertEquals(v, m.remove(k)) + } + } else { + m.clear() + } + assertEquals(0, m.size) + for ((k, v) in expect) { + assertNull(m.get(k)) + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-debug/src/CoroutineInfo.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt index 11224f53b3..ce1478ad07 100644 --- a/kotlinx-coroutines-debug/src/CoroutineInfo.kt +++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt @@ -18,10 +18,12 @@ public class CoroutineInfo internal constructor(delegate: DebugCoroutineInfo) { * [Coroutine context][coroutineContext] of the coroutine */ public val context: CoroutineContext = delegate.context + /** * Last observed state of the coroutine */ public val state: State = State.valueOf(delegate.state) + private val creationStackBottom: CoroutineStackFrame? = delegate.creationStackBottom /** diff --git a/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt b/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt new file mode 100644 index 0000000000..bf34917b77 --- /dev/null +++ b/kotlinx-coroutines-debug/test/DebugLeaksStressTest.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. + */ + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import org.junit.* + +/** + * This stress tests ensure that no actual [OutOfMemoryError] occurs when lots of coroutines are created and + * leaked in various ways under debugger. A faster but more fragile version of this test is in [DebugLeaksTest]. + */ +class DebugLeaksStressTest : DebugTestBase() { + private val nRepeat = 100_000 * stressTestMultiplier + private val nBytes = 100_000 + + @Test + fun testIteratorLeak() { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + iterator { yield(bytes) } + } + } + + @Test + fun testLazyGlobalCoroutineLeak() { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + GlobalScope.launch(start = CoroutineStart.LAZY) { println(bytes) } + } + } + + @Test + fun testLazyCancelledChildCoroutineLeak() = runTest { + coroutineScope { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + val child = launch(start = CoroutineStart.LAZY) { println(bytes) } + child.cancel() + } + } + } + + @Test + fun testAbandonedGlobalCoroutineLeak() { + repeat(nRepeat) { + val bytes = ByteArray(nBytes) + GlobalScope.launch { + suspendForever() + println(bytes) + } + } + } + + private suspend fun suspendForever() = suspendCancellableCoroutine { } +} diff --git a/kotlinx-coroutines-debug/test/DebugLeaksTest.kt b/kotlinx-coroutines-debug/test/DebugLeaksTest.kt new file mode 100644 index 0000000000..a43b33be67 --- /dev/null +++ b/kotlinx-coroutines-debug/test/DebugLeaksTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import kotlinx.coroutines.debug.internal.* +import org.junit.* + +/** + * This is fast but fragile version of [DebugLeaksStressTest] that check reachability of a captured object + * in [DebugProbesImpl] via [FieldWalker]. + */ +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +class DebugLeaksTest : DebugTestBase() { + private class Captured + + @Test + fun testIteratorLeak() { + val captured = Captured() + iterator { yield(captured) } + assertNoCapturedReference() + } + + @Test + fun testLazyGlobalCoroutineLeak() { + val captured = Captured() + GlobalScope.launch(start = CoroutineStart.LAZY) { println(captured) } + assertNoCapturedReference() + } + + @Test + fun testLazyCancelledChildCoroutineLeak() = runTest { + val captured = Captured() + coroutineScope { + val child = launch(start = CoroutineStart.LAZY) { println(captured) } + child.cancel() + } + assertNoCapturedReference() + } + + @Test + fun testAbandonedGlobalCoroutineLeak() { + val captured = Captured() + GlobalScope.launch { + suspendForever() + println(captured) + } + assertNoCapturedReference() + } + + private suspend fun suspendForever() = suspendCancellableCoroutine { } + + private fun assertNoCapturedReference() { + FieldWalker.assertReachableCount(0, DebugProbesImpl, rootStatics = true) { it is Captured } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-debug/test/DumpWithoutCreationStackTraceTest.kt b/kotlinx-coroutines-debug/test/DumpWithoutCreationStackTraceTest.kt index 89782e4c45..6e405ca293 100644 --- a/kotlinx-coroutines-debug/test/DumpWithoutCreationStackTraceTest.kt +++ b/kotlinx-coroutines-debug/test/DumpWithoutCreationStackTraceTest.kt @@ -21,13 +21,13 @@ class DumpWithoutCreationStackTraceTest : DebugTestBase() { yield() verifyDump( "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@70d1cb56, state: RUNNING\n" + - "\tat java.lang.Thread.getStackTrace(Thread.java:1559)\n" + - "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDump(DebugProbesImpl.kt:188)\n" + - "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutinesSynchronized(DebugProbesImpl.kt:153)\n" + - "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:141)", + "\tat java.lang.Thread.getStackTrace(Thread.java)\n" + + "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDumpImpl(DebugProbesImpl.kt)\n" + + "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutinesSynchronized(DebugProbesImpl.kt)\n" + + "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt)", "Coroutine \"coroutine#2\":DeferredCoroutine{Active}@383fa309, state: SUSPENDED\n" + - "\tat kotlinx.coroutines.debug.DumpWithoutCreationStackTraceTest\$createActiveDeferred\$1.invokeSuspend(DumpWithoutCreationStackTraceTest.kt:63)" + "\tat kotlinx.coroutines.debug.DumpWithoutCreationStackTraceTest\$createActiveDeferred\$1.invokeSuspend(DumpWithoutCreationStackTraceTest.kt)" ) deferred.cancelAndJoin() } diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index 4c13f5e67c..e7fdeede79 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -150,16 +150,16 @@ class RunningThreadStackMergeTest : DebugTestBase() { @Test fun testRunBlocking() = runBlocking { verifyDump("Coroutine \"coroutine#1\":BlockingCoroutine{Active}@4bcd176c, state: RUNNING\n" + - "\tat java.lang.Thread.getStackTrace(Thread.java:1552)\n" + - "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDump(DebugProbesImpl.kt:147)\n" + - "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutinesSynchronized(DebugProbesImpl.kt:122)\n" + - "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:109)\n" + - "\tat kotlinx.coroutines.debug.DebugProbes.dumpCoroutines(DebugProbes.kt:122)\n" + - "\tat kotlinx.coroutines.debug.StracktraceUtilsKt.verifyDump(StracktraceUtils.kt)\n" + - "\tat kotlinx.coroutines.debug.StracktraceUtilsKt.verifyDump\$default(StracktraceUtils.kt)\n" + - "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$testRunBlocking\$1.invokeSuspend(RunningThreadStackMergeTest.kt:112)\n" + + "\tat java.lang.Thread.getStackTrace(Thread.java)\n" + + "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDumpImpl(DebugProbesImpl.kt)\n" + + "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutinesSynchronized(DebugProbesImpl.kt)\n" + + "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbes.dumpCoroutines(DebugProbes.kt)\n" + + "\tat kotlinx.coroutines.debug.StacktraceUtilsKt.verifyDump(StacktraceUtils.kt)\n" + + "\tat kotlinx.coroutines.debug.StacktraceUtilsKt.verifyDump\$default(StacktraceUtils.kt)\n" + + "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$testRunBlocking\$1.invokeSuspend(RunningThreadStackMergeTest.kt)\n" + "\t(Coroutine creation stacktrace)\n" + - "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n") + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n") } diff --git a/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/kotlinx-coroutines-debug/test/StacktraceUtils.kt similarity index 100% rename from kotlinx-coroutines-debug/test/StracktraceUtils.kt rename to kotlinx-coroutines-debug/test/StacktraceUtils.kt From d55d8e80e152e46c170edc4dcfa08ccb7b2c4a03 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 16 Jul 2020 20:30:46 +0300 Subject: [PATCH 085/257] Version 1.3.8 --- CHANGES.md | 20 +++++++++++++++++++ README.md | 16 +++++++-------- gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 4 ++-- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 8 files changed, 35 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4fa05cb122..a1e3953351 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,25 @@ # Change log for kotlinx.coroutines +## Version 1.3.8 + +### New experimental features + +* Added `Flow.transformWhile operator` (#2065). +* Replaced `scanReduce` with `runningReduce` to be consistent with the Kotlin standard library (#2139). + +### Bug fixes and improvements + +* Improve user experience for the upcoming coroutines debugger (#2093, #2118, #2131). +* Debugger no longer retains strong references to the running coroutines (#2129). +* Fixed race in `Flow.asPublisher` (#2109). +* Fixed `ensureActive` to work in the empty context case to fix `IllegalStateException` when using flow from `suspend fun main` (#2044). +* Fixed a problem with `AbortFlowException` in the `Flow.first` operator to avoid erroneous `NoSuchElementException` (#2051). +* Fixed JVM dependency on Android annotations (#2075). +* Removed keep rules mentioning `kotlinx.coroutines.android` from core module (#2061 by @mkj-gram). +* Corrected some docs and examples (#2062, #2071, #2076, #2107, #2098, #2127, #2078, #2135). +* Improved the docs and guide on flow cancellation (#2043). +* Updated Gradle version to `6.3` (it only affects multiplatform artifacts in this release). + ## Version 1.3.7 * Fixed problem that triggered Android Lint failure (#2004). diff --git a/README.md b/README.md index 795616c88d..9f8bae65ba 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.7) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.7) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.8) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.8) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. This is a companion version for Kotlin `1.3.71` release. @@ -84,7 +84,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.3.7 + 1.3.8 ``` @@ -102,7 +102,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8' } ``` @@ -128,7 +128,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8") } ``` @@ -147,7 +147,7 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re Core modules of `kotlinx.coroutines` are also available for [Kotlin/JS](#js) and [Kotlin/Native](#native). In common code that should get compiled for different platforms, add dependency to -[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.7/jar) +[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.8/jar) (follow the link to get the dependency declaration snippet). ### Android @@ -156,7 +156,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' ``` This gives you access to Android [Dispatchers.Main] @@ -172,7 +172,7 @@ For more details see ["Optimization" section for Android](ui/kotlinx-coroutines- ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.7/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.8/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -180,7 +180,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.7/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.8/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/gradle.properties b/gradle.properties index bb93165edf..6a1ae653f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.3.7-SNAPSHOT +version=1.3.8-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.3.71 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index bd025159b9..81e62e772a 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -23,7 +23,7 @@ https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.7' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.8' } ``` @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.7.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.8.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index ec37aaac51..97a1178f3c 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.7' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index fe42c82360..071f794cb2 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 9a5627f24e..0be3b9c1cb 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.3.71 -coroutines_version=1.3.7 +coroutines_version=1.3.8 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 9a5627f24e..0be3b9c1cb 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.3.71 -coroutines_version=1.3.7 +coroutines_version=1.3.8 android.useAndroidX=true android.enableJetifier=true From e60bcbd7bf5b8ccbe3cd057012b373e8fdee3701 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 17 Jul 2020 12:29:59 +0300 Subject: [PATCH 086/257] Fix hanging RunInterruptibleStressTest on Windows with JDK 1.6 (#2145) The test is improved so that it fails, not hangs, on a failure. However, it will not pass on Windows + JDK 1.6 due to a bug in JDK (which can be easily confirmed with this modification to a test), so it excluded when running on JDK 1.6. Fixes #2144 --- kotlinx-coroutines-core/build.gradle | 1 + .../jvm/test/RunInterruptibleStressTest.kt | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index be7622aa49..59dc5da894 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -131,6 +131,7 @@ task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { exclude '**/*LCStressTest.*' // lin-check tests use LinChecker which needs JDK8 exclude '**/exceptions/**' // exceptions tests check suppressed exception which needs JDK8 exclude '**/ExceptionsGuideTest.*' + exclude '**/RunInterruptibleStressTest.*' // fails on JDK 1.6 due to JDK bug } // Run these tests only during nightly stress test diff --git a/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt b/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt index 79251bb3b8..701b958afd 100644 --- a/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/RunInterruptibleStressTest.kt @@ -9,25 +9,29 @@ import org.junit.Test import java.util.concurrent.atomic.* import kotlin.test.* +/** + * Stress test for [runInterruptible]. + * It does not pass on JDK 1.6 on Windows: [Thread.sleep] times out without being interrupted despite the + * fact that thread interruption flag is set. + */ class RunInterruptibleStressTest : TestBase() { - @get:Rule val dispatcher = ExecutorRule(4) - private val REPEAT_TIMES = 1000 * stressTestMultiplier + private val repeatTimes = 1000 * stressTestMultiplier @Test - fun testStress() = runBlocking { - val interruptLeak = AtomicBoolean(false) + fun testStress() = runTest { val enterCount = AtomicInteger(0) val interruptedCount = AtomicInteger(0) - repeat(REPEAT_TIMES) { + repeat(repeatTimes) { val job = launch(dispatcher) { try { runInterruptible { enterCount.incrementAndGet() try { - Thread.sleep(Long.MAX_VALUE) + Thread.sleep(10_000) + error("Sleep was not interrupted, Thread.isInterrupted=${Thread.currentThread().isInterrupted}") } catch (e: InterruptedException) { interruptedCount.incrementAndGet() throw e @@ -36,19 +40,17 @@ class RunInterruptibleStressTest : TestBase() { } catch (e: CancellationException) { // Expected } finally { - interruptLeak.set(interruptLeak.get() || Thread.currentThread().isInterrupted) + assertFalse(Thread.currentThread().isInterrupted, "Interrupt flag should not leak") } } // Add dispatch delay val cancelJob = launch(dispatcher) { job.cancel() } - - job.start() joinAll(job, cancelJob) } - - assertFalse(interruptLeak.get()) + println("Entered runInterruptible ${enterCount.get()} times") + assertTrue(enterCount.get() > 0) // ensure timing is Ok and we don't cancel it all prematurely assertEquals(enterCount.get(), interruptedCount.get()) } } From 716d21c6a412cb414fab8a883e1cf56e32e7f9b3 Mon Sep 17 00:00:00 2001 From: Ibraheem Zaman <1zaman@users.noreply.github.com> Date: Mon, 20 Jul 2020 12:16:22 +0400 Subject: [PATCH 087/257] Fix docs for CancellableContinuation (#2112) Basically reverts the documentation update in commit 49b63053698195b645002ee0ad530a2c4e315178 due to the changes in commit 876e9bab95d34f311be030a0706497564f96d95a --- .../common/src/CancellableContinuation.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 0d3fe847dc..f5b511cb9c 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -28,10 +28,8 @@ import kotlin.coroutines.intrinsics.* * * A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted]. * - * Invocation of [resume] or [resumeWithException] in _resumed_ state produces an [IllegalStateException]. - * Invocation of [resume] in _cancelled_ state is ignored (it is a trivial race between resume from the continuation owner and - * outer job's cancellation, and the cancellation wins). - * Invocation of [resumeWithException] in _cancelled_ state triggers exception handling of the passed exception. + * Invocation of [resume] or [resumeWithException] in _resumed_ state produces an [IllegalStateException], + * but is ignored in _cancelled_ state. * * ``` * +-----------+ resume +---------+ From 3cc9b94f8f9ce68d99d46120ee48e6f6ff61c160 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Tue, 21 Jul 2020 18:05:00 +0200 Subject: [PATCH 088/257] Add a keep rule for AndroidDispatcherFactory for R8 version > 1.6.0 (#2154) In commit #3a8a0ea this keep rule was removed from kotlinx-coroutines-core (which is good), but this caused AndroidDispatcherFactory to be minimized in newer R8 versions. Adding a rule explicitly fixes the problem. --- .../META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro index fd25b215c3..0d04990ad9 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-from-1.6.0/coroutines.pro @@ -9,6 +9,8 @@ boolean ANDROID_DETECTED return true; } +-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} + # Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher -assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt { boolean SUPPORT_MISSING return false; From 9139c64eefc5975619b7a788212a202f148bdbd0 Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Wed, 5 Aug 2020 17:54:09 +0430 Subject: [PATCH 089/257] Add missing article (#2180) --- docs/coroutine-context-and-dispatchers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index 4909206e17..e0bbc59617 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -652,7 +652,7 @@ stored in a thread-local variable. However, in this case you are fully responsib potentially concurrent modifications to the variable in this mutable box. For advanced usage, for example for integration with logging MDC, transactional contexts or any other libraries -which internally use thread-locals for passing data, see documentation of the [ThreadContextElement] interface +which internally use thread-locals for passing data, see the documentation of the [ThreadContextElement] interface that should be implemented. From 1a6beba52fa5027452bb14b53b04e0b6a4fbed99 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 10 Aug 2020 18:12:22 +0300 Subject: [PATCH 090/257] Support context in Flow.asPublisher and similar methods (#2156) Fixes #2155 Co-authored-by: Vsevolod Tolstopyatov --- .../api/kotlinx-coroutines-jdk9.api | 2 + .../src/ReactiveFlow.kt | 14 ++- .../api/kotlinx-coroutines-reactive.api | 4 +- .../src/ReactiveFlow.kt | 23 +++-- .../test/FlowAsPublisherTest.kt | 76 +++++++++++++++- .../api/kotlinx-coroutines-reactor.api | 2 + .../src/ReactorFlow.kt | 19 +++- .../test/FlowAsFluxTest.kt | 79 +++++++++++++++- .../api/kotlinx-coroutines-rx2.api | 4 + .../kotlinx-coroutines-rx2/src/RxConvert.kt | 20 ++++- .../test/FlowAsFlowableTest.kt | 89 +++++++++++++++++++ .../test/FlowAsObservableTest.kt | 69 ++++++++++++++ .../api/kotlinx-coroutines-rx3.api | 4 + .../kotlinx-coroutines-rx3/src/RxConvert.kt | 20 ++++- .../test/FlowAsFlowableTest.kt | 89 +++++++++++++++++++ .../test/FlowAsObservableTest.kt | 69 ++++++++++++++ 16 files changed, 562 insertions(+), 21 deletions(-) create mode 100644 reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt diff --git a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api index d4bc1698ef..1f5bdec7d0 100644 --- a/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api +++ b/reactive/kotlinx-coroutines-jdk9/api/kotlinx-coroutines-jdk9.api @@ -15,6 +15,8 @@ public final class kotlinx/coroutines/jdk9/PublishKt { public final class kotlinx/coroutines/jdk9/ReactiveFlowKt { public static final fun asFlow (Ljava/util/concurrent/Flow$Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Ljava/util/concurrent/Flow$Publisher; + public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Ljava/util/concurrent/Flow$Publisher; + public static synthetic fun asPublisher$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Ljava/util/concurrent/Flow$Publisher; public static final fun collect (Ljava/util/concurrent/Flow$Publisher;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt index 89caf82c54..5d546dffd3 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/ReactiveFlow.kt @@ -4,12 +4,14 @@ package kotlinx.coroutines.jdk9 +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asPublisher import kotlinx.coroutines.reactive.collect +import org.reactivestreams.* +import kotlin.coroutines.* import java.util.concurrent.Flow as JFlow -import org.reactivestreams.FlowAdapters /** * Transforms the given reactive [Publisher] into [Flow]. @@ -25,9 +27,15 @@ public fun JFlow.Publisher.asFlow(): Flow = /** * Transforms the given flow to a reactive specification compliant [Publisher]. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -public fun Flow.asPublisher(): JFlow.Publisher { - val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher() +@JvmOverloads // binary compatibility +public fun Flow.asPublisher(context: CoroutineContext = EmptyCoroutineContext): JFlow.Publisher { + val reactivePublisher : org.reactivestreams.Publisher = this.asPublisher(context) return FlowAdapters.toFlowPublisher(reactivePublisher) } diff --git a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api index bed065d582..5783edeaa1 100644 --- a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api +++ b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api @@ -32,7 +32,7 @@ public final class kotlinx/coroutines/reactive/FlowKt { public final class kotlinx/coroutines/reactive/FlowSubscription : kotlinx/coroutines/AbstractCoroutine, org/reactivestreams/Subscription { public final field flow Lkotlinx/coroutines/flow/Flow; public final field subscriber Lorg/reactivestreams/Subscriber; - public fun (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;)V + public fun (Lkotlinx/coroutines/flow/Flow;Lorg/reactivestreams/Subscriber;Lkotlin/coroutines/CoroutineContext;)V public fun cancel ()V public fun request (J)V } @@ -65,5 +65,7 @@ public final class kotlinx/coroutines/reactive/PublisherCoroutine : kotlinx/coro public final class kotlinx/coroutines/reactive/ReactiveFlowKt { public static final fun asFlow (Lorg/reactivestreams/Publisher;)Lkotlinx/coroutines/flow/Flow; public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;)Lorg/reactivestreams/Publisher; + public static final fun asPublisher (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lorg/reactivestreams/Publisher; + public static synthetic fun asPublisher$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; } diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index efa9c9c9f1..a51f583b77 100644 --- a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt +++ b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt @@ -34,8 +34,15 @@ public fun Publisher.asFlow(): Flow = * * This function is integrated with `ReactorContext` from `kotlinx-coroutines-reactor` module, * see its documentation for additional details. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -public fun Flow.asPublisher(): Publisher = FlowAsPublisher(this) +@JvmOverloads // binary compatibility +public fun Flow.asPublisher(context: CoroutineContext = EmptyCoroutineContext): Publisher = + FlowAsPublisher(this, Dispatchers.Unconfined + context) private class PublisherAsFlow( private val publisher: Publisher, @@ -153,11 +160,14 @@ internal fun Publisher.injectCoroutineContext(coroutineContext: Coroutine * Adapter that transforms [Flow] into TCK-complaint [Publisher]. * [cancel] invocation cancels the original flow. */ -@Suppress("PublisherImplementation") -private class FlowAsPublisher(private val flow: Flow) : Publisher { +@Suppress("ReactiveStreamsPublisherImplementation") +private class FlowAsPublisher( + private val flow: Flow, + private val context: CoroutineContext +) : Publisher { override fun subscribe(subscriber: Subscriber?) { if (subscriber == null) throw NullPointerException() - subscriber.onSubscribe(FlowSubscription(flow, subscriber)) + subscriber.onSubscribe(FlowSubscription(flow, subscriber, context)) } } @@ -165,8 +175,9 @@ private class FlowAsPublisher(private val flow: Flow) : Publisher @InternalCoroutinesApi public class FlowSubscription( @JvmField public val flow: Flow, - @JvmField public val subscriber: Subscriber -) : Subscription, AbstractCoroutine(Dispatchers.Unconfined, true) { + @JvmField public val subscriber: Subscriber, + context: CoroutineContext +) : Subscription, AbstractCoroutine(context, true) { private val requested = atomic(0L) private val producer = atomic?>(createInitialContinuation()) diff --git a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt index c044d92725..e7b8cb17ae 100644 --- a/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/FlowAsPublisherTest.kt @@ -8,10 +8,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test import org.reactivestreams.* +import java.util.concurrent.* import kotlin.test.* class FlowAsPublisherTest : TestBase() { - @Test fun testErrorOnCancellationIsReported() { expect(1) @@ -75,4 +75,78 @@ class FlowAsPublisherTest : TestBase() { }) finish(4) } + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asPublisher().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsPublisherTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asPublisher(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } } diff --git a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api index 422f36b1ea..3b5c6b9522 100644 --- a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api +++ b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api @@ -38,6 +38,8 @@ public final class kotlinx/coroutines/reactor/ReactorContextKt { public final class kotlinx/coroutines/reactor/ReactorFlowKt { public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;)Lreactor/core/publisher/Flux; + public static final fun asFlux (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lreactor/core/publisher/Flux; + public static synthetic fun asFlux$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lreactor/core/publisher/Flux; } public final class kotlinx/coroutines/reactor/SchedulerCoroutineDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay { diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt index d665c88d35..a478ab1ef8 100644 --- a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt +++ b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt @@ -4,25 +4,38 @@ package kotlinx.coroutines.reactor +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.reactive.FlowSubscription +import org.reactivestreams.* import reactor.core.CoreSubscriber import reactor.core.publisher.Flux +import kotlin.coroutines.* /** * Converts the given flow to a cold flux. * The original flow is cancelled when the flux subscriber is disposed. * * This function is integrated with [ReactorContext], see its documentation for additional details. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ -public fun Flow.asFlux(): Flux = FlowAsFlux(this) +@JvmOverloads // binary compatibility +public fun Flow.asFlux(context: CoroutineContext = EmptyCoroutineContext): Flux = + FlowAsFlux(this, Dispatchers.Unconfined + context) -private class FlowAsFlux(private val flow: Flow) : Flux() { +private class FlowAsFlux( + private val flow: Flow, + private val context: CoroutineContext +) : Flux() { override fun subscribe(subscriber: CoreSubscriber?) { if (subscriber == null) throw NullPointerException() val hasContext = !subscriber.currentContext().isEmpty val source = if (hasContext) flow.flowOn(subscriber.currentContext().asCoroutineContext()) else flow - subscriber.onSubscribe(FlowSubscription(source, subscriber)) + subscriber.onSubscribe(FlowSubscription(source, subscriber, context)) } } diff --git a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt index e4bd8b315b..cecc89592e 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt @@ -4,10 +4,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* import org.junit.Test +import org.reactivestreams.* import reactor.core.publisher.* import reactor.util.context.Context +import java.util.concurrent.* import kotlin.test.* +@Suppress("ReactiveStreamsSubscriberImplementation") class FlowAsFluxTest : TestBase() { @Test fun testFlowAsFluxContextPropagation() { @@ -68,4 +71,78 @@ class FlowAsFluxTest : TestBase() { } finish(4) } -} \ No newline at end of file + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asFlux().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsFluxTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asFlux(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api index 22f40384f0..06ddb68e9c 100644 --- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api +++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api @@ -35,6 +35,10 @@ public final class kotlinx/coroutines/rx2/RxConvertKt { public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Single; public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; } public final class kotlinx/coroutines/rx2/RxFlowableKt { diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index 0be606ffc2..264cdad43c 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* +import org.reactivestreams.* import java.util.concurrent.atomic.* import kotlin.coroutines.* @@ -106,15 +107,21 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { /** * Converts the given flow to a cold observable. * The original flow is cancelled when the observable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Observer] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ +@JvmOverloads // binary compatibility @JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asObservable() : Observable = Observable.create { emitter -> +public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ - val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { + val job = GlobalScope.launch(Dispatchers.Unconfined + context, start = CoroutineStart.ATOMIC) { try { collect { value -> emitter.onNext(value) } emitter.onComplete() @@ -135,7 +142,14 @@ public fun Flow.asObservable() : Observable = Observable.create { /** * Converts the given flow to a cold flowable. * The original flow is cancelled when the flowable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ +@JvmOverloads // binary compatibility @JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) +public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + Flowable.fromPublisher(asPublisher(context)) diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt new file mode 100644 index 0000000000..1cbded6dc3 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx2/test/FlowAsFlowableTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx2 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import org.reactivestreams.* +import java.util.concurrent.* +import kotlin.test.* + +@Suppress("ReactiveStreamsSubscriberImplementation") +class FlowAsFlowableTest : TestBase() { + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asFlowable().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsFlowableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asFlowable(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt index 0908b34cf2..3cde182260 100644 --- a/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/FlowAsObservableTest.kt @@ -4,9 +4,12 @@ package kotlinx.coroutines.rx2 +import io.reactivex.* +import io.reactivex.disposables.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test +import java.util.concurrent.* import kotlin.test.* class FlowAsObservableTest : TestBase() { @@ -139,4 +142,70 @@ class FlowAsObservableTest : TestBase() { observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) } + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asObservable().subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsObservableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asObservable(dispatcher).subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(e: Throwable) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } } diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api index 27c3d3dfa0..4f15eda7d4 100644 --- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -30,6 +30,10 @@ public final class kotlinx/coroutines/rx3/RxConvertKt { public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Single; public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; + public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; + public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; } public final class kotlinx/coroutines/rx3/RxFlowableKt { diff --git a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt index f9e2e2158f..c7ab237cea 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* +import org.reactivestreams.* import java.util.concurrent.atomic.* import kotlin.coroutines.* @@ -91,15 +92,21 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { /** * Converts the given flow to a cold observable. * The original flow is cancelled when the observable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Observer] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ +@JvmOverloads // binary compatibility @JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asObservable() : Observable = Observable.create { emitter -> +public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ - val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { + val job = GlobalScope.launch(Dispatchers.Unconfined + context, start = CoroutineStart.ATOMIC) { try { collect { value -> emitter.onNext(value) } emitter.onComplete() @@ -120,7 +127,14 @@ public fun Flow.asObservable() : Observable = Observable.create { /** * Converts the given flow to a cold flowable. * The original flow is cancelled when the flowable subscriber is disposed. + * + * An optional [context] can be specified to control the execution context of calls to [Subscriber] methods. + * You can set a [CoroutineDispatcher] to confine them to a specific thread and/or various [ThreadContextElement] to + * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher + * is used, so calls are performed from an arbitrary thread. */ +@JvmOverloads // binary compatibility @JvmName("from") @ExperimentalCoroutinesApi -public fun Flow.asFlowable(): Flowable = Flowable.fromPublisher(asPublisher()) +public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + Flowable.fromPublisher(asPublisher(context)) diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt new file mode 100644 index 0000000000..a73fee469e --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/FlowAsFlowableTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import org.reactivestreams.* +import java.util.concurrent.* +import kotlin.test.* + +@Suppress("ReactiveStreamsSubscriberImplementation") +class FlowAsFlowableTest : TestBase() { + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asFlowable().subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsFlowableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asFlowable(dispatcher).subscribe(object : Subscriber { + private lateinit var subscription: Subscription + + override fun onSubscribe(s: Subscription) { + expect(2) + subscription = s + subscription.request(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(t: Throwable?) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } +} diff --git a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt index 50c4ae7dad..5759f9f426 100644 --- a/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx3/test/FlowAsObservableTest.kt @@ -4,9 +4,12 @@ package kotlinx.coroutines.rx3 +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.disposables.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.junit.Test +import java.util.concurrent.* import kotlin.test.* class FlowAsObservableTest : TestBase() { @@ -139,4 +142,70 @@ class FlowAsObservableTest : TestBase() { observable.subscribe({ expect(2) }, { expectUnreached() }, { finish(3) }) } + + @Test + fun testUnconfinedDefaultContext() { + expect(1) + val thread = Thread.currentThread() + fun checkThread() { + assertSame(thread, Thread.currentThread()) + } + flowOf(42).asObservable().subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + } + + override fun onError(t: Throwable) { + expectUnreached() + } + }) + finish(5) + } + + @Test + fun testConfinedContext() { + expect(1) + val threadName = "FlowAsObservableTest.testConfinedContext" + fun checkThread() { + val currentThread = Thread.currentThread() + assertTrue(currentThread.name.startsWith(threadName), "Unexpected thread $currentThread") + } + val completed = CountDownLatch(1) + newSingleThreadContext(threadName).use { dispatcher -> + flowOf(42).asObservable(dispatcher).subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + expect(2) + } + + override fun onNext(t: Int) { + checkThread() + expect(3) + assertEquals(42, t) + } + + override fun onComplete() { + checkThread() + expect(4) + completed.countDown() + } + + override fun onError(e: Throwable) { + expectUnreached() + } + }) + completed.await() + } + finish(5) + } } From abc6cd88a48ef7f87fa305ebc985a3b1591e4377 Mon Sep 17 00:00:00 2001 From: katia-energizer <57506198+katia-energizer@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:51:36 +0300 Subject: [PATCH 091/257] Feat: updated debugging coroutines in the documentation (#2191) --- coroutines-guide.md | 2 ++ docs/coroutine-context-and-dispatchers.md | 33 ++++++++++++++++++++-- docs/images/coroutine-debugger.png | Bin 0 -> 390955 bytes 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/images/coroutine-debugger.png diff --git a/coroutines-guide.md b/coroutines-guide.md index 4b3c09c40f..ea512ba68d 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -32,6 +32,8 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m * [Dispatchers and threads](docs/coroutine-context-and-dispatchers.md#dispatchers-and-threads) * [Unconfined vs confined dispatcher](docs/coroutine-context-and-dispatchers.md#unconfined-vs-confined-dispatcher) * [Debugging coroutines and threads](docs/coroutine-context-and-dispatchers.md#debugging-coroutines-and-threads) + * [Debugging with IDEA](docs/coroutine-context-and-dispatchers.md#debugging-with-idea) + * [Debugging using logging](docs/coroutine-context-and-dispatchers.md#debugging-using-logging) * [Jumping between threads](docs/coroutine-context-and-dispatchers.md#jumping-between-threads) * [Job in the context](docs/coroutine-context-and-dispatchers.md#job-in-the-context) * [Children of a coroutine](docs/coroutine-context-and-dispatchers.md#children-of-a-coroutine) diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index e0bbc59617..1d3abc4928 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -8,6 +8,8 @@ * [Dispatchers and threads](#dispatchers-and-threads) * [Unconfined vs confined dispatcher](#unconfined-vs-confined-dispatcher) * [Debugging coroutines and threads](#debugging-coroutines-and-threads) + * [Debugging with IDEA](#debugging-with-idea) + * [Debugging using logging](#debugging-using-logging) * [Jumping between threads](#jumping-between-threads) * [Job in the context](#job-in-the-context) * [Children of a coroutine](#children-of-a-coroutine) @@ -155,8 +157,35 @@ The unconfined dispatcher should not be used in general code. Coroutines can suspend on one thread and resume on another thread. Even with a single-threaded dispatcher it might be hard to -figure out what the coroutine was doing, where, and when. The common approach to debugging applications with -threads is to print the thread name in the log file on each log statement. This feature is universally supported +figure out what the coroutine was doing, where, and when if you don't have special tooling. + +#### Debugging with IDEA + +The Coroutine Debugger of the Kotlin plugin simplifies debugging coroutines in IntelliJ IDEA. + +> Debugging works for versions 1.3.8 or later of `kotlinx-coroutines-core`. + +The **Debug Tool Window** contains a **Coroutines** tab. In this tab, you can find information about both currently +running and suspended coroutines. The coroutines are grouped by the dispatcher they are running on. + +![Debugging coroutines](images/coroutine-debugger.png) + +You can: +* Easily check the state of each coroutine. +* See the values of local and captured variables for both running and suspended coroutines. +* See a full coroutine creation stack, as well as a call stack inside the coroutine. The stack includes all frames with +variable values, even those that would be lost during standard debugging. + +If you need a full report containing the state of each coroutine and its stack, right-click inside the **Coroutines** tab, and then +click **Get Coroutines Dump**. + +Learn more about debugging coroutines in [this blog post](https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-rc-debugging-coroutines/) +and [IntelliJ IDEA documentation](https://www.jetbrains.com/help/idea/debug-kotlin-coroutines.html). + +#### Debugging using logging + +Another approach to debugging applications with +threads without Coroutine Debugger is to print the thread name in the log file on each log statement. This feature is universally supported by logging frameworks. When using coroutines, the thread name alone does not give much of a context, so `kotlinx.coroutines` includes debugging facilities to make it easier. diff --git a/docs/images/coroutine-debugger.png b/docs/images/coroutine-debugger.png new file mode 100644 index 0000000000000000000000000000000000000000..3f59ee0c8a71b14807ab90c9475414968b82d41a GIT binary patch literal 390955 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®iCdU)=Z0#UQ)8-H3x&Q{UiR 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%0DewO!Nz2~ny;Mnpri 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 literal 0 HcmV?d00001 From 6f01402af69537735bc906454d70a342967f76ef Mon Sep 17 00:00:00 2001 From: Stanislav Erokhin Date: Wed, 15 Jul 2020 20:24:44 +0300 Subject: [PATCH 092/257] Update api dump for 1.4.0 compiler --- .../api/kotlinx-coroutines-core.api | 2 +- .../jvm/resources/DebugProbesKt.bin | Bin 1730 -> 1728 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 4609c68e36..36cbdb6960 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -787,7 +787,7 @@ public abstract interface class kotlinx/coroutines/channels/ReceiveChannel { public abstract fun iterator ()Lkotlinx/coroutines/channels/ChannelIterator; public abstract fun poll ()Ljava/lang/Object; public abstract fun receive (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun receiveOrClosed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun receiveOrClosed-ZYPwvRU (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun receiveOrNull (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin index 8a7160fbad39f25ba582998c532c6ff65c788d5f..76ee41159dbc84c6de5231dbec11bebf0d37b008 100644 GIT binary patch delta 72 zcmX@adw_RCA)}aRaz<)$wqJgUUujNGKw?p1ZfZ$t(dJG@b0%&UW(FW&WME1v+kBdN Z2NS=RCxbSFPBa6vCxa%CIe8MB0RS(N6Kenf delta 74 zcmX@Wdx&>KA)|zNaz<)$c0giLVs2_lYLRELUw(;SX->}Oc1Ck1ZUIIHrlc}v1|Zmc bl6eOczosXH7K3&)1G6Uskf}3y0-FH Date: Thu, 13 Aug 2020 15:34:49 +0300 Subject: [PATCH 093/257] Version 1.3.9 --- CHANGES.md | 6 +++++ README.md | 24 +++++++++---------- build.gradle | 17 +++++++++++-- gradle.properties | 8 +++---- kotlinx-coroutines-debug/README.md | 4 ++-- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 4 ++-- .../example-app/gradle.properties | 4 ++-- 9 files changed, 45 insertions(+), 26 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a1e3953351..9e708c4898 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change log for kotlinx.coroutines +## Version 1.3.9 + +* Support of `CoroutineContext` in `Flow.asPublisher` and similar reactive builders (#2155). +* Transition to new HMPP publication scheme for multiplatform usages. +* Kotlin updated to 1.4.0. + ## Version 1.3.8 ### New experimental features diff --git a/README.md b/README.md index 9f8bae65ba..44a4f0338e 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.8) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.8) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.9) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.9) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.3.71` release. +This is a companion version for Kotlin `1.4.0` release. ```kotlin suspend fun main() = coroutineScope { @@ -84,7 +84,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.3.8 + 1.3.9 ``` @@ -92,7 +92,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.3.71 + 1.4.0 ``` @@ -102,7 +102,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9' } ``` @@ -110,7 +110,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.3.71' + ext.kotlin_version = '1.4.0' } ``` @@ -128,7 +128,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") } ``` @@ -136,7 +136,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.3.71" + kotlin("jvm") version "1.4.0" } ``` @@ -147,7 +147,7 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re Core modules of `kotlinx.coroutines` are also available for [Kotlin/JS](#js) and [Kotlin/Native](#native). In common code that should get compiled for different platforms, add dependency to -[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.8/jar) +[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.9/jar) (follow the link to get the dependency declaration snippet). ### Android @@ -156,7 +156,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' ``` This gives you access to Android [Dispatchers.Main] @@ -172,7 +172,7 @@ For more details see ["Optimization" section for Android](ui/kotlinx-coroutines- ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.8/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.9/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -180,7 +180,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.8/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.9/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/build.gradle b/build.gradle index a758393b6c..55383484ce 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,13 @@ buildscript { repositories { jcenter() - maven { url "https://kotlin.bintray.com/kotlinx" } + maven { + url "https://kotlin.bintray.com/kotlinx" + credentials { + username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" + password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" + } + } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { @@ -155,7 +161,14 @@ allprojects { } } maven { url "https://kotlin.bintray.com/kotlin-eap" } - maven { url "https://kotlin.bintray.com/kotlinx" } + maven { + url "https://kotlin.bintray.com/kotlinx" + credentials { + username = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') ?: "" + password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" + } + } + mavenLocal() } } diff --git a/gradle.properties b/gradle.properties index 6a1ae653f1..238a9c6ac9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,13 +3,13 @@ # # Kotlin -version=1.3.8-SNAPSHOT +version=1.3.9-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.3.71 +kotlin_version=1.4.0 # Dependencies junit_version=4.12 -atomicfu_version=0.14.2 +atomicfu_version=0.14.4 knit_version=0.1.3 html_version=0.6.8 lincheck_version=2.7.1 @@ -59,4 +59,4 @@ systemProp.org.gradle.internal.publish.checksums.insecure=true # This is commented out, and the property is set conditionally in build.gradle, because 1.3.71 doesn't work with it. # Once this property is set by default in new versions or 1.3.71 is dropped, either uncomment or remove this line. #kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.mpp.enableCompatibilityMetadataVariant=true \ No newline at end of file +kotlin.mpp.enableCompatibilityMetadataVariant=true diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 81e62e772a..7c4501ab7a 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -23,7 +23,7 @@ https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.8' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.9' } ``` @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.8.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.9.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 97a1178f3c..f0c01cc48e 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 071f794cb2..fb73cd26b9 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 0be3b9c1cb..3f4084b7a5 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -20,8 +20,8 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.3.71 -coroutines_version=1.3.8 +kotlin_version=1.4.0 +coroutines_version=1.3.9 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 0be3b9c1cb..3f4084b7a5 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -20,8 +20,8 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin_version=1.3.71 -coroutines_version=1.3.8 +kotlin_version=1.4.0 +coroutines_version=1.3.9 android.useAndroidX=true android.enableJetifier=true From 807d628e252cd61aff53eac821db23900d010780 Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Fri, 14 Aug 2020 13:47:55 +0430 Subject: [PATCH 094/257] Update flow.md (#2196) --- docs/flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flow.md b/docs/flow.md index 143f9e9300..caefc82f90 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -50,7 +50,7 @@ ## Asynchronous Flow -Suspending functions asynchronously returns a single value, but how can we return +A suspending function asynchronously returns a single value, but how can we return multiple asynchronously computed values? This is where Kotlin Flows come in. ### Representing multiple values From a61c6cc8c799392d6cc384df6760567829a5fe9b Mon Sep 17 00:00:00 2001 From: Shubham <7660565+shubham08gupta@users.noreply.github.com> Date: Sat, 15 Aug 2020 21:21:49 +0900 Subject: [PATCH 095/257] Update typo in flow.md (#2199) --- docs/flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flow.md b/docs/flow.md index caefc82f90..b1d6669a5d 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -153,7 +153,7 @@ This code prints the numbers after waiting for a second. #### Flows Using the `List` result type, means we can only return all the values at once. To represent -the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would the `Sequence` type for synchronously computed values: +the stream of values that are being asynchronously computed, we can use a [`Flow`][Flow] type just like we would use the `Sequence` type for synchronously computed values:
From 6649745d3deeffce74c78fc6178553ca168f18fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20S=C3=B6ndermann?= Date: Mon, 17 Aug 2020 08:54:55 +0100 Subject: [PATCH 096/257] Add missing 'the' (#2200) --- docs/coroutines-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/coroutines-guide.md b/docs/coroutines-guide.md index e3f18d208e..2d15a7bbff 100644 --- a/docs/coroutines-guide.md +++ b/docs/coroutines-guide.md @@ -10,7 +10,7 @@ coroutine-enabled primitives that this guide covers, including `launch`, `async` This is a guide on core features of `kotlinx.coroutines` with a series of examples, divided up into different topics. -In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on `kotlinx-coroutines-core` module as explained +In order to use coroutines as well as follow the examples in this guide, you need to add a dependency on the `kotlinx-coroutines-core` module as explained [in the project README](../README.md#using-in-your-projects). ## Table of contents From 398fee2b0dd4a3ece165a0c05fd8060385adac47 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 13 Aug 2020 20:44:08 +0300 Subject: [PATCH 097/257] Update documentation --- CHANGES.md | 5 ++++- README.md | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9e708c4898..99a600c48c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,8 +3,11 @@ ## Version 1.3.9 * Support of `CoroutineContext` in `Flow.asPublisher` and similar reactive builders (#2155). -* Transition to new HMPP publication scheme for multiplatform usages. * Kotlin updated to 1.4.0. +* Transition to new HMPP publication scheme for multiplatform usages: + * Artifacts `kotlinx-coroutines-core-common` and `kotlinx-coroutines-core-native` are removed. + * For multiplatform usages, it's enough to [depend directly](README.md#multiplatform) on `kotlinx-coroutines-core` in `commonMain` source-set. + * The same artifact coordinates can be used to depend on platform-specific artifact in platform-specific source-set. ## Version 1.3.8 diff --git a/README.md b/README.md index 44a4f0338e..06e04e4d30 100644 --- a/README.md +++ b/README.md @@ -146,9 +146,14 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re Core modules of `kotlinx.coroutines` are also available for [Kotlin/JS](#js) and [Kotlin/Native](#native). -In common code that should get compiled for different platforms, add dependency to -[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.9/jar) -(follow the link to get the dependency declaration snippet). +In common code that should get compiled for different platforms, you can add dependency to `kotlinx-coroutines-core` right to the `commonMain` source set: +```groovy +commonMain { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + } +} +``` ### Android From bbc99b90e73c16f36ba6cb28df4aa7aeff7c3953 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 17 Aug 2020 14:52:21 +0300 Subject: [PATCH 098/257] Remove redundant release step, it has been automated by try.kotlinlang.org team --- RELEASE.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index efb361f1e5..22cb61c42f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -64,18 +64,14 @@ To release new `` of `kotlinx-coroutines`: 5. Announce new release in [Slack](https://kotlinlang.slack.com) -6. Create a ticket to update coroutines version on [try.kotlinlang.org](try.kotlinlang.org). - * Use [KT-30870](https://youtrack.jetbrains.com/issue/KT-30870) as a template - * This step should be skipped for eap versions that are not merged to `master` - -7. Switch into `develop` branch:
+6. Switch into `develop` branch:
`git checkout develop` -8. Fetch the latest `master`:
+7. Fetch the latest `master`:
`git fetch` -9. Merge release from `master`:
+8. Merge release from `master`:
`git merge origin/master` -0. Push updates to `develop`:
+9. Push updates to `develop`:
`git push` From a34a4fc77ec52a1e5e2be844dcd9ecd36154b1fa Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Wed, 19 Aug 2020 00:00:22 +0430 Subject: [PATCH 099/257] Fix some sample outputs not being displayed in flow.md (#2201) Some sample outputs in section `Flow Exceptions` are not displayed in the documentation. This commit fixes this issue. --- docs/flow.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/flow.md b/docs/flow.md index b1d6669a5d..2b1dfd59a9 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -1463,13 +1463,15 @@ fun main() = runBlocking { A "Caught ..." message is not printed despite there being a `catch` operator: - +``` + + #### Catching declaratively @@ -1510,12 +1512,14 @@ fun main() = runBlocking { Now we can see that a "Caught ..." message is printed and so we can catch all the exceptions without explicitly using a `try/catch` block: - +``` + + ### Flow completion From 0436576a204da63d67b0827b52c6f108fafafebb Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 19 Aug 2020 12:14:56 +0300 Subject: [PATCH 100/257] Remove redundant and flaky (due to its nature, not implementation problems) DebugLeaksStressTest --- .../test/DebugLeaksStressTest.kt | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt diff --git a/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt b/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt deleted file mode 100644 index bf34917b77..0000000000 --- a/kotlinx-coroutines-debug/test/DebugLeaksStressTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -import kotlinx.coroutines.* -import kotlinx.coroutines.debug.* -import org.junit.* - -/** - * This stress tests ensure that no actual [OutOfMemoryError] occurs when lots of coroutines are created and - * leaked in various ways under debugger. A faster but more fragile version of this test is in [DebugLeaksTest]. - */ -class DebugLeaksStressTest : DebugTestBase() { - private val nRepeat = 100_000 * stressTestMultiplier - private val nBytes = 100_000 - - @Test - fun testIteratorLeak() { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - iterator { yield(bytes) } - } - } - - @Test - fun testLazyGlobalCoroutineLeak() { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - GlobalScope.launch(start = CoroutineStart.LAZY) { println(bytes) } - } - } - - @Test - fun testLazyCancelledChildCoroutineLeak() = runTest { - coroutineScope { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - val child = launch(start = CoroutineStart.LAZY) { println(bytes) } - child.cancel() - } - } - } - - @Test - fun testAbandonedGlobalCoroutineLeak() { - repeat(nRepeat) { - val bytes = ByteArray(nBytes) - GlobalScope.launch { - suspendForever() - println(bytes) - } - } - } - - private suspend fun suspendForever() = suspendCancellableCoroutine { } -} From 230ade72b5f99821bbb7cf3160df148736b8ad67 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 20 Aug 2020 05:12:03 -0700 Subject: [PATCH 101/257] Fix sporadic ConcurrentWeakMapTest failures (#2187) --- .../jvm/test/internal/ConcurrentWeakMapTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt index ae4b5fce30..e4fa5e9bfe 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/ConcurrentWeakMapTest.kt @@ -12,8 +12,8 @@ import org.junit.* class ConcurrentWeakMapTest : TestBase() { @Test fun testSimple() { - val expect = (1..1000).associateWith { it.toString() } - val m = ConcurrentWeakMap() + val expect = (1..1000).associate { it.toString().let { it to it } } + val m = ConcurrentWeakMap() // repeat adding/removing a few times repeat(5) { assertEquals(0, m.size) @@ -27,7 +27,7 @@ class ConcurrentWeakMapTest : TestBase() { assertEquals(expect.keys, m.keys) assertEquals(expect.entries, m.entries) for ((k, v) in expect) { - assertEquals(v, m.get(k)) + assertEquals(v, m[k]) } assertEquals(expect.size, m.size) if (it % 2 == 0) { @@ -38,9 +38,9 @@ class ConcurrentWeakMapTest : TestBase() { m.clear() } assertEquals(0, m.size) - for ((k, v) in expect) { - assertNull(m.get(k)) + for ((k, _) in expect) { + assertNull(m[k]) } } } -} \ No newline at end of file +} From 1ec50feb7f6601f8df08d2e21dec4cf324f5d6fd Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 24 Aug 2020 10:25:03 +0200 Subject: [PATCH 102/257] Fix typos in gradle.properties (#2211) --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 238a9c6ac9..b778ce2b64 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,10 +45,10 @@ source_map_support_version=0.5.3 kotlin.incremental.multiplatform=true kotlin.native.ignoreDisabledTargets=true -# Site deneration +# Site generation jekyll_version=4.0 -# JS IR baceknd sometimes crashes with out-of-memory +# JS IR backend sometimes crashes with out-of-memory # TODO: Remove once KT-37187 is fixed org.gradle.jvmargs=-Xmx2g From a8bfc0e55ad3fd92c1253eaba04334df5f6fedea Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 24 Aug 2020 11:25:27 +0300 Subject: [PATCH 103/257] Cherry-picks from native-mt branch to reduce maintenance burden (#2204) * Immediate dispatcher on JS, test added * Style fixes --- buildSrc/src/main/kotlin/Idea.kt | 1 + kotlinx-coroutines-core/js/src/Dispatchers.kt | 21 +++++------- .../js/test/ImmediateDispatcherTest.kt | 32 +++++++++++++++++++ kotlinx-coroutines-core/jvm/src/Executors.kt | 2 +- .../native/src/Dispatchers.kt | 8 ----- .../build.gradle.kts | 4 --- 6 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt diff --git a/buildSrc/src/main/kotlin/Idea.kt b/buildSrc/src/main/kotlin/Idea.kt index 802b387b0d..615b8aad74 100644 --- a/buildSrc/src/main/kotlin/Idea.kt +++ b/buildSrc/src/main/kotlin/Idea.kt @@ -1,4 +1,5 @@ object Idea { + @JvmStatic // for Gradle val active: Boolean get() = System.getProperty("idea.active") == "true" } diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 033b39c7e0..06b938d41a 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -7,24 +7,19 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - - public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default) - + public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default, false) public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined } -private class JsMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { - - override val immediate: MainCoroutineDispatcher - get() = throw UnsupportedOperationException("Immediate dispatching is not supported on JS") - +private class JsMainDispatcher( + val delegate: CoroutineDispatcher, + private val invokeImmediately: Boolean +) : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher = + if (invokeImmediately) this else JsMainDispatcher(delegate, true) + override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt b/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt new file mode 100644 index 0000000000..7ca6a242b2 --- /dev/null +++ b/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.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 + +import kotlin.test.* + +class ImmediateDispatcherTest : TestBase() { + + @Test + fun testImmediate() = runTest { + expect(1) + val job = launch { expect(3) } + withContext(Dispatchers.Main.immediate) { + expect(2) + } + job.join() + finish(4) + } + + @Test + fun testMain() = runTest { + expect(1) + val job = launch { expect(2) } + withContext(Dispatchers.Main) { + expect(3) + } + job.join() + finish(4) + } +} diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index a4d6b46c43..8c36bfa14f 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* * Instances of [ExecutorCoroutineDispatcher] should be closed by the owner of the dispatcher. * * This class is generally used as a bridge between coroutine-based API and - * asynchronous API which requires instance of the [Executor]. + * asynchronous API that requires an instance of the [Executor]. */ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable { /** @suppress */ diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/native/src/Dispatchers.kt index aca1cc0693..c06b7c2f0a 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/native/src/Dispatchers.kt @@ -7,24 +7,16 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default) - public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing } private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { - override val immediate: MainCoroutineDispatcher get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native") - override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = toStringInternalImpl() ?: delegate.toString() } diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts index 4be32fc5c6..7c9e7020b5 100644 --- a/ui/kotlinx-coroutines-android/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/build.gradle.kts @@ -6,10 +6,6 @@ import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink import org.jetbrains.dokka.gradle.DokkaTask import java.net.URL -repositories { - google() -} - configurations { create("r8") } From 3cbf4ada91825e6102f460117008bbe4ddedea99 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Mon, 24 Aug 2020 11:27:15 +0300 Subject: [PATCH 104/257] Kotlin and Slack channel shields (#2205) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 06e04e4d30..4b6f99be0b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) [![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.9) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.9) +[![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. This is a companion version for Kotlin `1.4.0` release. From 63156a85a551dcc66c6b5b4f4c59f2704b0c634b Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 24 Aug 2020 16:15:52 +0300 Subject: [PATCH 105/257] Add space staging repository --- build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 55383484ce..6e44f9d7dd 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,6 @@ buildscript { if (using_snapshot_version) { repositories { mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } } @@ -59,6 +58,8 @@ buildscript { password = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') ?: "" } } + // Future replacement for kotlin-dev, with cache redirector + maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { @@ -145,7 +146,6 @@ apiValidation { // Configure repositories allprojects { - String projectName = it.name repositories { /* * google should be first in the repository list because some of the play services @@ -153,6 +153,8 @@ allprojects { */ google() jcenter() + // Future replacement for kotlin-dev, with cache redirector + maven { url "https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } maven { url "https://kotlin.bintray.com/kotlin-dev" credentials { From 964cd92d7a12e11161740b3e4016eb7b8223e8e3 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Sat, 2 May 2020 19:52:56 +0300 Subject: [PATCH 106/257] Transition to the Gradle kts Non-deprecated 'jmhJar' configuration (#2032) (+9 squashed commits) Squashed commits: [8d07d3695] Use new Kotlin/JS plugin (#1983) * Use new Kotlin/JS plugin * Support legacy DCE mode for 1.4-M2 [d22464030] Add Dokka configuration method [56e1c9bfe] Dokka plugin in 'buildSrc' [dfdd202e6] Remove unused repositories [4cf1d02dc] Kotlin DSL - 'javafx' [d8f7d507e] Avoid task name duplication [f06a56b8b] Avoid task name duplication [a09df3de5] Separate 'UnpackAar' action [fd5bf6b9c] Separate 'RunR8' task --- benchmarks/build.gradle.kts | 12 +++-- build.gradle | 2 +- buildSrc/build.gradle.kts | 16 ++++++ buildSrc/src/main/kotlin/Dokka.kt | 23 ++++++++ buildSrc/src/main/kotlin/MavenCentral.kt | 5 -- buildSrc/src/main/kotlin/Properties.kt | 11 ++++ buildSrc/src/main/kotlin/RunR8.kt | 47 +++++++++++++++++ buildSrc/src/main/kotlin/UnpackAar.kt | 31 +++++++++++ gradle/compile-js.gradle | 13 +++-- gradle/compile-jvm-multiplatform.gradle | 4 -- gradle/compile-jvm.gradle | 4 -- .../build.gradle | 34 ------------ js/example-frontend-js/build.gradle | 31 ++--------- js/example-frontend-js/npm/webpack.config.js | 1 + .../build.gradle.kts | 47 +++++++---------- site/build.gradle.kts | 4 +- .../build.gradle.kts | 52 ++----------------- ui/kotlinx-coroutines-javafx/build.gradle | 34 ------------ ui/kotlinx-coroutines-javafx/build.gradle.kts | 50 ++++++++++++++++++ 19 files changed, 226 insertions(+), 195 deletions(-) create mode 100644 buildSrc/src/main/kotlin/Dokka.kt create mode 100644 buildSrc/src/main/kotlin/Properties.kt create mode 100644 buildSrc/src/main/kotlin/RunR8.kt create mode 100644 buildSrc/src/main/kotlin/UnpackAar.kt delete mode 100644 ui/kotlinx-coroutines-javafx/build.gradle create mode 100644 ui/kotlinx-coroutines-javafx/build.gradle.kts diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 7df4510bf4..5da40f261e 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -2,6 +2,8 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("UnstableApiUsage") + import me.champeau.gradle.* import org.jetbrains.kotlin.gradle.tasks.* @@ -33,7 +35,7 @@ tasks.named("compileJmhKotlin") { * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. */ -val removeRedundantFiles = tasks.register("removeRedundantFiles") { +val removeRedundantFiles by tasks.registering(Delete::class) { delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class") delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class") @@ -71,10 +73,10 @@ extensions.configure("jmh") { } tasks.named("jmhJar") { - baseName = "benchmarks" - classifier = null - version = null - destinationDir = file("$rootDir") + archiveBaseName by "benchmarks" + archiveClassifier by null + archiveVersion by null + destinationDirectory.file("$rootDir") } dependencies { diff --git a/build.gradle b/build.gradle index 6e44f9d7dd..1088e2bef4 100644 --- a/build.gradle +++ b/build.gradle @@ -252,7 +252,7 @@ configure(subprojects.findAll { it.name != coreModule && it.name != rootModule } } // Redefine source sets because we are not using 'kotlin/main/fqn' folder convention -configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "benchmarks" && it.name != coreModule }) { +configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "benchmarks" && it.name != 'example-frontend-js' && it.name != coreModule }) { sourceSets { main.kotlin.srcDirs = ['src'] test.kotlin.srcDirs = ['test'] diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 91b8bda92b..bbbf2a3015 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,11 +1,27 @@ +import java.util.* + plugins { `kotlin-dsl` } repositories { gradlePluginPortal() + maven("https://kotlin.bintray.com/kotlin-eap") + maven("https://kotlin.bintray.com/kotlin-dev") } kotlinDslPluginOptions { experimentalWarning.set(false) } + +val props = Properties().apply { + file("../gradle.properties").inputStream().use { load(it) } +} + +fun version(target: String): String = + props.getProperty("${target}_version") + +dependencies { + implementation(kotlin("gradle-plugin", version("kotlin"))) + implementation("org.jetbrains.dokka:dokka-gradle-plugin:${version("dokka")}") +} diff --git a/buildSrc/src/main/kotlin/Dokka.kt b/buildSrc/src/main/kotlin/Dokka.kt new file mode 100644 index 0000000000..72365c1ee0 --- /dev/null +++ b/buildSrc/src/main/kotlin/Dokka.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.delegateClosureOf +import org.gradle.kotlin.dsl.withType +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink.Builder +import org.jetbrains.dokka.gradle.DokkaTask +import java.io.File +import java.net.URL + +fun Project.externalDocumentationLink( + url: String, + packageList: File = projectDir.resolve("package.list") +) { + tasks.withType().configureEach { + externalDocumentationLink(delegateClosureOf { + this.url = URL(url) + packageListUrl = packageList.toPath().toUri().toURL() + }) + } +} diff --git a/buildSrc/src/main/kotlin/MavenCentral.kt b/buildSrc/src/main/kotlin/MavenCentral.kt index 0d7e18cf15..9a96f3c2de 100644 --- a/buildSrc/src/main/kotlin/MavenCentral.kt +++ b/buildSrc/src/main/kotlin/MavenCentral.kt @@ -5,7 +5,6 @@ @file:Suppress("UnstableApiUsage") import org.gradle.api.Project -import org.gradle.api.provider.Property import org.gradle.api.publish.maven.MavenPom // --------------- pom configuration --------------- @@ -36,7 +35,3 @@ fun MavenPom.configureMavenCentralMetadata(project: Project) { url by "https://github.com/Kotlin/kotlinx.coroutines" } } - -private infix fun Property.by(value: T) { - set(value) -} diff --git a/buildSrc/src/main/kotlin/Properties.kt b/buildSrc/src/main/kotlin/Properties.kt new file mode 100644 index 0000000000..a0968ee699 --- /dev/null +++ b/buildSrc/src/main/kotlin/Properties.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. + */ + +@file:Suppress("UnstableApiUsage") + +import org.gradle.api.provider.* + +infix fun Property.by(value: T) { + set(value) +} diff --git a/buildSrc/src/main/kotlin/RunR8.kt b/buildSrc/src/main/kotlin/RunR8.kt new file mode 100644 index 0000000000..fc91dc0a57 --- /dev/null +++ b/buildSrc/src/main/kotlin/RunR8.kt @@ -0,0 +1,47 @@ +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.bundling.Zip +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.named +import java.io.File + +open class RunR8 : JavaExec() { + + @OutputDirectory + lateinit var outputDex: File + + @InputFile + lateinit var inputConfig: File + + @InputFile + val inputConfigCommon: File = File("testdata/r8-test-common.pro") + + @InputFiles + val jarFile: File = project.tasks.named("jar").get().archivePath + + init { + classpath = project.configurations["r8"] + main = "com.android.tools.r8.R8" + } + + override fun exec() { + // Resolve classpath only during execution + val arguments = mutableListOf( + "--release", + "--no-desugaring", + "--output", outputDex.absolutePath, + "--pg-conf", inputConfig.absolutePath + ) + arguments.addAll(project.configurations["runtimeClasspath"].files.map { it.absolutePath }) + arguments.add(jarFile.absolutePath) + + args = arguments + + project.delete(outputDex) + outputDex.mkdirs() + + super.exec() + } +} diff --git a/buildSrc/src/main/kotlin/UnpackAar.kt b/buildSrc/src/main/kotlin/UnpackAar.kt new file mode 100644 index 0000000000..2b96df3f86 --- /dev/null +++ b/buildSrc/src/main/kotlin/UnpackAar.kt @@ -0,0 +1,31 @@ +import org.gradle.api.artifacts.transform.InputArtifact +import org.gradle.api.artifacts.transform.TransformAction +import org.gradle.api.artifacts.transform.TransformOutputs +import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.provider.Provider +import java.io.File +import java.nio.file.Files +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +@Suppress("UnstableApiUsage") +abstract class UnpackAar : TransformAction { + @get:InputArtifact + abstract val inputArtifact: Provider + + override fun transform(outputs: TransformOutputs) { + ZipFile(inputArtifact.get().asFile).use { zip -> + zip.entries().asSequence() + .filter { !it.isDirectory } + .filter { it.name.endsWith(".jar") } + .forEach { zip.unzip(it, outputs.file(it.name)) } + } + } +} + +private fun ZipFile.unzip(entry: ZipEntry, output: File) { + getInputStream(entry).use { + Files.copy(it, output.toPath()) + } +} diff --git a/gradle/compile-js.gradle b/gradle/compile-js.gradle index d0697cfd3a..77de787fb8 100644 --- a/gradle/compile-js.gradle +++ b/gradle/compile-js.gradle @@ -4,14 +4,21 @@ // Platform-specific configuration to compile JS modules -apply plugin: 'kotlin2js' +apply plugin: 'org.jetbrains.kotlin.js' + +kotlin.sourceSets { + main.kotlin.srcDirs = ['src'] + test.kotlin.srcDirs = ['test'] + main.resources.srcDirs = ['resources'] + test.resources.srcDirs = ['test-resources'] +} dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } -tasks.withType(compileKotlin2Js.getClass()) { +tasks.withType(compileKotlinJs.getClass()) { kotlinOptions { moduleKind = "umd" sourceMap = true @@ -19,7 +26,7 @@ tasks.withType(compileKotlin2Js.getClass()) { } } -compileKotlin2Js { +compileKotlinJs { kotlinOptions { // drop -js suffix from outputFile def baseName = project.name - "-js" diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle index b226c97a57..74495ba1dc 100644 --- a/gradle/compile-jvm-multiplatform.gradle +++ b/gradle/compile-jvm-multiplatform.gradle @@ -5,10 +5,6 @@ sourceCompatibility = 1.6 targetCompatibility = 1.6 -repositories { - maven { url "https://dl.bintray.com/devexperts/Maven/" } -} - kotlin { targets { fromPreset(presets.jvm, 'jvm') diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index a8116595f5..232a185d7f 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -19,10 +19,6 @@ dependencies { testCompile "junit:junit:$junit_version" } -repositories { - maven { url "https://dl.bintray.com/devexperts/Maven/" } -} - compileKotlin { kotlinOptions { freeCompilerArgs += ['-Xexplicit-api=strict'] diff --git a/integration/kotlinx-coroutines-play-services/build.gradle b/integration/kotlinx-coroutines-play-services/build.gradle index eb554866ed..29ce3d606f 100644 --- a/integration/kotlinx-coroutines-play-services/build.gradle +++ b/integration/kotlinx-coroutines-play-services/build.gradle @@ -2,12 +2,6 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -import org.gradle.api.artifacts.transform.* - -import java.nio.file.Files -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - ext.tasks_version = '16.0.1' def artifactType = Attribute.of("artifactType", String) @@ -49,31 +43,3 @@ tasks.withType(dokka.getClass()) { packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() } } - -abstract class UnpackAar implements TransformAction { - @InputArtifact - abstract Provider getInputArtifact() - - @Override - void transform(TransformOutputs outputs) { - ZipFile zip = new ZipFile(inputArtifact.get().asFile) - try { - for (entry in zip.entries()) { - if (!entry.isDirectory() && entry.name.endsWith(".jar")) { - unzipEntryTo(zip, entry, outputs.file(entry.name)) - } - } - } finally { - zip.close() - } - } - - private static void unzipEntryTo(ZipFile zip, ZipEntry entry, File output) { - InputStream stream = zip.getInputStream(entry) - try { - Files.copy(stream, output.toPath()) - } finally { - stream.close() - } - } -} diff --git a/js/example-frontend-js/build.gradle b/js/example-frontend-js/build.gradle index 735a70d55b..f81efdd2f9 100644 --- a/js/example-frontend-js/build.gradle +++ b/js/example-frontend-js/build.gradle @@ -5,36 +5,19 @@ apply plugin: 'kotlin-dce-js' apply from: rootProject.file('gradle/node-js.gradle') -// Workaround resolving new Gradle metadata with kotlin2js -// TODO: Remove once KT-37188 is fixed -try { - def jsCompilerType = Class.forName("org.jetbrains.kotlin.gradle.targets.js.KotlinJsCompilerAttribute") - def jsCompilerAttr = Attribute.of("org.jetbrains.kotlin.js.compiler", jsCompilerType) - project.dependencies.attributesSchema.attribute(jsCompilerAttr) - configurations { - matching { - it.name.endsWith("Classpath") - }.forEach { - it.attributes.attribute(jsCompilerAttr, jsCompilerType.legacy) - } - } -} catch (java.lang.ClassNotFoundException e) { - // org.jetbrains.kotlin.gradle.targets.js.JsCompilerType is missing in 1.3.x - // But 1.3.x doesn't generate Gradle metadata, so this workaround is not needed -} - dependencies { compile "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" } -compileKotlin2Js { +compileKotlinJs { kotlinOptions { main = "call" } } -task bundle(type: NpmTask, dependsOn: [npmInstall, runDceKotlinJs]) { +task bundle(type: NpmTask, dependsOn: [npmInstall, build]) { inputs.files(fileTree("$buildDir/kotlin-js-min/main")) + inputs.files(fileTree("$buildDir/kotlin-js-min/legacy/main")) inputs.files(fileTree(file("src/main/web"))) inputs.file("npm/webpack.config.js") outputs.dir("$buildDir/dist") @@ -44,11 +27,3 @@ task bundle(type: NpmTask, dependsOn: [npmInstall, runDceKotlinJs]) { task start(type: NpmTask, dependsOn: bundle) { args = ["run", "start"] } - -// we have not tests but kotlin-dce-js still tries to work with them and crashed. -// todo: Remove when KT-22028 is fixed -afterEvaluate { - if (tasks.findByName('unpackDependenciesTestKotlinJs')) { - tasks.unpackDependenciesTestKotlinJs.enabled = false - } -} diff --git a/js/example-frontend-js/npm/webpack.config.js b/js/example-frontend-js/npm/webpack.config.js index a208d047b3..8428c55741 100644 --- a/js/example-frontend-js/npm/webpack.config.js +++ b/js/example-frontend-js/npm/webpack.config.js @@ -38,6 +38,7 @@ module.exports = { resolve: { modules: [ path.resolve(__dirname, "kotlin-js-min/main"), + path.resolve(__dirname, "kotlin-js-min/legacy/main"), path.resolve(__dirname, "../src/main/web/") ] }, diff --git a/reactive/kotlinx-coroutines-reactive/build.gradle.kts b/reactive/kotlinx-coroutines-reactive/build.gradle.kts index c69148fecf..d21b28f8ce 100644 --- a/reactive/kotlinx-coroutines-reactive/build.gradle.kts +++ b/reactive/kotlinx-coroutines-reactive/build.gradle.kts @@ -2,10 +2,6 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink -import org.jetbrains.dokka.gradle.DokkaTask -import java.net.URL - val reactiveStreamsVersion = property("reactive_streams_version") dependencies { @@ -13,31 +9,26 @@ dependencies { testCompile("org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion") } -tasks { - val testNG = register("testNG") { - useTestNG() - reports.html.destination = file("$buildDir/reports/testng") - include("**/*ReactiveStreamTckTest.*") - // Skip testNG when tests are filtered with --tests, otherwise it simply fails - onlyIf { - filter.includePatterns.isEmpty() - } - doFirst { - // Classic gradle, nothing works without doFirst - println("TestNG tests: ($includes)") - } +val testNG by tasks.registering(Test::class) { + useTestNG() + reports.html.destination = file("$buildDir/reports/testng") + include("**/*ReactiveStreamTckTest.*") + // Skip testNG when tests are filtered with --tests, otherwise it simply fails + onlyIf { + filter.includePatterns.isEmpty() } - - named("test") { - reports.html.destination = file("$buildDir/reports/junit") - - dependsOn(testNG) + doFirst { + // Classic gradle, nothing works without doFirst + println("TestNG tests: ($includes)") } +} - withType().configureEach { - externalDocumentationLink(delegateClosureOf { - url = URL("https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - }) - } +tasks.test { + reports.html.destination = file("$buildDir/reports/junit") + + dependsOn(testNG) } + +externalDocumentationLink( + url = "https://www.reactive-streams.org/reactive-streams-$reactiveStreamsVersion-javadoc/" +) diff --git a/site/build.gradle.kts b/site/build.gradle.kts index a18062a371..686e81d336 100644 --- a/site/build.gradle.kts +++ b/site/build.gradle.kts @@ -7,7 +7,7 @@ import groovy.lang.* val buildDocsDir = "$buildDir/docs" val jekyllDockerImage = "jekyll/jekyll:${version("jekyll")}" -val copyDocs = tasks.register("copyDocs") { +val copyDocs by tasks.registering(Copy::class) { val dokkaTasks = rootProject.getTasksByName("dokka", true) from(dokkaTasks.map { "${it.project.buildDir}/dokka" }) { @@ -21,7 +21,7 @@ val copyDocs = tasks.register("copyDocs") { dependsOn(dokkaTasks) } -val copyExampleFrontendJs = tasks.register("copyExampleFrontendJs") { +val copyExampleFrontendJs by tasks.registering(Copy::class) { val srcBuildDir = project(":example-frontend-js").buildDir from("$srcBuildDir/dist") into("$buildDocsDir/example-frontend-js") diff --git a/ui/kotlinx-coroutines-android/build.gradle.kts b/ui/kotlinx-coroutines-android/build.gradle.kts index 7c9e7020b5..4f24788359 100644 --- a/ui/kotlinx-coroutines-android/build.gradle.kts +++ b/ui/kotlinx-coroutines-android/build.gradle.kts @@ -21,59 +21,20 @@ dependencies { "r8"("com.android.tools.build:builder:4.0.0-alpha06") // Contains r8-2.0.4-dev } -open class RunR8Task : JavaExec() { - - @OutputDirectory - lateinit var outputDex: File - - @InputFile - lateinit var inputConfig: File - - @InputFile - val inputConfigCommon: File = File("testdata/r8-test-common.pro") - - @InputFiles - val jarFile: File = project.tasks.named("jar").get().archivePath - - init { - classpath = project.configurations["r8"] - main = "com.android.tools.r8.R8" - } - - override fun exec() { - // Resolve classpath only during execution - val arguments = mutableListOf( - "--release", - "--no-desugaring", - "--output", outputDex.absolutePath, - "--pg-conf", inputConfig.absolutePath - ) - arguments.addAll(project.configurations.runtimeClasspath.files.map { it.absolutePath }) - arguments.add(jarFile.absolutePath) - - args = arguments - - project.delete(outputDex) - outputDex.mkdirs() - - super.exec() - } -} - val optimizedDexDir = File(buildDir, "dex-optim/") val unOptimizedDexDir = File(buildDir, "dex-unoptim/") val optimizedDexFile = File(optimizedDexDir, "classes.dex") val unOptimizedDexFile = File(unOptimizedDexDir, "classes.dex") -val runR8 = tasks.register("runR8") { +val runR8 by tasks.registering(RunR8::class) { outputDex = optimizedDexDir inputConfig = file("testdata/r8-test-rules.pro") dependsOn("jar") } -val runR8NoOptim = tasks.register("runR8NoOptim") { +val runR8NoOptim by tasks.registering(RunR8::class) { outputDex = unOptimizedDexDir inputConfig = file("testdata/r8-test-rules-no-optim.pro") @@ -96,9 +57,6 @@ tasks.test { } } -tasks.withType().configureEach { - externalDocumentationLink(delegateClosureOf { - url = URL("https://developer.android.com/reference/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - }) -} +externalDocumentationLink( + url = "https://developer.android.com/reference/" +) diff --git a/ui/kotlinx-coroutines-javafx/build.gradle b/ui/kotlinx-coroutines-javafx/build.gradle deleted file mode 100644 index 77f1b09650..0000000000 --- a/ui/kotlinx-coroutines-javafx/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -plugins { - id 'org.openjfx.javafxplugin' -} - -javafx { - version = javafx_version - modules = ['javafx.controls'] - configuration = 'compile' -} - -task checkJdk8() { - // only fail w/o JDK_18 when actually trying to test, not during project setup phase - doLast { - if (!System.env.JDK_18) { - throw new GradleException("JDK_18 environment variable is not defined. " + - "Can't run JDK 8 compatibility tests. " + - "Please ensure JDK 8 is installed and that JDK_18 points to it.") - } - } -} - -task jdk8Test(type: Test, dependsOn: [compileTestKotlin, checkJdk8]) { - classpath = files { test.classpath } - testClassesDirs = files { test.testClassesDirs } - executable = "$System.env.JDK_18/bin/java" -} - -// Run these tests only during nightly stress test -jdk8Test.onlyIf { project.properties['stressTest'] != null } -build.dependsOn jdk8Test diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts new file mode 100644 index 0000000000..112441e0ed --- /dev/null +++ b/ui/kotlinx-coroutines-javafx/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("org.openjfx.javafxplugin") +} + +javafx { + version = version("javafx") + modules = listOf("javafx.controls") + configuration = "compile" +} + +val JDK_18: String? by lazy { + System.getenv("JDK_18") +} + +val checkJdk8 by tasks.registering { + // only fail w/o JDK_18 when actually trying to test, not during project setup phase + doLast { + if (JDK_18 == null) { + throw GradleException( + """ + JDK_18 environment variable is not defined. + Can't run JDK 8 compatibility tests. + Please ensure JDK 8 is installed and that JDK_18 points to it. + """.trimIndent() + ) + } + } +} + +val jdk8Test by tasks.registering(Test::class) { + // Run these tests only during nightly stress test + onlyIf { project.properties["stressTest"] != null } + + val test = tasks.test.get() + + classpath = test.classpath + testClassesDirs = test.testClassesDirs + executable = "$JDK_18/bin/java" + + dependsOn("compileTestKotlin") + dependsOn(checkJdk8) +} + +tasks.build { + dependsOn(jdk8Test) +} From fcaa6df08800438cc32a38e28ed403fd9f5b1fbc Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 24 Aug 2020 18:57:22 +0300 Subject: [PATCH 107/257] Migrate examples to new JS plugin * General cleanup and TODOs Co-authored with: ilya.goncharov@jetbrains.com --- build.gradle | 9 +++- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/kotlin/Dokka.kt | 3 ++ buildSrc/src/main/kotlin/MavenCentral.kt | 2 +- buildSrc/src/main/kotlin/Platform.kt | 1 + buildSrc/src/main/kotlin/RunR8.kt | 5 ++ buildSrc/src/main/kotlin/UnpackAar.kt | 1 + gradle/compile-js.gradle | 30 +++++------ js/example-frontend-js/README.md | 4 +- js/example-frontend-js/build.gradle | 48 +++++++++-------- js/example-frontend-js/npm/package.json | 22 -------- js/example-frontend-js/npm/webpack.config.js | 54 ------------------- js/example-frontend-js/src/ExampleMain.kt | 3 ++ js/example-frontend-js/src/main/web/main.js | 8 --- .../webpack.config.d/custom-config.js | 18 +++++++ site/build.gradle.kts | 2 +- 16 files changed, 84 insertions(+), 128 deletions(-) delete mode 100644 js/example-frontend-js/npm/package.json delete mode 100644 js/example-frontend-js/npm/webpack.config.js delete mode 100644 js/example-frontend-js/src/main/web/main.js create mode 100644 js/example-frontend-js/webpack.config.d/custom-config.js diff --git a/build.gradle b/build.gradle index 1088e2bef4..f93a8083f0 100644 --- a/build.gradle +++ b/build.gradle @@ -252,7 +252,14 @@ configure(subprojects.findAll { it.name != coreModule && it.name != rootModule } } // Redefine source sets because we are not using 'kotlin/main/fqn' folder convention -configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "benchmarks" && it.name != 'example-frontend-js' && it.name != coreModule }) { +configure(subprojects.findAll { + !sourceless.contains(it.name) && + it.name != "benchmarks" && + it.name != coreModule && + it.name != "example-frontend-js" +}) { + // Pure JS and pure MPP doesn't have this notion and are configured separately + // TODO detect it via platformOf and migrate benchmarks to the same scheme sourceSets { main.kotlin.srcDirs = ['src'] test.kotlin.srcDirs = ['test'] diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index bbbf2a3015..f998017c0b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -14,7 +14,7 @@ kotlinDslPluginOptions { experimentalWarning.set(false) } -val props = Properties().apply { +private val props = Properties().apply { file("../gradle.properties").inputStream().use { load(it) } } diff --git a/buildSrc/src/main/kotlin/Dokka.kt b/buildSrc/src/main/kotlin/Dokka.kt index 72365c1ee0..dd5f1ea48d 100644 --- a/buildSrc/src/main/kotlin/Dokka.kt +++ b/buildSrc/src/main/kotlin/Dokka.kt @@ -10,6 +10,9 @@ import org.jetbrains.dokka.gradle.DokkaTask import java.io.File import java.net.URL +/** + * Package-list by external URL for documentation generation. + */ fun Project.externalDocumentationLink( url: String, packageList: File = projectDir.resolve("package.list") diff --git a/buildSrc/src/main/kotlin/MavenCentral.kt b/buildSrc/src/main/kotlin/MavenCentral.kt index 9a96f3c2de..3efaf33f1c 100644 --- a/buildSrc/src/main/kotlin/MavenCentral.kt +++ b/buildSrc/src/main/kotlin/MavenCentral.kt @@ -7,7 +7,7 @@ import org.gradle.api.Project import org.gradle.api.publish.maven.MavenPom -// --------------- pom configuration --------------- +// Pom configuration fun MavenPom.configureMavenCentralMetadata(project: Project) { name by project.name diff --git a/buildSrc/src/main/kotlin/Platform.kt b/buildSrc/src/main/kotlin/Platform.kt index 4cacd9e026..b667a138a8 100644 --- a/buildSrc/src/main/kotlin/Platform.kt +++ b/buildSrc/src/main/kotlin/Platform.kt @@ -1,5 +1,6 @@ import org.gradle.api.Project +// Use from Groovy for now fun platformOf(project: Project): String = when (project.name.substringAfterLast("-")) { "js" -> "js" diff --git a/buildSrc/src/main/kotlin/RunR8.kt b/buildSrc/src/main/kotlin/RunR8.kt index fc91dc0a57..d9eba79bd4 100644 --- a/buildSrc/src/main/kotlin/RunR8.kt +++ b/buildSrc/src/main/kotlin/RunR8.kt @@ -7,6 +7,11 @@ import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.named import java.io.File +/* + * Task used by our ui/android tests to test minification results + * and keep track of size of the binary. + * TODO move back to kotlinx-coroutines-android when it's migrated to the kts + */ open class RunR8 : JavaExec() { @OutputDirectory diff --git a/buildSrc/src/main/kotlin/UnpackAar.kt b/buildSrc/src/main/kotlin/UnpackAar.kt index 2b96df3f86..c7d0b53d04 100644 --- a/buildSrc/src/main/kotlin/UnpackAar.kt +++ b/buildSrc/src/main/kotlin/UnpackAar.kt @@ -9,6 +9,7 @@ import java.nio.file.Files import java.util.zip.ZipEntry import java.util.zip.ZipFile +// TODO move back to kotlinx-coroutines-play-services when it's migrated to the kts @Suppress("UnstableApiUsage") abstract class UnpackAar : TransformAction { @get:InputArtifact diff --git a/gradle/compile-js.gradle b/gradle/compile-js.gradle index 77de787fb8..523380df01 100644 --- a/gradle/compile-js.gradle +++ b/gradle/compile-js.gradle @@ -6,16 +6,22 @@ apply plugin: 'org.jetbrains.kotlin.js' -kotlin.sourceSets { - main.kotlin.srcDirs = ['src'] - test.kotlin.srcDirs = ['test'] - main.resources.srcDirs = ['resources'] - test.resources.srcDirs = ['test-resources'] +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" - testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" +kotlin { + js(LEGACY) { + moduleName = project.name - "-js" + } + + sourceSets { + main.kotlin.srcDirs = ['src'] + test.kotlin.srcDirs = ['test'] + main.resources.srcDirs = ['resources'] + test.resources.srcDirs = ['test-resources'] + } } tasks.withType(compileKotlinJs.getClass()) { @@ -25,11 +31,3 @@ tasks.withType(compileKotlinJs.getClass()) { metaInfo = true } } - -compileKotlinJs { - kotlinOptions { - // drop -js suffix from outputFile - def baseName = project.name - "-js" - outputFile = new File(outputFile.parent, baseName + ".js") - } -} diff --git a/js/example-frontend-js/README.md b/js/example-frontend-js/README.md index 4e534e427a..ad61372dc9 100644 --- a/js/example-frontend-js/README.md +++ b/js/example-frontend-js/README.md @@ -3,7 +3,7 @@ Build application with ``` -gradlew :example-frontend-js:bundle +gradlew :example-frontend-js:build ``` The resulting application can be found in `build/dist` subdirectory. @@ -11,7 +11,7 @@ The resulting application can be found in `build/dist` subdirectory. You can start application with webpack-dev-server using: ``` -gradlew :example-frontend-js:start +gradlew :example-frontend-js:run ``` Built and deployed application is available at the library documentation site diff --git a/js/example-frontend-js/build.gradle b/js/example-frontend-js/build.gradle index f81efdd2f9..7abde63964 100644 --- a/js/example-frontend-js/build.gradle +++ b/js/example-frontend-js/build.gradle @@ -2,28 +2,32 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -apply plugin: 'kotlin-dce-js' -apply from: rootProject.file('gradle/node-js.gradle') - -dependencies { - compile "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" -} - -compileKotlinJs { - kotlinOptions { - main = "call" +project.kotlin { + js(LEGACY) { + binaries.executable() + browser { + distribution { + directory = new File(directory.parentFile, "dist") + } + webpackTask { + cssSupport.enabled = true + } + runTask { + cssSupport.enabled = true + } + testTask { + useKarma { + useChromeHeadless() + webpackConfig.cssSupport.enabled = true + } + } + } } -} -task bundle(type: NpmTask, dependsOn: [npmInstall, build]) { - inputs.files(fileTree("$buildDir/kotlin-js-min/main")) - inputs.files(fileTree("$buildDir/kotlin-js-min/legacy/main")) - inputs.files(fileTree(file("src/main/web"))) - inputs.file("npm/webpack.config.js") - outputs.dir("$buildDir/dist") - args = ["run", "bundle"] -} - -task start(type: NpmTask, dependsOn: bundle) { - args = ["run", "start"] + sourceSets { + main.dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-html-js:$html_version" + implementation(npm("html-webpack-plugin", "3.2.0")) + } + } } diff --git a/js/example-frontend-js/npm/package.json b/js/example-frontend-js/npm/package.json deleted file mode 100644 index 7668cefba3..0000000000 --- a/js/example-frontend-js/npm/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "example-frontend-js", - "version": "$version", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/Kotlin/kotlinx.coroutines.git" - }, - "devDependencies": { - "webpack": "4.29.1", - "webpack-cli": "3.2.3", - "webpack-dev-server": "3.1.14", - "html-webpack-plugin": "3.2.0", - "uglifyjs-webpack-plugin": "2.1.1", - "style-loader": "0.23.1", - "css-loader": "2.1.0" - }, - "scripts": { - "bundle": "webpack", - "start": "webpack-dev-server --open --no-inline" - } -} diff --git a/js/example-frontend-js/npm/webpack.config.js b/js/example-frontend-js/npm/webpack.config.js deleted file mode 100644 index 8428c55741..0000000000 --- a/js/example-frontend-js/npm/webpack.config.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// This script is copied to "build" directory and run from there - -const webpack = require("webpack"); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); -const path = require("path"); - -const dist = path.resolve(__dirname, "dist"); - -module.exports = { - mode: "production", - entry: { - main: "main" - }, - output: { - filename: "[name].bundle.js", - path: dist, - publicPath: "" - }, - devServer: { - contentBase: dist - }, - module: { - rules: [ - { - test: /\.css$/, - use: [ - 'style-loader', - 'css-loader' - ] - } - ] - }, - resolve: { - modules: [ - path.resolve(__dirname, "kotlin-js-min/main"), - path.resolve(__dirname, "kotlin-js-min/legacy/main"), - path.resolve(__dirname, "../src/main/web/") - ] - }, - devtool: 'source-map', - plugins: [ - new HtmlWebpackPlugin({ - title: 'Kotlin Coroutines JS Example' - }), - new UglifyJSPlugin({ - sourceMap: true - }) - ] -}; diff --git a/js/example-frontend-js/src/ExampleMain.kt b/js/example-frontend-js/src/ExampleMain.kt index 25a73c61c0..da6e4196a6 100644 --- a/js/example-frontend-js/src/ExampleMain.kt +++ b/js/example-frontend-js/src/ExampleMain.kt @@ -13,7 +13,10 @@ import kotlin.coroutines.* import kotlin.math.* import kotlin.random.Random +external fun require(resource: String) + fun main() { + require("style.css") println("Starting example application...") document.addEventListener("DOMContentLoaded", { Application().start() diff --git a/js/example-frontend-js/src/main/web/main.js b/js/example-frontend-js/src/main/web/main.js deleted file mode 100644 index d2440ffaef..0000000000 --- a/js/example-frontend-js/src/main/web/main.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -// ------ Main bundle for example application ------ - -require("example-frontend"); -require("style.css"); diff --git a/js/example-frontend-js/webpack.config.d/custom-config.js b/js/example-frontend-js/webpack.config.d/custom-config.js new file mode 100644 index 0000000000..21939bece0 --- /dev/null +++ b/js/example-frontend-js/webpack.config.d/custom-config.js @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +;(function (config) { + const HtmlWebpackPlugin = require('html-webpack-plugin'); + + config.output.filename = "[name].bundle.js" + + config.plugins.push( + new HtmlWebpackPlugin({ + title: 'Kotlin Coroutines JS Example' + }) + ) + + // path from /js/packages/example-frontend-js to src/main/web + config.resolve.modules.push("../../../../js/example-frontend-js/src/main/web"); +})(config); diff --git a/site/build.gradle.kts b/site/build.gradle.kts index 686e81d336..eba7b1a1bc 100644 --- a/site/build.gradle.kts +++ b/site/build.gradle.kts @@ -26,7 +26,7 @@ val copyExampleFrontendJs by tasks.registering(Copy::class) { from("$srcBuildDir/dist") into("$buildDocsDir/example-frontend-js") - dependsOn(":example-frontend-js:bundle") + dependsOn(":example-frontend-js:browserDistribution") } tasks.register("site") { From fe2fedc69cd4259bbe0b8b8d1fbdb47cb552ea54 Mon Sep 17 00:00:00 2001 From: Victor Turansky Date: Thu, 27 Aug 2020 15:47:50 +0300 Subject: [PATCH 108/257] New Kotlin/JVM & Kotlin/Multiplatform plugins (#2018) --- gradle/compile-jvm.gradle | 2 +- kotlinx-coroutines-core/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index 232a185d7f..4ed0f840cd 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -4,7 +4,7 @@ // Platform-specific configuration to compile JVM modules -apply plugin: 'kotlin' +apply plugin: 'org.jetbrains.kotlin.jvm' sourceCompatibility = 1.6 targetCompatibility = 1.6 diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 59dc5da894..55c44088f5 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -2,7 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -apply plugin: 'kotlin-multiplatform' +apply plugin: 'org.jetbrains.kotlin.multiplatform' apply from: rootProject.file("gradle/targets.gradle") apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle") apply from: rootProject.file("gradle/compile-common.gradle") From 0cab30bc79575dfe7702766a99a7b9753218bb13 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 8 Sep 2020 19:42:36 +0300 Subject: [PATCH 109/257] Contributing Guidelines (#2232) Also moving there building instructions from README.md as only contributors shall care about them. --- CONTRIBUTING.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 32 ++------------- 2 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..abd458e6f2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,106 @@ +# Contributing Guidelines + +There are two main ways to contribute to the project — submitting issues and submitting +fixes/changes/improvements via pull requests. + +## Submitting issues + +Both bug reports and feature requests are welcome. +Submit issues [here](https://github.com/Kotlin/kotlinx.coroutines/issues). + +* Search for existing issues to avoid reporting duplicates. +* When submitting a bug report: + * Test it against the most recently released version. It might have been already fixed. + * By default, we assume that your problem reproduces in Kotlin/JVM. Please, mention if the problem is + specific to Kotlin/JS or Kotlin/Native. + * Include the code that reproduces the problem. Provide the complete reproducer code, yet minimize it as much as possible. + * However, don't put off reporting any weird or rarely appearing issues just because you cannot consistently + reproduce them. + * If the bug is in behavior, then explain what behavior you've expected and what you've got. +* When submitting a feature request: + * Explain why you need the feature — what's your use-case, what's your domain. + * Explaining the problem you face is more important than suggesting a solution. + Report your problem even if you don't have any proposed solution. + * If there is an alternative way to do what you need, then show the code of the alternative. + +## Submitting PRs + +We love PRs. Submit PRs [here](https://github.com/Kotlin/kotlinx.coroutines/pulls). +However, please keep in mind that maintainers will have to support the resulting code of the project, +so do familiarize yourself with the following guidelines. + +* All development (both new features and bug fixes) is performed in the `develop` branch. + * The `master` branch always contains sources of the most recently released version. + * Base PRs against the `develop` branch. + * The `develop` branch is pushed to the `master` branch during release. + * Documentation in markdown files can be updated directly in the `master` branch, + unless the documentation is in the source code, and the patch changes line numbers. +* If you fix documentation: + * After fixing/changing code examples in the [`docs`](docs) folder or updating any references in the markdown files + run the [Knit tool](#running-the-knit-tool) and commit the resulting changes as well. + It will not pass the tests otherwise. + * If you plan extensive rewrites/additions to the docs, then please [contact the maintainers](#contacting-maintainers) + to coordinate the work in advance. +* If you make any code changes: + * Follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html). + Use 4 spaces for indentation. + * [Build the project](#building) to make sure it all works and passes the tests. +* If you fix a bug: + * Write the test the reproduces the bug. + * Fixes without tests are accepted only in exceptional circumstances if it can be shown that writing the + corresponding test is too hard or otherwise impractical. + * Follow the style of writing tests that is used in this project: + name test functions as `testXxx`. Don't use backticks in test names. +* If you introduce any new public APIs: + * All new APIs must come with documentation and tests. + * All new APIs are initially released with `@ExperimentalCoroutineApi` annotation and are graduated later. + * [Update the public API dumps](#updating-the-public-api-dump) and commit the resulting changes as well. + It will not pass the tests otherwise. + * If you plan large API additions, then please start by submitting an issue with the proposed API design + to gather community feedback. + * [Contact the maintainers](#contacting-maintainers) to coordinate any big piece of work in advance. +* Comment on the existing issue if you want to work on it. Ensure that the issue not only describes a problem, + but also describes a solution that had received a positive feedback. Propose a solution if there isn't any. +* Steps for contributing new integration modules are explained [here](integration/README.md#Contributing). + +## Building + +This library is built with Gradle. + +* Run `./gradlew build` to build. It also runs all the tests. +* Run `./gradlew :test` to test the module you are looking at to speed + things up during development. +* Run `./gradlew jvmTest` to perform only fast JVM tests of the core multiplatform module. + +You can import this project into IDEA, but you have to delegate build actions +to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner) + +### Environment requirements + +* JDK >= 11 referred to by the `JAVA_HOME` environment variable. +* JDK 1.6 referred to by the `JDK_16` environment variable. + It is OK to have `JDK_16` pointing to `JAVA_HOME` for external contributions. +* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. + It is OK to have `JDK_18` pointing to `JAVA_HOME` for external contributions. + +### Running the Knit tool + +* Use [Knit](https://github.com/Kotlin/kotlinx-knit/blob/master/README.md) for updates to documentation: + * Run `./gradlew knit` to update example files, links, tables of content. + * Commit updated documents and examples together with other changes. + +### Updating the public API dump + +* Use [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator/blob/master/README.md) for updates to public API: + * Run `./gradlew apiDump` to update API index files. + * Commit updated API indexes together with other changes. + +## Releases + +* Full release procedure checklist is [here](RELEASE.md). + +## Contacting maintainers + +* If something cannot be done, not convenient, or does not work — submit an [issue](#submitting-issues). +* "How to do something" questions — [StackOverflow](https://stackoverflow.com). +* Discussions and general inquiries — use `#coroutines` channel in [KotlinLang Slack](https://kotl.in/slack). diff --git a/README.md b/README.md index 06e04e4d30..d004a8d118 100644 --- a/README.md +++ b/README.md @@ -199,35 +199,9 @@ enableFeaturePreview('GRADLE_METADATA') Since Kotlin/Native does not generally provide binary compatibility between versions, you should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`. -## Building - -This library is built with Gradle. To build it, use `./gradlew build`. -You can import this project into IDEA, but you have to delegate build actions -to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner) - -### Requirements - -* JDK >= 11 referred to by the `JAVA_HOME` environment variable. -* JDK 1.6 referred to by the `JDK_16` environment variable. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. -* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is okay to have `JDK_18` pointing to `JAVA_HOME` for external contributions. - -## Contributions and releases - -All development (both new features and bug fixes) is performed in `develop` branch. -This way `master` sources always contain sources of the most recently released version. -Please send PRs with bug fixes to `develop` branch. -Fixes to documentation in markdown files are an exception to this rule. They are updated directly in `master`. - -The `develop` branch is pushed to `master` during release. - -* Full release procedure checklist is [here](RELEASE.md). -* Steps for contributing new integration modules are explained [here](integration/README.md#Contributing). -* Use [Knit](https://github.com/Kotlin/kotlinx-knit/blob/master/README.md) for updates to documentation: - * In project root directory run `./gradlew knit`. - * Commit updated documents and examples together with other changes. -* Use [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator/blob/master/README.md) for updates to public API: - * In project root directory run `./gradlew apiDump`. - * Commit updated API index together with other changes. +## Building and Contributing + +See [Contributing Guidelines](CONTRIBUTING.md). From 879881e7dc321270c7643982349e9d83cd4b3102 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 11 Sep 2020 14:38:29 +0300 Subject: [PATCH 110/257] Fixed CoroutinesScope.ensureActive docs (#2242) This is leftover from #2044 fix. Fixes #2241 --- kotlinx-coroutines-core/common/src/CoroutineScope.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 7dbd6a6d7b..0dde6c9352 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -226,10 +226,10 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni /** * Ensures that current scope is [active][CoroutineScope.isActive]. - * Throws [IllegalStateException] if the context does not have a job in it. * * If the job is no longer active, throws [CancellationException]. * If the job was cancelled, thrown exception contains the original cancellation cause. + * This function does not do anything if there is no [Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext]. * * This method is a drop-in replacement for the following code, but with more precise exception: * ``` @@ -237,6 +237,8 @@ public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Uni * throw CancellationException() * } * ``` + * + * @see CoroutineContext.ensureActive */ public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive() From 2d0686b9818c834a1344737afd0805afd2367845 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Wed, 2 Sep 2020 02:04:28 +0200 Subject: [PATCH 111/257] Add awaitCancellation (#2225) Fixes #2213 --- .../api/kotlinx-coroutines-core.api | 1 + kotlinx-coroutines-core/common/src/Delay.kt | 41 +++++++++++++++++++ .../common/test/AwaitCancellationTest.kt | 27 ++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 36cbdb6960..5df13700d0 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -267,6 +267,7 @@ public final class kotlinx/coroutines/Delay$DefaultImpls { } public final class kotlinx/coroutines/DelayKt { + public static final fun awaitCancellation (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun delay-p9JZ4hM (DLkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index ab80912269..ff0a6fc73b 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -58,12 +58,51 @@ public interface Delay { DefaultDelay.invokeOnTimeout(timeMillis, block) } +/** + * Suspends until cancellation, in which case it will throw a [CancellationException]. + * + * This function returns [Nothing], so it can be used in any coroutine, + * regardless of the required return type. + * + * Usage example in callback adapting code: + * + * ```kotlin + * fun currentTemperature(): Flow = callbackFlow { + * val callback = SensorCallback { degreesCelsius: Double -> + * trySend(Temperature.celsius(degreesCelsius)) + * } + * try { + * registerSensorCallback(callback) + * awaitCancellation() // Suspends to keep getting updates until cancellation. + * } finally { + * unregisterSensorCallback(callback) + * } + * } + * ``` + * + * Usage example in (non declarative) UI code: + * + * ```kotlin + * suspend fun showStuffUntilCancelled(content: Stuff): Nothing { + * someSubView.text = content.title + * anotherSubView.text = content.description + * someView.visibleInScope { + * awaitCancellation() // Suspends so the view stays visible. + * } + * } + * ``` + */ +@ExperimentalCoroutinesApi +public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} + /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. * + * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. + * * Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * * Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context. @@ -82,6 +121,8 @@ public suspend fun delay(timeMillis: Long) { * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. * + * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. + * * Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * * Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context. diff --git a/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt b/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt new file mode 100644 index 0000000000..2fe0c914e6 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/AwaitCancellationTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class AwaitCancellationTest : TestBase() { + + @Test + fun testCancellation() = runTest(expected = { it is CancellationException }) { + expect(1) + coroutineScope { + val deferred: Deferred = async { + expect(2) + awaitCancellation() + } + yield() + expect(3) + require(deferred.isActive) + deferred.cancel() + finish(4) + deferred.await() + } + } +} From 85b1a2bb07158b57a58ce8ef55e4c32f21e6ca42 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 14 Sep 2020 17:48:17 +0300 Subject: [PATCH 112/257] Support JetBrains cache-redirector service for stable CI builds (#2247) On CI set CACHE_REDIRECTOR=true environment property. --- build.gradle | 9 ++ buildSrc/build.gradle.kts | 34 ++++- buildSrc/src/main/kotlin/CacheRedirector.kt | 152 ++++++++++++++++++++ 3 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 buildSrc/src/main/kotlin/CacheRedirector.kt diff --git a/build.gradle b/build.gradle index f93a8083f0..153714e90c 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,8 @@ buildscript { // JMH plugins classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" } + + CacheRedirector.configureBuildScript(buildscript, rootProject) } import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType @@ -295,7 +297,14 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) { // Report Kotlin compiler version when building project println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION") +// --------------- Cache redirector --------------- + +allprojects { + CacheRedirector.configure(project) +} + // --------------- Configure sub-projects that are published --------------- + def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm", true) task deploy(dependsOn: publishTasks) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index f998017c0b..65213e22fd 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,13 +1,41 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + import java.util.* +buildscript { + val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + + if (cacheRedirectorEnabled) { + println("Redirecting repositories for buildSrc buildscript") + } + + repositories { + if (cacheRedirectorEnabled) { + maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") + } else { + maven("https://plugins.gradle.org/m2") + } + } +} + plugins { `kotlin-dsl` } +val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + repositories { - gradlePluginPortal() - maven("https://kotlin.bintray.com/kotlin-eap") - maven("https://kotlin.bintray.com/kotlin-dev") + if (cacheRedirectorEnabled) { + maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") + maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-eap") + maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-dev") + } else { + maven("https://plugins.gradle.org/m2") + maven("https://dl.bintray.com/kotlin/kotlin-eap") + maven("https://dl.bintray.com/kotlin/kotlin-dev") + } } kotlinDslPluginOptions { diff --git a/buildSrc/src/main/kotlin/CacheRedirector.kt b/buildSrc/src/main/kotlin/CacheRedirector.kt new file mode 100644 index 0000000000..7cf01d8e76 --- /dev/null +++ b/buildSrc/src/main/kotlin/CacheRedirector.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.gradle.api.* +import org.gradle.api.artifacts.dsl.* +import org.gradle.api.artifacts.repositories.* +import org.gradle.api.initialization.dsl.* +import java.net.* + +/** + * Enabled via environment variable, so that it can be reliably accessed from any piece of the build script, + * including buildSrc within TeamCity CI. + */ +private val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + +/** + * The list of repositories supported by cache redirector should be synced with the list at https://cache-redirector.jetbrains.com/redirects_generated.html + * To add a repository to the list create an issue in ADM project (example issue https://youtrack.jetbrains.com/issue/IJI-149) + */ +private val mirroredUrls = listOf( + "https://cdn.azul.com/zulu/bin", + "https://clojars.org/repo", + "https://dl.bintray.com/d10xa/maven", + "https://dl.bintray.com/groovy/maven", + "https://dl.bintray.com/jetbrains/maven-patched", + "https://dl.bintray.com/jetbrains/scala-plugin-deps", + "https://dl.bintray.com/kodein-framework/Kodein-DI", + "https://dl.bintray.com/konsoletyper/teavm", + "https://dl.bintray.com/kotlin/kotlin-dev", + "https://dl.bintray.com/kotlin/kotlin-eap", + "https://dl.bintray.com/kotlin/kotlinx.html", + "https://dl.bintray.com/kotlin/kotlinx", + "https://dl.bintray.com/kotlin/ktor", + "https://dl.bintray.com/scalacenter/releases", + "https://dl.bintray.com/scalamacros/maven", + "https://dl.bintray.com/kotlin/exposed", + "https://dl.bintray.com/cy6ergn0m/maven", + "https://dl.bintray.com/kotlin/kotlin-js-wrappers", + "https://dl.google.com/android/repository", + "https://dl.google.com/dl/android/maven2", + "https://dl.google.com/dl/android/studio/ide-zips", + "https://dl.google.com/go", + "https://download.jetbrains.com", + "https://jcenter.bintray.com", + "https://jetbrains.bintray.com/dekaf", + "https://jetbrains.bintray.com/intellij-jbr", + "https://jetbrains.bintray.com/intellij-jdk", + "https://jetbrains.bintray.com/intellij-plugin-service", + "https://jetbrains.bintray.com/intellij-terraform", + "https://jetbrains.bintray.com/intellij-third-party-dependencies", + "https://jetbrains.bintray.com/jediterm", + "https://jetbrains.bintray.com/kotlin-native-dependencies", + "https://jetbrains.bintray.com/markdown", + "https://jetbrains.bintray.com/teamcity-rest-client", + "https://jetbrains.bintray.com/test-discovery", + "https://jetbrains.bintray.com/wormhole", + "https://jitpack.io", + "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev", + "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap", + "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/eap", + "https://kotlin.bintray.com/dukat", + "https://kotlin.bintray.com/kotlin-dependencies", + "https://oss.sonatype.org/content/repositories/releases", + "https://oss.sonatype.org/content/repositories/snapshots", + "https://oss.sonatype.org/content/repositories/staging", + "https://packages.confluent.io/maven/", + "https://plugins.gradle.org/m2", + "https://plugins.jetbrains.com/maven", + "https://repo1.maven.org/maven2", + "https://repo.grails.org/grails/core", + "https://repo.jenkins-ci.org/releases", + "https://repo.maven.apache.org/maven2", + "https://repo.spring.io/milestone", + "https://repo.typesafe.com/typesafe/ivy-releases", + "https://services.gradle.org", + "https://www.exasol.com/artifactory/exasol-releases", + "https://www.myget.org/F/intellij-go-snapshots/maven", + "https://www.myget.org/F/rd-model-snapshots/maven", + "https://www.myget.org/F/rd-snapshots/maven", + "https://www.python.org/ftp", + "https://www.jetbrains.com/intellij-repository/nightly", + "https://www.jetbrains.com/intellij-repository/releases", + "https://www.jetbrains.com/intellij-repository/snapshots" +) + +private val aliases = mapOf( + "https://repo.maven.apache.org/maven2" to "https://repo1.maven.org/maven2", // Maven Central + "https://kotlin.bintray.com/kotlin-dev" to "https://dl.bintray.com/kotlin/kotlin-dev", + "https://kotlin.bintray.com/kotlin-eap" to "https://dl.bintray.com/kotlin/kotlin-eap", + "https://kotlin.bintray.com/kotlinx" to "https://dl.bintray.com/kotlin/kotlinx" +) + +private fun URI.toCacheRedirectorUri() = URI("https://cache-redirector.jetbrains.com/$host/$path") + +private fun URI.maybeRedirect(): URI? { + val url = toString().trimEnd('/') + val dealiasedUrl = aliases.getOrDefault(url, url) + + return if (mirroredUrls.any { dealiasedUrl.startsWith(it) }) { + URI(dealiasedUrl).toCacheRedirectorUri() + } else { + null + } +} + +private fun URI.isCachedOrLocal() = scheme == "file" || + host == "cache-redirector.jetbrains.com" || + host == "teamcity.jetbrains.com" || + host == "buildserver.labs.intellij.net" + +private fun Project.checkRedirectUrl(url: URI, containerName: String): URI { + val redirected = url.maybeRedirect() + if (redirected == null && !url.isCachedOrLocal()) { + val msg = "Repository $url in $containerName should be cached with cache-redirector" + val details = "Using non cached repository may lead to download failures in CI builds." + + " Check buildSrc/src/main/kotlin/CacheRedirector.kt for details." + logger.warn("WARNING - $msg\n$details") + } + return if (cacheRedirectorEnabled) redirected ?: url else url +} + +private fun Project.checkRedirect(repositories: RepositoryHandler, containerName: String) { + if (cacheRedirectorEnabled) { + logger.info("Redirecting repositories for $containerName") + } + for (repository in repositories) { + when (repository) { + is MavenArtifactRepository -> repository.url = checkRedirectUrl(repository.url, containerName) + is IvyArtifactRepository -> repository.url = checkRedirectUrl(repository.url, containerName) + } + } +} + +// Used from Groovy scripts +object CacheRedirector { + /** + * Substitutes repositories in buildScript { } block. + */ + @JvmStatic + fun ScriptHandler.configureBuildScript(rootProject: Project) { + rootProject.checkRedirect(repositories, "${rootProject.displayName} buildscript") + } + + /** + * Substitutes repositories in a project. + */ + @JvmStatic + fun Project.configure() { + checkRedirect(repositories, displayName) + } +} From 5dd94a337e96d9bfe8b39e54c5ad155a79c10e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 22 Sep 2020 17:18:53 +0200 Subject: [PATCH 113/257] Add Publisher.awaitSingleOrDefault|Null|Else extensions (#2260) This commit adds awaitSingle variants similar to awaitFirst ones, but always emitting the value during onComplete(). Fixes #1993 --- .../api/kotlinx-coroutines-reactive.api | 3 + .../kotlinx-coroutines-reactive/src/Await.kt | 48 ++++++++++++-- .../test/IntegrationTest.kt | 11 +++- .../test/FluxSingleTest.kt | 66 +++++++++++++++++++ 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api index 5783edeaa1..961fdbe238 100644 --- a/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api +++ b/reactive/kotlinx-coroutines-reactive/api/kotlinx-coroutines-reactive.api @@ -5,6 +5,9 @@ public final class kotlinx/coroutines/reactive/AwaitKt { public static final fun awaitFirstOrNull (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitLast (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitSingle (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingleOrDefault (Lorg/reactivestreams/Publisher;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingleOrElse (Lorg/reactivestreams/Publisher;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun awaitSingleOrNull (Lorg/reactivestreams/Publisher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class kotlinx/coroutines/reactive/ChannelKt { diff --git a/reactive/kotlinx-coroutines-reactive/src/Await.kt b/reactive/kotlinx-coroutines-reactive/src/Await.kt index 9ea2e3c50e..7956c26010 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Await.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Await.kt @@ -80,13 +80,53 @@ public suspend fun Publisher.awaitLast(): T = awaitOne(Mode.LAST) */ public suspend fun Publisher.awaitSingle(): T = awaitOne(Mode.SINGLE) +/** + * Awaits for the single value from the given publisher or the [default] value if none is emitted without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingleOrDefault(default: T): T = awaitOne(Mode.SINGLE_OR_DEFAULT, default) + +/** + * Awaits for the single value from the given publisher or `null` value if none is emitted without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingleOrNull(): T = awaitOne(Mode.SINGLE_OR_DEFAULT) + +/** + * Awaits for the single value from the given publisher or call [defaultValue] to get a value if none is emitted without blocking a thread and + * returns the resulting value or throws the corresponding exception if this publisher had produced error. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * immediately resumes with [CancellationException]. + * + * @throws NoSuchElementException if publisher does not emit any value + * @throws IllegalArgumentException if publisher emits more than one value + */ +public suspend fun Publisher.awaitSingleOrElse(defaultValue: () -> T): T = awaitOne(Mode.SINGLE_OR_DEFAULT) ?: defaultValue() + // ------------------------ private ------------------------ private enum class Mode(val s: String) { FIRST("awaitFirst"), FIRST_OR_DEFAULT("awaitFirstOrDefault"), LAST("awaitLast"), - SINGLE("awaitSingle"); + SINGLE("awaitSingle"), + SINGLE_OR_DEFAULT("awaitSingleOrDefault"); override fun toString(): String = s } @@ -114,8 +154,8 @@ private suspend fun Publisher.awaitOne( cont.resume(t) } } - Mode.LAST, Mode.SINGLE -> { - if (mode == Mode.SINGLE && seenValue) { + Mode.LAST, Mode.SINGLE, Mode.SINGLE_OR_DEFAULT -> { + if ((mode == Mode.SINGLE || mode == Mode.SINGLE_OR_DEFAULT) && seenValue) { subscription.cancel() if (cont.isActive) cont.resumeWithException(IllegalArgumentException("More than one onNext value for $mode")) @@ -134,7 +174,7 @@ private suspend fun Publisher.awaitOne( return } when { - mode == Mode.FIRST_OR_DEFAULT -> { + (mode == Mode.FIRST_OR_DEFAULT || mode == Mode.SINGLE_OR_DEFAULT) -> { cont.resume(default as T) } cont.isActive -> { diff --git a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt index 6f7d98480b..18cd012d16 100644 --- a/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/IntegrationTest.kt @@ -48,6 +48,9 @@ class IntegrationTest( assertEquals("ELSE", pub.awaitFirstOrElse { "ELSE" }) assertFailsWith { pub.awaitLast() } assertFailsWith { pub.awaitSingle() } + assertEquals("OK", pub.awaitSingleOrDefault("OK")) + assertNull(pub.awaitSingleOrNull()) + assertEquals("ELSE", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { cnt++ } assertEquals(0, cnt) @@ -65,6 +68,9 @@ class IntegrationTest( assertEquals("OK", pub.awaitFirstOrElse { "ELSE" }) assertEquals("OK", pub.awaitLast()) assertEquals("OK", pub.awaitSingle()) + assertEquals("OK", pub.awaitSingleOrDefault("!")) + assertEquals("OK", pub.awaitSingleOrNull()) + assertEquals("OK", pub.awaitSingleOrElse { "ELSE" }) var cnt = 0 pub.collect { assertEquals("OK", it) @@ -84,10 +90,13 @@ class IntegrationTest( } assertEquals(1, pub.awaitFirst()) assertEquals(1, pub.awaitFirstOrDefault(0)) - assertEquals(n, pub.awaitLast()) assertEquals(1, pub.awaitFirstOrNull()) assertEquals(1, pub.awaitFirstOrElse { 0 }) + assertEquals(n, pub.awaitLast()) assertFailsWith { pub.awaitSingle() } + assertFailsWith { pub.awaitSingleOrDefault(0) } + assertFailsWith { pub.awaitSingleOrNull() } + assertFailsWith { pub.awaitSingleOrElse { 0 } } checkNumbers(n, pub) val channel = pub.openSubscription() checkNumbers(n, channel.asPublisher(ctx(coroutineContext))) diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt index 3879c62c71..cc336ba6b5 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt @@ -68,6 +68,72 @@ class FluxSingleTest : TestBase() { } } + @Test + fun testAwaitSingleOrDefault() { + val flux = flux { + send(Flux.empty().awaitSingleOrDefault("O") + "K") + } + + checkSingleValue(flux) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitSingleOrDefaultException() { + val flux = flux { + send(Flux.just("O", "#").awaitSingleOrDefault("!") + "K") + } + + checkErroneous(flux) { + assert(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitSingleOrNull() { + val flux = flux { + send(Flux.empty().awaitSingleOrNull() ?: "OK") + } + + checkSingleValue(flux) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitSingleOrNullException() { + val flux = flux { + send((Flux.just("O", "#").awaitSingleOrNull() ?: "!") + "K") + } + + checkErroneous(flux) { + assert(it is IllegalArgumentException) + } + } + + @Test + fun testAwaitSingleOrElse() { + val flux = flux { + send(Flux.empty().awaitSingleOrElse { "O" } + "K") + } + + checkSingleValue(flux) { + assertEquals("OK", it) + } + } + + @Test + fun testAwaitSingleOrElseException() { + val flux = flux { + send(Flux.just("O", "#").awaitSingleOrElse { "!" } + "K") + } + + checkErroneous(flux) { + assert(it is IllegalArgumentException) + } + } + @Test fun testAwaitFirst() { val flux = flux { From 5b712106c5d12adc73a4f2ea6696090a295df7e9 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 30 Sep 2020 03:05:51 -0700 Subject: [PATCH 114/257] Lint settings for 1.4 (#2038) The latest part of #2026 --- .../api/kotlinx-coroutines-core.api | 4 ++ .../common/src/flow/operators/Lint.kt | 56 ++++++++++--------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 5df13700d0..6c44e14b44 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -1030,9 +1030,13 @@ public final class kotlinx/coroutines/flow/FlowKt { } public final class kotlinx/coroutines/flow/LintKt { + public static final fun cancel (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;)V + public static synthetic fun cancel$default (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V 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 getCoroutineContext (Lkotlinx/coroutines/flow/FlowCollector;)Lkotlin/coroutines/CoroutineContext; + public static final fun isActive (Lkotlinx/coroutines/flow/FlowCollector;)Z } public abstract interface class kotlinx/coroutines/flow/MutableStateFlow : kotlinx/coroutines/flow/StateFlow { diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index 5500034e9f..c2ae6e9d24 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -2,6 +2,8 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("unused") + package kotlinx.coroutines.flow import kotlinx.coroutines.* @@ -43,30 +45,30 @@ public fun StateFlow.conflate(): Flow = noImpl() ) public fun StateFlow.distinctUntilChanged(): Flow = noImpl() -//@Deprecated( -// message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." + -// "Use currentCoroutineContext().isActive or cancellable() operator instead " + -// "or specify the receiver of isActive explicitly. " + -// "Additionally, flow {} builder emissions are cancellable by default.", -// level = DeprecationLevel.WARNING, // ERROR in 1.4 -// replaceWith = ReplaceWith("currentCoroutineContext().isActive") -//) -//public val FlowCollector<*>.isActive: Boolean -// get() = noImpl() -// -//@Deprecated( -// message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." + -// "Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly", -// level = DeprecationLevel.WARNING, // ERROR in 1.4 -// replaceWith = ReplaceWith("currentCoroutineContext().cancel(cause)") -//) -//public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl() -// -//@Deprecated( -// message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." + -// "Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly", -// level = DeprecationLevel.WARNING, // ERROR in 1.4 -// replaceWith = ReplaceWith("currentCoroutineContext()") -//) -//public val FlowCollector<*>.coroutineContext: CoroutineContext -// get() = noImpl() \ No newline at end of file +@Deprecated( + message = "isActive is resolved into the extension of outer CoroutineScope which is likely to be an error." + + "Use currentCoroutineContext().isActive or cancellable() operator instead " + + "or specify the receiver of isActive explicitly. " + + "Additionally, flow {} builder emissions are cancellable by default.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("currentCoroutineContext().isActive") +) +public val FlowCollector<*>.isActive: Boolean + get() = noImpl() + +@Deprecated( + message = "cancel() is resolved into the extension of outer CoroutineScope which is likely to be an error." + + "Use currentCoroutineContext().cancel() instead or specify the receiver of cancel() explicitly", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("currentCoroutineContext().cancel(cause)") +) +public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = noImpl() + +@Deprecated( + message = "coroutineContext is resolved into the property of outer CoroutineScope which is likely to be an error." + + "Use currentCoroutineContext() instead or specify the receiver of coroutineContext explicitly", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("currentCoroutineContext()") +) +public val FlowCollector<*>.coroutineContext: CoroutineContext + get() = noImpl() \ No newline at end of file From f02aadc5984377ec69ef53e8adb5ed847f62f158 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 30 Sep 2020 03:06:18 -0700 Subject: [PATCH 115/257] Remove parent.start() from CancellableContinuationImpl.kt (#2036) It seems to be the legacy from times when CC was a Job, in order to observe behaviour change, a specific example should be accurately made --- .../common/src/CancellableContinuationImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index e25ebd3a37..166cb3b353 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -114,7 +114,6 @@ internal open class CancellableContinuationImpl( if (checkCompleted()) return if (parentHandle !== null) return // fast path 2 -- was already initialized val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent - parent.start() // make sure the parent is started val handle = parent.invokeOnCompletion( onCancelling = true, handler = ChildContinuation(parent, this).asHandler From d2ed1d809805d9eadd8e8379427971f0e14380ce Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Thu, 1 Oct 2020 11:39:24 +0300 Subject: [PATCH 116/257] Fix wrong Proguard rules (#2273) Fixes #2046 Fixes #2266 --- .../jvm/resources/META-INF/proguard/coroutines.pro | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro index d69e573c2e..60c8d61243 100644 --- a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro +++ b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro @@ -12,3 +12,9 @@ volatile ; } +# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when +# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used. +-dontwarn java.lang.instrument.ClassFileTransformer +-dontwarn sun.misc.SignalHandler +-dontwarn java.lang.instrument.Instrumentation +-dontwarn sun.misc.Signal From 4c28f94ab69627303715d78bfb395c44603eeefd Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Thu, 1 Oct 2020 18:57:55 +0330 Subject: [PATCH 117/257] Update exception-handling.md (#2271) Add a missing article --- docs/exception-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 5618cafbdc..d0b6b517a8 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -19,7 +19,7 @@ ## Exception Handling This section covers exception handling and cancellation on exceptions. -We already know that cancelled coroutine throws [CancellationException] in suspension points and that it +We already know that a cancelled coroutine throws [CancellationException] in suspension points and that it is ignored by the coroutines' machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same coroutine throw an exception. From 3af136f9452ac5ff1c22657265d4b9e5e508ab71 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 5 Oct 2020 10:07:06 -0700 Subject: [PATCH 118/257] Kts transition (#2261) * Kotlin DSL - 'reactive:reactor' * Kotlin DSL - 'reactive:jdk9' * Kotlin DSL - 'kotlinx-coroutines-slf4j' * Kotlin DSL - 'kotlinx-coroutines-guava' * Plugin repo management in 'pluginManagement' block * Remove redundant stdlib dependencies (#2253) Co-authored-by: Victor Turansky --- buildSrc/build.gradle.kts | 18 +------------ buildSrc/settings.gradle.kts | 17 +++++++++++++ gradle/compile-common.gradle | 4 --- gradle/compile-js-multiplatform.gradle | 4 --- gradle/compile-js.gradle | 1 - gradle/compile-jvm-multiplatform.gradle | 4 --- gradle/compile-jvm.gradle | 1 - .../kotlinx-coroutines-guava/build.gradle | 16 ------------ .../kotlinx-coroutines-guava/build.gradle.kts | 13 ++++++++++ .../kotlinx-coroutines-slf4j/build.gradle | 17 ------------- .../kotlinx-coroutines-slf4j/build.gradle.kts | 14 +++++++++++ reactive/kotlinx-coroutines-jdk9/build.gradle | 24 ------------------ .../kotlinx-coroutines-jdk9/build.gradle.kts | 22 ++++++++++++++++ .../kotlinx-coroutines-reactor/build.gradle | 23 ----------------- .../build.gradle.kts | 25 +++++++++++++++++++ 15 files changed, 92 insertions(+), 111 deletions(-) create mode 100644 buildSrc/settings.gradle.kts delete mode 100644 integration/kotlinx-coroutines-guava/build.gradle create mode 100644 integration/kotlinx-coroutines-guava/build.gradle.kts delete mode 100644 integration/kotlinx-coroutines-slf4j/build.gradle create mode 100644 integration/kotlinx-coroutines-slf4j/build.gradle.kts delete mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle create mode 100644 reactive/kotlinx-coroutines-jdk9/build.gradle.kts delete mode 100644 reactive/kotlinx-coroutines-reactor/build.gradle create mode 100644 reactive/kotlinx-coroutines-reactor/build.gradle.kts diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 65213e22fd..96b17a3d99 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -4,22 +4,6 @@ import java.util.* -buildscript { - val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true - - if (cacheRedirectorEnabled) { - println("Redirecting repositories for buildSrc buildscript") - } - - repositories { - if (cacheRedirectorEnabled) { - maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") - } else { - maven("https://plugins.gradle.org/m2") - } - } -} - plugins { `kotlin-dsl` } @@ -42,7 +26,7 @@ kotlinDslPluginOptions { experimentalWarning.set(false) } -private val props = Properties().apply { +val props = Properties().apply { file("../gradle.properties").inputStream().use { load(it) } } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000000..e5267ea3e2 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +pluginManagement { + repositories { + val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true + + if (cacheRedirectorEnabled) { + println("Redirecting repositories for buildSrc buildscript") + + maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") + } else { + maven("https://plugins.gradle.org/m2") + } + } +} diff --git a/gradle/compile-common.gradle b/gradle/compile-common.gradle index bee61429df..0dc1b5c014 100644 --- a/gradle/compile-common.gradle +++ b/gradle/compile-common.gradle @@ -3,10 +3,6 @@ */ kotlin.sourceSets { - commonMain.dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" - } - commonTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" api "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version" diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle index 93d371a21f..b52cfc5230 100644 --- a/gradle/compile-js-multiplatform.gradle +++ b/gradle/compile-js-multiplatform.gradle @@ -26,10 +26,6 @@ kotlin { } sourceSets { - jsMain.dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" - } - jsTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } diff --git a/gradle/compile-js.gradle b/gradle/compile-js.gradle index 523380df01..55c81fe56e 100644 --- a/gradle/compile-js.gradle +++ b/gradle/compile-js.gradle @@ -7,7 +7,6 @@ apply plugin: 'org.jetbrains.kotlin.js' dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle index 74495ba1dc..e72d30511e 100644 --- a/gradle/compile-jvm-multiplatform.gradle +++ b/gradle/compile-jvm-multiplatform.gradle @@ -10,10 +10,6 @@ kotlin { fromPreset(presets.jvm, 'jvm') } sourceSets { - jvmMain.dependencies { - api 'org.jetbrains.kotlin:kotlin-stdlib' - } - jvmTest.dependencies { api "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index 4ed0f840cd..caa5c45f60 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -10,7 +10,6 @@ sourceCompatibility = 1.6 targetCompatibility = 1.6 dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" diff --git a/integration/kotlinx-coroutines-guava/build.gradle b/integration/kotlinx-coroutines-guava/build.gradle deleted file mode 100644 index 16bdea50fd..0000000000 --- a/integration/kotlinx-coroutines-guava/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -ext.guava_version = '28.0-jre' - -dependencies { - compile "com.google.guava:guava:$guava_version" -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://google.github.io/guava/releases/$guava_version/api/docs/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} diff --git a/integration/kotlinx-coroutines-guava/build.gradle.kts b/integration/kotlinx-coroutines-guava/build.gradle.kts new file mode 100644 index 0000000000..53e91add44 --- /dev/null +++ b/integration/kotlinx-coroutines-guava/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +val guavaVersion = "28.0-jre" + +dependencies { + compile("com.google.guava:guava:$guavaVersion") +} + +externalDocumentationLink( + url = "https://google.github.io/guava/releases/$guavaVersion/api/docs/" +) diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle b/integration/kotlinx-coroutines-slf4j/build.gradle deleted file mode 100644 index 05accb75d3..0000000000 --- a/integration/kotlinx-coroutines-slf4j/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile 'org.slf4j:slf4j-api:1.7.25' - testCompile 'io.github.microutils:kotlin-logging:1.5.4' - testRuntime 'ch.qos.logback:logback-classic:1.2.3' - testRuntime 'ch.qos.logback:logback-core:1.2.3' -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - url = new URL("https://www.slf4j.org/apidocs/") - } -} diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle.kts b/integration/kotlinx-coroutines-slf4j/build.gradle.kts new file mode 100644 index 0000000000..c7d0d82d62 --- /dev/null +++ b/integration/kotlinx-coroutines-slf4j/build.gradle.kts @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + compile("org.slf4j:slf4j-api:1.7.25") + testCompile("io.github.microutils:kotlin-logging:1.5.4") + testRuntime("ch.qos.logback:logback-classic:1.2.3") + testRuntime("ch.qos.logback:logback-core:1.2.3") +} + +externalDocumentationLink( + url = "https://www.slf4j.org/apidocs/" +) diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle b/reactive/kotlinx-coroutines-jdk9/build.gradle deleted file mode 100644 index 8737e8ed6d..0000000000 --- a/reactive/kotlinx-coroutines-jdk9/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -targetCompatibility = 9 - -dependencies { - compile project(":kotlinx-coroutines-reactive") - compile "org.reactivestreams:reactive-streams-flow-adapters:$reactive_streams_version" -} - -compileTestKotlin { - kotlinOptions.jvmTarget = "9" -} - -compileKotlin { - kotlinOptions.jvmTarget = "9" -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} diff --git a/reactive/kotlinx-coroutines-jdk9/build.gradle.kts b/reactive/kotlinx-coroutines-jdk9/build.gradle.kts new file mode 100644 index 0000000000..c721746f3b --- /dev/null +++ b/reactive/kotlinx-coroutines-jdk9/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +dependencies { + compile(project(":kotlinx-coroutines-reactive")) + compile("org.reactivestreams:reactive-streams-flow-adapters:${version("reactive_streams")}") +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "9" + } + + compileTestKotlin { + kotlinOptions.jvmTarget = "9" + } +} + +externalDocumentationLink( + url = "https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html" +) diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle b/reactive/kotlinx-coroutines-reactor/build.gradle deleted file mode 100644 index 3b640bd5cc..0000000000 --- a/reactive/kotlinx-coroutines-reactor/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -dependencies { - compile "io.projectreactor:reactor-core:$reactor_version" - compile project(':kotlinx-coroutines-reactive') -} - -tasks.withType(dokka.getClass()) { - externalDocumentationLink { - url = new URL("https://projectreactor.io/docs/core/$reactor_version/api/") - packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() - } -} - -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" -} - -compileKotlin { - kotlinOptions.jvmTarget = "1.8" -} diff --git a/reactive/kotlinx-coroutines-reactor/build.gradle.kts b/reactive/kotlinx-coroutines-reactor/build.gradle.kts new file mode 100644 index 0000000000..d5fd208a27 --- /dev/null +++ b/reactive/kotlinx-coroutines-reactor/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +val reactorVersion = version("reactor") + +dependencies { + compile("io.projectreactor:reactor-core:$reactorVersion") + compile(project(":kotlinx-coroutines-reactive")) +} + + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "1.8" + } + + compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" + } +} + +externalDocumentationLink( + url = "https://projectreactor.io/docs/core/$reactorVersion/api/" +) From 3cb61fc44bec51f85abde11f83bc5f556e5e313a Mon Sep 17 00:00:00 2001 From: Nicklas Ansman Giertz Date: Wed, 7 Oct 2020 12:15:18 -0400 Subject: [PATCH 119/257] Clarify the env requirements in CONTRIBUTING.md (#2281) The wording is a bit confusing and does not clearly indicate that you can set `JDK_XX` to whatever JDK version you want. Also add a shell snippet Co-authored-by: Roman Elizarov Co-authored-by: EdwarDDay <4127904+EdwarDDay@users.noreply.github.com> --- CONTRIBUTING.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abd458e6f2..29187c8308 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,9 +79,16 @@ to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Grad * JDK >= 11 referred to by the `JAVA_HOME` environment variable. * JDK 1.6 referred to by the `JDK_16` environment variable. - It is OK to have `JDK_16` pointing to `JAVA_HOME` for external contributions. + It is OK to have `JDK_16` pointing to a non 1.6 JDK (e.g. `JAVA_HOME`) for external contributions. * JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. - It is OK to have `JDK_18` pointing to `JAVA_HOME` for external contributions. + It is OK to have `JDK_18` to a non 8 JDK (e.g. `JAVA_HOME`) for external contributions. + +For external contributions you can for example add this to your shell startup scripts (e.g. `~/.zshrc`): +```shell +# This assumes JAVA_HOME is set already to a JDK >= 11 version +export JDK_16="$JAVA_HOME" +export JDK_18="$JAVA_HOME" +``` ### Running the Knit tool From 2554f560d357a7161a4f79fc0d1ea9f3bf2cfb5b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 8 Oct 2020 10:32:54 +0300 Subject: [PATCH 120/257] Consistent JDK versions in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29187c8308..93f1330943 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,7 +81,7 @@ to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Grad * JDK 1.6 referred to by the `JDK_16` environment variable. It is OK to have `JDK_16` pointing to a non 1.6 JDK (e.g. `JAVA_HOME`) for external contributions. * JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. - It is OK to have `JDK_18` to a non 8 JDK (e.g. `JAVA_HOME`) for external contributions. + It is OK to have `JDK_18` to a non 1.8 JDK (e.g. `JAVA_HOME`) for external contributions. For external contributions you can for example add this to your shell startup scripts (e.g. `~/.zshrc`): ```shell From d4c55ce63403737b8aacf7df9410ce5374dc3786 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 8 Oct 2020 02:00:38 -0700 Subject: [PATCH 121/257] Clarify documentation for IO dispatcher (#2286) Fixes #2272 --- kotlinx-coroutines-core/jvm/src/Dispatchers.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt index 8cd3bb1bae..8033fb38e5 100644 --- a/kotlinx-coroutines-core/jvm/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/Dispatchers.kt @@ -97,7 +97,7 @@ public actual object Dispatchers { * The [CoroutineDispatcher] that is designed for offloading blocking IO tasks to a shared pool of threads. * * Additional threads in this pool are created and are shutdown on demand. - * The number of threads used by this dispatcher is limited by the value of + * The number of threads used by tasks in this dispatcher is limited by the value of * "`kotlinx.coroutines.io.parallelism`" ([IO_PARALLELISM_PROPERTY_NAME]) system property. * It defaults to the limit of 64 threads or the number of cores (whichever is larger). * @@ -106,9 +106,13 @@ public actual object Dispatchers { * If you need a higher number of parallel threads, * you should use a custom dispatcher backed by your own thread pool. * + * ### Implementation note + * * This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using * `withContext(Dispatchers.IO) { ... }` does not lead to an actual switching to another thread — * typically execution continues in the same thread. + * As a result of thread sharing, more than 64 (default parallelism) threads can be created (but not used) + * during operations over IO dispatcher. */ @JvmStatic public val IO: CoroutineDispatcher = DefaultScheduler.IO From 448106a4d097468df862407bffc31430bc78b0f7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 8 Oct 2020 02:01:09 -0700 Subject: [PATCH 122/257] =?UTF-8?q?Provide=20asFlowable=20and=20asObservab?= =?UTF-8?q?le=20by=20their=20names=20in=20binary=20instead=20=E2=80=A6=20(?= =?UTF-8?q?#2285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Provide asFlowable and asObservable by their names in binary instead of 'from' function to prevent naming clash for Java users. * Do not provide @JvmOverloads for convenience of Java interop * Deprecate ReceiveChannel.asObservable by the way Fixes #2182 --- .../api/kotlinx-coroutines-rx2.api | 12 +++-- .../kotlinx-coroutines-rx2/src/RxConvert.kt | 46 ++++++++++--------- .../test/ConvertTest.kt | 7 +-- .../test/IntegrationTest.kt | 5 +- .../api/kotlinx-coroutines-rx3.api | 12 +++-- .../kotlinx-coroutines-rx3/src/RxConvert.kt | 21 ++++++--- 6 files changed, 63 insertions(+), 40 deletions(-) diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api index 06ddb68e9c..2cf7bc815b 100644 --- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api +++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api @@ -30,13 +30,17 @@ public final class kotlinx/coroutines/rx2/RxCompletableKt { public final class kotlinx/coroutines/rx2/RxConvertKt { public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Completable; public static final fun asFlow (Lio/reactivex/ObservableSource;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlowable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; + public static synthetic fun asFlowable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Maybe; public static final fun asObservable (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static final fun asObservable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static synthetic fun asObservable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Single; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/Observable; public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Flowable; public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/Observable; } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index 264cdad43c..cf73ef2ea8 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -16,7 +16,7 @@ import kotlin.coroutines.* /** * Converts this job to the hot reactive completable that signals - * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. + * with [onCompleted][CompletableObserver.onComplete] when the corresponding job completes. * * Every subscriber gets the signal at the same time. * Unsubscribing from the resulting completable **does not** affect the original job in any way. @@ -50,7 +50,7 @@ public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMay /** * Converts this deferred value to the hot reactive single that signals either - * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. + * [onSuccess][SingleObserver.onSuccess] or [onError][SingleObserver.onError]. * * Every subscriber gets the same completion value. * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. @@ -65,21 +65,6 @@ public fun Deferred.asSingle(context: CoroutineContext): Single this@asSingle.await() } -/** - * Converts a stream of elements received from the channel to the hot reactive observable. - * - * Every subscriber receives values from this channel in **fan-out** fashion. If the are multiple subscribers, - * they'll receive values in round-robin way. - */ -@Deprecated( - message = "Deprecated in the favour of Flow", - level = DeprecationLevel.WARNING, replaceWith = ReplaceWith("this.consumeAsFlow().asObservable()") -) -public fun ReceiveChannel.asObservable(context: CoroutineContext): Observable = rxObservable(context) { - for (t in this@asObservable) - send(t) -} - /** * Transforms given cold [ObservableSource] into cold [Flow]. * @@ -113,8 +98,6 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -@JvmName("from") @ExperimentalCoroutinesApi public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> /* @@ -148,8 +131,29 @@ public fun Flow.asObservable(context: CoroutineContext = EmptyCorout * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -@JvmName("from") @ExperimentalCoroutinesApi public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = Flowable.fromPublisher(asPublisher(context)) + +@Deprecated( + message = "Deprecated in the favour of Flow", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("this.consumeAsFlow().asObservable(context)", "kotlinx.coroutines.flow.consumeAsFlow") +) // Deprecated since 1.4.0 +public fun ReceiveChannel.asObservable(context: CoroutineContext): Observable = rxObservable(context) { + for (t in this@asObservable) + send(t) +} + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + asFlowable(context) + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = asObservable(context) diff --git a/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt b/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt index a43366555e..cfc3240741 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ConvertTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* import org.junit.Assert import org.junit.Test import kotlin.test.* @@ -126,7 +127,7 @@ class ConvertTest : TestBase() { delay(50) send("K") } - val observable = c.asObservable(Dispatchers.Unconfined) + val observable = c.consumeAsFlow().asObservable(Dispatchers.Unconfined) checkSingleValue(observable.reduce { t1, t2 -> t1 + t2 }.toSingle()) { assertEquals("OK", it) } @@ -140,7 +141,7 @@ class ConvertTest : TestBase() { delay(50) throw TestException("K") } - val observable = c.asObservable(Dispatchers.Unconfined) + val observable = c.consumeAsFlow().asObservable(Dispatchers.Unconfined) val single = rxSingle(Dispatchers.Unconfined) { var result = "" try { @@ -155,4 +156,4 @@ class ConvertTest : TestBase() { assertEquals("OK", it) } } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt index 22e0e72191..540fa76b7e 100644 --- a/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/IntegrationTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import org.junit.Test import org.junit.runner.* import org.junit.runners.* @@ -92,7 +93,7 @@ class IntegrationTest( assertFailsWith { observable.awaitSingle() } checkNumbers(n, observable) val channel = observable.openSubscription() - checkNumbers(n, channel.asObservable(ctx(coroutineContext))) + checkNumbers(n, channel.consumeAsFlow().asObservable(ctx(coroutineContext))) channel.cancel() } @@ -131,4 +132,4 @@ class IntegrationTest( assertEquals(n, last) } -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api index 4f15eda7d4..eb92fd3285 100644 --- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -26,12 +26,16 @@ public final class kotlinx/coroutines/rx3/RxCompletableKt { public final class kotlinx/coroutines/rx3/RxConvertKt { public static final fun asCompletable (Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Completable; public static final fun asFlow (Lio/reactivex/rxjava3/core/ObservableSource;)Lkotlinx/coroutines/flow/Flow; + public static final fun asFlowable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; + public static synthetic fun asFlowable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; public static final fun asMaybe (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Maybe; + public static final fun asObservable (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; + public static synthetic fun asObservable$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; public static final fun asSingle (Lkotlinx/coroutines/Deferred;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Single; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; - public static final fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;)Lio/reactivex/rxjava3/core/Observable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Flowable; + public static final synthetic fun from (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lio/reactivex/rxjava3/core/Observable; public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Flowable; public static synthetic fun from$default (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lio/reactivex/rxjava3/core/Observable; } diff --git a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt index c7ab237cea..9bb38c088f 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt @@ -16,7 +16,7 @@ import kotlin.coroutines.* /** * Converts this job to the hot reactive completable that signals - * with [onCompleted][CompletableSubscriber.onCompleted] when the corresponding job completes. + * with [onCompleted][CompletableObserver.onComplete] when the corresponding job completes. * * Every subscriber gets the signal at the same time. * Unsubscribing from the resulting completable **does not** affect the original job in any way. @@ -50,7 +50,7 @@ public fun Deferred.asMaybe(context: CoroutineContext): Maybe = rxMay /** * Converts this deferred value to the hot reactive single that signals either - * [onSuccess][SingleSubscriber.onSuccess] or [onError][SingleSubscriber.onError]. + * [onSuccess][SingleObserver.onSuccess] or [onError][SingleObserver.onError]. * * Every subscriber gets the same completion value. * Unsubscribing from the resulting single **does not** affect the original deferred value in any way. @@ -98,8 +98,6 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -@JvmName("from") @ExperimentalCoroutinesApi public fun Flow.asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = Observable.create { emitter -> /* @@ -133,8 +131,19 @@ public fun Flow.asObservable(context: CoroutineContext = EmptyCorout * inject additional context into the caller thread. By default, the [Unconfined][Dispatchers.Unconfined] dispatcher * is used, so calls are performed from an arbitrary thread. */ -@JvmOverloads // binary compatibility -@JvmName("from") @ExperimentalCoroutinesApi public fun Flow.asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = Flowable.fromPublisher(asPublisher(context)) + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asFlowable(context: CoroutineContext = EmptyCoroutineContext): Flowable = + asFlowable(context) + +@Suppress("UNUSED") // KT-42513 +@JvmOverloads // binary compatibility +@JvmName("from") +@Deprecated(level = DeprecationLevel.HIDDEN, message = "") // Since 1.4, was experimental prior to that +public fun Flow._asObservable(context: CoroutineContext = EmptyCoroutineContext) : Observable = asObservable(context) From 7897f7029e177a119951ff9a7722bf3054c7f734 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 8 Oct 2020 02:50:44 -0700 Subject: [PATCH 123/257] Pr/2230 (#2287) * Allow nullable types in Flow.firstOrNull * Allow nullable types in Flow.singleOrNull * Align Flow.single and Flow.singleOrNull with Kotlin standard library Fixes #2229 Fixes #2289 Co-authored-by: Nicklas Ansman Giertz --- .../common/src/flow/terminal/Reduce.kt | 37 +++++++++++-------- .../test/flow/operators/OnCompletionTest.kt | 4 +- .../common/test/flow/terminal/FirstTest.kt | 6 +++ .../common/test/flow/terminal/SingleTest.kt | 27 ++++++++++++-- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index d36e1bbf7b..83f5498e4d 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -9,6 +9,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.flow.internal.* +import kotlinx.coroutines.internal.Symbol import kotlin.jvm.* /** @@ -47,33 +48,39 @@ public suspend inline fun Flow.fold( } /** - * The terminal operator, that awaits for one and only one value to be published. + * The terminal operator that awaits for one and only one value to be emitted. * Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow * that contains more than one element. */ public suspend fun Flow.single(): T { var result: Any? = NULL collect { value -> - if (result !== NULL) error("Expected only one element") + require(result === NULL) { "Flow has more than one element" } result = value } - if (result === NULL) throw NoSuchElementException("Expected at least one element") - @Suppress("UNCHECKED_CAST") + if (result === NULL) throw NoSuchElementException("Flow is empty") return result as T } /** - * The terminal operator, that awaits for one and only one value to be published. - * Throws [IllegalStateException] for flow that contains more than one element. + * The terminal operator that awaits for one and only one value to be emitted. + * Returns the single value or `null`, if the flow was empty or emitted more than one value. */ -public suspend fun Flow.singleOrNull(): T? { - var result: T? = null - collect { value -> - if (result != null) error("Expected only one element") - result = value +public suspend fun Flow.singleOrNull(): T? { + var result: Any? = NULL + collectWhile { + // No values yet, update result + if (result === NULL) { + result = it + true + } else { + // Second value, reset result and bail out + result = NULL + false + } } - return result + return if (result === NULL) null else result as T } /** @@ -112,7 +119,7 @@ public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { * The terminal operator that returns the first element emitted by the flow and then cancels flow's collection. * Returns `null` if the flow was empty. */ -public suspend fun Flow.firstOrNull(): T? { +public suspend fun Flow.firstOrNull(): T? { var result: T? = null collectWhile { result = it @@ -122,10 +129,10 @@ public suspend fun Flow.firstOrNull(): T? { } /** - * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. + * The terminal operator that returns the first element emitted by the flow matching the given [predicate] and then cancels flow's collection. * Returns `null` if the flow did not contain an element matching the [predicate]. */ -public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { +public suspend fun Flow.firstOrNull(predicate: suspend (T) -> Boolean): T? { var result: T? = null collectWhile { if (predicate(it)) { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt index 7f0c548ca6..f55e8beeb2 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt @@ -231,7 +231,7 @@ class OnCompletionTest : TestBase() { @Test fun testSingle() = runTest { - assertFailsWith { + assertFailsWith { flowOf(239).onCompletion { assertNull(it) expect(1) @@ -240,7 +240,7 @@ class OnCompletionTest : TestBase() { expectUnreached() } catch (e: Throwable) { // Second emit -- failure - assertTrue { e is IllegalStateException } + assertTrue { e is IllegalArgumentException } throw e } }.single() diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt index edb9f00fa6..fa7fc9cb6c 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/FirstTest.kt @@ -128,6 +128,12 @@ class FirstTest : TestBase() { assertNull(emptyFlow().firstOrNull { true }) } + @Test + fun testFirstOrNullWithNullElement() = runTest { + assertNull(flowOf(null).firstOrNull()) + assertNull(flowOf(null).firstOrNull { true }) + } + @Test fun testFirstOrNullWhenErrorCancelsUpstream() = runTest { val latch = Channel() diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt index 4e89b93bd7..2c1277b1e1 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/SingleTest.kt @@ -7,7 +7,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.test.* -class SingleTest : TestBase() { +class SingleTest : TestBase() { @Test fun testSingle() = runTest { @@ -25,8 +25,8 @@ class SingleTest : TestBase() { emit(239L) emit(240L) } - assertFailsWith { flow.single() } - assertFailsWith { flow.singleOrNull() } + assertFailsWith { flow.single() } + assertNull(flow.singleOrNull()) } @Test @@ -61,6 +61,10 @@ class SingleTest : TestBase() { assertEquals(1, flowOf(1).single()) assertNull(flowOf(null).single()) assertFailsWith { flowOf().single() } + + assertEquals(1, flowOf(1).singleOrNull()) + assertNull(flowOf(null).singleOrNull()) + assertNull(flowOf().singleOrNull()) } @Test @@ -69,5 +73,22 @@ class SingleTest : TestBase() { val flow = flowOf(instance) assertSame(instance, flow.single()) assertSame(instance, flow.singleOrNull()) + + val flow2 = flow { + emit(BadClass()) + emit(BadClass()) + } + assertFailsWith { flow2.single() } + } + + @Test + fun testSingleNoWait() = runTest { + val flow = flow { + emit(1) + emit(2) + awaitCancellation() + } + + assertNull(flow.singleOrNull()) } } From b97ebfcf8c2030e7217988dc21ed2e80f662d30b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 8 Oct 2020 16:04:41 +0300 Subject: [PATCH 124/257] Update Flow.sample KDoc example timings, add tests (#2259) * All Flow.debounce/sample KDoc example code snippets are automatically tested with Knit. * Flow.sample timings are made larger, so that they produce an expected output when run under the real time, too. Fixes #2243 --- docs/knit.properties | 12 ---- docs/knit.test.template | 1 + gradle.properties | 2 +- knit.properties | 16 +++++ .../common/src/flow/operators/Delay.kt | 72 +++++++++++++++---- .../jvm/test/examples/example-delay-01.kt | 24 +++++++ .../jvm/test/examples/example-delay-02.kt | 19 +++++ .../examples/example-delay-duration-01.kt | 26 +++++++ .../examples/example-delay-duration-02.kt | 21 ++++++ .../jvm/test/examples/test/FlowDelayTest.kt | 39 ++++++++++ .../jvm/test/guide/test/BasicsGuideTest.kt | 1 + .../test/guide/test/CancellationGuideTest.kt | 1 + .../jvm/test/guide/test/ChannelsGuideTest.kt | 1 + .../jvm/test/guide/test/ComposingGuideTest.kt | 1 + .../test/guide/test/DispatcherGuideTest.kt | 1 + .../test/guide/test/ExceptionsGuideTest.kt | 1 + .../jvm/test/guide/test/FlowGuideTest.kt | 1 + .../jvm/test/guide/test/SelectGuideTest.kt | 1 + .../test/guide/test/SharedStateGuideTest.kt | 1 + .../jvm/test/{guide/test => knit}/TestUtil.kt | 4 +- kotlinx-coroutines-core/knit.properties | 10 +++ 21 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 knit.properties create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt rename kotlinx-coroutines-core/jvm/test/{guide/test => knit}/TestUtil.kt (98%) create mode 100644 kotlinx-coroutines-core/knit.properties diff --git a/docs/knit.properties b/docs/knit.properties index ab2508a114..2028ecb416 100644 --- a/docs/knit.properties +++ b/docs/knit.properties @@ -4,19 +4,7 @@ knit.package=kotlinx.coroutines.guide knit.dir=../kotlinx-coroutines-core/jvm/test/guide/ -knit.pattern=example-[a-zA-Z0-9-]+-##\\.kt -knit.include=knit.code.include test.package=kotlinx.coroutines.guide.test test.dir=../kotlinx-coroutines-core/jvm/test/guide/test/ -test.template=knit.test.template -# Various test validation modes and their corresponding methods from TestUtil -test.mode.=verifyLines -test.mode.STARTS_WITH=verifyLinesStartWith -test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime -test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime -test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread -test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered -test.mode.LINES_START=verifyLinesStart -test.mode.EXCEPTION=verifyExceptions \ No newline at end of file diff --git a/docs/knit.test.template b/docs/knit.test.template index a912555a43..727493c662 100644 --- a/docs/knit.test.template +++ b/docs/knit.test.template @@ -5,6 +5,7 @@ // This file was automatically generated from ${file.name} by Knit tool. Do not edit. package ${test.package} +import kotlinx.coroutines.knit.* import org.junit.Test class ${test.name} { diff --git a/gradle.properties b/gradle.properties index 238a9c6ac9..196a4a9ac0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin_version=1.4.0 # Dependencies junit_version=4.12 atomicfu_version=0.14.4 -knit_version=0.1.3 +knit_version=0.2.0 html_version=0.6.8 lincheck_version=2.7.1 dokka_version=0.9.16-rdev-2-mpp-hacks diff --git a/knit.properties b/knit.properties new file mode 100644 index 0000000000..bc177ce44c --- /dev/null +++ b/knit.properties @@ -0,0 +1,16 @@ +# +# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +knit.include=docs/knit.code.include +test.template=docs/knit.test.template + +# Various test validation modes and their corresponding methods from TestUtil +test.mode.=verifyLines +test.mode.STARTS_WITH=verifyLinesStartWith +test.mode.ARBITRARY_TIME=verifyLinesArbitraryTime +test.mode.FLEXIBLE_TIME=verifyLinesFlexibleTime +test.mode.FLEXIBLE_THREAD=verifyLinesFlexibleThread +test.mode.LINES_START_UNORDERED=verifyLinesStartUnordered +test.mode.LINES_START=verifyLinesStart +test.mode.EXCEPTION=verifyExceptions \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index 5f3c900c1b..aa55fea721 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -14,13 +14,30 @@ import kotlinx.coroutines.selects.* import kotlin.jvm.* import kotlin.time.* +/* Scaffolding for Knit code examples + + +*/ + /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout][timeoutMillis]. * The latest value is always emitted. * * Example: - * ``` + * + * ```kotlin * flow { * emit(1) * delay(90) @@ -33,7 +50,14 @@ import kotlin.time.* * emit(5) * }.debounce(1000) * ``` - * produces `3, 4, 5`. + * + * + * produces the following emissions + * + * ```text + * 3, 4, 5 + * ``` + * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. @@ -77,7 +101,8 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * The latest value is always emitted. * * Example: - * ``` + * + * ```kotlin * flow { * emit(1) * delay(90.milliseconds) @@ -90,7 +115,14 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { * emit(5) * }.debounce(1000.milliseconds) * ``` - * produces `3, 4, 5`. + * + * + * produces the following emissions + * + * ```text + * 3, 4, 5 + * ``` + * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] milliseconds. @@ -103,15 +135,23 @@ public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.t * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis]. * * Example: - * ``` + * + * ```kotlin * flow { * repeat(10) { * emit(it) - * delay(50) + * delay(110) * } - * }.sample(100) + * }.sample(200) + * ``` + * + * + * produces the following emissions + * + * ```text + * 1, 3, 5, 7, 9 * ``` - * produces `1, 3, 5, 7, 9`. + * * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @@ -166,15 +206,23 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period]. * * Example: - * ``` + * + * ```kotlin * flow { * repeat(10) { * emit(it) - * delay(50.milliseconds) + * delay(110.milliseconds) * } - * }.sample(100.milliseconds) + * }.sample(200.milliseconds) + * ``` + * + * + * produces the following emissions + * + * ```text + * 1, 3, 5, 7, 9 * ``` - * produces `1, 3, 5, 7, 9`. + * * * Note that the latest element is not emitted if it does not fit into the sampling window. */ diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt new file mode 100644 index 0000000000..d2a5d536b2 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-01.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelay01 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + emit(1) + delay(90) + emit(2) + delay(90) + emit(3) + delay(1010) + emit(4) + delay(1010) + emit(5) +}.debounce(1000) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt new file mode 100644 index 0000000000..1b6b12f041 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelay02 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + repeat(10) { + emit(it) + delay(110) + } +}.sample(200) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt new file mode 100644 index 0000000000..a19e6cb181 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-01.kt @@ -0,0 +1,26 @@ +@file:OptIn(ExperimentalTime::class) +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelayDuration01 + +import kotlin.time.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + emit(1) + delay(90.milliseconds) + emit(2) + delay(90.milliseconds) + emit(3) + delay(1010.milliseconds) + emit(4) + delay(1010.milliseconds) + emit(5) +}.debounce(1000.milliseconds) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt new file mode 100644 index 0000000000..e43dfd1e05 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt @@ -0,0 +1,21 @@ +@file:OptIn(ExperimentalTime::class) +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelayDuration02 + +import kotlin.time.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + repeat(10) { + emit(it) + delay(110.milliseconds) + } +}.sample(200.milliseconds) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt new file mode 100644 index 0000000000..226d31cc00 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.test + +import kotlinx.coroutines.knit.* +import org.junit.Test + +class FlowDelayTest { + @Test + fun testExampleDelay01() { + test("ExampleDelay01") { kotlinx.coroutines.examples.exampleDelay01.main() }.verifyLines( + "3, 4, 5" + ) + } + + @Test + fun testExampleDelayDuration01() { + test("ExampleDelayDuration01") { kotlinx.coroutines.examples.exampleDelayDuration01.main() }.verifyLines( + "3, 4, 5" + ) + } + + @Test + fun testExampleDelay02() { + test("ExampleDelay02") { kotlinx.coroutines.examples.exampleDelay02.main() }.verifyLines( + "1, 3, 5, 7, 9" + ) + } + + @Test + fun testExampleDelayDuration02() { + test("ExampleDelayDuration02") { kotlinx.coroutines.examples.exampleDelayDuration02.main() }.verifyLines( + "1, 3, 5, 7, 9" + ) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt index 7fc57c2ee3..ea5003b0c7 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/BasicsGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from basics.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class BasicsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt index a2e91de82d..9e8cd28f16 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class CancellationGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt index 209d439663..d15a550adb 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ChannelsGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from channels.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class ChannelsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt index 50c3fd7e62..1f9692d56b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ComposingGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from composing-suspending-functions.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class ComposingGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt index c0c32410d5..d6f1c21dc0 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/DispatcherGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from coroutine-context-and-dispatchers.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class DispatcherGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt index c42ba31d3a..f34fd3f83b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from exception-handling.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class ExceptionsGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt index 7fa9cc84dd..c7d4a72082 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/FlowGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from flow.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class FlowGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt index e3f47b9648..55650d4c6a 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/SelectGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from select-expression.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class SelectGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt index 8d534a09ea..3162b24cbc 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/SharedStateGuideTest.kt @@ -5,6 +5,7 @@ // This file was automatically generated from shared-mutable-state-and-concurrency.md by Knit tool. Do not edit. package kotlinx.coroutines.guide.test +import kotlinx.coroutines.knit.* import org.junit.Test class SharedStateGuideTest { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt similarity index 98% rename from kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt rename to kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt index fb1c85bce1..7eda9043db 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/TestUtil.kt +++ b/kotlinx-coroutines-core/jvm/test/knit/TestUtil.kt @@ -1,8 +1,8 @@ /* - * 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.guide.test +package kotlinx.coroutines.knit import kotlinx.coroutines.* import kotlinx.coroutines.internal.* diff --git a/kotlinx-coroutines-core/knit.properties b/kotlinx-coroutines-core/knit.properties new file mode 100644 index 0000000000..93ce87db8f --- /dev/null +++ b/kotlinx-coroutines-core/knit.properties @@ -0,0 +1,10 @@ +# +# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +knit.package=kotlinx.coroutines.examples +knit.dir=jvm/test/examples/ + +test.package=kotlinx.coroutines.examples.test +test.dir=jvm/test/examples/test/ + From 20ca97ad47adc249c6211ad9d12e31f1074984d7 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 8 Oct 2020 17:16:40 +0300 Subject: [PATCH 125/257] Added docs on withTimeout asynchrony and its use with resources (#2252) This is a tricky gotcha that needs additional explanation. There are two examples added, one showing the bad code and explaining why it does not work, and the other showing the correct way to write it. Fixes #2233 --- coroutines-guide.md | 1 + docs/cancellation-and-timeouts.md | 109 ++++++++++++++++++ kotlinx-coroutines-core/common/src/Timeout.kt | 36 +++++- .../jvm/test/guide/example-cancel-08.kt | 31 +++++ .../jvm/test/guide/example-cancel-09.kt | 36 ++++++ .../test/guide/test/CancellationGuideTest.kt | 7 ++ 6 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt create mode 100644 kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt diff --git a/coroutines-guide.md b/coroutines-guide.md index ea512ba68d..09cfb93cab 100644 --- a/coroutines-guide.md +++ b/coroutines-guide.md @@ -20,6 +20,7 @@ The main coroutines guide has moved to the [docs folder](docs/coroutines-guide.m *
[Closing resources with `finally`](docs/cancellation-and-timeouts.md#closing-resources-with-finally) * [Run non-cancellable block](docs/cancellation-and-timeouts.md#run-non-cancellable-block) * [Timeout](docs/cancellation-and-timeouts.md#timeout) + * [Asynchronous timeout and resources](docs/cancellation-and-timeouts.md#asynchronous-timeout-and-resources) * [Composing Suspending Functions](docs/composing-suspending-functions.md#composing-suspending-functions) * [Sequential by default](docs/composing-suspending-functions.md#sequential-by-default) diff --git a/docs/cancellation-and-timeouts.md b/docs/cancellation-and-timeouts.md index d8d5b7bad4..b296bde493 100644 --- a/docs/cancellation-and-timeouts.md +++ b/docs/cancellation-and-timeouts.md @@ -11,6 +11,7 @@ * [Closing resources with `finally`](#closing-resources-with-finally) * [Run non-cancellable block](#run-non-cancellable-block) * [Timeout](#timeout) + * [Asynchronous timeout and resources](#asynchronous-timeout-and-resources) @@ -355,6 +356,114 @@ Result is null +### Asynchronous timeout and resources + + + +The timeout event in [withTimeout] is asynchronous with respect to the code running in its block and may happen at any time, +even right before the return from inside of the timeout block. Keep this in mind if you open or acquire some +resource inside the block that needs closing or release outside of the block. + +For example, here we imitate a closeable resource with the `Resource` class, that simply keeps track of how many times +it was created by incrementing the `acquired` counter and decrementing this counter from its `close` function. +Let us run a lot of coroutines with the small timeout try acquire this resource from inside +of the `withTimeout` block after a bit of delay and release it from outside. + +
+ +```kotlin +import kotlinx.coroutines.* + +//sampleStart +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + val resource = withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + Resource() // Acquire a resource and return it from withTimeout block + } + resource.close() // Release the resource + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +} +//sampleEnd +``` + +
+ +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt). + + + +If you run the above code you'll see that it does not always print zero, though it may depend on the timings +of your machine you may need to tweak timeouts in this example to actually see non-zero values. + +> Note, that incrementing and decrementing `acquired` counter here from 100K coroutines is completely safe, +> since it always happens from the same main thread. More on that will be explained in the next chapter +> on coroutine context. + +To workaround this problem you can store a reference to the resource in the variable as opposed to returning it +from the `withTimeout` block. + +
+ +```kotlin +import kotlinx.coroutines.* + +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { +//sampleStart + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + var resource: Resource? = null // Not acquired yet + try { + withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + resource = Resource() // Store a resource to the variable if acquired + } + // We can do something else with the resource here + } finally { + resource?.close() // Release the resource if it was acquired + } + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +//sampleEnd +} +``` + +
+ +> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt). + +This example always prints zero. Resources do not leak. + + + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index c8e4455c92..8547358b78 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -24,7 +24,14 @@ import kotlin.time.* * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -48,7 +55,14 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * The sibling function that does not throw an exception on timeout is [withTimeoutOrNull]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { @@ -68,7 +82,14 @@ public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineSc * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. * * @param timeMillis timeout time in milliseconds. */ @@ -101,7 +122,14 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout * The sibling function that throws an exception on timeout is [withTimeout]. * Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause. * - * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. + * **The timeout event is asynchronous with respect to the code running in the block** and may happen at any time, + * even right before the return from inside of the timeout [block]. Keep this in mind if you open or acquire some + * resource inside the [block] that needs closing or release outside of the block. + * See the + * [Asynchronous timeout and resources][https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#asynchronous-timeout-and-resources] + * section of the coroutines guide for details. + * + * > Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime public suspend fun withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? = diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt new file mode 100644 index 0000000000..e7def132ae --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-08.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleCancel08 + +import kotlinx.coroutines.* + +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + val resource = withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + Resource() // Acquire a resource and return it from withTimeout block + } + resource.close() // Release the resource + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.kt new file mode 100644 index 0000000000..95424f5108 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/guide/example-cancel-09.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. + */ + +// This file was automatically generated from cancellation-and-timeouts.md by Knit tool. Do not edit. +package kotlinx.coroutines.guide.exampleCancel09 + +import kotlinx.coroutines.* + +var acquired = 0 + +class Resource { + init { acquired++ } // Acquire the resource + fun close() { acquired-- } // Release the resource +} + +fun main() { + runBlocking { + repeat(100_000) { // Launch 100K coroutines + launch { + var resource: Resource? = null // Not acquired yet + try { + withTimeout(60) { // Timeout of 60 ms + delay(50) // Delay for 50 ms + resource = Resource() // Store a resource to the variable if acquired + } + // We can do something else with the resource here + } finally { + resource?.close() // Release the resource if it was acquired + } + } + } + } + // Outside of runBlocking all coroutines have completed + println(acquired) // Print the number of resources still acquired +} diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt index 9e8cd28f16..0cff63a834 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/CancellationGuideTest.kt @@ -88,4 +88,11 @@ class CancellationGuideTest { "Result is null" ) } + + @Test + fun testExampleCancel09() { + test("ExampleCancel09") { kotlinx.coroutines.guide.exampleCancel09.main() }.verifyLines( + "0" + ) + } } From b82439e6b570249cfbd25622dc06cb0ec156c006 Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Fri, 9 Oct 2020 12:01:05 +0300 Subject: [PATCH 126/257] Document problems with building coroutines-using Android projects (#2288) Added instructions to work around #2023 Also, the answer to #2274 is now documented. Co-authored-by: Vsevolod Tolstopyatov --- README.md | 11 ++++ kotlinx-coroutines-debug/README.md | 92 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/README.md b/README.md index 4b6f99be0b..7f71d288d4 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,17 @@ threads are handled by Android runtime. R8 and ProGuard rules are bundled into the [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module. For more details see ["Optimization" section for Android](ui/kotlinx-coroutines-android/README.md#optimization). +#### Avoiding including the debug infrastructure in the resulting APK + +The `kotlinx-coroutines-core` artifact contains a resource file that is not required for the coroutines to operate +normally and is only used by the debugger. To exclude it at no loss of functionality, add the following snippet to the +`android` block in your gradle file for the application subproject: +```groovy +packagingOptions { + exclude "DebugProbesKt.bin" +} +``` + ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 7c4501ab7a..1278ed132c 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -170,6 +170,98 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana at kotlinx.coroutines.debug.DebugProbes.install(DebugProbes.kt:49) --> +#### Build failures due to duplicate resource files + +Building an Android project that depends on `kotlinx-coroutines-debug` (usually introduced by being a transitive +dependency of `kotlinx-coroutines-test`) may fail with `DuplicateRelativeFileException` for `META-INF/AL2.0`, +`META-INF/LGPL2.1`, or `win32-x86/attach_hotspot_windows.dll` when trying to merge the Android resource. + +The problem is that Android merges the resources of all its dependencies into a single directory and complains about +conflicts, but: +* `kotlinx-coroutines-debug` transitively depends on JNA and JNA-platform, both of which include license files in their + META-INF directories. Trying to merge these files leads to conflicts, which means that any Android project that + depends on JNA and JNA-platform will experience build failures. +* Additionally, `kotlinx-coroutines-debug` embeds `byte-buddy-agent` and `byte-buddy`, along with their resource files. + Then, if the project separately depends on `byte-buddy`, merging the resources of `kotlinx-coroutines-debug` with ones + from `byte-buddy` and `byte-buddy-agent` will lead to conflicts as the resource files are duplicated. + +One possible workaround for these issues is to add the following to the `android` block in your gradle file for the +application subproject: +```groovy + packagingOptions { + // for JNA and JNA-platform + exclude "META-INF/AL2.0" + exclude "META-INF/LGPL2.1" + // for byte-buddy + exclude "META-INF/licenses/ASM" + pickFirst "win32-x86-64/attach_hotspot_windows.dll" + pickFirst "win32-x86/attach_hotspot_windows.dll" + } +``` +This will cause the resource merge algorithm to exclude the problematic license files altogether and only leave a single +copy of the files needed for `byte-buddy-agent` to work. + +Alternatively, avoid depending on `kotlinx-coroutines-debug`. In particular, if the only reason why this library a +dependency of your project is that `kotlinx-coroutines-test` in turn depends on it, you may change your dependency on +`kotlinx.coroutines.test` to exclude `kotlinx-coroutines-debug`. For example, you could replace +```kotlin +androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") +``` +with +```groovy +androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") { + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" +} +``` + [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html From 20341f2c849ada43de15d2c11d97dac9581cc4f4 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 9 Oct 2020 16:40:15 +0300 Subject: [PATCH 127/257] Cancel current Job on RejectedExecutionException (#2012) When the Executor that was used with Executor.asCoroutineDispatcher() extension rejects the submitted task, it means that it had reached its capacity and so the executing current Job should be canceled to terminate it as soon as possible. This way RejectedExecutionException works as a rate-limiter just like it serves this purpose in executor-based Java code. Fixes #2003 --- .../api/kotlinx-coroutines-core.api | 4 +- kotlinx-coroutines-core/common/src/Delay.kt | 4 +- kotlinx-coroutines-core/common/src/Timeout.kt | 2 +- .../common/src/selects/Select.kt | 2 +- .../common/test/flow/VirtualTime.kt | 4 +- .../js/src/JSDispatcher.kt | 4 +- kotlinx-coroutines-core/jvm/src/CommonPool.kt | 2 + .../jvm/src/DefaultExecutor.kt | 3 +- kotlinx-coroutines-core/jvm/src/Executors.kt | 36 +++- .../jvm/src/ThreadPoolDispatcher.kt | 10 ++ .../jvm/src/internal/MainDispatchers.kt | 9 +- .../jvm/src/scheduling/Dispatcher.kt | 8 +- .../jvm/src/test_/TestCoroutineContext.kt | 2 +- .../jvm/test/ExecutorsTest.kt | 6 +- .../test/FailingCoroutinesMachineryTest.kt | 39 ++-- .../jvm/test/RejectedExecutionTest.kt | 168 ++++++++++++++++++ .../native/src/CoroutineContext.kt | 4 +- .../native/src/EventLoop.kt | 3 +- .../api/kotlinx-coroutines-test.api | 2 +- .../src/TestCoroutineDispatcher.kt | 2 +- .../src/internal/MainTestDispatcher.kt | 4 +- .../api/kotlinx-coroutines-reactor.api | 2 +- .../src/Scheduler.kt | 2 +- .../api/kotlinx-coroutines-rx2.api | 2 +- .../kotlinx-coroutines-rx2/src/RxScheduler.kt | 2 +- .../api/kotlinx-coroutines-rx3.api | 2 +- .../kotlinx-coroutines-rx3/src/RxScheduler.kt | 2 +- .../api/kotlinx-coroutines-android.api | 2 +- .../src/HandlerDispatcher.kt | 2 +- .../api/kotlinx-coroutines-javafx.api | 2 +- .../src/JavaFxDispatcher.kt | 2 +- .../api/kotlinx-coroutines-swing.api | 2 +- .../src/SwingDispatcher.kt | 2 +- 33 files changed, 282 insertions(+), 60 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 6c44e14b44..6611995361 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -257,13 +257,13 @@ public final class kotlinx/coroutines/Deferred$DefaultImpls { public abstract interface class kotlinx/coroutines/Delay { public abstract fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public abstract fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public abstract fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } public final class kotlinx/coroutines/Delay$DefaultImpls { public static fun delay (Lkotlinx/coroutines/Delay;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public static fun invokeOnTimeout (Lkotlinx/coroutines/Delay;JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; } public final class kotlinx/coroutines/DelayKt { diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index ff0a6fc73b..ee28ec3b0e 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -54,8 +54,8 @@ public interface Delay { * * This implementation uses a built-in single-threaded scheduled executor service. */ - public fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = - DefaultDelay.invokeOnTimeout(timeMillis, block) + public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + DefaultDelay.invokeOnTimeout(timeMillis, block, context) } /** diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 8547358b78..4bfff118e8 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -142,7 +142,7 @@ private fun setupTimeout( // schedule cancellation of this coroutine on time val cont = coroutine.uCont val context = cont.context - coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine)) + coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine, coroutine.context)) // restart the block using a new coroutine with a new job, // however, start it undispatched, because we already are in the proper context return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block) diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index e744a0c724..628d6f7aa5 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -655,7 +655,7 @@ internal class SelectBuilderImpl( if (trySelect()) block.startCoroutineCancellable(completion) // shall be cancellable while waits for dispatch } - disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action)) + disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action, context)) } private class DisposeNode( diff --git a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt index 9b257d933e..a607a4722a 100644 --- a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt +++ b/kotlinx-coroutines-core/common/test/flow/VirtualTime.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 @@ -50,7 +50,7 @@ private class VirtualTimeDispatcher(enclosingScope: CoroutineScope) : CoroutineD @ExperimentalCoroutinesApi override fun isDispatchNeeded(context: CoroutineContext): Boolean = originalDispatcher.isDispatchNeeded(context) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val task = TimedTask(block, currentTime + timeMillis) heap += task return task diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index a0dfcba2b7..e1b3dcd7a9 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -35,7 +35,7 @@ internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { messageQueue.enqueue(block) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) return ClearTimeout(handle) } @@ -81,7 +81,7 @@ internal class WindowDispatcher(private val window: Window) : CoroutineDispatche window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis)) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-core/jvm/src/CommonPool.kt b/kotlinx-coroutines-core/jvm/src/CommonPool.kt index 60f30cfe14..2203313120 100644 --- a/kotlinx-coroutines-core/jvm/src/CommonPool.kt +++ b/kotlinx-coroutines-core/jvm/src/CommonPool.kt @@ -103,6 +103,8 @@ internal object CommonPool : ExecutorCoroutineDispatcher() { (pool ?: getOrCreatePoolSync()).execute(wrapTask(block)) } catch (e: RejectedExecutionException) { unTrackTask() + // CommonPool only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.enqueue(block) } } diff --git a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt index ed84f55e74..787cbf9c44 100644 --- a/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt +++ b/kotlinx-coroutines-core/jvm/src/DefaultExecutor.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import java.util.concurrent.* +import kotlin.coroutines.* internal actual val DefaultDelay: Delay = DefaultExecutor @@ -54,7 +55,7 @@ internal actual object DefaultExecutor : EventLoopImplBase(), Runnable { * Livelock is possible only if `runBlocking` is called on internal default executed (which is used by default [delay]), * but it's not exposed as public API. */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) override fun run() { diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index 8c36bfa14f..8ffc22d8bb 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -38,6 +38,12 @@ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closea /** * Converts an instance of [ExecutorService] to an implementation of [ExecutorCoroutineDispatcher]. + * + * If the underlying executor throws [RejectedExecutionException] on + * attempt to submit a continuation task (it happens when [closing][ExecutorCoroutineDispatcher.close] the + * resulting dispatcher, on underlying executor [shutdown][ExecutorService.shutdown], or when it uses limited queues), + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. */ @JvmName("from") // this is for a nice Java API, see issue #255 public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher = @@ -45,6 +51,12 @@ public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher /** * Converts an instance of [Executor] to an implementation of [CoroutineDispatcher]. + * + * If the underlying executor throws [RejectedExecutionException] on + * attempt to submit a continuation task (it happens when [closing][ExecutorCoroutineDispatcher.close] the + * resulting dispatcher, on underlying executor [shutdown][ExecutorService.shutdown], or when it uses limited queues), + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. */ @JvmName("from") // this is for a nice Java API, see issue #255 public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher = @@ -82,7 +94,8 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa executor.execute(wrapTask(block)) } catch (e: RejectedExecutionException) { unTrackTask() - DefaultExecutor.enqueue(block) + cancelJobOnRejection(context, e) + Dispatchers.IO.dispatch(context, block) } } @@ -93,7 +106,7 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { val future = if (removesFutureOnCancellation) { - scheduleBlock(ResumeUndispatchedRunnable(this, continuation), timeMillis, TimeUnit.MILLISECONDS) + scheduleBlock(ResumeUndispatchedRunnable(this, continuation), continuation.context, timeMillis) } else { null } @@ -106,24 +119,31 @@ internal abstract class ExecutorCoroutineDispatcherBase : ExecutorCoroutineDispa DefaultExecutor.scheduleResumeAfterDelay(timeMillis, continuation) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val future = if (removesFutureOnCancellation) { - scheduleBlock(block, timeMillis, TimeUnit.MILLISECONDS) + scheduleBlock(block, context, timeMillis) } else { null } - - return if (future != null ) DisposableFutureHandle(future) else DefaultExecutor.invokeOnTimeout(timeMillis, block) + return when { + future != null -> DisposableFutureHandle(future) + else -> DefaultExecutor.invokeOnTimeout(timeMillis, block, context) + } } - private fun scheduleBlock(block: Runnable, time: Long, unit: TimeUnit): ScheduledFuture<*>? { + private fun scheduleBlock(block: Runnable, context: CoroutineContext, timeMillis: Long): ScheduledFuture<*>? { return try { - (executor as? ScheduledExecutorService)?.schedule(block, time, unit) + (executor as? ScheduledExecutorService)?.schedule(block, timeMillis, TimeUnit.MILLISECONDS) } catch (e: RejectedExecutionException) { + cancelJobOnRejection(context, e) null } } + private fun cancelJobOnRejection(context: CoroutineContext, exception: RejectedExecutionException) { + context.cancel(CancellationException("The task was rejected", exception)) + } + override fun close() { (executor as? ExecutorService)?.shutdown() } diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt index a0e1ffa2c7..aa18cd38d6 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt @@ -14,6 +14,11 @@ import kotlin.coroutines.* * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its thread). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * + * If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and + * attempt to submit a continuation task is made, + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. + * * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due @@ -35,6 +40,11 @@ public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its threads). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * + * If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and + * attempt to submit a continuation task is made, + * then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the + * [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete. + * * **NOTE: This API will be replaced in the future**. A different API to create thread-limited thread pools * that is based on a shared thread-pool and does not require the resulting dispatcher to be explicitly closed * will be provided, thus avoiding potential thread leaks and also significantly improving performance, due diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index ddfcdbb142..5b2b9ff68c 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -87,17 +87,14 @@ private class MissingMainCoroutineDispatcher( override val immediate: MainCoroutineDispatcher get() = this - override fun isDispatchNeeded(context: CoroutineContext): Boolean { + override fun isDispatchNeeded(context: CoroutineContext): Boolean = missing() - } - override suspend fun delay(time: Long) { + override suspend fun delay(time: Long) = missing() - } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = missing() - } override fun dispatch(context: CoroutineContext, block: Runnable) = missing() diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt index e0890eff66..202c6e1d06 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Dispatcher.kt @@ -65,6 +65,8 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block) } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.dispatch(context, block) } @@ -72,6 +74,8 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block, tailDispatch = true) } catch (e: RejectedExecutionException) { + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. DefaultExecutor.dispatchYield(context, block) } @@ -110,7 +114,9 @@ public open class ExperimentalCoroutineDispatcher( try { coroutineScheduler.dispatch(block, context, tailDispatch) } catch (e: RejectedExecutionException) { - // Context shouldn't be lost here to properly invoke before/after task + // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved + // for testing purposes, so we don't have to worry about cancelling the affected Job here. + // TaskContext shouldn't be lost here to properly invoke before/after task DefaultExecutor.enqueue(coroutineScheduler.createTask(block, context)) } } diff --git a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt index e7c8b6b671..649c95375d 100644 --- a/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/test_/TestCoroutineContext.kt @@ -230,7 +230,7 @@ public class TestCoroutineContext(private val name: String? = null) : CoroutineC }, timeMillis) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val node = postDelayed(block, timeMillis) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt index 033b9b7bc9..ebf08a03d0 100644 --- a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.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 @@ -29,6 +29,8 @@ class ExecutorsTest : TestBase() { val context = newFixedThreadPoolContext(2, "TestPool") runBlocking(context) { checkThreadName("TestPool") + delay(10) + checkThreadName("TestPool") // should dispatch on the right thread } context.close() } @@ -38,6 +40,8 @@ class ExecutorsTest : TestBase() { val executor = Executors.newSingleThreadExecutor { r -> Thread(r, "TestExecutor") } runBlocking(executor.asCoroutineDispatcher()) { checkThreadName("TestExecutor") + delay(10) + checkThreadName("TestExecutor") // should dispatch on the right thread } executor.shutdown() } diff --git a/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt b/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt index 9bf8ffad85..c9f722a5b8 100644 --- a/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/FailingCoroutinesMachineryTest.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 @@ -15,8 +15,21 @@ import kotlin.test.* @RunWith(Parameterized::class) class FailingCoroutinesMachineryTest( private val element: CoroutineContext.Element, - private val dispatcher: CoroutineDispatcher + private val dispatcher: TestDispatcher ) : TestBase() { + class TestDispatcher(val name: String, val block: () -> CoroutineDispatcher) { + private var _value: CoroutineDispatcher? = null + + val value: CoroutineDispatcher + get() = _value ?: block().also { _value = it } + + override fun toString(): String = name + + fun reset() { + runCatching { (_value as? ExecutorCoroutineDispatcher)?.close() } + _value = null + } + } private var caught: Throwable? = null private val latch = CountDownLatch(1) @@ -75,7 +88,7 @@ class FailingCoroutinesMachineryTest( @After fun tearDown() { - runCatching { (dispatcher as? ExecutorCoroutineDispatcher)?.close() } + dispatcher.reset() if (lazyOuterDispatcher.isInitialized()) lazyOuterDispatcher.value.close() } @@ -84,14 +97,14 @@ class FailingCoroutinesMachineryTest( @Parameterized.Parameters(name = "Element: {0}, dispatcher: {1}") fun dispatchers(): List> { val elements = listOf(FailingRestore, FailingUpdate) - val dispatchers = listOf( - Dispatchers.Unconfined, - Dispatchers.Default, - Executors.newFixedThreadPool(1).asCoroutineDispatcher(), - Executors.newScheduledThreadPool(1).asCoroutineDispatcher(), - ThrowingDispatcher, ThrowingDispatcher2 + val dispatchers = listOf( + TestDispatcher("Dispatchers.Unconfined") { Dispatchers.Unconfined }, + TestDispatcher("Dispatchers.Default") { Dispatchers.Default }, + TestDispatcher("Executors.newFixedThreadPool(1)") { Executors.newFixedThreadPool(1).asCoroutineDispatcher() }, + TestDispatcher("Executors.newScheduledThreadPool(1)") { Executors.newScheduledThreadPool(1).asCoroutineDispatcher() }, + TestDispatcher("ThrowingDispatcher") { ThrowingDispatcher }, + TestDispatcher("ThrowingDispatcher2") { ThrowingDispatcher2 } ) - return elements.flatMap { element -> dispatchers.map { dispatcher -> arrayOf(element, dispatcher) @@ -102,13 +115,13 @@ class FailingCoroutinesMachineryTest( @Test fun testElement() = runTest { - launch(NonCancellable + dispatcher + exceptionHandler + element) {} + launch(NonCancellable + dispatcher.value + exceptionHandler + element) {} checkException() } @Test fun testNestedElement() = runTest { - launch(NonCancellable + dispatcher + exceptionHandler) { + launch(NonCancellable + dispatcher.value + exceptionHandler) { launch(element) { } } checkException() @@ -117,7 +130,7 @@ class FailingCoroutinesMachineryTest( @Test fun testNestedDispatcherAndElement() = runTest { launch(lazyOuterDispatcher.value + NonCancellable + exceptionHandler) { - launch(element + dispatcher) { } + launch(element + dispatcher.value) { } } checkException() } diff --git a/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt new file mode 100644 index 0000000000..a6f4dd6b18 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.scheduling.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class RejectedExecutionTest : TestBase() { + private val threadName = "RejectedExecutionTest" + private val executor = RejectingExecutor() + + @After + fun tearDown() { + executor.shutdown() + executor.awaitTermination(10, TimeUnit.SECONDS) + } + + @Test + fun testRejectOnLaunch() = runTest { + expect(1) + val job = launch(executor.asCoroutineDispatcher()) { + expectUnreached() + } + assertEquals(1, executor.submittedTasks) + assertTrue(job.isCancelled) + finish(2) + } + + @Test + fun testRejectOnLaunchAtomic() = runTest { + expect(1) + val job = launch(executor.asCoroutineDispatcher(), start = CoroutineStart.ATOMIC) { + expect(2) + assertEquals(true, coroutineContext[Job]?.isCancelled) + assertIoThread() // was rejected on start, but start was atomic + } + assertEquals(1, executor.submittedTasks) + job.join() + finish(3) + } + + @Test + fun testRejectOnWithContext() = runTest { + expect(1) + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expectUnreached() + } + } + assertEquals(1, executor.submittedTasks) + finish(2) + } + + @Test + fun testRejectOnResumeInContext() = runTest { + expect(1) + executor.acceptTasks = 1 // accept one task + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expect(2) + assertExecutorThread() + try { + withContext(Dispatchers.Default) { + expect(3) + assertDefaultDispatcherThread() + // We have to wait until caller executor thread had already suspended (if not running task), + // so that we resume back to it a new task is posted + executor.awaitNotRunningTask() + expect(4) + assertDefaultDispatcherThread() + } + // cancelled on resume back + } finally { + expect(5) + assertIoThread() + } + expectUnreached() + } + } + assertEquals(2, executor.submittedTasks) + finish(6) + } + + @Test + fun testRejectOnDelay() = runTest { + expect(1) + executor.acceptTasks = 1 // accept one task + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expect(2) + assertExecutorThread() + try { + delay(10) // cancelled + } finally { + // Since it was cancelled on attempt to delay, it still stays on the same thread + assertExecutorThread() + } + expectUnreached() + } + } + assertEquals(2, executor.submittedTasks) + finish(3) + } + + @Test + fun testRejectWithTimeout() = runTest { + expect(1) + executor.acceptTasks = 1 // accept one task + assertFailsWith { + withContext(executor.asCoroutineDispatcher()) { + expect(2) + assertExecutorThread() + withTimeout(1000) { + expect(3) // atomic entry into the block (legacy behavior, it seem to be Ok with way) + assertEquals(true, coroutineContext[Job]?.isCancelled) // but the job is already cancelled + } + expectUnreached() + } + } + assertEquals(2, executor.submittedTasks) + finish(4) + } + + private inner class RejectingExecutor : ScheduledThreadPoolExecutor(1, { r -> Thread(r, threadName) }) { + var acceptTasks = 0 + var submittedTasks = 0 + val runningTask = MutableStateFlow(false) + + override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> { + submittedTasks++ + if (submittedTasks > acceptTasks) throw RejectedExecutionException() + val wrapper = Runnable { + runningTask.value = true + try { + command.run() + } finally { + runningTask.value = false + } + } + return super.schedule(wrapper, delay, unit) + } + + suspend fun awaitNotRunningTask() = runningTask.first { !it } + } + + private fun assertExecutorThread() { + val thread = Thread.currentThread() + if (!thread.name.startsWith(threadName)) error("Not an executor thread: $thread") + } + + private fun assertDefaultDispatcherThread() { + val thread = Thread.currentThread() + if (thread !is CoroutineScheduler.Worker) error("Not a thread from Dispatchers.Default: $thread") + assertEquals(CoroutineScheduler.WorkerState.CPU_ACQUIRED, thread.state) + } + + private fun assertIoThread() { + val thread = Thread.currentThread() + if (thread !is CoroutineScheduler.Worker) error("Not a thread from Dispatchers.IO: $thread") + assertEquals(CoroutineScheduler.WorkerState.BLOCKING, thread.state) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt index bcc7f48963..4ec1289ee7 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt @@ -16,8 +16,8 @@ internal actual object DefaultExecutor : CoroutineDispatcher(), Delay { takeEventLoop().dispatch(context, block) override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = takeEventLoop().scheduleResumeAfterDelay(timeMillis, continuation) - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = - takeEventLoop().invokeOnTimeout(timeMillis, block) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = + takeEventLoop().invokeOnTimeout(timeMillis, block, context) actual fun enqueue(task: Runnable): Unit = loopWasShutDown() } diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt index d6c6525504..b397d6f182 100644 --- a/kotlinx-coroutines-core/native/src/EventLoop.kt +++ b/kotlinx-coroutines-core/native/src/EventLoop.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlin.coroutines.* import kotlin.system.* internal actual abstract class EventLoopImplPlatform: EventLoop() { @@ -13,7 +14,7 @@ internal actual abstract class EventLoopImplPlatform: EventLoop() { } internal class EventLoopImpl: EventLoopImplBase() { - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) } diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index e3b1f73e4f..c99ec5cbf1 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -25,7 +25,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun getCurrentTime ()J - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun pauseDispatcher ()V public fun pauseDispatcher (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun resumeDispatcher ()V diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt index 4706d627ef..cad2636f97 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt @@ -65,7 +65,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val node = postDelayed(block, timeMillis) return object : DisposableHandle { override fun dispose() { diff --git a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt index c18e4108bb..baa1aa5fd2 100644 --- a/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt +++ b/kotlinx-coroutines-test/src/internal/MainTestDispatcher.kt @@ -46,8 +46,8 @@ internal class TestMainDispatcher(private val mainFactory: MainDispatcherFactory delay.delay(time) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - return delay.invokeOnTimeout(timeMillis, block) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + return delay.invokeOnTimeout(timeMillis, block, context) } public fun setDispatcher(dispatcher: CoroutineDispatcher) { diff --git a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api index 3b5c6b9522..b46fe338e5 100644 --- a/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api +++ b/reactive/kotlinx-coroutines-reactor/api/kotlinx-coroutines-reactor.api @@ -49,7 +49,7 @@ public final class kotlinx/coroutines/reactor/SchedulerCoroutineDispatcher : kot public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lreactor/core/scheduler/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt index e176c07bb9..4fb5514322 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Scheduler.kt @@ -39,7 +39,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle = scheduler.schedule(block, timeMillis, TimeUnit.MILLISECONDS).asDisposableHandle() /** @suppress */ diff --git a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api index 2cf7bc815b..4370325f58 100644 --- a/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api +++ b/reactive/kotlinx-coroutines-rx2/api/kotlinx-coroutines-rx2.api @@ -84,7 +84,7 @@ public final class kotlinx/coroutines/rx2/SchedulerCoroutineDispatcher : kotlinx public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lio/reactivex/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt index 3ddb67649e..9952eb91a0 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt @@ -38,7 +38,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) return DisposableHandle { disposable.dispose() } } diff --git a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api index eb92fd3285..6d2dd63d2c 100644 --- a/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api +++ b/reactive/kotlinx-coroutines-rx3/api/kotlinx-coroutines-rx3.api @@ -71,7 +71,7 @@ public final class kotlinx/coroutines/rx3/SchedulerCoroutineDispatcher : kotlinx public fun equals (Ljava/lang/Object;)Z public final fun getScheduler ()Lio/reactivex/rxjava3/core/Scheduler; public fun hashCode ()I - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V public fun toString ()Ljava/lang/String; } diff --git a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt index 6e91aeea85..a426aea6ba 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxScheduler.kt @@ -38,7 +38,7 @@ public class SchedulerCoroutineDispatcher( } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val disposable = scheduler.scheduleDirect(block, timeMillis, TimeUnit.MILLISECONDS) return DisposableHandle { disposable.dispose() } } diff --git a/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api b/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api index b97d8462c3..090c14e09c 100644 --- a/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api +++ b/ui/kotlinx-coroutines-android/api/kotlinx-coroutines-android.api @@ -1,7 +1,7 @@ public abstract class kotlinx/coroutines/android/HandlerDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getImmediate ()Lkotlinx/coroutines/android/HandlerDispatcher; - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; } public final class kotlinx/coroutines/android/HandlerDispatcherKt { diff --git a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt index 1693409875..af79da7c97 100644 --- a/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt +++ b/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt @@ -140,7 +140,7 @@ internal class HandlerContext private constructor( continuation.invokeOnCancellation { handler.removeCallbacks(block) } } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY)) return object : DisposableHandle { override fun dispose() { diff --git a/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api b/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api index 620e904612..e2c3b8f326 100644 --- a/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api +++ b/ui/kotlinx-coroutines-javafx/api/kotlinx-coroutines-javafx.api @@ -5,7 +5,7 @@ public final class kotlinx/coroutines/javafx/JavaFxConvertKt { public abstract class kotlinx/coroutines/javafx/JavaFxDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index a13a68368e..c3069d636f 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -42,7 +42,7 @@ public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay { } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS, EventHandler { block.run() }) diff --git a/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api b/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api index 09556e807f..d33191fd96 100644 --- a/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api +++ b/ui/kotlinx-coroutines-swing/api/kotlinx-coroutines-swing.api @@ -1,7 +1,7 @@ public abstract class kotlinx/coroutines/swing/SwingDispatcher : kotlinx/coroutines/MainCoroutineDispatcher, kotlinx/coroutines/Delay { public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V - public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle; + public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; public fun scheduleResumeAfterDelay (JLkotlinx/coroutines/CancellableContinuation;)V } diff --git a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt index 77f109df91..054ed1f60e 100644 --- a/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt +++ b/ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt @@ -36,7 +36,7 @@ public sealed class SwingDispatcher : MainCoroutineDispatcher(), Delay { } /** @suppress */ - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { val timer = schedule(timeMillis, TimeUnit.MILLISECONDS, ActionListener { block.run() }) From 738f5a2d15000254e0363e88df966dcd4f21a7a9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 19:03:46 +0300 Subject: [PATCH 128/257] Refactor mpp/native build, introduce "concurrent" source set, test launcher (#2074) New source sets: * "concurrent" source set is shared between "jvm" and "native" * "native" source set is subdivided into "nativeDarwin" (Apple) and "nativeOther" (Linux, etc) Native tests are launched in two variants: * A default "test" task runs tests with memory leak checker from "mainNoExit" entry point. * A special "backgroundTest" task runs tests in a background worker from "mainBackground" entry point. Other build improvement: * Modernize old-style IDEA-active hacks to kts helper. * Extract versions of JS test runner dependencies. * Remove redundant google repo reference from android tests. --- build.gradle | 4 +- gradle.properties | 11 +- gradle/compile-native-multiplatform.gradle | 40 +++--- gradle/targets.gradle | 28 ----- gradle/test-mocha-js.gradle | 4 +- kotlinx-coroutines-core/build.gradle | 118 +++++++++++++++--- .../native/src/WorkerMain.native.kt | 8 ++ .../native/test/WorkerTest.kt | 4 +- .../nativeDarwin/src/WorkerMain.kt | 13 ++ .../nativeDarwin/test/Launcher.kt | 28 +++++ .../nativeOther/src/WorkerMain.kt | 7 ++ .../nativeOther/test/Launcher.kt | 23 ++++ 12 files changed, 206 insertions(+), 82 deletions(-) delete mode 100644 gradle/targets.gradle create mode 100644 kotlinx-coroutines-core/native/src/WorkerMain.native.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt create mode 100644 kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt create mode 100644 kotlinx-coroutines-core/nativeOther/test/Launcher.kt diff --git a/build.gradle b/build.gradle index 153714e90c..79c7f3553e 100644 --- a/build.gradle +++ b/build.gradle @@ -89,8 +89,8 @@ buildscript { import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -// Hierarchical project structures are not fully supported in 1.3.7x MPP, enable conditionally for 1.4.x -if (VersionNumber.parse(kotlin_version) > VersionNumber.parse("1.3.79")) { +// todo:KLUDGE: Hierarchical project structures are not fully supported in IDEA, enable only for a regular built +if (!Idea.active) { ext.set("kotlin.mpp.enableGranularSourceSetsMetadata", "true") } diff --git a/gradle.properties b/gradle.properties index 196a4a9ac0..821f546e8a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,10 +36,12 @@ kotlin.js.compiler=both gradle_node_version=1.2.0 node_version=8.9.3 npm_version=5.7.1 -mocha_version=4.1.0 +mocha_version=6.2.2 mocha_headless_chrome_version=1.8.2 -mocha_teamcity_reporter_version=2.2.2 -source_map_support_version=0.5.3 +mocha_teamcity_reporter_version=3.0.0 +source_map_support_version=0.5.16 +jsdom_version=15.2.1 +jsdom_global_version=3.0.2 # Settings kotlin.incremental.multiplatform=true @@ -56,7 +58,6 @@ org.gradle.jvmargs=-Xmx2g # https://github.com/gradle/gradle/issues/11412 systemProp.org.gradle.internal.publish.checksums.insecure=true -# This is commented out, and the property is set conditionally in build.gradle, because 1.3.71 doesn't work with it. -# Once this property is set by default in new versions or 1.3.71 is dropped, either uncomment or remove this line. +# todo:KLUDGE: This is commented out, and the property is set conditionally in build.gradle, because IDEA doesn't work with it. #kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.mpp.enableCompatibilityMetadataVariant=true diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle index 378e4f5f98..4487446799 100644 --- a/gradle/compile-native-multiplatform.gradle +++ b/gradle/compile-native-multiplatform.gradle @@ -13,36 +13,24 @@ kotlin { } targets { - if (project.ext.ideaActive) { - fromPreset(project.ext.ideaPreset, 'native') - } else { - addTarget(presets.linuxX64) - addTarget(presets.iosArm64) - addTarget(presets.iosArm32) - addTarget(presets.iosX64) - addTarget(presets.macosX64) - addTarget(presets.mingwX64) - addTarget(presets.tvosArm64) - addTarget(presets.tvosX64) - addTarget(presets.watchosArm32) - addTarget(presets.watchosArm64) - addTarget(presets.watchosX86) - } + addTarget(presets.linuxX64) + addTarget(presets.iosArm64) + addTarget(presets.iosArm32) + addTarget(presets.iosX64) + addTarget(presets.macosX64) + addTarget(presets.mingwX64) + addTarget(presets.tvosArm64) + addTarget(presets.tvosX64) + addTarget(presets.watchosArm32) + addTarget(presets.watchosArm64) + addTarget(presets.watchosX86) } sourceSets { nativeMain { dependsOn commonMain } - // Empty source set is required in order to have native tests task - nativeTest {} + nativeTest { dependsOn commonTest } - if (!project.ext.ideaActive) { - configure(nativeMainSets) { - dependsOn nativeMain - } - - configure(nativeTestSets) { - dependsOn nativeTest - } - } + configure(nativeMainSets) { dependsOn nativeMain } + configure(nativeTestSets) { dependsOn nativeTest } } } diff --git a/gradle/targets.gradle b/gradle/targets.gradle deleted file mode 100644 index 08f3d989aa..0000000000 --- a/gradle/targets.gradle +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -/* - * This is a hack to avoid creating unsupported native source sets when importing project into IDEA - */ -project.ext.ideaActive = System.getProperty('idea.active') == 'true' - -kotlin { - targets { - def manager = project.ext.hostManager - def linuxEnabled = manager.isEnabled(presets.linuxX64.konanTarget) - def macosEnabled = manager.isEnabled(presets.macosX64.konanTarget) - def winEnabled = manager.isEnabled(presets.mingwX64.konanTarget) - - project.ext.isLinuxHost = linuxEnabled - project.ext.isMacosHost = macosEnabled - project.ext.isWinHost = winEnabled - - if (project.ext.ideaActive) { - def ideaPreset = presets.linuxX64 - if (macosEnabled) ideaPreset = presets.macosX64 - if (winEnabled) ideaPreset = presets.mingwX64 - project.ext.ideaPreset = ideaPreset - } - } -} diff --git a/gradle/test-mocha-js.gradle b/gradle/test-mocha-js.gradle index 6676dc9268..7de79b9939 100644 --- a/gradle/test-mocha-js.gradle +++ b/gradle/test-mocha-js.gradle @@ -86,8 +86,8 @@ task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) { task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) { args = ['install', "mocha@$mocha_version", - 'jsdom@15.2.1', - 'jsdom-global@3.0.2', + "jsdom@$jsdom_version", + "jsdom-global@$jsdom_global_version", "source-map-support@$source_map_support_version", '--no-save'] if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"] diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 55c44088f5..f98f6a529c 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -3,13 +3,60 @@ */ apply plugin: 'org.jetbrains.kotlin.multiplatform' -apply from: rootProject.file("gradle/targets.gradle") apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle") apply from: rootProject.file("gradle/compile-common.gradle") apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") apply from: rootProject.file('gradle/publish-npm-js.gradle') +/* ========================================================================== + Configure source sets structure for kotlinx-coroutines-core: + + TARGETS SOURCE SETS + ------- ---------------------------------------------- + + js -----------------------------------------------------+ + | + V + jvm -------------------------------> concurrent ---> common + ^ + ios \ | + macos | ---> nativeDarwin ---> native --+ + tvos | ^ + watchos / | + | + linux \ ---> nativeOther -------+ + mingw / + + ========================================================================== */ + +project.ext.sourceSetSuffixes = ["Main", "Test"] + +void defineSourceSet(newName, dependsOn, includedInPred) { + for (suffix in project.ext.sourceSetSuffixes) { + def newSS = kotlin.sourceSets.maybeCreate(newName + suffix) + for (dep in dependsOn) { + newSS.dependsOn(kotlin.sourceSets[dep + suffix]) + } + for (curSS in kotlin.sourceSets) { + def curName = curSS.name + if (curName.endsWith(suffix)) { + def prefix = curName.substring(0, curName.length() - suffix.length()) + if (includedInPred(prefix)) curSS.dependsOn(newSS) + } + } + } +} + +static boolean isNativeDarwin(String name) { return ["ios", "macos", "tvos", "watchos"].any { name.startsWith(it) } } +static boolean isNativeOther(String name) { return ["linux", "mingw"].any { name.startsWith(it) } } + +defineSourceSet("concurrent", ["common"]) { it in ["jvm", "native"] } +defineSourceSet("nativeDarwin", ["native"]) { isNativeDarwin(it) } +defineSourceSet("nativeOther", ["native"]) { isNativeOther(it) } + +/* ========================================================================== */ + /* * All platform plugins and configuration magic happens here instead of build.gradle * because JMV-only projects depend on core, thus core should always be initialized before configuration. @@ -18,7 +65,7 @@ kotlin { configure(sourceSets) { def srcDir = name.endsWith('Main') ? 'src' : 'test' def platform = name[0..-5] - kotlin.srcDir "$platform/$srcDir" + kotlin.srcDirs = ["$platform/$srcDir"] if (name == "jvmMain") { resources.srcDirs = ["$platform/resources"] } else if (name == "jvmTest") { @@ -31,12 +78,18 @@ kotlin { } configure(targets) { - def targetName = it.name - compilations.all { compilation -> - def compileTask = tasks.getByName(compilation.compileKotlinTaskName) - // binary compatibility support - if (targetName.contains("jvm") && compilation.compilationName == "main") { - compileTask.kotlinOptions.freeCompilerArgs += ["-Xdump-declarations-to=${buildDir}/visibilities.json"] + // Configure additional binaries and test runs -- one for each OS + if (["macos", "linux", "mingw"].any { name.startsWith(it) }) { + binaries { + // Test for memory leaks using a special entry point that does not exit but returns from main + binaries.getTest("DEBUG").freeCompilerArgs += ["-e", "kotlinx.coroutines.mainNoExit"] + // Configure a separate test where code runs in background + test("background", [org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG]) { + freeCompilerArgs += ["-e", "kotlinx.coroutines.mainBackground"] + } + } + testRuns { + background { setExecutionSourceFrom(binaries.backgroundDebugTest) } } } } @@ -54,23 +107,52 @@ compileKotlinMetadata { } } +// :KLUDGE: Idea.active: This is needed to workaround resolve problems after importing this project to IDEA +def configureNativeSourceSetPreset(name, preset) { + def hostMainCompilation = project.kotlin.targetFromPreset(preset).compilations.main + // Look for platform libraries in "implementation" for default source set + def implementationConfiguration = configurations[hostMainCompilation.defaultSourceSet.implementationMetadataConfigurationName] + // Now find the libraries: Finds platform libs & stdlib, but platform declarations are still not resolved due to IDE bugs + def hostNativePlatformLibs = files( + provider { + implementationConfiguration.findAll { + it.path.endsWith(".klib") || it.absolutePath.contains("klib${File.separator}platform") || it.absolutePath.contains("stdlib") + } + } + ) + // Add all those dependencies + for (suffix in sourceSetSuffixes) { + configure(kotlin.sourceSets[name + suffix]) { + dependencies.add(implementationMetadataConfigurationName, hostNativePlatformLibs) + } + } +} + +// :KLUDGE: Idea.active: Configure platform libraries for native source sets when working in IDEA +if (Idea.active) { + def manager = project.ext.hostManager + def linuxPreset = kotlin.presets.linuxX64 + def macosPreset = kotlin.presets.macosX64 + // linux should be always available (cross-compilation capable) -- use it as default + assert manager.isEnabled(linuxPreset.konanTarget) + // use macOS libs for nativeDarwin if available + def macosAvailable = manager.isEnabled(macosPreset.konanTarget) + // configure source sets + configureNativeSourceSetPreset("native", linuxPreset) + configureNativeSourceSetPreset("nativeOther", linuxPreset) + configureNativeSourceSetPreset("nativeDarwin", macosAvailable ? macosPreset : linuxPreset) +} + kotlin.sourceSets { jvmMain.dependencies { compileOnly "com.google.android:annotations:4.1.1.4" } jvmTest.dependencies { - // This is a workaround for https://youtrack.jetbrains.com/issue/KT-39037 - def excludingCurrentProject = { dependency -> - def result = project.dependencies.create(dependency) - result.exclude(module: project.name) - return result - } - - api(excludingCurrentProject("org.jetbrains.kotlinx:lincheck:$lincheck_version")) + api "org.jetbrains.kotlinx:lincheck:$lincheck_version" api "org.jetbrains.kotlinx:kotlinx-knit-test:$knit_version" api "com.esotericsoftware:kryo:4.0.0" - implementation(excludingCurrentProject(project(":android-unit-tests"))) + implementation project(":android-unit-tests") } } @@ -97,7 +179,7 @@ jvmTest { enableAssertions = true systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' // 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true" - if (!project.ext.ideaActive && rootProject.properties['stress'] == null) { + if (!Idea.active && rootProject.properties['stress'] == null) { exclude '**/*StressTest.*' } systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test diff --git a/kotlinx-coroutines-core/native/src/WorkerMain.native.kt b/kotlinx-coroutines-core/native/src/WorkerMain.native.kt new file mode 100644 index 0000000000..84cc9f42b9 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/WorkerMain.native.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +// It is used in the main sources of native-mt branch +internal expect inline fun workerMain(block: () -> Unit) diff --git a/kotlinx-coroutines-core/native/test/WorkerTest.kt b/kotlinx-coroutines-core/native/test/WorkerTest.kt index 84acedac94..d6b5fad182 100644 --- a/kotlinx-coroutines-core/native/test/WorkerTest.kt +++ b/kotlinx-coroutines-core/native/test/WorkerTest.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 @@ -19,6 +19,7 @@ class WorkerTest : TestBase() { delay(1) } }.result + worker.requestTermination() } @Test @@ -31,5 +32,6 @@ class WorkerTest : TestBase() { }.join() } }.result + worker.requestTermination() } } diff --git a/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt new file mode 100644 index 0000000000..3445cb9897 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/src/WorkerMain.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.cinterop.* + +internal actual inline fun workerMain(block: () -> Unit) { + autoreleasepool { + block() + } +} diff --git a/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt new file mode 100644 index 0000000000..78ed765967 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import platform.CoreFoundation.* +import kotlin.native.concurrent.* +import kotlin.native.internal.test.* +import kotlin.system.* + +// This is a separate entry point for tests in background +fun mainBackground(args: Array) { + val worker = Worker.start(name = "main-background") + worker.execute(TransferMode.SAFE, { args.freeze() }) { + val result = testLauncherEntryPoint(it) + exitProcess(result) + } + CFRunLoopRun() + error("CFRunLoopRun should never return") +} + +// This is a separate entry point for tests with leak checker +fun mainNoExit(args: Array) { + workerMain { // autoreleasepool to make sure interop objects are properly freed + testLauncherEntryPoint(args) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt b/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt new file mode 100644 index 0000000000..cac0530e4e --- /dev/null +++ b/kotlinx-coroutines-core/nativeOther/src/WorkerMain.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +internal actual inline fun workerMain(block: () -> Unit) = block() diff --git a/kotlinx-coroutines-core/nativeOther/test/Launcher.kt b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt new file mode 100644 index 0000000000..feddd4c097 --- /dev/null +++ b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.native.concurrent.* +import kotlin.native.internal.test.* +import kotlin.system.* + +// This is a separate entry point for tests in background +fun mainBackground(args: Array) { + val worker = Worker.start(name = "main-background") + worker.execute(TransferMode.SAFE, { args.freeze() }) { + val result = testLauncherEntryPoint(it) + exitProcess(result) + }.result // block main thread +} + +// This is a separate entry point for tests with leak checker +fun mainNoExit(args: Array) { + testLauncherEntryPoint(args) +} \ No newline at end of file From 8773a2691dd638cbfb097283ce682d4ec40ecfc9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 19:09:48 +0300 Subject: [PATCH 129/257] Breaking: Get rid of atomic cancellation and provide a replacement (#1937) This is a problematic for Android when Main dispatcher is cancelled on destroyed activity. Atomic nature of channels is designed to prevent loss of elements, which is really not an issue for a typical application, but creates problem when used with channels. * Internal suspendAtomicCancellableCoroutine -> suspendCancellableCoroutine * Internal suspendAtomicCancellableCoroutineReusable -> suspendCancellableCoroutineReusable * Remove atomic cancellation from docs * Ensures that flowOn does not resume downstream after cancellation. * MODE_ATOMIC_DEFAULT renamed into MODE_ATOMIC * Introduced MODE_CANCELLABLE_REUSABLE to track suspendCancellableCoroutineReusable * Better documentation for MODE_XXX constants. * Added stress test for proper handling of MODE_CANCELLABLE_REUSABLE and fixed test for #1123 bug with job.join (working in MODE_CANCELLABLE) that was not properly failing in the absence of the proper code in CancellableContinuationImpl.getResult * Added test for Flow.combine that should be fixed * Support extended invokeOnCancellation contract * Introduced internal tryResumeAtomic * Channel onUnderliveredElement is introduced as a replacement. Fixes #1265 Fixes #1813 Fixes #1915 Fixes #1936 Co-authored-by: Louis CAD Co-authored-by: Vsevolod Tolstopyatov --- .../benchmarks/tailcall/SimpleChannel.kt | 8 +- .../api/kotlinx-coroutines-core.api | 15 +- kotlinx-coroutines-core/common/src/Await.kt | 15 +- .../common/src/Builders.common.kt | 5 +- .../common/src/CancellableContinuation.kt | 189 ++++++++--- .../common/src/CancellableContinuationImpl.kt | 299 ++++++++++++------ ...tedExceptionally.kt => CompletionState.kt} | 18 +- .../common/src/CoroutineDispatcher.kt | 1 + .../common/src/Deferred.kt | 2 + kotlinx-coroutines-core/common/src/Delay.kt | 9 + kotlinx-coroutines-core/common/src/Yield.kt | 3 + .../common/src/channels/AbstractChannel.kt | 153 ++++++--- .../src/channels/ArrayBroadcastChannel.kt | 6 +- .../common/src/channels/ArrayChannel.kt | 29 +- .../common/src/channels/Channel.kt | 143 ++++++--- .../common/src/channels/Channels.common.kt | 9 +- .../src/channels/ConflatedBroadcastChannel.kt | 2 +- .../common/src/channels/ConflatedChannel.kt | 27 +- .../common/src/channels/LinkedListChannel.kt | 2 +- .../common/src/channels/Produce.kt | 6 +- .../common/src/channels/RendezvousChannel.kt | 4 +- .../common/src/flow/Builders.kt | 9 +- .../common/src/flow/Channels.kt | 3 + .../common/src/flow/operators/Context.kt | 6 +- .../common/src/internal/Atomic.kt | 7 +- .../src/internal/DispatchedContinuation.kt | 57 ++-- .../common/src/internal/DispatchedTask.kt | 99 ++++-- .../src/internal/LockFreeLinkedList.common.kt | 1 + .../src/internal/OnUndeliveredElement.kt | 43 +++ .../common/src/intrinsics/Cancellable.kt | 8 +- .../common/src/selects/Select.kt | 10 +- .../common/src/sync/Mutex.kt | 37 +-- .../common/src/sync/Semaphore.kt | 25 +- .../test/AtomicCancellationCommonTest.kt | 13 +- .../CancellableContinuationHandlersTest.kt | 72 ++++- .../test/CancellableContinuationTest.kt | 22 ++ .../common/test/CancellableResumeTest.kt | 156 ++++++++- .../common/test/DispatchedContinuationTest.kt | 78 +++++ .../test/channels/BasicOperationsTest.kt | 20 +- .../common/test/channels/BroadcastTest.kt | 4 +- .../ChannelUndeliveredElementFailureTest.kt | 143 +++++++++ .../channels/ChannelUndeliveredElementTest.kt | 104 ++++++ .../test/channels/ConflatedChannelTest.kt | 2 +- .../common/test/channels/TestChannelKind.kt | 14 +- .../common/test/flow/operators/CatchTest.kt | 7 +- .../common/test/flow/operators/CombineTest.kt | 18 +- .../common/test/flow/operators/FlowOnTest.kt | 15 + .../common/test/flow/operators/ZipTest.kt | 25 +- .../common/test/selects/SelectLoopTest.kt | 23 +- kotlinx-coroutines-core/js/src/Promise.kt | 2 + .../js/src/internal/LinkedList.kt | 2 + .../jvm/src/DebugStrings.kt | 1 + .../jvm/src/internal/LockFreeLinkedList.kt | 19 +- .../jvm/test/AtomicCancellationTest.kt | 27 +- .../jvm/test/JobStructuredJoinStressTest.kt | 41 ++- .../ReusableCancellableContinuationTest.kt | 52 +-- kotlinx-coroutines-core/jvm/test/TestBase.kt | 2 + .../BroadcastChannelMultiReceiveStressTest.kt | 13 +- .../channels/ChannelAtomicCancelStressTest.kt | 156 --------- ...annelCancelUndeliveredElementStressTest.kt | 102 ++++++ .../channels/ChannelSendReceiveStressTest.kt | 2 +- .../ChannelUndeliveredElementStressTest.kt | 255 +++++++++++++++ .../test/channels/InvokeOnCloseStressTest.kt | 2 +- .../test/channels/SimpleSendReceiveJvmTest.kt | 2 +- .../jvm/test/sync/MutexStressTest.kt | 64 ++++ .../jvm/test/sync/SemaphoreStressTest.kt | 8 +- .../native/src/internal/LinkedList.kt | 2 + .../test/CoroutinesDumpTest.kt | 26 +- .../test/DebugProbesTest.kt | 27 +- .../src/Channel.kt | 2 +- .../kotlinx-coroutines-rx2/src/RxChannel.kt | 2 +- .../kotlinx-coroutines-rx3/src/RxChannel.kt | 2 +- 72 files changed, 2098 insertions(+), 679 deletions(-) rename kotlinx-coroutines-core/common/src/{CompletedExceptionally.kt => CompletionState.kt} (78%) create mode 100644 kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt create mode 100644 kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt create mode 100644 kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt index c217fcae91..d961dab8d9 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt @@ -70,12 +70,12 @@ class NonCancellableChannel : SimpleChannel() { } class CancellableChannel : SimpleChannel() { - override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutine { + override suspend fun suspendReceive(): Int = suspendCancellableCoroutine { consumer = it.intercepted() COROUTINE_SUSPENDED } - override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutine { + override suspend fun suspendSend(element: Int) = suspendCancellableCoroutine { enqueuedValue = element producer = it.intercepted() COROUTINE_SUSPENDED @@ -84,13 +84,13 @@ class CancellableChannel : SimpleChannel() { class CancellableReusableChannel : SimpleChannel() { @Suppress("INVISIBLE_MEMBER") - override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutineReusable { + override suspend fun suspendReceive(): Int = suspendCancellableCoroutineReusable { consumer = it.intercepted() COROUTINE_SUSPENDED } @Suppress("INVISIBLE_MEMBER") - override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutineReusable { + override suspend fun suspendSend(element: Int) = suspendCancellableCoroutineReusable { enqueuedValue = element producer = it.intercepted() COROUTINE_SUSPENDED diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 6611995361..4ccd340f52 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -46,6 +46,7 @@ public abstract interface class kotlinx/coroutines/CancellableContinuation : kot public abstract fun resumeUndispatched (Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Object;)V public abstract fun resumeUndispatchedWithException (Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Throwable;)V public abstract fun tryResume (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun tryResume (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun tryResumeWithException (Ljava/lang/Throwable;)Ljava/lang/Object; } @@ -56,6 +57,8 @@ public final class kotlinx/coroutines/CancellableContinuation$DefaultImpls { public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/jvm/internal/CoroutineStackFrame, kotlinx/coroutines/CancellableContinuation { public fun (Lkotlin/coroutines/Continuation;I)V + public final fun callCancelHandler (Lkotlinx/coroutines/CancelHandler;Ljava/lang/Throwable;)V + public final fun callOnCancellation (Lkotlin/jvm/functions/Function1;Ljava/lang/Throwable;)V public fun cancel (Ljava/lang/Throwable;)Z public fun completeResume (Ljava/lang/Object;)V public fun getCallerFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; @@ -75,14 +78,12 @@ public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/ public fun resumeWith (Ljava/lang/Object;)V public fun toString ()Ljava/lang/String; public fun tryResume (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun tryResume (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun tryResumeWithException (Ljava/lang/Throwable;)Ljava/lang/Object; } public final class kotlinx/coroutines/CancellableContinuationKt { public static final fun disposeOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Lkotlinx/coroutines/DisposableHandle;)V - public static final fun suspendAtomicCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static final fun suspendAtomicCancellableCoroutine (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun suspendAtomicCancellableCoroutine$default (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun suspendCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -272,10 +273,6 @@ public final class kotlinx/coroutines/DelayKt { public static final fun delay-p9JZ4hM (DLkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class kotlinx/coroutines/DispatchedContinuationKt { - public static final fun resumeCancellableWith (Lkotlin/coroutines/Continuation;Ljava/lang/Object;)V -} - public final class kotlinx/coroutines/Dispatchers { public static final field INSTANCE Lkotlinx/coroutines/Dispatchers; public static final fun getDefault ()Lkotlinx/coroutines/CoroutineDispatcher; @@ -613,8 +610,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 (ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/channels/Channel; public static synthetic fun Channel$default (IILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; + public static synthetic fun Channel$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; } public final class kotlinx/coroutines/channels/ChannelsKt { diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt index dd1e1771f2..7189349024 100644 --- a/kotlinx-coroutines-core/common/src/Await.kt +++ b/kotlinx-coroutines-core/common/src/Await.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import kotlinx.atomicfu.* +import kotlinx.coroutines.channels.* import kotlin.coroutines.* /** @@ -18,6 +19,8 @@ import kotlin.coroutines.* * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun awaitAll(vararg deferreds: Deferred): List = if (deferreds.isEmpty()) emptyList() else AwaitAll(deferreds).await() @@ -33,6 +36,8 @@ public suspend fun awaitAll(vararg deferreds: Deferred): List = * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Collection>.awaitAll(): List = if (isEmpty()) emptyList() else AwaitAll(toTypedArray()).await() @@ -41,8 +46,11 @@ public suspend fun Collection>.awaitAll(): List = * Suspends current coroutine until all given jobs are complete. * This method is semantically equivalent to joining all given jobs one by one with `jobs.forEach { it.join() }`. * - * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() } @@ -50,8 +58,11 @@ public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() } * Suspends current coroutine until all given jobs are complete. * This method is semantically equivalent to joining all given jobs one by one with `forEach { it.join() }`. * - * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, * this function immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Collection.joinAll(): Unit = forEach { it.join() } diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 64bff500dc..c0924a0238 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -129,8 +129,9 @@ private class LazyDeferredCoroutine( * This function uses dispatcher from the new context, shifting execution of the [block] into the * different thread if a new dispatcher is specified, and back to the original dispatcher * when it completes. Note that the result of `withContext` invocation is - * dispatched into the original context in a cancellable way, which means that if the original [coroutineContext], - * in which `withContext` was invoked, is cancelled by the time its dispatcher starts to execute the code, + * dispatched into the original context in a cancellable way with a **prompt cancellation guarantee**, + * which means that if the original [coroutineContext], in which `withContext` was invoked, + * is cancelled by the time its dispatcher starts to execute the code, * it discards the result of `withContext` and throws [CancellationException]. */ public suspend fun withContext( diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index f5b511cb9c..7d9315afbf 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -15,6 +15,8 @@ import kotlin.coroutines.intrinsics.* * When the [cancel] function is explicitly invoked, this continuation immediately resumes with a [CancellationException] or * the specified cancel cause. * + * An instance of `CancellableContinuation` is created by the [suspendCancellableCoroutine] function. + * * Cancellable continuation has three states (as subset of [Job] states): * * | **State** | [isActive] | [isCompleted] | [isCancelled] | @@ -24,11 +26,11 @@ import kotlin.coroutines.intrinsics.* * | _Canceled_ (final _completed_ state)| `false` | `true` | `true` | * * Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while - * invocation of [resume] or [resumeWithException] transitions it from _active_ to _resumed_ state. + * invocation of [Continuation.resume] or [Continuation.resumeWithException] transitions it from _active_ to _resumed_ state. * * A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted]. * - * Invocation of [resume] or [resumeWithException] in _resumed_ state produces an [IllegalStateException], + * Invocation of [Continuation.resume] or [Continuation.resumeWithException] in _resumed_ state produces an [IllegalStateException], * but is ignored in _cancelled_ state. * * ``` @@ -41,7 +43,6 @@ import kotlin.coroutines.intrinsics.* * +-----------+ * | Cancelled | * +-----------+ - * * ``` */ public interface CancellableContinuation : Continuation { @@ -76,6 +77,14 @@ public interface CancellableContinuation : Continuation { @InternalCoroutinesApi public fun tryResume(value: T, idempotent: Any? = null): Any? + /** + * Same as [tryResume] but with [onCancellation] handler that called if and only if the value is not + * delivered to the caller because of the dispatch in the process, so that atomicity delivery + * guaranteed can be provided by having a cancellation fallback. + */ + @InternalCoroutinesApi + public fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? + /** * Tries to resume this continuation with the specified [exception] and returns a non-null object token if successful, * or `null` otherwise (it was already resumed or cancelled). When a non-null object is returned, @@ -110,8 +119,8 @@ public interface CancellableContinuation : Continuation { public fun cancel(cause: Throwable? = null): Boolean /** - * Registers a [handler] to be **synchronously** invoked on cancellation (regular or exceptional) of this continuation. - * When the continuation is already cancelled, the handler will be immediately invoked + * Registers a [handler] to be **synchronously** invoked on [cancellation][cancel] (regular or exceptional) of this continuation. + * When the continuation is already cancelled, the handler is immediately invoked * with the cancellation exception. Otherwise, the handler will be invoked as soon as this * continuation is cancelled. * @@ -120,7 +129,15 @@ public interface CancellableContinuation : Continuation { * processed as an uncaught exception in the context of the current coroutine * (see [CoroutineExceptionHandler]). * - * At most one [handler] can be installed on a continuation. + * At most one [handler] can be installed on a continuation. Attempt to call `invokeOnCancellation` second + * time produces [IllegalStateException]. + * + * This handler is also called when this continuation [resumes][Continuation.resume] normally (with a value) and then + * is cancelled while waiting to be dispatched. More generally speaking, this handler is called whenever + * the caller of [suspendCancellableCoroutine] is getting a [CancellationException]. + * + * A typical example for `invokeOnCancellation` usage is given in + * the documentation for the [suspendCancellableCoroutine] function. * * **Note**: Implementation of `CompletionHandler` must be fast, non-blocking, and thread-safe. * This `handler` can be invoked concurrently with the surrounding code. @@ -163,7 +180,7 @@ public interface CancellableContinuation : Continuation { * (see [CoroutineExceptionHandler]). * * This function shall be used when resuming with a resource that must be closed by - * code that called the corresponding suspending function, e.g.: + * code that called the corresponding suspending function, for example: * * ``` * continuation.resume(resource) { @@ -171,17 +188,119 @@ public interface CancellableContinuation : Continuation { * } * ``` * + * A more complete example and further details are given in + * the documentation for the [suspendCancellableCoroutine] function. + * * **Note**: The [onCancellation] handler must be fast, non-blocking, and thread-safe. * It can be invoked concurrently with the surrounding code. * There is no guarantee on the execution context of its invocation. */ @ExperimentalCoroutinesApi // since 1.2.0 - public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) + public fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) } /** * Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to - * the [block]. This function throws a [CancellationException] if the coroutine is cancelled or completed while suspended. + * the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is + * cancelled or completed while it is suspended. + * + * A typical use of this function is to suspend a coroutine while waiting for a result + * from a single-shot callback API and to return the result to the caller. + * For multi-shot callback APIs see [callbackFlow][kotlinx.coroutines.flow.callbackFlow]. + * + * ``` + * suspend fun awaitCallback(): T = suspendCancellableCoroutine { continuation -> + * val callback = object : Callback { // Implementation of some callback interface + * override fun onCompleted(value: T) { + * // Resume coroutine with a value provided by the callback + * continuation.resume(value) + * } + * override fun onApiError(cause: Throwable) { + * // Resume coroutine with an exception provided by the callback + * continuation.resumeWithException(cause) + * } + * } + * // Register callback with an API + * api.register(callback) + * // Remove callback on cancellation + * continuation.invokeOnCancellation { api.unregister(callback) } + * // At this point the coroutine is suspended by suspendCancellableCoroutine until callback fires + * } + * ``` + * + * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because + * > `invokeOnCancellation` block can be called at any time due to asynchronous nature of cancellation, even + * > concurrently with the call of the callback. + * + * ### Prompt cancellation guarantee + * + * This function provides **prompt cancellation guarantee**. + * If the [Job] of the current coroutine was cancelled while this function was suspended it will not resume + * successfully. + * + * The cancellation of the coroutine's job is generally asynchronous with respect to the suspended coroutine. + * The suspended coroutine is resumed with the call it to its [Continuation.resumeWith] member function or to + * [resume][Continuation.resume] extension function. + * However, when coroutine is resumed, it does not immediately start executing, but is passed to its + * [CoroutineDispatcher] to schedule its execution when dispatcher's resources become available for execution. + * The job's cancellation can happen both before, after, and concurrently with the call to `resume`. In any + * case, prompt cancellation guarantees that the the coroutine will not resume its code successfully. + * + * If the coroutine was resumed with an exception (for example, using [Continuation.resumeWithException] extension + * function) and cancelled, then the resulting exception of the `suspendCancellableCoroutine` function is determined + * by whichever action (exceptional resume or cancellation) that happened first. + * + * ### Returning resources from a suspended coroutine + * + * As a result of a prompt cancellation guarantee, when a closeable resource + * (like open file or a handle to another native resource) is returned from a suspended coroutine as a value + * it can be lost when the coroutine is cancelled. In order to ensure that the resource can be properly closed + * in this case, the [CancellableContinuation] interface provides two functions. + * + * * [invokeOnCancellation][CancellableContinuation.invokeOnCancellation] installs a handler that is called + * whenever a suspend coroutine is being cancelled. In addition to the example at the beginning, it can be + * used to ensure that a resource that was opened before the call to + * `suspendCancellableCoroutine` or in its body is closed in case of cancellation. + * + * ``` + * suspendCancellableCoroutine { continuation -> + * val resource = openResource() // Opens some resource + * continuation.invokeOnCancellation { + * resource.close() // Ensures the resource is closed on cancellation + * } + * // ... + * } + * ``` + * + * * [resume(value) { ... }][CancellableContinuation.resume] method on a [CancellableContinuation] takes + * an optional `onCancellation` block. It can be used when resuming with a resource that must be closed by + * the code that called the corresponding suspending function. + * + * ``` + * suspendCancellableCoroutine { continuation -> + * val callback = object : Callback { // Implementation of some callback interface + * // A callback provides a reference to some closeable resource + * override fun onCompleted(resource: T) { + * // Resume coroutine with a value provided by the callback and ensure the resource is closed in case + * // when the coroutine is cancelled before the caller gets a reference to the resource. + * continuation.resume(resource) { + * resource.close() // Close the resource on cancellation + * } + * } + * // ... + * } + * ``` + * + * ### Implementation details and custom continuation interceptors + * + * The prompt cancellation guarantee is the result of a coordinated implementation inside `suspendCancellableCoroutine` + * function and the [CoroutineDispatcher] class. The coroutine dispatcher checks for the status of the [Job] immediately + * before continuing its normal execution and aborts this normal execution, calling all the corresponding + * cancellation handlers, if the job was cancelled. + * + * If a custom implementation of [ContinuationInterceptor] is used in a coroutine's context that does not extend + * [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor + * can resume execution of a previously suspended coroutine even if its job was already cancelled. */ public suspend inline fun suspendCancellableCoroutine( crossinline block: (CancellableContinuation) -> Unit @@ -199,29 +318,10 @@ public suspend inline fun suspendCancellableCoroutine( } /** - * Suspends the coroutine like [suspendCancellableCoroutine], but with *atomic cancellation*. - * - * When the suspended function throws a [CancellationException], it means that the continuation was not resumed. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when the continuation - * was already resumed and was posted for execution to the thread's queue. - * - * @suppress **This an internal API and should not be used from general code.** + * Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of + * [CancellableContinuationImpl] is reused. */ -@InternalCoroutinesApi -public suspend inline fun suspendAtomicCancellableCoroutine( - crossinline block: (CancellableContinuation) -> Unit -): T = - suspendCoroutineUninterceptedOrReturn { uCont -> - val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_ATOMIC_DEFAULT) - block(cancellable) - cancellable.getResult() - } - -/** - * Suspends coroutine similar to [suspendAtomicCancellableCoroutine], but an instance of [CancellableContinuationImpl] is reused if possible. - */ -internal suspend inline fun suspendAtomicCancellableCoroutineReusable( +internal suspend inline fun suspendCancellableCoroutineReusable( crossinline block: (CancellableContinuation) -> Unit ): T = suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = getOrCreateCancellableContinuation(uCont.intercepted()) @@ -232,12 +332,12 @@ internal suspend inline fun suspendAtomicCancellableCoroutineReusable( internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl { // If used outside of our dispatcher if (delegate !is DispatchedContinuation) { - return CancellableContinuationImpl(delegate, resumeMode = MODE_ATOMIC_DEFAULT) + return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE) } /* * Attempt to claim reusable instance. * - * suspendAtomicCancellableCoroutineReusable { // <- claimed + * suspendCancellableCoroutineReusable { // <- claimed * // Any asynchronous cancellation is "postponed" while this block * // is being executed * } // postponed cancellation is checked here. @@ -248,26 +348,13 @@ internal fun getOrCreateCancellableContinuation(delegate: Continuation): * thus leaking CC instance for indefinite time. * 2) Continuation was cancelled. Then we should prevent any further reuse and bail out. */ - return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetState() } - ?: return CancellableContinuationImpl(delegate, MODE_ATOMIC_DEFAULT) + return delegate.claimReusableCancellableContinuation()?.takeIf { it.resetStateReusable() } + ?: return CancellableContinuationImpl(delegate, MODE_CANCELLABLE_REUSABLE) } /** - * @suppress **Deprecated** - */ -@Deprecated( - message = "holdCancellability parameter is deprecated and is no longer used", - replaceWith = ReplaceWith("suspendAtomicCancellableCoroutine(block)") -) -@InternalCoroutinesApi -public suspend inline fun suspendAtomicCancellableCoroutine( - holdCancellability: Boolean = false, - crossinline block: (CancellableContinuation) -> Unit -): T = - suspendAtomicCancellableCoroutine(block) - -/** - * Removes the specified [node] on cancellation. + * Removes the specified [node] on cancellation. This function assumes that this node is already + * removed on successful resume and does not try to remove it if the continuation is cancelled during dispatch. */ internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) = invokeOnCancellation(handler = RemoveOnCancel(node).asHandler) @@ -288,7 +375,7 @@ public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHa // --------------- implementation details --------------- -private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : CancelHandler() { +private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : BeforeResumeCancelHandler() { override fun invoke(cause: Throwable?) { node.remove() } override fun toString() = "RemoveOnCancel[$node]" } diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 166cb3b353..cdb1b78882 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -27,6 +27,10 @@ internal open class CancellableContinuationImpl( final override val delegate: Continuation, resumeMode: Int ) : DispatchedTask(resumeMode), CancellableContinuation, CoroutineStackFrame { + init { + assert { resumeMode != MODE_UNINITIALIZED } // invalid mode for CancellableContinuationImpl + } + public override val context: CoroutineContext = delegate.context /* @@ -88,15 +92,17 @@ internal open class CancellableContinuationImpl( private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this) /** - * Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work. - * Invariant: used only by [suspendAtomicCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. + * Resets cancellability state in order to [suspendCancellableCoroutineReusable] to work. + * Invariant: used only by [suspendCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. */ - @JvmName("resetState") // Prettier stack traces - internal fun resetState(): Boolean { + @JvmName("resetStateReusable") // Prettier stack traces + internal fun resetStateReusable(): Boolean { + assert { resumeMode == MODE_CANCELLABLE_REUSABLE } // invalid mode for CancellableContinuationImpl assert { parentHandle !== NonDisposableHandle } val state = _state.value assert { state !is NotCompleted } - if (state is CompletedIdempotentResult) { + if (state is CompletedContinuation && state.idempotentResume != null) { + // Cannot reuse continuation that was resumed with idempotent marker detachChild() return false } @@ -129,7 +135,7 @@ internal open class CancellableContinuationImpl( private fun checkCompleted(): Boolean { val completed = isCompleted - if (resumeMode != MODE_ATOMIC_DEFAULT) return completed // Do not check postponed cancellation for non-reusable continuations + if (!resumeMode.isReusableMode) return completed // Do not check postponed cancellation for non-reusable continuations val dispatched = delegate as? DispatchedContinuation<*> ?: return completed val cause = dispatched.checkPostponedCancellation(this) ?: return completed if (!completed) { @@ -146,10 +152,26 @@ internal open class CancellableContinuationImpl( override fun takeState(): Any? = state - override fun cancelResult(state: Any?, cause: Throwable) { - if (state is CompletedWithCancellation) { - invokeHandlerSafely { - state.onCancellation(cause) + // Note: takeState does not clear the state so we don't use takenState + // and we use the actual current state where in CAS-loop + override fun cancelCompletedResult(takenState: Any?, cause: Throwable): Unit = _state.loop { state -> + when (state) { + is NotCompleted -> error("Not completed") + is CompletedExceptionally -> return // already completed exception or cancelled, nothing to do + is CompletedContinuation -> { + check(!state.cancelled) { "Must be called at most once" } + val update = state.copy(cancelCause = cause) + if (_state.compareAndSet(state, update)) { + state.invokeHandlers(this, cause) + return // done + } + } + else -> { + // completed normally without marker class, promote to CompletedContinuation in case + // if invokeOnCancellation if called later + if (_state.compareAndSet(state, CompletedContinuation(state, cancelCause = cause))) { + return // done + } } } } @@ -158,7 +180,7 @@ internal open class CancellableContinuationImpl( * Attempt to postpone cancellation for reusable cancellable continuation */ private fun cancelLater(cause: Throwable): Boolean { - if (resumeMode != MODE_ATOMIC_DEFAULT) return false + if (!resumeMode.isReusableMode) return false val dispatched = (delegate as? DispatchedContinuation<*>) ?: return false return dispatched.postponeCancellation(cause) } @@ -170,10 +192,10 @@ internal open class CancellableContinuationImpl( val update = CancelledContinuation(this, cause, handled = state is CancelHandler) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure // Invoke cancel handler if it was present - if (state is CancelHandler) invokeHandlerSafely { state.invoke(cause) } + (state as? CancelHandler)?.let { callCancelHandler(it, cause) } // Complete state update detachChildIfNonResuable() - dispatchResume(mode = MODE_ATOMIC_DEFAULT) + dispatchResume(resumeMode) // no need for additional cancellation checks return true } } @@ -185,14 +207,36 @@ internal open class CancellableContinuationImpl( detachChildIfNonResuable() } - private inline fun invokeHandlerSafely(block: () -> Unit) { + private inline fun callCancelHandlerSafely(block: () -> Unit) { + try { + block() + } catch (ex: Throwable) { + // Handler should never fail, if it does -- it is an unhandled exception + handleCoroutineException( + context, + CompletionHandlerException("Exception in invokeOnCancellation handler for $this", ex) + ) + } + } + + private fun callCancelHandler(handler: CompletionHandler, cause: Throwable?) = + /* + * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, + * because we play type tricks on Kotlin/JS and handler is not necessarily a function there + */ + callCancelHandlerSafely { handler.invokeIt(cause) } + + fun callCancelHandler(handler: CancelHandler, cause: Throwable?) = + callCancelHandlerSafely { handler.invoke(cause) } + + fun callOnCancellation(onCancellation: (cause: Throwable) -> Unit, cause: Throwable) { try { - block() + onCancellation.invoke(cause) } catch (ex: Throwable) { // Handler should never fail, if it does -- it is an unhandled exception handleCoroutineException( context, - CompletionHandlerException("Exception in cancellation handler for $this", ex) + CompletionHandlerException("Exception in resume onCancellation handler for $this", ex) ) } } @@ -231,64 +275,75 @@ internal open class CancellableContinuationImpl( val state = this.state if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this) // if the parent job was already cancelled, then throw the corresponding cancellation exception - // otherwise, there is a race is suspendCancellableCoroutine { cont -> ... } does cont.resume(...) + // otherwise, there is a race if suspendCancellableCoroutine { cont -> ... } does cont.resume(...) // before the block returns. This getResult would return a result as opposed to cancellation // exception that should have happened if the continuation is dispatched for execution later. - if (resumeMode == MODE_CANCELLABLE) { + if (resumeMode.isCancellableMode) { val job = context[Job] if (job != null && !job.isActive) { val cause = job.getCancellationException() - cancelResult(state, cause) + cancelCompletedResult(state, cause) throw recoverStackTrace(cause, this) } } return getSuccessfulResult(state) } - override fun resumeWith(result: Result) { + override fun resumeWith(result: Result) = resumeImpl(result.toState(this), resumeMode) - } - override fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) { - val cancelled = resumeImpl(CompletedWithCancellation(value, onCancellation), resumeMode) - if (cancelled != null) { - // too late to resume (was cancelled) -- call handler - invokeHandlerSafely { - onCancellation(cancelled.cause) - } - } - } + override fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) = + resumeImpl(value, resumeMode, onCancellation) public override fun invokeOnCancellation(handler: CompletionHandler) { - var handleCache: CancelHandler? = null + val cancelHandler = makeCancelHandler(handler) _state.loop { state -> when (state) { is Active -> { - val node = handleCache ?: makeHandler(handler).also { handleCache = it } - if (_state.compareAndSet(state, node)) return // quit on cas success + if (_state.compareAndSet(state, cancelHandler)) return // quit on cas success } is CancelHandler -> multipleHandlersError(handler, state) - is CancelledContinuation -> { + is CompletedExceptionally -> { /* - * Continuation was already cancelled, invoke directly. + * Continuation was already cancelled or completed exceptionally. * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed, - * so we check to make sure that handler was installed just once. + * so we check to make sure handler was installed just once. */ if (!state.makeHandled()) multipleHandlersError(handler, state) /* + * Call the handler only if it was cancelled (not called when completed exceptionally). * :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, * because we play type tricks on Kotlin/JS and handler is not necessarily a function there */ - invokeHandlerSafely { handler.invokeIt((state as? CompletedExceptionally)?.cause) } + if (state is CancelledContinuation) { + callCancelHandler(handler, (state as? CompletedExceptionally)?.cause) + } return } + is CompletedContinuation -> { + /* + * Continuation was already completed, and might already have cancel handler. + */ + if (state.cancelHandler != null) multipleHandlersError(handler, state) + // BeforeResumeCancelHandler does not need to be called on a completed continuation + if (cancelHandler is BeforeResumeCancelHandler) return + if (state.cancelled) { + // Was already cancelled while being dispatched -- invoke the handler directly + callCancelHandler(handler, state.cancelCause) + return + } + val update = state.copy(cancelHandler = cancelHandler) + if (_state.compareAndSet(state, update)) return // quit on cas success + } else -> { /* - * Continuation was already completed, do nothing. - * NOTE: multiple invokeOnCancellation calls with different handlers are not allowed, - * but we have no way to check that it was installed just once in this case. + * Continuation was already completed normally, but might get cancelled while being dispatched. + * Change its state to CompletedContinuation, unless we have BeforeResumeCancelHandler which + * does not need to be called in this case. */ - return + if (cancelHandler is BeforeResumeCancelHandler) return + val update = CompletedContinuation(state, cancelHandler = cancelHandler) + if (_state.compareAndSet(state, update)) return // quit on cas success } } } @@ -298,7 +353,7 @@ internal open class CancellableContinuationImpl( error("It's prohibited to register multiple handlers, tried to register $handler, already has $state") } - private fun makeHandler(handler: CompletionHandler): CancelHandler = + private fun makeCancelHandler(handler: CompletionHandler): CancelHandler = if (handler is CancelHandler) handler else InvokeOnCancel(handler) private fun dispatchResume(mode: Int) { @@ -307,15 +362,39 @@ internal open class CancellableContinuationImpl( dispatch(mode) } - // returns null when successfully dispatched resumed, CancelledContinuation if too late (was already cancelled) - private fun resumeImpl(proposedUpdate: Any?, resumeMode: Int): CancelledContinuation? { + private fun resumedState( + state: NotCompleted, + proposedUpdate: Any?, + resumeMode: Int, + onCancellation: ((cause: Throwable) -> Unit)?, + idempotent: Any? + ): Any? = when { + proposedUpdate is CompletedExceptionally -> { + assert { idempotent == null } // there are no idempotent exceptional resumes + assert { onCancellation == null } // only successful results can be cancelled + proposedUpdate + } + !resumeMode.isCancellableMode && idempotent == null -> proposedUpdate // cannot be cancelled in process, all is fine + onCancellation != null || (state is CancelHandler && state !is BeforeResumeCancelHandler) || idempotent != null -> + // mark as CompletedContinuation if special cases are present: + // Cancellation handlers that shall be called after resume or idempotent resume + CompletedContinuation(proposedUpdate, state as? CancelHandler, onCancellation, idempotent) + else -> proposedUpdate // simple case -- use the value directly + } + + private fun resumeImpl( + proposedUpdate: Any?, + resumeMode: Int, + onCancellation: ((cause: Throwable) -> Unit)? = null + ) { _state.loop { state -> when (state) { is NotCompleted -> { - if (!_state.compareAndSet(state, proposedUpdate)) return@loop // retry on cas failure + val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent = null) + if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure detachChildIfNonResuable() - dispatchResume(resumeMode) - return null + dispatchResume(resumeMode) // dispatch resume, but it might get cancelled in process + return // done } is CancelledContinuation -> { /* @@ -323,14 +402,48 @@ internal open class CancellableContinuationImpl( * because cancellation is asynchronous and may race with resume. * Racy exceptions will be lost, too. */ - if (state.makeResumed()) return state // tried to resume just once, but was cancelled + if (state.makeResumed()) { // check if trying to resume one (otherwise error) + // call onCancellation + onCancellation?.let { callOnCancellation(it, state.cause) } + return // done + } + } + } + alreadyResumedError(proposedUpdate) // otherwise, an error (second resume attempt) + } + } + + /** + * Similar to [tryResume], but does not actually completes resume (needs [completeResume] call). + * Returns [RESUME_TOKEN] when resumed, `null` when it was already resumed or cancelled. + */ + private fun tryResumeImpl( + proposedUpdate: Any?, + idempotent: Any?, + onCancellation: ((cause: Throwable) -> Unit)? + ): Symbol? { + _state.loop { state -> + when (state) { + is NotCompleted -> { + val update = resumedState(state, proposedUpdate, resumeMode, onCancellation, idempotent) + if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure + detachChildIfNonResuable() + return RESUME_TOKEN + } + is CompletedContinuation -> { + return if (idempotent != null && state.idempotentResume === idempotent) { + assert { state.result == proposedUpdate } // "Non-idempotent resume" + RESUME_TOKEN // resumed with the same token -- ok + } else { + null // resumed with a different token or non-idempotent -- too late + } } + else -> return null // cannot resume -- not active anymore } - alreadyResumedError(proposedUpdate) // otherwise -- an error (second resume attempt) } } - private fun alreadyResumedError(proposedUpdate: Any?) { + private fun alreadyResumedError(proposedUpdate: Any?): Nothing { error("Already resumed, but proposed with update $proposedUpdate") } @@ -342,7 +455,7 @@ internal open class CancellableContinuationImpl( /** * Detaches from the parent. - * Invariant: used used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` + * Invariant: used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` */ internal fun detachChild() { val handle = parentHandle @@ -351,42 +464,14 @@ internal open class CancellableContinuationImpl( } // Note: Always returns RESUME_TOKEN | null - override fun tryResume(value: T, idempotent: Any?): Any? { - _state.loop { state -> - when (state) { - is NotCompleted -> { - val update: Any? = if (idempotent == null) value else - CompletedIdempotentResult(idempotent, value) - if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - detachChildIfNonResuable() - return RESUME_TOKEN - } - is CompletedIdempotentResult -> { - return if (state.idempotentResume === idempotent) { - assert { state.result === value } // "Non-idempotent resume" - RESUME_TOKEN - } else { - null - } - } - else -> return null // cannot resume -- not active anymore - } - } - } + override fun tryResume(value: T, idempotent: Any?): Any? = + tryResumeImpl(value, idempotent, onCancellation = null) - override fun tryResumeWithException(exception: Throwable): Any? { - _state.loop { state -> - when (state) { - is NotCompleted -> { - val update = CompletedExceptionally(exception) - if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - detachChildIfNonResuable() - return RESUME_TOKEN - } - else -> return null // cannot resume -- not active anymore - } - } - } + override fun tryResume(value: T, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? = + tryResumeImpl(value, idempotent, onCancellation) + + override fun tryResumeWithException(exception: Throwable): Any? = + tryResumeImpl(CompletedExceptionally(exception), idempotent = null, onCancellation = null) // note: token is always RESUME_TOKEN override fun completeResume(token: Any) { @@ -407,11 +492,15 @@ internal open class CancellableContinuationImpl( @Suppress("UNCHECKED_CAST") override fun getSuccessfulResult(state: Any?): T = when (state) { - is CompletedIdempotentResult -> state.result as T - is CompletedWithCancellation -> state.result as T + is CompletedContinuation -> state.result as T else -> state as T } + // The exceptional state in CancellableContinuationImpl is stored directly and it is not recovered yet. + // The stacktrace recovery is invoked here. + override fun getExceptionalResult(state: Any?): Throwable? = + super.getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } + // For nicer debugging public override fun toString(): String = "${nameString()}(${delegate.toDebugString()}){$state}@$hexAddress" @@ -428,8 +517,20 @@ private object Active : NotCompleted { override fun toString(): String = "Active" } +/** + * Base class for all [CancellableContinuation.invokeOnCancellation] handlers to avoid an extra instance + * on JVM, yet support JS where you cannot extend from a functional type. + */ internal abstract class CancelHandler : CancelHandlerBase(), NotCompleted +/** + * Base class for all [CancellableContinuation.invokeOnCancellation] handlers that don't need to be invoked + * if continuation is cancelled after resumption, during dispatch, because the corresponding resources + * were already released before calling `resume`. This cancel handler is called only before `resume`. + * It avoids allocation of [CompletedContinuation] instance during resume on JVM. + */ +internal abstract class BeforeResumeCancelHandler : CancelHandler() + // Wrapper for lambdas, for the performance sake CancelHandler can be subclassed directly private class InvokeOnCancel( // Clashes with InvokeOnCancellation private val handler: CompletionHandler @@ -440,16 +541,18 @@ private class InvokeOnCancel( // Clashes with InvokeOnCancellation override fun toString() = "InvokeOnCancel[${handler.classSimpleName}@$hexAddress]" } -private class CompletedIdempotentResult( - @JvmField val idempotentResume: Any?, - @JvmField val result: Any? -) { - override fun toString(): String = "CompletedIdempotentResult[$result]" -} - -private class CompletedWithCancellation( +// Completed with additional metadata +private data class CompletedContinuation( @JvmField val result: Any?, - @JvmField val onCancellation: (cause: Throwable) -> Unit + @JvmField val cancelHandler: CancelHandler? = null, // installed via invokeOnCancellation + @JvmField val onCancellation: ((cause: Throwable) -> Unit)? = null, // installed via resume block + @JvmField val idempotentResume: Any? = null, + @JvmField val cancelCause: Throwable? = null ) { - override fun toString(): String = "CompletedWithCancellation[$result]" + val cancelled: Boolean get() = cancelCause != null + + fun invokeHandlers(cont: CancellableContinuationImpl<*>, cause: Throwable) { + cancelHandler?.let { cont.callCancelHandler(it, cause) } + onCancellation?.let { cont.callOnCancellation(it, cause) } + } } diff --git a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt b/kotlinx-coroutines-core/common/src/CompletionState.kt similarity index 78% rename from kotlinx-coroutines-core/common/src/CompletedExceptionally.kt rename to kotlinx-coroutines-core/common/src/CompletionState.kt index b426785bd7..f09aa3ccd9 100644 --- a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt +++ b/kotlinx-coroutines-core/common/src/CompletionState.kt @@ -9,10 +9,17 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -internal fun Result.toState(): Any? = fold({ it }, { CompletedExceptionally(it) }) +internal fun Result.toState( + onCancellation: ((cause: Throwable) -> Unit)? = null +): Any? = fold( + onSuccess = { if (onCancellation != null) CompletedWithCancellation(it, onCancellation) else it }, + onFailure = { CompletedExceptionally(it) } +) -internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold({ it }, - { CompletedExceptionally(recoverStackTrace(it, caller)) }) +internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold( + onSuccess = { it }, + onFailure = { CompletedExceptionally(recoverStackTrace(it, caller)) } +) @Suppress("RESULT_CLASS_IN_RETURN_TYPE", "UNCHECKED_CAST") internal fun recoverResult(state: Any?, uCont: Continuation): Result = @@ -21,6 +28,11 @@ internal fun recoverResult(state: Any?, uCont: Continuation): Result = else Result.success(state as T) +internal data class CompletedWithCancellation( + @JvmField val result: Any?, + @JvmField val onCancellation: (cause: Throwable) -> Unit +) + /** * Class for an internal state of a job that was cancelled (completed exceptionally). * diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 1b6e7eb00f..ab1e814b8a 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlin.coroutines.* /** diff --git a/kotlinx-coroutines-core/common/src/Deferred.kt b/kotlinx-coroutines-core/common/src/Deferred.kt index 72f3fde141..ff996756a3 100644 --- a/kotlinx-coroutines-core/common/src/Deferred.kt +++ b/kotlinx-coroutines-core/common/src/Deferred.kt @@ -43,6 +43,8 @@ public interface Deferred : Job { * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * This function can be used in [select] invocation with [onAwait] clause. * Use [isCompleted] to check for completion of this deferred value without waiting. diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index ee28ec3b0e..adde8e433c 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -21,9 +21,12 @@ import kotlin.time.* public interface Delay { /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. + * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun delay(time: Long) { if (time <= 0) return // don't delay @@ -97,9 +100,12 @@ public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} /** * Delays coroutine for a given time without blocking a thread and resumes it after a specified time. + * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. * @@ -117,9 +123,12 @@ public suspend fun delay(timeMillis: Long) { /** * Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time. + * * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * If you want to delay forever (until cancellation), consider using [awaitCancellation] instead. * diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index e0af04ddb7..0d8bd3bc2f 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -13,6 +14,8 @@ import kotlin.coroutines.intrinsics.* * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed when this suspending function is invoked or while * this function is waiting for dispatch, it resumes with a [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend. * diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 28c7ceabe1..3cd6de5f51 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -16,7 +16,9 @@ import kotlin.native.concurrent.* /** * Abstract send channel. It is a base class for all send channel implementations. */ -internal abstract class AbstractSendChannel : SendChannel { +internal abstract class AbstractSendChannel( + @JvmField protected val onUndeliveredElement: OnUndeliveredElement? +) : SendChannel { /** @suppress **This is unstable API and it is subject to change.** */ protected val queue = LockFreeLinkedListHead() @@ -151,24 +153,34 @@ internal abstract class AbstractSendChannel : SendChannel { // We should check for closed token on offer as well, otherwise offer won't be linearizable // in the face of concurrent close() // See https://github.com/Kotlin/kotlinx.coroutines/issues/359 - throw recoverStackTrace(helpCloseAndGetSendException(closedForSend ?: return false)) + throw recoverStackTrace(helpCloseAndGetSendException(element, closedForSend ?: return false)) + } + result is Closed<*> -> { + throw recoverStackTrace(helpCloseAndGetSendException(element, result)) } - result is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(result)) else -> error("offerInternal returned $result") } } - private fun helpCloseAndGetSendException(closed: Closed<*>): Throwable { + private fun helpCloseAndGetSendException(element: E, closed: Closed<*>): Throwable { // To ensure linearizablity we must ALWAYS help close the channel when we observe that it was closed // See https://github.com/Kotlin/kotlinx.coroutines/issues/1419 helpClose(closed) + // Element was not delivered -> cals onUndeliveredElement + onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let { + // If it crashes, add send exception as suppressed for better diagnostics + it.addSuppressed(closed.sendException) + throw it + } return closed.sendException } - private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont -> loop@ while (true) { if (isFullImpl) { - val send = SendElement(element, cont) + val send = if (onUndeliveredElement == null) + SendElement(element, cont) else + SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement) val enqueueResult = enqueueSend(send) when { enqueueResult == null -> { // enqueued successfully @@ -176,7 +188,7 @@ internal abstract class AbstractSendChannel : SendChannel { return@sc } enqueueResult is Closed<*> -> { - cont.helpCloseAndResumeWithSendException(enqueueResult) + cont.helpCloseAndResumeWithSendException(element, enqueueResult) return@sc } enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead @@ -193,7 +205,7 @@ internal abstract class AbstractSendChannel : SendChannel { } offerResult === OFFER_FAILED -> continue@loop offerResult is Closed<*> -> { - cont.helpCloseAndResumeWithSendException(offerResult) + cont.helpCloseAndResumeWithSendException(element, offerResult) return@sc } else -> error("offerInternal returned $offerResult") @@ -201,9 +213,15 @@ internal abstract class AbstractSendChannel : SendChannel { } } - private fun Continuation<*>.helpCloseAndResumeWithSendException(closed: Closed<*>) { + private fun Continuation<*>.helpCloseAndResumeWithSendException(element: E, closed: Closed<*>) { helpClose(closed) - resumeWithException(closed.sendException) + val sendException = closed.sendException + onUndeliveredElement?.callUndeliveredElementCatchingException(element)?.let { + it.addSuppressed(sendException) + resumeWithException(it) + return + } + resumeWithException(sendException) } /** @@ -375,7 +393,7 @@ internal abstract class AbstractSendChannel : SendChannel { select.disposeOnSelect(node) return } - enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(enqueueResult)) + enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, enqueueResult)) enqueueResult === ENQUEUE_FAILED -> {} // try to offer enqueueResult is Receive<*> -> {} // try to offer else -> error("enqueueSend returned $enqueueResult ") @@ -391,7 +409,7 @@ internal abstract class AbstractSendChannel : SendChannel { block.startCoroutineUnintercepted(receiver = this, completion = select.completion) return } - offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(offerResult)) + offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(element, offerResult)) else -> error("offerSelectInternal returned $offerResult") } } @@ -431,7 +449,7 @@ internal abstract class AbstractSendChannel : SendChannel { // ------ private ------ private class SendSelect( - override val pollResult: Any?, + override val pollResult: E, // E | Closed - the result pollInternal returns when it rendezvous with this node @JvmField val channel: AbstractSendChannel, @JvmField val select: SelectInstance, @JvmField val block: suspend (SendChannel) -> R @@ -440,11 +458,13 @@ internal abstract class AbstractSendChannel : SendChannel { select.trySelectOther(otherOp) as Symbol? // must return symbol override fun completeResumeSend() { - block.startCoroutine(receiver = channel, completion = select.completion) + block.startCoroutineCancellable(receiver = channel, completion = select.completion) } override fun dispose() { // invoked on select completion - remove() + if (!remove()) return + // if the node was successfully removed (meaning it was added but was not received) then element not delivered + undeliveredElement() } override fun resumeSendClosed(closed: Closed<*>) { @@ -452,6 +472,10 @@ internal abstract class AbstractSendChannel : SendChannel { select.resumeSelectWithException(closed.sendException) } + override fun undeliveredElement() { + channel.onUndeliveredElement?.callUndeliveredElement(pollResult, select.completion.context) + } + override fun toString(): String = "SendSelect@$hexAddress($pollResult)[$channel, $select]" } @@ -469,7 +493,9 @@ internal abstract class AbstractSendChannel : SendChannel { /** * Abstract send/receive channel. It is a base class for all channel implementations. */ -internal abstract class AbstractChannel : AbstractSendChannel(), Channel { +internal abstract class AbstractChannel( + onUndeliveredElement: OnUndeliveredElement? +) : AbstractSendChannel(onUndeliveredElement), Channel { // ------ extension points for buffered channels ------ /** @@ -501,6 +527,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutineReusable sc@ { cont -> - val receive = ReceiveElement(cont as CancellableContinuation, receiveMode) + private suspend fun receiveSuspend(receiveMode: Int): R = suspendCancellableCoroutineReusable sc@ { cont -> + val receive = if (onUndeliveredElement == null) + ReceiveElement(cont as CancellableContinuation, receiveMode) else + ReceiveElementWithUndeliveredHandler(cont as CancellableContinuation, receiveMode, onUndeliveredElement) while (true) { if (enqueueReceive(receive)) { removeReceiveOnCancel(cont, receive) @@ -561,7 +591,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel @@ -785,7 +820,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel, receive: Receive<*>) = cont.invokeOnCancellation(handler = RemoveReceiveOnCancel(receive).asHandler) - private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : CancelHandler() { + private inner class RemoveReceiveOnCancel(private val receive: Receive<*>) : BeforeResumeCancelHandler() { override fun invoke(cause: Throwable?) { if (receive.remove()) onReceiveDequeued() @@ -793,7 +828,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel(val channel: AbstractChannel) : ChannelIterator { + private class Itr(@JvmField val channel: AbstractChannel) : ChannelIterator { var result: Any? = POLL_FAILED // E | POLL_FAILED | Closed override suspend fun hasNext(): Boolean { @@ -814,7 +849,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel + private suspend fun hasNextSuspend(): Boolean = suspendCancellableCoroutineReusable sc@ { cont -> val receive = ReceiveHasNext(this, cont) while (true) { if (channel.enqueueReceive(receive)) { @@ -832,7 +867,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel( + private open class ReceiveElement( @JvmField val cont: CancellableContinuation, @JvmField val receiveMode: Int ) : Receive() { @@ -860,9 +896,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel value } - @Suppress("IMPLICIT_CAST_TO_ANY") override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(resumeValue(value), otherOp?.desc) ?: return null + val token = cont.tryResume(resumeValue(value), otherOp?.desc, resumeOnCancellationFun(value)) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -881,12 +916,22 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( + private class ReceiveElementWithUndeliveredHandler( + cont: CancellableContinuation, + receiveMode: Int, + @JvmField val onUndeliveredElement: OnUndeliveredElement + ) : ReceiveElement(cont, receiveMode) { + override fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = + onUndeliveredElement.bindCancellationFun(value, cont.context) + } + + private open class ReceiveHasNext( @JvmField val iterator: Itr, @JvmField val cont: CancellableContinuation ) : Receive() { override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(true, otherOp?.desc) ?: return null + val token = cont.tryResume(true, otherOp?.desc, resumeOnCancellationFun(value)) + ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -906,13 +951,17 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel Unit)? = + iterator.channel.onUndeliveredElement?.bindCancellationFun(value, cont.context) + override fun toString(): String = "ReceiveHasNext@$hexAddress" } @@ -927,16 +976,20 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { if (!select.trySelect()) return when (receiveMode) { RECEIVE_THROWS_ON_CLOSE -> select.resumeSelectWithException(closed.receiveException) - RECEIVE_RESULT -> block.startCoroutine(ValueOrClosed.closed(closed.closeCause), select.completion) + RECEIVE_RESULT -> block.startCoroutineCancellable(ValueOrClosed.closed(closed.closeCause), select.completion) RECEIVE_NULL_ON_CLOSE -> if (closed.closeCause == null) { - block.startCoroutine(null, select.completion) + block.startCoroutineCancellable(null, select.completion) } else { select.resumeSelectWithException(closed.receiveException) } @@ -948,6 +1001,9 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel Unit)? = + channel.onUndeliveredElement?.bindCancellationFun(value, select.completion.context) + override fun toString(): String = "ReceiveSelect@$hexAddress[$select,receiveMode=$receiveMode]" } } @@ -957,6 +1013,10 @@ internal const val RECEIVE_THROWS_ON_CLOSE = 0 internal const val RECEIVE_NULL_ON_CLOSE = 1 internal const val RECEIVE_RESULT = 2 +@JvmField +@SharedImmutable +internal val EMPTY = Symbol("EMPTY") // marker for Conflated & Buffered channels + @JvmField @SharedImmutable internal val OFFER_SUCCESS: Any = Symbol("OFFER_SUCCESS") @@ -983,7 +1043,7 @@ internal typealias Handler = (Throwable?) -> Unit * Represents sending waiter in the queue. */ internal abstract class Send : LockFreeLinkedListNode() { - abstract val pollResult: Any? // E | Closed + abstract val pollResult: Any? // E | Closed - the result pollInternal returns when it rendezvous with this node // Returns: null - failure, // RETRY_ATOMIC for retry (only when otherOp != null), // RESUME_TOKEN on success (call completeResumeSend) @@ -991,6 +1051,7 @@ internal abstract class Send : LockFreeLinkedListNode() { abstract fun tryResumeSend(otherOp: PrepareOp?): Symbol? abstract fun completeResumeSend() abstract fun resumeSendClosed(closed: Closed<*>) + open fun undeliveredElement() {} } /** @@ -1009,9 +1070,8 @@ internal interface ReceiveOrClosed { /** * Represents sender for a specific element. */ -@Suppress("UNCHECKED_CAST") -internal class SendElement( - override val pollResult: Any?, +internal open class SendElement( + override val pollResult: E, @JvmField val cont: CancellableContinuation ) : Send() { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { @@ -1021,9 +1081,27 @@ internal class SendElement( otherOp?.finishPrepare() // finish preparations return RESUME_TOKEN } + override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) - override fun toString(): String = "SendElement@$hexAddress($pollResult)" + override fun toString(): String = "$classSimpleName@$hexAddress($pollResult)" +} + +internal class SendElementWithUndeliveredHandler( + pollResult: E, + cont: CancellableContinuation, + @JvmField val onUndeliveredElement: OnUndeliveredElement +) : SendElement(pollResult, cont) { + override fun remove(): Boolean { + if (!super.remove()) return false + // if the node was successfully removed (meaning it was added but was not received) then we have undelivered element + undeliveredElement() + return true + } + + override fun undeliveredElement() { + onUndeliveredElement.callUndeliveredElement(pollResult, cont.context) + } } /** @@ -1048,6 +1126,7 @@ internal class Closed( internal abstract class Receive : LockFreeLinkedListNode(), ReceiveOrClosed { override val offerResult get() = OFFER_SUCCESS abstract fun resumeReceiveClosed(closed: Closed<*>) + open fun resumeOnCancellationFun(value: E): ((Throwable) -> Unit)? = null } @Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index 155652fd6f..91b5473c41 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -28,7 +28,7 @@ internal class ArrayBroadcastChannel( * Buffer capacity. */ val capacity: Int -) : AbstractSendChannel(), BroadcastChannel { +) : AbstractSendChannel(null), BroadcastChannel { init { require(capacity >= 1) { "ArrayBroadcastChannel capacity must be at least 1, but $capacity was specified" } } @@ -180,6 +180,8 @@ internal class ArrayBroadcastChannel( this.tail = tail + 1 return@withLock // go out of lock to wakeup this sender } + // Too late, already cancelled, but we removed it from the queue and need to release resources. + // However, ArrayBroadcastChannel does not support onUndeliveredElement, so nothing to do } } } @@ -205,7 +207,7 @@ internal class ArrayBroadcastChannel( private class Subscriber( private val broadcastChannel: ArrayBroadcastChannel - ) : AbstractChannel(), ReceiveChannel { + ) : AbstractChannel(null), ReceiveChannel { private val subLock = ReentrantLock() private val _subHead = atomic(0L) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index e26579eff7..8a08106516 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -23,8 +23,9 @@ internal open class ArrayChannel( /** * Buffer capacity. */ - val capacity: Int -) : AbstractChannel() { + val capacity: Int, + onUndeliveredElement: OnUndeliveredElement? +) : AbstractChannel(onUndeliveredElement) { init { require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } } @@ -34,7 +35,8 @@ internal open class ArrayChannel( * Guarded by lock. * Allocate minimum of capacity and 16 to avoid excess memory pressure for large channels when it's not necessary. */ - private var buffer: Array = arrayOfNulls(min(capacity, 8)) + private var buffer: Array = arrayOfNulls(min(capacity, 8)).apply { fill(EMPTY) } + private var head: Int = 0 private val size = atomic(0) // Invariant: size <= capacity @@ -143,6 +145,7 @@ internal open class ArrayChannel( for (i in 0 until currentSize) { newBuffer[i] = buffer[(head + i) % buffer.size] } + newBuffer.fill(EMPTY, currentSize, newSize) buffer = newBuffer head = 0 } @@ -172,6 +175,8 @@ internal open class ArrayChannel( replacement = send!!.pollResult break@loop } + // too late, already cancelled, but we removed it from the queue and need to notify on undelivered element + send!!.undeliveredElement() } } if (replacement !== POLL_FAILED && replacement !is Closed<*>) { @@ -254,17 +259,23 @@ internal open class ArrayChannel( // Note: this function is invoked when channel is already closed override fun onCancelIdempotent(wasClosed: Boolean) { // clear buffer first, but do not wait for it in helpers - if (wasClosed) { - lock.withLock { - repeat(size.value) { - buffer[head] = 0 - head = (head + 1) % buffer.size + val onUndeliveredElement = onUndeliveredElement + var undeliveredElementException: UndeliveredElementException? = null // first cancel exception, others suppressed + lock.withLock { + repeat(size.value) { + val value = buffer[head] + if (onUndeliveredElement != null && value !== EMPTY) { + @Suppress("UNCHECKED_CAST") + undeliveredElementException = onUndeliveredElement.callUndeliveredElementCatchingException(value as E, undeliveredElementException) } - size.value = 0 + buffer[head] = EMPTY + head = (head + 1) % buffer.size } + size.value = 0 } // then clean all queued senders super.onCancelIdempotent(wasClosed) + undeliveredElementException?.let { throw it } // throw cancel exception at the end if there was one } // ------ debug ------ diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index c4b4a9b25e..25085ed2d2 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -44,19 +44,17 @@ public interface SendChannel { * Sends the specified [element] to this channel, suspending the caller while the buffer of this channel is full * or if it does not exist, or throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). * - * Note that closing a channel _after_ this function has suspended does not cause this suspended [send] invocation + * [Closing][close] a channel _after_ this function has suspended does not cause this suspended [send] invocation * to abort, because closing a channel is conceptually like sending a special "close token" over this channel. * All elements sent over the channel are delivered in first-in first-out order. The sent element * will be delivered to receivers before the close token. * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `send` is atomic*: when this function - * throws a [CancellationException], it means that the [element] was not sent to this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this `send` operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `send` call can send the element to the channel, + * but then throw [CancellationException], thus an exception should not be treated as a failure to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -81,6 +79,11 @@ public interface SendChannel { * in situations when `send` suspends. * * Throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). + * + * When `offer` call returns `false` it guarantees that the element was not delivered to the consumer and it + * it does not call `onUndeliveredElement` that was installed for this channel. If the channel was closed, + * then it calls `onUndeliveredElement` before throwing an exception. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. */ public fun offer(element: E): Boolean @@ -170,12 +173,10 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this `receive` operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `receive` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -200,12 +201,10 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this `receive` operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `receiveOrNull` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -250,12 +249,10 @@ public interface ReceiveChannel { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `receiveOrClosed` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -332,7 +329,7 @@ public interface ReceiveChannel { * @suppress *This is an internal API, do not use*: Inline classes ABI is not stable yet and * [KT-27524](https://youtrack.jetbrains.com/issue/KT-27524) needs to be fixed. */ -@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") +@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS", "EXPERIMENTAL_FEATURE_WARNING") @InternalCoroutinesApi // until https://youtrack.jetbrains.com/issue/KT-27524 is fixed public inline class ValueOrClosed internal constructor(private val holder: Any?) { @@ -439,12 +436,10 @@ public interface ChannelIterator { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with a [CancellationException]. - * - * *Cancellation of suspended `receive` is atomic*: when this function - * throws a [CancellationException], it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. The `hasNext` call can retrieve the element from the channel, + * but then throw [CancellationException], thus failing to deliver the element. + * See "Undelivered elements" section in [Channel] documentation for details on handling undelivered elements. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -508,6 +503,69 @@ public interface ChannelIterator { * * 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. + * + * ### Prompt cancellation guarantee + * + * All suspending functions with channels provide **prompt cancellation guarantee**. + * If the job was cancelled while send or receive function was suspended, it will not resume successfully, + * but throws a [CancellationException]. + * With a single-threaded [dispatcher][CoroutineDispatcher] like [Dispatchers.Main] this gives a + * guarantee that if a piece code running in this thread cancels a [Job], then a coroutine running this job cannot + * resume successfully and continue to run, ensuring a prompt response to its cancellation. + * + * > **Prompt cancellation guarantee** for channel operations was added since `kotlinx.coroutines` version `1.4.0` + * > and had replaced a channel-specific atomic-cancellation that was not consistent with other suspending functions. + * > The low-level mechanics of prompt cancellation are explained in [suspendCancellableCoroutine] function. + * + * ### Undelivered elements + * + * As a result of a prompt cancellation guarantee, when a closeable resource + * (like open file or a handle to another native resource) is transferred via channel from one coroutine to another + * it can fail to be delivered and will be lost if either send or receive operations are cancelled in transit. + * + * A `Channel()` constructor function has an `onUndeliveredElement` optional parameter. + * When `onUndeliveredElement` parameter is set, the corresponding function is called once for each element + * that was sent to the channel with the call to the [send][SendChannel.send] function but failed to be delivered, + * which can happen in the following cases: + * + * * When [send][SendChannel.send] operation throws an exception because it was cancelled before it had a chance to actually + * send the element or because the channel was [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel]. + * * When [offer][SendChannel.offer] operation throws an exception when + * the channel was [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel]. + * * When [receive][ReceiveChannel.receive], [receiveOrNull][ReceiveChannel.receiveOrNull], or [hasNext][ChannelIterator.hasNext] + * operation throws an exception when it had retrieved the element from the + * channel but was cancelled before the code following the receive call resumed. + * * The channel was [cancelled][ReceiveChannel.cancel], in which case `onUndeliveredElement` is called on every + * remaining element in the channel's buffer. + * + * Note, that `onUndeliveredElement` function is called synchronously in an arbitrary context. It should be fast, non-blocking, + * and should not throw exceptions. Any exception thrown by `onUndeliveredElement` is wrapped into an internal runtime + * exception which is either rethrown from the caller method or handed off to the exception handler in the current context + * (see [CoroutineExceptionHandler]) when one is available. + * + * A typical usage for `onDeliveredElement` is to close a resource that is being transferred via the channel. The + * following code pattern guarantees that opened resources are closed even if producer, consumer, and/or channel + * are cancelled. Resources are never lost. + * + * ``` + * // Create the channel with onUndeliveredElement block that closes a resource + * val channel = Channel(capacity) { resource -> resource.close() } + * + * // Producer code + * val resourceToSend = openResource() + * channel.send(resourceToSend) + * + * // Consumer code + * val resourceReceived = channel.receive() + * try { + * // work with received resource + * } finally { + * resourceReceived.close() + * } + * ``` + * + * > Note, that if you do any kind of work in between `openResource()` and `channel.send(...)`, then you should + * > ensure that resource gets closed in case this additional code fails. */ public interface Channel : SendChannel, ReceiveChannel { /** @@ -557,17 +615,26 @@ 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 onUndeliveredElement an optional function that is called when element was sent but was not delivered to the consumer. + * See "Undelivered elements" section in [Channel] documentation. * @throws IllegalArgumentException when [capacity] < -2 */ -public fun Channel(capacity: Int = RENDEZVOUS): Channel = +public fun Channel(capacity: Int = RENDEZVOUS, onUndeliveredElement: ((E) -> Unit)? = null): Channel = when (capacity) { - RENDEZVOUS -> RendezvousChannel() - UNLIMITED -> LinkedListChannel() - CONFLATED -> ConflatedChannel() - BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) - else -> ArrayChannel(capacity) + RENDEZVOUS -> RendezvousChannel(onUndeliveredElement) + UNLIMITED -> LinkedListChannel(onUndeliveredElement) + CONFLATED -> ConflatedChannel(onUndeliveredElement) + BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY, onUndeliveredElement) + else -> ArrayChannel(capacity, onUndeliveredElement) } +/** + * @suppress Binary compatibility only, should not be documented + */ +// This declaration is hidden since version 1.4.0 +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility") +public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity, onUndeliveredElement = null) + /** * 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/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index 8c61928aa4..d19028bf63 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -40,12 +40,9 @@ public inline fun BroadcastChannel.consume(block: ReceiveChannel.() * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this receive operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. If the `receiveOrNull` call threw [CancellationException] there is no way + * to tell if some element was already received from the channel or not. See [Channel] documentation for details. * * Note, that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 2b9375ddec..ba2ccea92d 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -282,7 +282,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { private class Subscriber( private val broadcastChannel: ConflatedBroadcastChannel - ) : ConflatedChannel(), ReceiveChannel { + ) : ConflatedChannel(null), ReceiveChannel { override fun onCancelIdempotent(wasClosed: Boolean) { if (wasClosed) { diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 4734766914..75e421c6e7 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* -import kotlin.native.concurrent.* /** * Channel that buffers at most one element and conflates all subsequent `send` and `offer` invocations, @@ -18,7 +17,7 @@ import kotlin.native.concurrent.* * * This channel is created by `Channel(Channel.CONFLATED)` factory function invocation. */ -internal open class ConflatedChannel : AbstractChannel() { +internal open class ConflatedChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = value === EMPTY protected final override val isBufferAlwaysFull: Boolean get() = false @@ -30,10 +29,6 @@ internal open class ConflatedChannel : AbstractChannel() { private var value: Any? = EMPTY - private companion object { - private val EMPTY = Symbol("EMPTY") - } - // result is `OFFER_SUCCESS | Closed` protected override fun offerInternal(element: E): Any { var receive: ReceiveOrClosed? = null @@ -54,7 +49,7 @@ internal open class ConflatedChannel : AbstractChannel() { } } } - value = element + updateValueLocked(element)?.let { throw it } return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -87,7 +82,7 @@ internal open class ConflatedChannel : AbstractChannel() { if (!select.trySelect()) { return ALREADY_SELECTED } - value = element + updateValueLocked(element)?.let { throw it } return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -120,12 +115,20 @@ internal open class ConflatedChannel : AbstractChannel() { } protected override fun onCancelIdempotent(wasClosed: Boolean) { - if (wasClosed) { - lock.withLock { - value = EMPTY - } + var undeliveredElementException: UndeliveredElementException? = null // resource cancel exception + lock.withLock { + undeliveredElementException = updateValueLocked(EMPTY) } super.onCancelIdempotent(wasClosed) + undeliveredElementException?.let { throw it } // throw exception at the end if there was one + } + + private fun updateValueLocked(element: Any?): UndeliveredElementException? { + val old = value + val undeliveredElementException = if (old === EMPTY) null else + onUndeliveredElement?.callUndeliveredElementCatchingException(old as E) + value = element + return undeliveredElementException } override fun enqueueReceiveInternal(receive: Receive): Boolean = lock.withLock { diff --git a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt index e66bbb2279..2f46421344 100644 --- a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.selects.* * * @suppress **This an internal API and should not be used from general code.** */ -internal open class LinkedListChannel : AbstractChannel() { +internal open class LinkedListChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = true protected final override val isBufferEmpty: Boolean get() = true protected final override val isBufferAlwaysFull: Boolean get() = false diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 1b1581a99e..6e454d4b27 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -27,7 +27,11 @@ public interface ProducerScope : CoroutineScope, SendChannel { /** * Suspends the current coroutine until the channel is either [closed][SendChannel.close] or [cancelled][ReceiveChannel.cancel] - * and invokes the given [block] before resuming the coroutine. This suspending function is cancellable. + * and invokes the given [block] before resuming the coroutine. + * + * This suspending function is cancellable. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * Note that when the producer channel is cancelled, this function resumes with a cancellation exception. * Therefore, in case of cancellation, no code after the call to this function will be executed. diff --git a/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt b/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt index 700f50908c..857a97938f 100644 --- a/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/RendezvousChannel.kt @@ -4,6 +4,8 @@ package kotlinx.coroutines.channels +import kotlinx.coroutines.internal.* + /** * Rendezvous channel. This channel does not have any buffer at all. An element is transferred from sender * to receiver only when [send] and [receive] invocations meet in time (rendezvous), so [send] suspends @@ -13,7 +15,7 @@ package kotlinx.coroutines.channels * * This implementation is fully lock-free. **/ -internal open class RendezvousChannel : AbstractChannel() { +internal open class RendezvousChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement) { protected final override val isBufferAlwaysEmpty: Boolean get() = true protected final override val isBufferEmpty: Boolean get() = true protected final override val isBufferAlwaysFull: Boolean get() = true diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 8fd9ae76a4..8076d2f878 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -283,11 +283,12 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * Adjacent applications of [callbackFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are * always fused so that only one properly configured channel is used for execution. * - * Example of usage: + * Example of usage that converts a multi-short callback API to a flow. + * For single-shot callbacks use [suspendCancellableCoroutine]. * * ``` * fun flowFrom(api: CallbackBasedApi): Flow = callbackFlow { - * val callback = object : Callback { // implementation of some callback interface + * val callback = object : Callback { // Implementation of some callback interface * override fun onNextValue(value: T) { * // To avoid blocking you can configure channel capacity using * // either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill @@ -311,6 +312,10 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * awaitClose { api.unregister(callback) } * } * ``` + * + * > The callback `register`/`unregister` methods provided by an external API must be thread-safe, because + * > `awaitClose` block can be called at any time due to asynchronous nature of cancellation, even + * > concurrently with the call of the callback. */ @ExperimentalCoroutinesApi public fun callbackFlow(@BuilderInference block: suspend ProducerScope.() -> Unit): Flow = CallbackFlowBuilder(block) diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 2d3ef95aa1..c7b9b71f5b 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -20,6 +20,9 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * the channel afterwards. If you need to iterate over the channel without consuming it, * a regular `for` loop should be used instead. * + * Note, that emitting values from a channel into a flow is not atomic. A value that was received from the + * channel many not reach the flow collector if it was cancelled and will be lost. + * * This function provides a more efficient shorthand for `channel.consumeEach { value -> emit(value) }`. * See [consumeEach][ReceiveChannel.consumeEach]. */ diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt index 010d781c02..cf9575b078 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Context.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Context.kt @@ -172,13 +172,17 @@ public fun Flow.conflate(): Flow = buffer(CONFLATED) * * For more explanation of context preservation please refer to [Flow] documentation. * - * This operators retains a _sequential_ nature of flow if changing the context does not call for changing + * This operator retains a _sequential_ nature of flow if changing the context does not call for changing * the [dispatcher][CoroutineDispatcher]. Otherwise, if changing dispatcher is required, it collects * flow emissions in one coroutine that is run using a specified [context] and emits them from another coroutines * with the original collector's context using a channel with a [default][Channel.BUFFERED] buffer size * between two coroutines similarly to [buffer] operator, unless [buffer] operator is explicitly called * before or after `flowOn`, which requests buffering behavior and specifies channel size. * + * Note, that flows operating across different dispatchers might lose some in-flight elements when cancelled. + * In particular, this operator ensures that downstream flow does not resume on cancellation even if the element + * was already emitted by the upstream flow. + * * ### Operator fusion * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are diff --git a/kotlinx-coroutines-core/common/src/internal/Atomic.kt b/kotlinx-coroutines-core/common/src/internal/Atomic.kt index 94f6ab9cf2..a27d5491d1 100644 --- a/kotlinx-coroutines-core/common/src/internal/Atomic.kt +++ b/kotlinx-coroutines-core/common/src/internal/Atomic.kt @@ -39,7 +39,8 @@ public abstract class OpDescriptor { } @SharedImmutable -private val NO_DECISION: Any = Symbol("NO_DECISION") +@JvmField +internal val NO_DECISION: Any = Symbol("NO_DECISION") /** * Descriptor for multi-word atomic operation. @@ -52,9 +53,13 @@ private val NO_DECISION: Any = Symbol("NO_DECISION") * * @suppress **This is unstable API and it is subject to change.** */ +@InternalCoroutinesApi public abstract class AtomicOp : OpDescriptor() { private val _consensus = atomic(NO_DECISION) + // Returns NO_DECISION when there is not decision yet + val consensus: Any? get() = _consensus.value + val isDecided: Boolean get() = _consensus.value !== NO_DECISION /** diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index cf31fcf07d..b7b2954f6a 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -2,10 +2,10 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.coroutines +package kotlinx.coroutines.internal import kotlinx.atomicfu.* -import kotlinx.coroutines.internal.* +import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.jvm.* import kotlin.native.concurrent.* @@ -19,7 +19,7 @@ internal val REUSABLE_CLAIMED = Symbol("REUSABLE_CLAIMED") internal class DispatchedContinuation( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation -) : DispatchedTask(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation by continuation { +) : DispatchedTask(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation by continuation { @JvmField @Suppress("PropertyName") internal var _state: Any? = UNDEFINED @@ -37,19 +37,19 @@ internal class DispatchedContinuation( * 3) [REUSABLE_CLAIMED]. CC is currently being reused and its owner executes `suspend` block: * ``` * // state == null | CC - * suspendAtomicCancellableCoroutineReusable { cont -> + * suspendCancellableCoroutineReusable { cont -> * // state == REUSABLE_CLAIMED * block(cont) * } * // state == CC * ``` - * 4) [Throwable] continuation was cancelled with this cause while being in [suspendAtomicCancellableCoroutineReusable], + * 4) [Throwable] continuation was cancelled with this cause while being in [suspendCancellableCoroutineReusable], * [CancellableContinuationImpl.getResult] will check for cancellation later. * * [REUSABLE_CLAIMED] state is required to prevent the lost resume in the channel. * AbstractChannel.receive method relies on the fact that the following pattern * ``` - * suspendAtomicCancellableCoroutineReusable { cont -> + * suspendCancellableCoroutineReusable { cont -> * val result = pollFastPath() * if (result != null) cont.resume(result) * } @@ -67,12 +67,12 @@ internal class DispatchedContinuation( /* * Reusability control: * `null` -> no reusability at all, false - * If current state is not CCI, then we are within `suspendAtomicCancellableCoroutineReusable`, true + * If current state is not CCI, then we are within `suspendCancellableCoroutineReusable`, true * Else, if result is CCI === requester. * Identity check my fail for the following pattern: * ``` * loop: - * suspendAtomicCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle + * suspendCancellableCoroutineReusable { } // Reusable, outer coroutine stores the child handle * suspendCancellableCoroutine { } // **Not reusable**, handle should be disposed after {}, otherwise * it will leak because it won't be freed by `releaseInterceptedContinuation` * ``` @@ -83,7 +83,7 @@ internal class DispatchedContinuation( } /** - * Claims the continuation for [suspendAtomicCancellableCoroutineReusable] block, + * Claims the continuation for [suspendCancellableCoroutineReusable] block, * so all cancellations will be postponed. */ @Suppress("UNCHECKED_CAST") @@ -119,7 +119,7 @@ internal class DispatchedContinuation( * If continuation was cancelled, it becomes non-reusable. * * ``` - * suspendAtomicCancellableCoroutineReusable { // <- claimed + * suspendCancellableCoroutineReusable { // <- claimed * // Any asynchronous cancellation is "postponed" while this block * // is being executed * } // postponed cancellation is checked here in `getResult` @@ -180,10 +180,10 @@ internal class DispatchedContinuation( val state = result.toState() if (dispatcher.isDispatchNeeded(context)) { _state = state - resumeMode = MODE_ATOMIC_DEFAULT + resumeMode = MODE_ATOMIC dispatcher.dispatch(context, this) } else { - executeUnconfined(state, MODE_ATOMIC_DEFAULT) { + executeUnconfined(state, MODE_ATOMIC) { withCoroutineContext(this.context, countOrElement) { continuation.resumeWith(result) } @@ -194,29 +194,42 @@ internal class DispatchedContinuation( // We inline it to save an entry on the stack in cases where it shows (unconfined dispatcher) // It is used only in Continuation.resumeCancellableWith @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancellableWith(result: Result) { - val state = result.toState() + inline fun resumeCancellableWith( + result: Result, + noinline onCancellation: ((cause: Throwable) -> Unit)? + ) { + val state = result.toState(onCancellation) if (dispatcher.isDispatchNeeded(context)) { _state = state resumeMode = MODE_CANCELLABLE dispatcher.dispatch(context, this) } else { executeUnconfined(state, MODE_CANCELLABLE) { - if (!resumeCancelled()) { + if (!resumeCancelled(state)) { resumeUndispatchedWith(result) } } } } + // takeState had already cleared the state so we cancel takenState here + override fun cancelCompletedResult(takenState: Any?, cause: Throwable) { + // It is Ok to call onCancellation here without try/catch around it, since this function only faces + // a "bound" cancellation handler that performs the safe call to the user-specified code. + if (takenState is CompletedWithCancellation) { + takenState.onCancellation(cause) + } + } + @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancelled(): Boolean { + inline fun resumeCancelled(state: Any?): Boolean { val job = context[Job] if (job != null && !job.isActive) { - resumeWithException(job.getCancellationException()) + val cause = job.getCancellationException() + cancelCompletedResult(state, cause) + resumeWithException(cause) return true } - return false } @@ -245,8 +258,11 @@ internal class DispatchedContinuation( * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public fun Continuation.resumeCancellableWith(result: Result): Unit = when (this) { - is DispatchedContinuation -> resumeCancellableWith(result) +public fun Continuation.resumeCancellableWith( + result: Result, + onCancellation: ((cause: Throwable) -> Unit)? = null +): Unit = when (this) { + is DispatchedContinuation -> resumeCancellableWith(result, onCancellation) else -> resumeWith(result) } @@ -265,6 +281,7 @@ private inline fun DispatchedContinuation<*>.executeUnconfined( contState: Any?, mode: Int, doYield: Boolean = false, block: () -> Unit ): Boolean { + assert { mode != MODE_UNINITIALIZED } // invalid execution mode val eventLoop = ThreadLocalEventLoop.eventLoop // If we are yielding and unconfined queue is empty, we can bail out as part of fast path if (doYield && eventLoop.isUnconfinedQueueEmpty) return false diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 32258ba101..1f4942a358 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -8,12 +8,44 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -@PublishedApi internal const val MODE_ATOMIC_DEFAULT = 0 // schedule non-cancellable dispatch for suspendCoroutine -@PublishedApi internal const val MODE_CANCELLABLE = 1 // schedule cancellable dispatch for suspendCancellableCoroutine -@PublishedApi internal const val MODE_UNDISPATCHED = 2 // when the thread is right, but need to mark it with current coroutine +/** + * Non-cancellable dispatch mode. + * + * **DO NOT CHANGE THE CONSTANT VALUE**. It might be inlined into legacy user code that was calling + * inline `suspendAtomicCancellableCoroutine` function and did not support reuse. + */ +internal const val MODE_ATOMIC = 0 + +/** + * Cancellable dispatch mode. It is used by user-facing [suspendCancellableCoroutine]. + * Note, that implementation of cancellability checks mode via [Int.isCancellableMode] extension. + * + * **DO NOT CHANGE THE CONSTANT VALUE**. It is being into the user code from [suspendCancellableCoroutine]. + */ +@PublishedApi +internal const val MODE_CANCELLABLE = 1 + +/** + * Cancellable dispatch mode for [suspendCancellableCoroutineReusable]. + * Note, that implementation of cancellability checks mode via [Int.isCancellableMode] extension; + * implementation of reuse checks mode via [Int.isReusableMode] extension. + */ +internal const val MODE_CANCELLABLE_REUSABLE = 2 + +/** + * Undispatched mode for [CancellableContinuation.resumeUndispatched]. + * It is used when the thread is right, but it needs to be marked with the current coroutine. + */ +internal const val MODE_UNDISPATCHED = 4 -internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE -internal val Int.isDispatchedMode get() = this == MODE_ATOMIC_DEFAULT || this == MODE_CANCELLABLE +/** + * Initial mode for [DispatchedContinuation] implementation, should never be used for dispatch, because it is always + * overwritten when continuation is resumed with the actual resume mode. + */ +internal const val MODE_UNINITIALIZED = -1 + +internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE || this == MODE_CANCELLABLE_REUSABLE +internal val Int.isReusableMode get() = this == MODE_CANCELLABLE_REUSABLE internal abstract class DispatchedTask( @JvmField public var resumeMode: Int @@ -22,16 +54,32 @@ internal abstract class DispatchedTask( internal abstract fun takeState(): Any? - internal open fun cancelResult(state: Any?, cause: Throwable) {} + /** + * Called when this task was cancelled while it was being dispatched. + */ + internal open fun cancelCompletedResult(takenState: Any?, cause: Throwable) {} + /** + * There are two implementations of `DispatchedTask`: + * * [DispatchedContinuation] keeps only simple values as successfully results. + * * [CancellableContinuationImpl] keeps additional data with values and overrides this method to unwrap it. + */ @Suppress("UNCHECKED_CAST") internal open fun getSuccessfulResult(state: Any?): T = state as T - internal fun getExceptionalResult(state: Any?): Throwable? = + /** + * There are two implementations of `DispatchedTask`: + * * [DispatchedContinuation] is just an intermediate storage that stores the exception that has its stack-trace + * properly recovered and is ready to pass to the [delegate] continuation directly. + * * [CancellableContinuationImpl] stores raw cause of the failure in its state; when it needs to be dispatched + * its stack-trace has to be recovered, so it overrides this method for that purpose. + */ + internal open fun getExceptionalResult(state: Any?): Throwable? = (state as? CompletedExceptionally)?.cause public final override fun run() { + assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching val taskContext = this.taskContext var fatalException: Throwable? = null try { @@ -41,19 +89,22 @@ internal abstract class DispatchedTask( val state = takeState() // NOTE: Must take state in any case, even if cancelled withCoroutineContext(context, delegate.countOrElement) { val exception = getExceptionalResult(state) - val job = if (resumeMode.isCancellableMode) context[Job] else null /* * Check whether continuation was originally resumed with an exception. * If so, it dominates cancellation, otherwise the original exception * will be silently lost. */ - if (exception == null && job != null && !job.isActive) { + val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null + if (job != null && !job.isActive) { val cause = job.getCancellationException() - cancelResult(state, cause) + cancelCompletedResult(state, cause) continuation.resumeWithStackTrace(cause) } else { - if (exception != null) continuation.resumeWithException(exception) - else continuation.resume(getSuccessfulResult(state)) + if (exception != null) { + continuation.resumeWithException(exception) + } else { + continuation.resume(getSuccessfulResult(state)) + } } } } catch (e: Throwable) { @@ -97,8 +148,10 @@ internal abstract class DispatchedTask( } internal fun DispatchedTask.dispatch(mode: Int) { + assert { mode != MODE_UNINITIALIZED } // invalid mode value for this method val delegate = this.delegate - if (mode.isDispatchedMode && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { + val undispatched = mode == MODE_UNDISPATCHED + if (!undispatched && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { // dispatch directly using this instance's Runnable implementation val dispatcher = delegate.dispatcher val context = delegate.context @@ -108,21 +161,21 @@ internal fun DispatchedTask.dispatch(mode: Int) { resumeUnconfined() } } else { - resume(delegate, mode) + // delegate is coming from 3rd-party interceptor implementation (and does not support cancellation) + // or undispatched mode was requested + resume(delegate, undispatched) } } @Suppress("UNCHECKED_CAST") -internal fun DispatchedTask.resume(delegate: Continuation, useMode: Int) { - // slow-path - use delegate +internal fun DispatchedTask.resume(delegate: Continuation, undispatched: Boolean) { + // This resume is never cancellable. The result is always delivered to delegate continuation. val state = takeState() - val exception = getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } + val exception = getExceptionalResult(state) val result = if (exception != null) Result.failure(exception) else Result.success(getSuccessfulResult(state)) - when (useMode) { - MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) - MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) - MODE_UNDISPATCHED -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) - else -> error("Invalid mode $useMode") + when { + undispatched -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) + else -> delegate.resumeWith(result) } } @@ -134,7 +187,7 @@ private fun DispatchedTask<*>.resumeUnconfined() { } else { // Was not active -- run event loop until all unconfined tasks are executed runUnconfinedEventLoop(eventLoop) { - resume(delegate, MODE_UNDISPATCHED) + resume(delegate, undispatched = true) } } } diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index f1663c3ddc..8508e39239 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -73,6 +73,7 @@ public expect abstract class AbstractAtomicDesc : AtomicDesc { protected open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure public open fun onPrepare(prepareOp: PrepareOp): Any? // non-null on failure + public open fun onRemoved(affected: LockFreeLinkedListNode) // non-null on failure protected abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } diff --git a/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt b/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt new file mode 100644 index 0000000000..1744359e93 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/OnUndeliveredElement.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* + +internal typealias OnUndeliveredElement = (E) -> Unit + +internal fun OnUndeliveredElement.callUndeliveredElementCatchingException( + element: E, + undeliveredElementException: UndeliveredElementException? = null +): UndeliveredElementException? { + try { + invoke(element) + } catch (ex: Throwable) { + // undeliveredElementException.cause !== ex is an optimization in case the same exception is thrown + // over and over again by on OnUndeliveredElement + if (undeliveredElementException != null && undeliveredElementException.cause !== ex) { + undeliveredElementException.addSuppressedThrowable(ex) + } else { + return UndeliveredElementException("Exception in undelivered element handler for $element", ex) + } + } + return undeliveredElementException +} + +internal fun OnUndeliveredElement.callUndeliveredElement(element: E, context: CoroutineContext) { + callUndeliveredElementCatchingException(element, null)?.let { ex -> + handleCoroutineException(context, ex) + } +} + +internal fun OnUndeliveredElement.bindCancellationFun(element: E, context: CoroutineContext): (Throwable) -> Unit = + { _: Throwable -> callUndeliveredElement(element, context) } + +/** + * Internal exception that is thrown when [OnUndeliveredElement] handler in + * a [kotlinx.coroutines.channels.Channel] throws an exception. + */ +internal class UndeliveredElementException(message: String, cause: Throwable) : RuntimeException(message, cause) diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt index 1b1c389dc4..f814b152b2 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.intrinsics import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -21,9 +22,12 @@ public fun (suspend () -> T).startCoroutineCancellable(completion: Continuat * Use this function to start coroutine in a cancellable way, so that it can be cancelled * while waiting to be dispatched. */ -internal fun (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation) = +internal fun (suspend (R) -> T).startCoroutineCancellable( + receiver: R, completion: Continuation, + onCancellation: ((cause: Throwable) -> Unit)? = null +) = runSafely(completion) { - createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit)) + createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation) } /** diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 628d6f7aa5..99c54f8417 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -189,14 +189,8 @@ public interface SelectInstance { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * Atomicity of cancellation depends on the clause: [onSend][SendChannel.onSend], [onReceive][ReceiveChannel.onReceive], - * [onReceiveOrNull][ReceiveChannel.onReceiveOrNull], and [onLock][Mutex.onLock] clauses are - * *atomically cancellable*. When select throws [CancellationException] it means that those clauses had not performed - * their respective operations. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this select operation - * was already resumed on atomically cancellable clause and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 61e046c77a..73aaab5fbf 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -45,12 +45,10 @@ public interface Mutex { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended lock invocation is atomic* -- when this function - * throws [CancellationException] it means that the mutex was not locked. - * As a side-effect of atomic cancellation, a thread-bound coroutine (to some UI thread, for example) may - * continue to execute even after it was cancelled from the same thread in the case when this lock operation - * was already resumed and the continuation was posted for execution to the thread's queue. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * This function releases the lock if it was already acquired by this function before the [CancellationException] + * was thrown. * * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. @@ -124,8 +122,6 @@ public suspend inline fun Mutex.withLock(owner: Any? = null, action: () -> T @SharedImmutable private val LOCK_FAIL = Symbol("LOCK_FAIL") @SharedImmutable -private val ENQUEUE_FAIL = Symbol("ENQUEUE_FAIL") -@SharedImmutable private val UNLOCK_FAIL = Symbol("UNLOCK_FAIL") @SharedImmutable private val SELECT_SUCCESS = Symbol("SELECT_SUCCESS") @@ -194,7 +190,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { return lockSuspend(owner) } - private suspend fun lockSuspend(owner: Any?) = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable sc@ { cont -> val waiter = LockCont(owner, cont) _state.loop { state -> when (state) { @@ -254,7 +250,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { } is LockedQueue -> { check(state.owner !== owner) { "Already locked by $owner" } - val node = LockSelect(owner, this, select, block) + val node = LockSelect(owner, select, block) if (state.addLastIf(node) { _state.value === state }) { // successfully enqueued select.disposeOnSelect(node) @@ -352,7 +348,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { override fun toString(): String = "LockedQueue[$owner]" } - private abstract class LockWaiter( + private abstract inner class LockWaiter( @JvmField val owner: Any? ) : LockFreeLinkedListNode(), DisposableHandle { final override fun dispose() { remove() } @@ -360,27 +356,32 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { abstract fun completeResumeLockWaiter(token: Any) } - private class LockCont( + private inner class LockCont( owner: Any?, @JvmField val cont: CancellableContinuation ) : LockWaiter(owner) { - override fun tryResumeLockWaiter() = cont.tryResume(Unit) + override fun tryResumeLockWaiter() = cont.tryResume(Unit, idempotent = null) { + // if this continuation gets cancelled during dispatch to the caller, then release the lock + unlock(owner) + } override fun completeResumeLockWaiter(token: Any) = cont.completeResume(token) - override fun toString(): String = "LockCont[$owner, $cont]" + override fun toString(): String = "LockCont[$owner, $cont] for ${this@MutexImpl}" } - private class LockSelect( + private inner class LockSelect( owner: Any?, - @JvmField val mutex: Mutex, @JvmField val select: SelectInstance, @JvmField val block: suspend (Mutex) -> R ) : LockWaiter(owner) { override fun tryResumeLockWaiter(): Any? = if (select.trySelect()) SELECT_SUCCESS else null override fun completeResumeLockWaiter(token: Any) { assert { token === SELECT_SUCCESS } - block.startCoroutine(receiver = mutex, completion = select.completion) + block.startCoroutineCancellable(receiver = this@MutexImpl, completion = select.completion) { + // if this continuation gets cancelled during dispatch to the caller, then release the lock + unlock(owner) + } } - override fun toString(): String = "LockSelect[$owner, $mutex, $select]" + override fun toString(): String = "LockSelect[$owner, $select] for ${this@MutexImpl}" } // atomic unlock operation that checks that waiters queue is empty diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index 27c976ce3f..84b7f4f8a2 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -33,9 +33,10 @@ public interface Semaphore { * * This suspending function is cancellable. If the [Job] of the current coroutine is cancelled or completed while this * function is suspended, this function immediately resumes with [CancellationException]. - * - * *Cancellation of suspended semaphore acquisition is atomic* -- when this function - * throws [CancellationException] it means that the semaphore was not acquired. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + * This function releases the semaphore if it was already acquired by this function before the [CancellationException] + * was thrown. * * Note, that this function does not check for cancellation when it does not suspend. * Use [CoroutineScope.isActive] or [CoroutineScope.ensureActive] to periodically @@ -148,6 +149,8 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se private val _availablePermits = atomic(permits - acquiredPermits) override val availablePermits: Int get() = max(_availablePermits.value, 0) + private val onCancellationRelease = { _: Throwable -> release() } + override fun tryAcquire(): Boolean { _availablePermits.loop { p -> if (p <= 0) return false @@ -164,7 +167,7 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se acquireSlowPath() } - private suspend fun acquireSlowPath() = suspendAtomicCancellableCoroutineReusable sc@ { cont -> + private suspend fun acquireSlowPath() = suspendCancellableCoroutineReusable sc@ { cont -> while (true) { if (addAcquireToQueue(cont)) return@sc val p = _availablePermits.getAndDecrement() @@ -203,6 +206,8 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se // On CAS failure -- the cell must be either PERMIT or BROKEN // If the cell already has PERMIT from tryResumeNextFromQueue, try to grab it if (segment.cas(i, PERMIT, TAKEN)) { // took permit thus eliminating acquire/release pair + // The following resume must always succeed, since continuation was not published yet and we don't have + // to pass onCancellationRelease handle, since the coroutine did not suspend yet and cannot be cancelled cont.resume(Unit) return true } @@ -232,15 +237,15 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se return !segment.cas(i, PERMIT, BROKEN) } cellState === CANCELLED -> return false // the acquire was already cancelled - else -> return (cellState as CancellableContinuation).tryResume() + else -> return (cellState as CancellableContinuation).tryResumeAcquire() } } -} -private fun CancellableContinuation.tryResume(): Boolean { - val token = tryResume(Unit) ?: return false - completeResume(token) - return true + private fun CancellableContinuation.tryResumeAcquire(): Boolean { + val token = tryResume(Unit, null, onCancellationRelease) ?: return false + completeResume(token) + return true + } } private class CancelSemaphoreAcquisitionHandler( diff --git a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt index a9f58dd6ee..c763faf225 100644 --- a/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt +++ b/kotlinx-coroutines-core/common/test/AtomicCancellationCommonTest.kt @@ -87,23 +87,23 @@ class AtomicCancellationCommonTest : TestBase() { } @Test - fun testLockAtomicCancel() = runTest { + fun testLockCancellable() = runTest { expect(1) val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) mutex.lock() // suspends - expect(4) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) mutex.unlock() // unlock mutex first job.cancel() // cancel the job next yield() // now yield - finish(5) + finish(4) } @Test - fun testSelectLockAtomicCancel() = runTest { + fun testSelectLockCancellable() = runTest { expect(1) val mutex = Mutex(true) // locked mutex val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -114,13 +114,12 @@ class AtomicCancellationCommonTest : TestBase() { "OK" } } - assertEquals("OK", result) - expect(5) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) mutex.unlock() // unlock mutex first job.cancel() // cancel the job next yield() // now yield - finish(6) + finish(4) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt index 00f719e632..3c11182e00 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationHandlersTest.kt @@ -23,10 +23,23 @@ class CancellableContinuationHandlersTest : TestBase() { fun testDoubleSubscriptionAfterCompletion() = runTest { suspendCancellableCoroutine { c -> c.resume(Unit) - // Nothing happened - c.invokeOnCancellation { expectUnreached() } - // Cannot validate after completion + // First invokeOnCancellation is Ok c.invokeOnCancellation { expectUnreached() } + // Second invokeOnCancellation is not allowed + assertFailsWith { c.invokeOnCancellation { expectUnreached() } } + } + } + + @Test + fun testDoubleSubscriptionAfterCompletionWithException() = runTest { + assertFailsWith { + suspendCancellableCoroutine { c -> + c.resumeWithException(TestException()) + // First invokeOnCancellation is Ok + c.invokeOnCancellation { expectUnreached() } + // Second invokeOnCancellation is not allowed + assertFailsWith { c.invokeOnCancellation { expectUnreached() } } + } } } @@ -46,6 +59,59 @@ class CancellableContinuationHandlersTest : TestBase() { } } + @Test + fun testSecondSubscriptionAfterCancellation() = runTest { + try { + suspendCancellableCoroutine { c -> + // Set IOC first + c.invokeOnCancellation { + assertNull(it) + expect(2) + } + expect(1) + // then cancel (it gets called) + c.cancel() + // then try to install another one + assertFailsWith { c.invokeOnCancellation { expectUnreached() } } + } + } catch (e: CancellationException) { + finish(3) + } + } + + @Test + fun testSecondSubscriptionAfterResumeCancelAndDispatch() = runTest { + var cont: CancellableContinuation? = null + val job = launch(start = CoroutineStart.UNDISPATCHED) { + // will be cancelled during dispatch + assertFailsWith { + suspendCancellableCoroutine { c -> + cont = c + // Set IOC first -- not called (completed) + c.invokeOnCancellation { + assertTrue(it is CancellationException) + expect(4) + } + expect(1) + } + } + expect(5) + } + expect(2) + // then resume it + cont!!.resume(Unit) // schedule cancelled continuation for dispatch + // then cancel the job during dispatch + job.cancel() + expect(3) + yield() // finish dispatching (will call IOC handler here!) + expect(6) + // then try to install another one after we've done dispatching it + assertFailsWith { + cont!!.invokeOnCancellation { expectUnreached() } + } + finish(7) + } + @Test fun testDoubleSubscriptionAfterCancellationWithCause() = runTest { try { diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt index 38fc9ff281..f9f610ceb5 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt @@ -116,4 +116,26 @@ class CancellableContinuationTest : TestBase() { continuation!!.resume(Unit) // Should not fail finish(4) } + + @Test + fun testCompleteJobWhileSuspended() = runTest { + expect(1) + val completableJob = Job() + val coroutineBlock = suspend { + assertFailsWith { + suspendCancellableCoroutine { cont -> + expect(2) + assertSame(completableJob, cont.context[Job]) + completableJob.complete() + } + expectUnreached() + } + expect(3) + } + coroutineBlock.startCoroutine(Continuation(completableJob) { + assertEquals(Unit, it.getOrNull()) + expect(4) + }) + finish(5) + } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt b/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt index 39176a9a94..fbfa082555 100644 --- a/kotlinx-coroutines-core/common/test/CancellableResumeTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableResumeTest.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. */ @file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913 @@ -44,6 +44,33 @@ class CancellableResumeTest : TestBase() { expectUnreached() } + @Test + fun testResumeImmediateAfterCancelWithHandlerFailure() = runTest( + expected = { it is TestException }, + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { + expect(1) + suspendCancellableCoroutine { cont -> + expect(2) + cont.invokeOnCancellation { + expect(3) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } + cont.cancel(TestException("FAIL")) + expect(4) + cont.resume("OK") { cause -> + expect(5) + assertTrue(cause is TestException) + throw TestException3("FAIL") // onCancellation block fails with exception + } + finish(6) + } + expectUnreached() + } + @Test fun testResumeImmediateAfterIndirectCancel() = runTest( expected = { it is CancellationException } @@ -63,6 +90,33 @@ class CancellableResumeTest : TestBase() { expectUnreached() } + @Test + fun testResumeImmediateAfterIndirectCancelWithHandlerFailure() = runTest( + expected = { it is CancellationException }, + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { + expect(1) + val ctx = coroutineContext + suspendCancellableCoroutine { cont -> + expect(2) + cont.invokeOnCancellation { + expect(3) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } + ctx.cancel() + expect(4) + cont.resume("OK") { cause -> + expect(5) + throw TestException3("FAIL") // onCancellation block fails with exception + } + finish(6) + } + expectUnreached() + } + @Test fun testResumeLaterNormally() = runTest { expect(1) @@ -110,7 +164,12 @@ class CancellableResumeTest : TestBase() { } @Test - fun testResumeCancelWhileDispatched() = runTest { + fun testResumeLaterAfterCancelWithHandlerFailure() = runTest( + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { expect(1) lateinit var cc: CancellableContinuation val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -118,36 +177,117 @@ class CancellableResumeTest : TestBase() { try { suspendCancellableCoroutine { cont -> expect(3) - // resumed first, then cancelled, so no invokeOnCancellation call - cont.invokeOnCancellation { expectUnreached() } + cont.invokeOnCancellation { + expect(5) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } cc = cont } expectUnreached() } catch (e: CancellationException) { - expect(8) + finish(9) } } expect(4) + job.cancel(TestCancellationException()) + expect(6) cc.resume("OK") { cause -> expect(7) assertTrue(cause is TestCancellationException) + throw TestException3("FAIL") // onCancellation block fails with exception + } + expect(8) + } + + @Test + fun testResumeCancelWhileDispatched() = runTest { + expect(1) + lateinit var cc: CancellableContinuation + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + try { + suspendCancellableCoroutine { cont -> + expect(3) + // resumed first, dispatched, then cancelled, but still got invokeOnCancellation call + cont.invokeOnCancellation { cause -> + // Note: invokeOnCancellation is called before cc.resume(value) { ... } handler + expect(7) + assertTrue(cause is TestCancellationException) + } + cc = cont + } + expectUnreached() + } catch (e: CancellationException) { + expect(9) + } + } + expect(4) + cc.resume("OK") { cause -> + // Note: this handler is called after invokeOnCancellation handler + expect(8) + assertTrue(cause is TestCancellationException) } expect(5) job.cancel(TestCancellationException()) // cancel while execution is dispatched expect(6) yield() // to coroutine -- throws cancellation exception - finish(9) + finish(10) } + @Test + fun testResumeCancelWhileDispatchedWithHandlerFailure() = runTest( + unhandled = listOf( + { it is CompletionHandlerException && it.cause is TestException2 }, + { it is CompletionHandlerException && it.cause is TestException3 } + ) + ) { + expect(1) + lateinit var cc: CancellableContinuation + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + try { + suspendCancellableCoroutine { cont -> + expect(3) + // resumed first, dispatched, then cancelled, but still got invokeOnCancellation call + cont.invokeOnCancellation { cause -> + // Note: invokeOnCancellation is called before cc.resume(value) { ... } handler + expect(7) + assertTrue(cause is TestCancellationException) + throw TestException2("FAIL") // invokeOnCancellation handler fails with exception + } + cc = cont + } + expectUnreached() + } catch (e: CancellationException) { + expect(9) + } + } + expect(4) + cc.resume("OK") { cause -> + // Note: this handler is called after invokeOnCancellation handler + expect(8) + assertTrue(cause is TestCancellationException) + throw TestException3("FAIL") // onCancellation block fails with exception + } + expect(5) + job.cancel(TestCancellationException()) // cancel while execution is dispatched + expect(6) + yield() // to coroutine -- throws cancellation exception + finish(10) + } @Test fun testResumeUnconfined() = runTest { val outerScope = this withContext(Dispatchers.Unconfined) { val result = suspendCancellableCoroutine { - outerScope.launch { it.resume("OK", {}) } + outerScope.launch { + it.resume("OK") { + expectUnreached() + } + } } assertEquals("OK", result) } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt b/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.kt new file mode 100644 index 0000000000..b69eb22e17 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/DispatchedContinuationTest.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 + +import kotlin.coroutines.* +import kotlin.test.* + +/** + * When using [suspendCoroutine] from the standard library the continuation must be dispatched atomically, + * without checking for cancellation at any point in time. + */ +class DispatchedContinuationTest : TestBase() { + private lateinit var cont: Continuation + + @Test + fun testCancelThenResume() = runTest { + expect(1) + launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + coroutineContext[Job]!!.cancel() + // a regular suspendCoroutine will still suspend despite the fact that coroutine was cancelled + val value = suspendCoroutine { + expect(3) + cont = it + } + expect(6) + assertEquals("OK", value) + } + expect(4) + cont.resume("OK") + expect(5) + yield() // to the launched job + finish(7) + } + + @Test + fun testCancelThenResumeUnconfined() = runTest { + expect(1) + launch(Dispatchers.Unconfined) { + expect(2) + coroutineContext[Job]!!.cancel() + // a regular suspendCoroutine will still suspend despite the fact that coroutine was cancelled + val value = suspendCoroutine { + expect(3) + cont = it + } + expect(5) + assertEquals("OK", value) + } + expect(4) + cont.resume("OK") // immediately resumes -- because unconfined + finish(6) + } + + @Test + fun testResumeThenCancel() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + val value = suspendCoroutine { + expect(3) + cont = it + } + expect(7) + assertEquals("OK", value) + } + expect(4) + cont.resume("OK") + expect(5) + // now cancel the job, which the coroutine is waiting to be dispatched + job.cancel() + expect(6) + yield() // to the launched job + finish(8) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt index a6ddd81185..91d941b32c 100644 --- a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt @@ -42,7 +42,7 @@ class BasicOperationsTest : TestBase() { @Test fun testInvokeOnClose() = TestChannelKind.values().forEach { kind -> reset() - val channel = kind.create() + val channel = kind.create() channel.invokeOnClose { if (it is AssertionError) { expect(3) @@ -59,7 +59,7 @@ class BasicOperationsTest : TestBase() { fun testInvokeOnClosed() = TestChannelKind.values().forEach { kind -> reset() expect(1) - val channel = kind.create() + val channel = kind.create() channel.close() channel.invokeOnClose { expect(2) } assertFailsWith { channel.invokeOnClose { expect(3) } } @@ -69,7 +69,7 @@ class BasicOperationsTest : TestBase() { @Test fun testMultipleInvokeOnClose() = TestChannelKind.values().forEach { kind -> reset() - val channel = kind.create() + val channel = kind.create() channel.invokeOnClose { expect(3) } expect(1) assertFailsWith { channel.invokeOnClose { expect(4) } } @@ -81,7 +81,7 @@ class BasicOperationsTest : TestBase() { @Test fun testIterator() = runTest { TestChannelKind.values().forEach { kind -> - val channel = kind.create() + val channel = kind.create() val iterator = channel.iterator() assertFailsWith { iterator.next() } channel.close() @@ -91,7 +91,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testReceiveOrNull(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async(NonCancellable) { channel.receive() } @@ -108,7 +108,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testReceiveOrNullException(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async(NonCancellable) { channel.receive() } @@ -132,7 +132,7 @@ class BasicOperationsTest : TestBase() { @Suppress("ReplaceAssertBooleanWithAssertEquality") private suspend fun testReceiveOrClosed(kind: TestChannelKind) = coroutineScope { reset() - val channel = kind.create() + val channel = kind.create() launch { expect(2) channel.send(1) @@ -159,7 +159,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testOffer(kind: TestChannelKind) = coroutineScope { - val channel = kind.create() + val channel = kind.create() val d = async { channel.send(42) } yield() channel.close() @@ -184,7 +184,7 @@ class BasicOperationsTest : TestBase() { private suspend fun testSendAfterClose(kind: TestChannelKind) { assertFailsWith { coroutineScope { - val channel = kind.create() + val channel = kind.create() channel.close() launch { @@ -195,7 +195,7 @@ class BasicOperationsTest : TestBase() { } private suspend fun testSendReceive(kind: TestChannelKind, iterations: Int) = coroutineScope { - val channel = kind.create() + val channel = kind.create() launch { repeat(iterations) { channel.send(it) } channel.close() diff --git a/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt b/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt index bb3142e54c..ab1a85d697 100644 --- a/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BroadcastTest.kt @@ -63,7 +63,7 @@ class BroadcastTest : TestBase() { val a = produce { expect(3) send("MSG") - expect(5) + expectUnreached() // is not executed, because send is cancelled } expect(2) yield() // to produce @@ -72,7 +72,7 @@ class BroadcastTest : TestBase() { expect(4) yield() // to abort produce assertTrue(a.isClosedForReceive) // the source channel was consumed - finish(6) + finish(5) } @Test diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt new file mode 100644 index 0000000000..d2ef3d2691 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementFailureTest.kt @@ -0,0 +1,143 @@ +/* + * 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 kotlinx.coroutines.internal.* +import kotlinx.coroutines.selects.* +import kotlin.test.* + +/** + * Tests for failures inside `onUndeliveredElement` handler in [Channel]. + */ +class ChannelUndeliveredElementFailureTest : TestBase() { + private val item = "LOST" + private val onCancelFail: (String) -> Unit = { throw TestException(it) } + private val shouldBeUnhandled: List<(Throwable) -> Boolean> = listOf({ it.isElementCancelException() }) + + private fun Throwable.isElementCancelException() = + this is UndeliveredElementException && cause is TestException && cause!!.message == item + + @Test + fun testSendCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.send(item) + expectUnreached() + } + job.cancel() + } + + @Test + fun testSendSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onSend(item) { + expectUnreached() + } + } + } + job.cancel() + } + + @Test + fun testReceiveCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.receive() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onReceive { + expectUnreached() + } + } + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrNullCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.receiveOrNull() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrNullSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onReceiveOrNull { + expectUnreached() + } + } + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrClosedCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.receiveOrClosed() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testReceiveOrClosedSelectCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + select { + channel.onReceiveOrClosed { + expectUnreached() + } + } + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testHasNextCancelledFail() = runTest(unhandled = shouldBeUnhandled) { + val channel = Channel(onUndeliveredElement = onCancelFail) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + channel.iterator().hasNext() + expectUnreached() // will be cancelled before it dispatches + } + channel.send(item) + job.cancel() + } + + @Test + fun testChannelCancelledFail() = runTest(expected = { it.isElementCancelException()}) { + val channel = Channel(1, onUndeliveredElement = onCancelFail) + channel.send(item) + channel.cancel() + expectUnreached() + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt new file mode 100644 index 0000000000..0391e00033 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/channels/ChannelUndeliveredElementTest.kt @@ -0,0 +1,104 @@ +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlin.test.* + +class ChannelUndeliveredElementTest : TestBase() { + @Test + fun testSendSuccessfully() = runAllKindsTest { kind -> + val channel = kind.create { it.cancel() } + val res = Resource("OK") + launch { + channel.send(res) + } + val ok = channel.receive() + assertEquals("OK", ok.value) + assertFalse(res.isCancelled) // was not cancelled + channel.close() + assertFalse(res.isCancelled) // still was not cancelled + } + + @Test + fun testRendezvousSendCancelled() = runTest { + val channel = Channel { it.cancel() } + val res = Resource("OK") + val sender = launch(start = CoroutineStart.UNDISPATCHED) { + assertFailsWith { + channel.send(res) // suspends & get cancelled + } + } + sender.cancelAndJoin() + assertTrue(res.isCancelled) + } + + @Test + fun testBufferedSendCancelled() = runTest { + val channel = Channel(1) { it.cancel() } + val resA = Resource("A") + val resB = Resource("B") + val sender = launch(start = CoroutineStart.UNDISPATCHED) { + channel.send(resA) // goes to buffer + assertFailsWith { + channel.send(resB) // suspends & get cancelled + } + } + sender.cancelAndJoin() + assertFalse(resA.isCancelled) // it is in buffer, not cancelled + assertTrue(resB.isCancelled) // send was cancelled + channel.cancel() // now cancel the channel + assertTrue(resA.isCancelled) // now cancelled in buffer + } + + @Test + fun testConflatedResourceCancelled() = runTest { + val channel = Channel(Channel.CONFLATED) { it.cancel() } + val resA = Resource("A") + val resB = Resource("B") + channel.send(resA) + assertFalse(resA.isCancelled) + assertFalse(resB.isCancelled) + channel.send(resB) + assertTrue(resA.isCancelled) // it was conflated (lost) and thus cancelled + assertFalse(resB.isCancelled) + channel.close() + assertFalse(resB.isCancelled) // not cancelled yet, can be still read by receiver + channel.cancel() + assertTrue(resB.isCancelled) // now it is cancelled + } + + @Test + fun testSendToClosedChannel() = runAllKindsTest { kind -> + val channel = kind.create { it.cancel() } + channel.close() // immediately close channel + val res = Resource("OK") + assertFailsWith { + channel.send(res) // send fails to closed channel, resource was not delivered + } + assertTrue(res.isCancelled) + } + + private fun runAllKindsTest(test: suspend CoroutineScope.(TestChannelKind) -> Unit) { + for (kind in TestChannelKind.values()) { + if (kind.viaBroadcast) continue // does not support onUndeliveredElement + try { + runTest { + test(kind) + } + } catch(e: Throwable) { + error("$kind: $e", e) + } + } + } + + private class Resource(val value: String) { + private val _cancelled = atomic(false) + + val isCancelled: Boolean + get() = _cancelled.value + + fun cancel() { + check(!_cancelled.getAndSet(true)) { "Already cancelled" } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index 4deb3858f0..eca19458be 100644 --- a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt @@ -21,7 +21,7 @@ class ConflatedChannelTest : TestBase() { @Test fun testConflatedSend() = runTest { - val q = ConflatedChannel() + val q = Channel(Channel.CONFLATED) q.send(1) q.send(2) // shall conflated previously sent assertEquals(2, q.receiveOrNull()) diff --git a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt index 69d8fd03e3..42330f1cae 100644 --- a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt +++ b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt @@ -7,9 +7,10 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.selects.* -enum class TestChannelKind(val capacity: Int, - private val description: String, - private val viaBroadcast: Boolean = false +enum class TestChannelKind( + val capacity: Int, + private val description: String, + val viaBroadcast: Boolean = false ) { RENDEZVOUS(0, "RendezvousChannel"), ARRAY_1(1, "ArrayChannel(1)"), @@ -22,8 +23,11 @@ enum class TestChannelKind(val capacity: Int, CONFLATED_BROADCAST(Channel.CONFLATED, "ConflatedBroadcastChannel", viaBroadcast = true) ; - fun create(): Channel = if (viaBroadcast) ChannelViaBroadcast(BroadcastChannel(capacity)) - else Channel(capacity) + fun create(onUndeliveredElement: ((T) -> Unit)? = null): Channel = when { + viaBroadcast && onUndeliveredElement != null -> error("Broadcast channels to do not support onUndeliveredElement") + viaBroadcast -> ChannelViaBroadcast(BroadcastChannel(capacity)) + else -> Channel(capacity, onUndeliveredElement) + } val isConflated get() = capacity == Channel.CONFLATED override fun toString(): String = description diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt index 802ba1ef2f..eedfac2ea3 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CatchTest.kt @@ -134,15 +134,14 @@ class CatchTest : TestBase() { // flowOn with a different dispatcher introduces asynchrony so that all exceptions in the // upstream flows are handled before they go downstream .onEach { value -> - expect(8) - assertEquals("OK", value) + expectUnreached() // already cancelled } .catch { e -> - expect(9) + expect(8) assertTrue(e is TestException) assertSame(d0, kotlin.coroutines.coroutineContext[ContinuationInterceptor] as CoroutineContext) } .collect() - finish(10) + finish(9) } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt index a619355b68..2893321998 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.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 @@ -238,6 +238,22 @@ abstract class CombineTestBase : TestBase() { assertFailsWith(flow) finish(7) } + + @Test + fun testCancelledCombine() = runTest( + expected = { it is CancellationException } + ) { + coroutineScope { + val flow = flow { + emit(Unit) // emit + } + cancel() // cancel the scope + flow.combineLatest(flow) { u, _ -> u }.collect { + // should not be reached, because cancelled before it runs + expectUnreached() + } + } + } } class CombineTest : CombineTestBase() { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt index f8350ff584..0eae1a3860 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -341,4 +341,19 @@ class FlowOnTest : TestBase() { assertEquals(expected, value) } } + + @Test + fun testCancelledFlowOn() = runTest { + assertFailsWith { + coroutineScope { + flow { + emit(Unit) // emit to buffer + cancel() // now cancel + }.flowOn(wrapperDispatcher()).collect { + // should not be reached, because cancelled before it runs + expectUnreached() + } + } + } + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index b28320c391..5f2b5a74cd 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -67,14 +67,12 @@ class ZipTest : TestBase() { val f1 = flow { emit("1") emit("2") - hang { - expect(1) - } + expectUnreached() // the above emit will get cancelled because f2 ends } val f2 = flowOf("a", "b") assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) - finish(2) + finish(1) } @Test @@ -92,25 +90,6 @@ class ZipTest : TestBase() { finish(2) } - @Test - fun testCancelWhenFlowIsDone2() = runTest { - val f1 = flow { - emit("1") - emit("2") - try { - emit("3") - expectUnreached() - } finally { - expect(1) - } - - } - - val f2 = flowOf("a", "b") - assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) - finish(2) - } - @Test fun testContextIsIsolatedReversed() = runTest { val f1 = flow { diff --git a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt index 5af68f6be5..e31ccfc16d 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectLoopTest.kt @@ -24,19 +24,20 @@ class SelectLoopTest : TestBase() { expect(3) throw TestException() } - var isDone = false - while (!isDone) { - select { - channel.onReceiveOrNull { - expect(4) - assertEquals(Unit, it) - } - job.onJoin { - expect(5) - isDone = true + try { + while (true) { + select { + channel.onReceiveOrNull { + expectUnreached() + } + job.onJoin { + expectUnreached() + } } } + } catch (e: CancellationException) { + // select will get cancelled because of the failure of job + finish(4) } - finish(6) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/js/src/Promise.kt b/kotlinx-coroutines-core/js/src/Promise.kt index 6c3de76426..ab2003236a 100644 --- a/kotlinx-coroutines-core/js/src/Promise.kt +++ b/kotlinx-coroutines-core/js/src/Promise.kt @@ -62,6 +62,8 @@ public fun Promise.asDeferred(): Deferred { * This suspending function is cancellable. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function * stops waiting for the promise and immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. */ public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> this@await.then( diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index 342b11c69a..b69850576e 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -124,6 +124,8 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { return null } + actual open fun onRemoved(affected: Node) {} + actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode val failure = failure(affected) diff --git a/kotlinx-coroutines-core/jvm/src/DebugStrings.kt b/kotlinx-coroutines-core/jvm/src/DebugStrings.kt index 184fb655e3..2ccfebc6d3 100644 --- a/kotlinx-coroutines-core/jvm/src/DebugStrings.kt +++ b/kotlinx-coroutines-core/jvm/src/DebugStrings.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlin.coroutines.* // internal debugging tools for string representation diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index 29f37dac28..97f9978139 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -416,20 +416,26 @@ public actual open class LockFreeLinkedListNode { val next = this.next val removed = next.removed() if (affected._next.compareAndSet(this, removed)) { + // The element was actually removed + desc.onRemoved(affected) // Complete removal operation here. It bails out if next node is also removed and it becomes // responsibility of the next's removes to call correctPrev which would help fix all the links. next.correctPrev(null) } return REMOVE_PREPARED } - val isDecided = if (decision != null) { + // We need to ensure progress even if it operation result consensus was already decided + val consensus = if (decision != null) { // some other logic failure, including RETRY_ATOMIC -- reach consensus on decision fail reason ASAP atomicOp.decide(decision) - true // atomicOp.isDecided will be true as a result } else { - atomicOp.isDecided // consult with current decision status like in Harris DCSS + atomicOp.consensus // consult with current decision status like in Harris DCSS + } + val update: Any = when { + consensus === NO_DECISION -> atomicOp // desc.onPrepare returned null -> start doing atomic op + consensus == null -> desc.updatedNext(affected, next) // move forward if consensus on success + else -> next // roll back if consensus if failure } - val update: Any = if (isDecided) next else atomicOp // restore if decision was already reached affected._next.compareAndSet(this, update) return null } @@ -445,9 +451,10 @@ public actual open class LockFreeLinkedListNode { protected open fun takeAffectedNode(op: OpDescriptor): Node? = affectedNode!! // null for RETRY_ATOMIC protected open fun failure(affected: Node): Any? = null // next: Node | Removed protected open fun retry(affected: Node, next: Any): Boolean = false // next: Node | Removed - protected abstract fun updatedNext(affected: Node, next: Node): Any protected abstract fun finishOnSuccess(affected: Node, next: Node) + public abstract fun updatedNext(affected: Node, next: Node): Any + public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure @@ -456,6 +463,8 @@ public actual open class LockFreeLinkedListNode { return null } + public open fun onRemoved(affected: Node) {} // called once when node was prepared & later removed + @Suppress("UNCHECKED_CAST") final override fun prepare(op: AtomicOp<*>): Any? { while (true) { // lock free loop on next diff --git a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt b/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt index 8a7dce01ee..2612b84153 100644 --- a/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AtomicCancellationTest.kt @@ -9,25 +9,24 @@ import kotlinx.coroutines.selects.* import kotlin.test.* class AtomicCancellationTest : TestBase() { - @Test - fun testSendAtomicCancel() = runBlocking { + fun testSendCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) channel.send(42) // suspends - expect(4) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) assertEquals(42, channel.receive()) // will schedule sender for further execution job.cancel() // cancel the job next yield() // now yield - finish(5) + finish(4) } @Test - fun testSelectSendAtomicCancel() = runBlocking { + fun testSelectSendCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -38,34 +37,33 @@ class AtomicCancellationTest : TestBase() { "OK" } } - assertEquals("OK", result) - expect(5) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) assertEquals(42, channel.receive()) // will schedule sender for further execution job.cancel() // cancel the job next yield() // now yield - finish(6) + finish(4) } @Test - fun testReceiveAtomicCancel() = runBlocking { + fun testReceiveCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { expect(2) assertEquals(42, channel.receive()) // suspends - expect(4) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) channel.send(42) // will schedule receiver for further execution job.cancel() // cancel the job next yield() // now yield - finish(5) + finish(4) } @Test - fun testSelectReceiveAtomicCancel() = runBlocking { + fun testSelectReceiveCancellable() = runBlocking { expect(1) val channel = Channel() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -77,14 +75,13 @@ class AtomicCancellationTest : TestBase() { "OK" } } - assertEquals("OK", result) - expect(5) // should execute despite cancellation + expectUnreached() // should NOT execute because of cancellation } expect(3) channel.send(42) // will schedule receiver for further execution job.cancel() // cancel the job next yield() // now yield - finish(6) + finish(4) } @Test diff --git a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt b/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt index ec3635ca36..50d86f32be 100644 --- a/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JobStructuredJoinStressTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import org.junit.* +import kotlin.coroutines.* /** * Test a race between job failure and join. @@ -12,22 +13,52 @@ import org.junit.* * See [#1123](https://github.com/Kotlin/kotlinx.coroutines/issues/1123). */ class JobStructuredJoinStressTest : TestBase() { - private val nRepeats = 1_000 * stressTestMultiplier + private val nRepeats = 10_000 * stressTestMultiplier @Test - fun testStress() { - repeat(nRepeats) { + fun testStressRegularJoin() { + stress(Job::join) + } + + @Test + fun testStressSuspendCancellable() { + stress { job -> + suspendCancellableCoroutine { cont -> + job.invokeOnCompletion { cont.resume(Unit) } + } + } + } + + @Test + fun testStressSuspendCancellableReusable() { + stress { job -> + suspendCancellableCoroutineReusable { cont -> + job.invokeOnCompletion { cont.resume(Unit) } + } + } + } + + private fun stress(join: suspend (Job) -> Unit) { + expect(1) + repeat(nRepeats) { index -> assertFailsWith { runBlocking { // launch in background val job = launch(Dispatchers.Default) { throw TestException("OK") // crash } - assertFailsWith { - job.join() + try { + join(job) + error("Should not complete successfully") + } catch (e: CancellationException) { + // must always crash with cancellation exception + expect(2 + index) + } catch (e: Throwable) { + error("Unexpected exception", e) } } } } + finish(2 + nRepeats) } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt index 892a2a62d4..56f1e28313 100644 --- a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt @@ -11,15 +11,14 @@ import kotlin.coroutines.* import kotlin.test.* class ReusableCancellableContinuationTest : TestBase() { - @Test fun testReusable() = runTest { - testContinuationsCount(10, 1, ::suspendAtomicCancellableCoroutineReusable) + testContinuationsCount(10, 1, ::suspendCancellableCoroutineReusable) } @Test fun testRegular() = runTest { - testContinuationsCount(10, 10, ::suspendAtomicCancellableCoroutine) + testContinuationsCount(10, 10, ::suspendCancellableCoroutine) } private suspend inline fun CoroutineScope.testContinuationsCount( @@ -51,7 +50,7 @@ class ReusableCancellableContinuationTest : TestBase() { fun testCancelledOnClaimedCancel() = runTest { expect(1) try { - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { it.cancel() } expectUnreached() @@ -65,7 +64,7 @@ class ReusableCancellableContinuationTest : TestBase() { expect(1) // Bind child at first var continuation: Continuation<*>? = null - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { expect(2) continuation = it launch { // Attach to the parent, avoid fast path @@ -77,13 +76,16 @@ class ReusableCancellableContinuationTest : TestBase() { ensureActive() // Verify child was bound FieldWalker.assertReachableCount(1, coroutineContext[Job]) { it === continuation } - suspendAtomicCancellableCoroutineReusable { - expect(5) - coroutineContext[Job]!!.cancel() - it.resume(Unit) + try { + suspendCancellableCoroutineReusable { + expect(5) + coroutineContext[Job]!!.cancel() + it.resume(Unit) // will not dispatch, will get CancellationException + } + } catch (e: CancellationException) { + assertFalse(isActive) + finish(6) } - assertFalse(isActive) - finish(6) } @Test @@ -93,7 +95,7 @@ class ReusableCancellableContinuationTest : TestBase() { launch { cont!!.resumeWith(Result.success(Unit)) } - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { cont = it } ensureActive() @@ -108,7 +110,7 @@ class ReusableCancellableContinuationTest : TestBase() { launch { // Attach to the parent, avoid fast path cont!!.resumeWith(Result.success(Unit)) } - suspendAtomicCancellableCoroutine { + suspendCancellableCoroutine { cont = it } ensureActive() @@ -121,7 +123,7 @@ class ReusableCancellableContinuationTest : TestBase() { expect(1) var cont: Continuation<*>? = null try { - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { cont = it it.cancel() } @@ -137,7 +139,7 @@ class ReusableCancellableContinuationTest : TestBase() { val currentJob = coroutineContext[Job]!! expect(1) // Bind child at first - suspendAtomicCancellableCoroutineReusable { + suspendCancellableCoroutineReusable { expect(2) // Attach to the parent, avoid fast path launch { @@ -153,15 +155,23 @@ class ReusableCancellableContinuationTest : TestBase() { assertFalse(isActive) // Child detached FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } - suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } - FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - + expect(5) + try { + // Resume is non-atomic, so it throws cancellation exception + suspendCancellableCoroutineReusable { + expect(6) // but the code inside the block is executed + it.resume(Unit) + } + } catch (e: CancellationException) { + FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } + expect(7) + } try { - suspendAtomicCancellableCoroutineReusable {} + // No resume -- still cancellation exception + suspendCancellableCoroutineReusable {} } catch (e: CancellationException) { FieldWalker.assertReachableCount(0, currentJob) { it is CancellableContinuation<*> } - finish(5) + finish(8) } } diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index bf462cc78f..26c176319e 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -69,6 +69,8 @@ public actual open class TestBase actual constructor() { throw makeError(message, cause) } + public fun hasError() = error.get() != null + private fun makeError(message: Any, cause: Throwable? = null): IllegalStateException = IllegalStateException(message.toString(), cause).also { setError(it) diff --git a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt index 54ba7b639f..2e73b2432a 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/BroadcastChannelMultiReceiveStressTest.kt @@ -48,8 +48,9 @@ class BroadcastChannelMultiReceiveStressTest( launch(pool + CoroutineName("Sender")) { var i = 0L while (isActive) { - broadcast.send(++i) - sentTotal.set(i) // set sentTotal only if `send` was not cancelled + i++ + broadcast.send(i) // could be cancelled + sentTotal.set(i) // only was for it if it was not cancelled } } val receivers = mutableListOf() @@ -88,10 +89,8 @@ class BroadcastChannelMultiReceiveStressTest( try { withTimeout(5000) { receivers.forEachIndexed { index, receiver -> - if (lastReceived[index].get() == total) - receiver.cancel() - else - receiver.join() + if (lastReceived[index].get() >= total) receiver.cancel() + receiver.join() } } } catch (e: Exception) { @@ -112,7 +111,7 @@ class BroadcastChannelMultiReceiveStressTest( check(i == last + 1) { "Last was $last, got $i" } receivedTotal.incrementAndGet() lastReceived[receiverIndex].set(i) - return i == stopOnReceive.get() + return i >= stopOnReceive.get() } private suspend fun doReceive(channel: ReceiveChannel, receiverIndex: Int) { diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt deleted file mode 100644 index 6556888a0f..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2016-2019 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 kotlinx.coroutines.selects.* -import org.junit.After -import org.junit.Test -import org.junit.runner.* -import org.junit.runners.* -import kotlin.random.Random -import java.util.concurrent.atomic.* -import kotlin.test.* - -/** - * Tests cancel atomicity for channel send & receive operations, including their select versions. - */ -@RunWith(Parameterized::class) -class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBase() { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun params(): Collection> = TestChannelKind.values().map { arrayOf(it) } - } - - private val TEST_DURATION = 1000L * stressTestMultiplier - - private val dispatcher = newFixedThreadPoolContext(2, "ChannelAtomicCancelStressTest") - private val scope = CoroutineScope(dispatcher) - - private val channel = kind.create() - private val senderDone = Channel(1) - private val receiverDone = Channel(1) - - private var lastSent = 0 - private var lastReceived = 0 - - private var stoppedSender = 0 - private var stoppedReceiver = 0 - - private var missedCnt = 0 - private var dupCnt = 0 - - val failed = AtomicReference() - - lateinit var sender: Job - lateinit var receiver: Job - - @After - fun tearDown() { - dispatcher.close() - } - - fun fail(e: Throwable) = failed.compareAndSet(null, e) - - private inline fun cancellable(done: Channel, block: () -> Unit) { - try { - block() - } finally { - if (!done.offer(true)) - fail(IllegalStateException("failed to offer to done channel")) - } - } - - @Test - fun testAtomicCancelStress() = runBlocking { - println("--- ChannelAtomicCancelStressTest $kind") - val deadline = System.currentTimeMillis() + TEST_DURATION - launchSender() - launchReceiver() - while (System.currentTimeMillis() < deadline && failed.get() == null) { - when (Random.nextInt(3)) { - 0 -> { // cancel & restart sender - stopSender() - launchSender() - } - 1 -> { // cancel & restart receiver - stopReceiver() - launchReceiver() - } - 2 -> yield() // just yield (burn a little time) - } - } - stopSender() - stopReceiver() - println(" Sent $lastSent ints to channel") - println(" Received $lastReceived ints from channel") - println(" Stopped sender $stoppedSender times") - println("Stopped receiver $stoppedReceiver times") - println(" Missed $missedCnt ints") - println(" Duplicated $dupCnt ints") - failed.get()?.let { throw it } - assertEquals(0, dupCnt) - if (!kind.isConflated) { - assertEquals(0, missedCnt) - assertEquals(lastSent, lastReceived) - } - } - - private fun launchSender() { - sender = scope.launch(start = CoroutineStart.ATOMIC) { - cancellable(senderDone) { - var counter = 0 - while (true) { - val trySend = lastSent + 1 - when (Random.nextInt(2)) { - 0 -> channel.send(trySend) - 1 -> select { channel.onSend(trySend) {} } - else -> error("cannot happen") - } - lastSent = trySend // update on success - when { - // must artificially slow down LINKED_LIST sender to avoid overwhelming receiver and going OOM - kind == TestChannelKind.LINKED_LIST -> while (lastSent > lastReceived + 100) yield() - // yield periodically to check cancellation on conflated channels - kind.isConflated -> if (counter++ % 100 == 0) yield() - } - } - } - } - } - - private suspend fun stopSender() { - stoppedSender++ - sender.cancel() - senderDone.receive() - } - - private fun launchReceiver() { - receiver = scope.launch(start = CoroutineStart.ATOMIC) { - cancellable(receiverDone) { - while (true) { - val received = when (Random.nextInt(2)) { - 0 -> channel.receive() - 1 -> select { channel.onReceive { it } } - else -> error("cannot happen") - } - val expected = lastReceived + 1 - if (received > expected) - missedCnt++ - if (received < expected) - dupCnt++ - lastReceived = received - } - } - } - } - - private suspend fun stopReceiver() { - stoppedReceiver++ - receiver.cancel() - receiverDone.receive() - } -} diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt new file mode 100644 index 0000000000..76713aa173 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelCancelUndeliveredElementStressTest.kt @@ -0,0 +1,102 @@ +/* + * 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 kotlinx.coroutines.selects.* +import java.util.concurrent.atomic.* +import kotlin.random.* +import kotlin.test.* + +class ChannelCancelUndeliveredElementStressTest : TestBase() { + private val repeatTimes = 10_000 * stressTestMultiplier + + // total counters + private var sendCnt = 0 + private var offerFailedCnt = 0 + private var receivedCnt = 0 + private var undeliveredCnt = 0 + + // last operation + private var lastReceived = 0 + private var dSendCnt = 0 + private var dSendExceptionCnt = 0 + private var dOfferFailedCnt = 0 + private var dReceivedCnt = 0 + private val dUndeliveredCnt = AtomicInteger() + + @Test + fun testStress() = runTest { + repeat(repeatTimes) { + val channel = Channel(1) { dUndeliveredCnt.incrementAndGet() } + val j1 = launch(Dispatchers.Default) { + sendOne(channel) // send first + sendOne(channel) // send second + } + val j2 = launch(Dispatchers.Default) { + receiveOne(channel) // receive one element from the channel + channel.cancel() // cancel the channel + } + + joinAll(j1, j2) + + // All elements must be either received or undelivered (IN every run) + if (dSendCnt - dOfferFailedCnt != dReceivedCnt + dUndeliveredCnt.get()) { + println(" Send: $dSendCnt") + println("Send Exception: $dSendExceptionCnt") + println(" Offer failed: $dOfferFailedCnt") + println(" Received: $dReceivedCnt") + println(" Undelivered: ${dUndeliveredCnt.get()}") + error("Failed") + } + offerFailedCnt += dOfferFailedCnt + receivedCnt += dReceivedCnt + undeliveredCnt += dUndeliveredCnt.get() + // clear for next run + dSendCnt = 0 + dSendExceptionCnt = 0 + dOfferFailedCnt = 0 + dReceivedCnt = 0 + dUndeliveredCnt.set(0) + } + // Stats + println(" Send: $sendCnt") + println(" Offer failed: $offerFailedCnt") + println(" Received: $receivedCnt") + println(" Undelivered: $undeliveredCnt") + assertEquals(sendCnt - offerFailedCnt, receivedCnt + undeliveredCnt) + } + + private suspend fun sendOne(channel: Channel) { + dSendCnt++ + val i = ++sendCnt + try { + when (Random.nextInt(2)) { + 0 -> channel.send(i) + 1 -> if (!channel.offer(i)) { + dOfferFailedCnt++ + } + } + } catch(e: Throwable) { + assertTrue(e is CancellationException) // the only exception possible in this test + dSendExceptionCnt++ + throw e + } + } + + private suspend fun receiveOne(channel: Channel) { + val received = when (Random.nextInt(3)) { + 0 -> channel.receive() + 1 -> channel.receiveOrNull() ?: error("Cannot be closed yet") + 2 -> select { + channel.onReceive { it } + } + else -> error("Cannot happen") + } + assertTrue(received > lastReceived) + dReceivedCnt++ + lastReceived = received + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt index 00c5a6090f..f414c33338 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelSendReceiveStressTest.kt @@ -35,7 +35,7 @@ class ChannelSendReceiveStressTest( private val maxBuffer = 10_000 // artificial limit for LinkedListChannel - val channel = kind.create() + val channel = kind.create() private val sendersCompleted = AtomicInteger() private val receiversCompleted = AtomicInteger() private val dupes = AtomicInteger() diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt new file mode 100644 index 0000000000..1188329a4c --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelUndeliveredElementStressTest.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import org.junit.After +import org.junit.Test +import org.junit.runner.* +import org.junit.runners.* +import kotlin.random.Random +import kotlin.test.* + +/** + * Tests resource transfer via channel send & receive operations, including their select versions, + * using `onUndeliveredElement` to detect lost resources and close them properly. + */ +@RunWith(Parameterized::class) +class ChannelUndeliveredElementStressTest(private val kind: TestChannelKind) : TestBase() { + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun params(): Collection> = + TestChannelKind.values() + .filter { !it.viaBroadcast } + .map { arrayOf(it) } + } + + private val iterationDurationMs = 100L + private val testIterations = 20 * stressTestMultiplier // 2 sec + + private val dispatcher = newFixedThreadPoolContext(2, "ChannelAtomicCancelStressTest") + private val scope = CoroutineScope(dispatcher) + + private val channel = kind.create { it.failedToDeliver() } + private val senderDone = Channel(1) + private val receiverDone = Channel(1) + + @Volatile + private var lastReceived = -1L + + private var stoppedSender = 0L + private var stoppedReceiver = 0L + + private var sentCnt = 0L // total number of send attempts + private var receivedCnt = 0L // actually received successfully + private var dupCnt = 0L // duplicates (should never happen) + private val failedToDeliverCnt = atomic(0L) // out of sent + + private val modulo = 1 shl 25 + private val mask = (modulo - 1).toLong() + private val sentStatus = ItemStatus() // 1 - send norm, 2 - send select, +2 - did not throw exception + private val receivedStatus = ItemStatus() // 1-6 received + private val failedStatus = ItemStatus() // 1 - failed + + lateinit var sender: Job + lateinit var receiver: Job + + @After + fun tearDown() { + dispatcher.close() + } + + private inline fun cancellable(done: Channel, block: () -> Unit) { + try { + block() + } finally { + if (!done.offer(true)) + error(IllegalStateException("failed to offer to done channel")) + } + } + + @Test + fun testAtomicCancelStress() = runBlocking { + println("=== ChannelAtomicCancelStressTest $kind") + var nextIterationTime = System.currentTimeMillis() + iterationDurationMs + var iteration = 0 + launchSender() + launchReceiver() + while (!hasError()) { + if (System.currentTimeMillis() >= nextIterationTime) { + nextIterationTime += iterationDurationMs + iteration++ + verify(iteration) + if (iteration % 10 == 0) printProgressSummary(iteration) + if (iteration >= testIterations) break + launchSender() + launchReceiver() + } + when (Random.nextInt(3)) { + 0 -> { // cancel & restart sender + stopSender() + launchSender() + } + 1 -> { // cancel & restart receiver + stopReceiver() + launchReceiver() + } + 2 -> yield() // just yield (burn a little time) + } + } + } + + private suspend fun verify(iteration: Int) { + stopSender() + drainReceiver() + stopReceiver() + try { + assertEquals(0, dupCnt) + assertEquals(sentCnt - failedToDeliverCnt.value, receivedCnt) + } catch (e: Throwable) { + printProgressSummary(iteration) + printErrorDetails() + throw e + } + sentStatus.clear() + receivedStatus.clear() + failedStatus.clear() + } + + private fun printProgressSummary(iteration: Int) { + println("--- ChannelAtomicCancelStressTest $kind -- $iteration of $testIterations") + println(" Sent $sentCnt times to channel") + println(" Received $receivedCnt times from channel") + println(" Failed to deliver ${failedToDeliverCnt.value} times") + println(" Stopped sender $stoppedSender times") + println(" Stopped receiver $stoppedReceiver times") + println(" Duplicated $dupCnt deliveries") + } + + private fun printErrorDetails() { + val min = minOf(sentStatus.min, receivedStatus.min, failedStatus.min) + val max = maxOf(sentStatus.max, receivedStatus.max, failedStatus.max) + for (x in min..max) { + val sentCnt = if (sentStatus[x] != 0) 1 else 0 + val receivedCnt = if (receivedStatus[x] != 0) 1 else 0 + val failedToDeliverCnt = failedStatus[x] + if (sentCnt - failedToDeliverCnt != receivedCnt) { + println("!!! Error for value $x: " + + "sentStatus=${sentStatus[x]}, " + + "receivedStatus=${receivedStatus[x]}, " + + "failedStatus=${failedStatus[x]}" + ) + } + } + } + + + private fun launchSender() { + sender = scope.launch(start = CoroutineStart.ATOMIC) { + cancellable(senderDone) { + var counter = 0 + while (true) { + val trySendData = Data(sentCnt++) + val sendMode = Random.nextInt(2) + 1 + sentStatus[trySendData.x] = sendMode + when (sendMode) { + 1 -> channel.send(trySendData) + 2 -> select { channel.onSend(trySendData) {} } + else -> error("cannot happen") + } + sentStatus[trySendData.x] = sendMode + 2 + when { + // must artificially slow down LINKED_LIST sender to avoid overwhelming receiver and going OOM + kind == TestChannelKind.LINKED_LIST -> while (sentCnt > lastReceived + 100) yield() + // yield periodically to check cancellation on conflated channels + kind.isConflated -> if (counter++ % 100 == 0) yield() + } + } + } + } + } + + private suspend fun stopSender() { + stoppedSender++ + sender.cancel() + senderDone.receive() + } + + private fun launchReceiver() { + receiver = scope.launch(start = CoroutineStart.ATOMIC) { + cancellable(receiverDone) { + while (true) { + val receiveMode = Random.nextInt(6) + 1 + val receivedData = when (receiveMode) { + 1 -> channel.receive() + 2 -> select { channel.onReceive { it } } + 3 -> channel.receiveOrNull() ?: error("Should not be closed") + 4 -> select { channel.onReceiveOrNull { it ?: error("Should not be closed") } } + 5 -> channel.receiveOrClosed().value + 6 -> { + val iterator = channel.iterator() + check(iterator.hasNext()) { "Should not be closed" } + iterator.next() + } + else -> error("cannot happen") + } + receivedCnt++ + val received = receivedData.x + if (received <= lastReceived) + dupCnt++ + lastReceived = received + receivedStatus[received] = receiveMode + } + } + } + } + + private suspend fun drainReceiver() { + while (!channel.isEmpty) yield() // burn time until receiver gets it all + } + + private suspend fun stopReceiver() { + stoppedReceiver++ + receiver.cancel() + receiverDone.receive() + } + + private inner class Data(val x: Long) { + private val failedToDeliver = atomic(false) + + fun failedToDeliver() { + check(failedToDeliver.compareAndSet(false, true)) { "onUndeliveredElement notified twice" } + failedToDeliverCnt.incrementAndGet() + failedStatus[x] = 1 + } + } + + inner class ItemStatus { + private val a = ByteArray(modulo) + private val _min = atomic(Long.MAX_VALUE) + private val _max = atomic(-1L) + + val min: Long get() = _min.value + val max: Long get() = _max.value + + operator fun set(x: Long, value: Int) { + a[(x and mask).toInt()] = value.toByte() + _min.update { y -> minOf(x, y) } + _max.update { y -> maxOf(x, y) } + } + + operator fun get(x: Long): Int = a[(x and mask).toInt()].toInt() + + fun clear() { + if (_max.value < 0) return + for (x in _min.value.._max.value) a[(x and mask).toInt()] = 0 + _min.value = Long.MAX_VALUE + _max.value = -1L + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt index 864a0b4c2e..888522c63c 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/InvokeOnCloseStressTest.kt @@ -39,7 +39,7 @@ class InvokeOnCloseStressTest : TestBase(), CoroutineScope { private suspend fun runStressTest(kind: TestChannelKind) { repeat(iterations) { val counter = AtomicInteger(0) - val channel = kind.create() + val channel = kind.create() val latch = CountDownLatch(1) val j1 = async { diff --git a/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt b/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt index 07c431bb4d..eeddfb5f49 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/SimpleSendReceiveJvmTest.kt @@ -28,7 +28,7 @@ class SimpleSendReceiveJvmTest( } } - val channel = kind.create() + val channel = kind.create() @Test fun testSimpleSendReceive() = runBlocking { diff --git a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt index 8ecb8fd741..bb713b258d 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.sync import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* import kotlin.test.* class MutexStressTest : TestBase() { @@ -26,4 +27,67 @@ class MutexStressTest : TestBase() { jobs.forEach { it.join() } assertEquals(n * k, shared) } + + @Test + fun stressUnlockCancelRace() = runTest { + val n = 10_000 * stressTestMultiplier + val mutex = Mutex(true) // create a locked mutex + newSingleThreadContext("SemaphoreStressTest").use { pool -> + repeat (n) { + // Initially, we hold the lock and no one else can `lock`, + // otherwise it's a bug. + assertTrue(mutex.isLocked) + var job1EnteredCriticalSection = false + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + mutex.lock() + job1EnteredCriticalSection = true + mutex.unlock() + } + // check that `job1` didn't finish the call to `acquire()` + assertEquals(false, job1EnteredCriticalSection) + val job2 = launch(pool) { + mutex.unlock() + } + // Because `job2` executes in a separate thread, this + // cancellation races with the call to `unlock()`. + job1.cancelAndJoin() + job2.join() + assertFalse(mutex.isLocked) + mutex.lock() + } + } + } + + @Test + fun stressUnlockCancelRaceWithSelect() = runTest { + val n = 10_000 * stressTestMultiplier + val mutex = Mutex(true) // create a locked mutex + newSingleThreadContext("SemaphoreStressTest").use { pool -> + repeat (n) { + // Initially, we hold the lock and no one else can `lock`, + // otherwise it's a bug. + assertTrue(mutex.isLocked) + var job1EnteredCriticalSection = false + val job1 = launch(start = CoroutineStart.UNDISPATCHED) { + select { + mutex.onLock { + job1EnteredCriticalSection = true + mutex.unlock() + } + } + } + // check that `job1` didn't finish the call to `acquire()` + assertEquals(false, job1EnteredCriticalSection) + val job2 = launch(pool) { + mutex.unlock() + } + // Because `job2` executes in a separate thread, this + // cancellation races with the call to `unlock()`. + job1.cancelAndJoin() + job2.join() + assertFalse(mutex.isLocked) + mutex.lock() + } + } + } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt index 9c77990862..374a1e3d7c 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt @@ -5,7 +5,6 @@ import org.junit.Test import kotlin.test.assertEquals class SemaphoreStressTest : TestBase() { - @Test fun stressTestAsMutex() = runBlocking(Dispatchers.Default) { val n = 10_000 * stressTestMultiplier @@ -71,14 +70,14 @@ class SemaphoreStressTest : TestBase() { // Initially, we hold the permit and no one else can `acquire`, // otherwise it's a bug. assertEquals(0, semaphore.availablePermits) - var job1_entered_critical_section = false + var job1EnteredCriticalSection = false val job1 = launch(start = CoroutineStart.UNDISPATCHED) { semaphore.acquire() - job1_entered_critical_section = true + job1EnteredCriticalSection = true semaphore.release() } // check that `job1` didn't finish the call to `acquire()` - assertEquals(false, job1_entered_critical_section) + assertEquals(false, job1EnteredCriticalSection) val job2 = launch(pool) { semaphore.release() } @@ -91,5 +90,4 @@ class SemaphoreStressTest : TestBase() { } } } - } diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt index 9657830e35..99ab042f3c 100644 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt @@ -124,6 +124,8 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { return null } + actual open fun onRemoved(affected: Node) {} + actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode val failure = failure(affected) diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index 8507721e30..fd0279123f 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -115,16 +115,22 @@ class CoroutinesDumpTest : DebugTestBase() { coroutineThread!!.interrupt() val expected = - ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" + - "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" + - "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" + - "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + - "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)").trimStackTrace() - assertTrue(result.startsWith(expected)) + "kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable\$default(Cancellable.kt)\n" + + "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt)\n" + + "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)" + if (!result.startsWith(expected)) { + println("=== Actual result") + println(result) + error("Does not start with expected lines") + } + } @Test diff --git a/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/kotlinx-coroutines-debug/test/DebugProbesTest.kt index 24050e563c..3b32db3a5a 100644 --- a/kotlinx-coroutines-debug/test/DebugProbesTest.kt +++ b/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -40,24 +40,25 @@ class DebugProbesTest : DebugTestBase() { val deferred = createDeferred() val traces = listOf( "java.util.concurrent.ExecutionException\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" + "\t(Coroutine boundary)\n" + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt)\n" + "\t(Coroutine creation stacktrace)\n" + - "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + - "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + - "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable\$default(Cancellable.kt)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt)\n" + "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)\n" + - "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:138)\n" + - "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:19)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:38)", + "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt)", "Caused by: java.util.concurrent.ExecutionException\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt)\n") nestedMethod(deferred, traces) deferred.join() } diff --git a/reactive/kotlinx-coroutines-reactive/src/Channel.kt b/reactive/kotlinx-coroutines-reactive/src/Channel.kt index 379fc4ed53..26f14ec63d 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Channel.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Channel.kt @@ -48,7 +48,7 @@ public suspend inline fun Publisher.collect(action: (T) -> Unit): Unit = @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "SubscriberImplementation") private class SubscriptionChannel( private val request: Int -) : LinkedListChannel(), Subscriber { +) : LinkedListChannel(null), Subscriber { init { require(request >= 0) { "Invalid request size: $request" } } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt index e253161db0..633693e756 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxChannel.kt @@ -64,7 +64,7 @@ public suspend inline fun ObservableSource.collect(action: (T) -> Unit): @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private class SubscriptionChannel : - LinkedListChannel(), Observer, MaybeObserver + LinkedListChannel(null), Observer, MaybeObserver { private val _subscription = atomic(null) diff --git a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt index acb907b765..737cf6710d 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxChannel.kt @@ -54,7 +54,7 @@ public suspend inline fun ObservableSource.collect(action: (T) -> Unit): @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") private class SubscriptionChannel : - LinkedListChannel(), Observer, MaybeObserver + LinkedListChannel(null), Observer, MaybeObserver { private val _subscription = atomic(null) From 030aee45f2f43ea4cc11465e6cf8b2c3c741ec6f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 12 Oct 2020 22:23:39 +0300 Subject: [PATCH 130/257] Fixed FlowOnTest after the merge of branches --- .../common/test/flow/operators/FlowOnTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt index 0eae1a3860..68653281cc 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/FlowOnTest.kt @@ -346,9 +346,10 @@ class FlowOnTest : TestBase() { fun testCancelledFlowOn() = runTest { assertFailsWith { coroutineScope { + val scope = this flow { emit(Unit) // emit to buffer - cancel() // now cancel + scope.cancel() // now cancel outer scope }.flowOn(wrapperDispatcher()).collect { // should not be reached, because cancelled before it runs expectUnreached() From 401c05c770655d0a143dfe4110ac50e9795f4e1f Mon Sep 17 00:00:00 2001 From: koshachy Date: Tue, 13 Oct 2020 13:32:34 +0300 Subject: [PATCH 131/257] Updated the docs on IDEA debugging (#2244) --- docs/coroutine-context-and-dispatchers.md | 17 ++++++++--------- docs/images/coroutine-debugger.png | Bin 390955 -> 0 bytes docs/images/coroutine-idea-debugging-1.png | Bin 0 -> 248706 bytes 3 files changed, 8 insertions(+), 9 deletions(-) delete mode 100644 docs/images/coroutine-debugger.png create mode 100644 docs/images/coroutine-idea-debugging-1.png diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index 1d3abc4928..36e049db89 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -165,22 +165,21 @@ The Coroutine Debugger of the Kotlin plugin simplifies debugging coroutines in I > Debugging works for versions 1.3.8 or later of `kotlinx-coroutines-core`. -The **Debug Tool Window** contains a **Coroutines** tab. In this tab, you can find information about both currently -running and suspended coroutines. The coroutines are grouped by the dispatcher they are running on. +The **Debug** tool window contains the **Coroutines** tab. In this tab, you can find information about both currently running and suspended coroutines. +The coroutines are grouped by the dispatcher they are running on. -![Debugging coroutines](images/coroutine-debugger.png) +![Debugging coroutines](images/coroutine-idea-debugging-1.png) -You can: -* Easily check the state of each coroutine. +With the coroutine debugger, you can: +* Check the state of each coroutine. * See the values of local and captured variables for both running and suspended coroutines. * See a full coroutine creation stack, as well as a call stack inside the coroutine. The stack includes all frames with variable values, even those that would be lost during standard debugging. +* Get a full report that contains the state of each coroutine and its stack. To obtain it, right-click inside the **Coroutines** tab, and then click **Get Coroutines Dump**. -If you need a full report containing the state of each coroutine and its stack, right-click inside the **Coroutines** tab, and then -click **Get Coroutines Dump**. +To start coroutine debugging, you just need to set breakpoints and run the application in debug mode. -Learn more about debugging coroutines in [this blog post](https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-rc-debugging-coroutines/) -and [IntelliJ IDEA documentation](https://www.jetbrains.com/help/idea/debug-kotlin-coroutines.html). +Learn more about coroutines debugging in the [tutorial](https://kotlinlang.org/docs/tutorials/coroutines/debug-coroutines-with-idea.html). #### Debugging using logging diff --git a/docs/images/coroutine-debugger.png b/docs/images/coroutine-debugger.png deleted file mode 100644 index 3f59ee0c8a71b14807ab90c9475414968b82d41a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390955 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®iCdU)=Z0#UQ)8-H3x&Q{UiR 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%0DewO!Nz2~ny;Mnpri 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 diff --git a/docs/images/coroutine-idea-debugging-1.png b/docs/images/coroutine-idea-debugging-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0afe9925157e44d5c03e5811e7703bb6d6642ce7 GIT binary patch literal 248706 zcmeEucT|(x_AMZ>pn!#P01+$*gr-ylX(A%MB%xO&ARVdFn}UJ@N|z22AoLbWC;}oF zszB&PPz3V zYU&#NnnFZXBqzfUg~;1675e%OH4FtIlDly;;ci#*uTsSDUZs5>{^&&ewRT6*#10>? zBzG69m6FcF?ZKIZnoc*#x^WBTPWQeNLk}C*rJA}00zB@Xn?sWNc_|W7^5g!K3q5(7 z)t7|qxc}&ppU#?8_;0=eLvdcMDdWHRiqqD#S#(_Aijl1Y2=43u#T#igjQetR)MsP^ z(HvBC#~p$5QYrjr&#NKgjJ!ISk3Vo}U?74gjY*IH4rmDpDI#hYKQnUS*yG2l;OqPU zZ|48~%t)2tJbxv4Op*=zXo^fEKKvNQgv_QVBpwO?Pm&UQH#pG zOrTZ#K3A=E1_h0#i8}6b?3M;I?)01%`jln4!=tk!$t76F@G<)%GxX=`thdM9sNtfd zf%}SyY&Kg#)$FIzmS`Jx_#88clJqA*v$7%XghTJK zG^$?+$kr?{v{~pc2)hfTJMJ#&)?r1;9Ja^aZgg;rde2eSn+$y7>lLwx0S|_4{x$KRXFMBu-xtZN%YC~JsIhRM(J6TymDR*Z|;4cG@ z6p^>0>ZqJYh3v+vD-6m9OU#D@DVcJ0%PcX?S3GDu>inT9vA61Zt5@&0;&8Yn|6&P<~9A{>%zqE!9O-lyr#i)dY1h}xWUK7nz%$x|5xJ@`>v_kka zDwr~f>rNIlsvbU%uA`#;wEdMmS-Zx4Gx%P>RBMdhc&(Ssk1ua|S63Da>UbTJ)1Yry zB*!36v&UkLyyJ>}p+4RsBlZCvd`(+2D*>b@P7ZWR9xeJeUh&?U$nu3E+cgRiZyANS z#l*y1T%(USS$+z9YEYR&POh*+wB~5venH)#Wtik)#pYa3^TW4{h`9+anolUCu=Ac| z-h3b;I!23YC+C7`8ihalm*q(xX%xDeH9{j{;z-z)@v)VdfgzLzC%oCGXGfc}G|?D@ zHAX*1@S3qfo;oiMe5-OIfyS%hDVC^v~WH!-Cf@eL6w?#&Y9ed)&4D z$%#``Z*+~`zid*B(NVctA6aCRv-}>}`b4Yb&GEL|$Cpw#e)w+58S{?8YPS;QWQoz6 z${$QGv8$9)OYV2|QSO?(RspUemIOSIOCs*HCRZMMeX>Aq2D>|!hWyTn~bK=|S29k

gYl8~3?t?Ee@ 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

xFmWv3f&Y;2fKQix{;PFy}WRA_96 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

V8Ybh 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�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 literal 0 HcmV?d00001 From 34c3464c80904e6e578b646db80f9d0169059c37 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 13 Oct 2020 14:02:52 +0300 Subject: [PATCH 132/257] Introduce SharedFlow and sharing operators (#2069) * 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 | 92 +- .../common/src/CompletableDeferred.kt | 2 +- .../common/src/CompletableJob.kt | 2 +- kotlinx-coroutines-core/common/src/Delay.kt | 5 +- .../common/src/channels/AbstractChannel.kt | 10 +- .../common/src/channels/ArrayChannel.kt | 133 +-- .../common/src/channels/Broadcast.kt | 7 +- .../common/src/channels/BroadcastChannel.kt | 7 +- .../common/src/channels/BufferOverflow.kt | 36 + .../common/src/channels/Channel.kt | 86 +- .../src/channels/ConflatedBroadcastChannel.kt | 6 +- .../common/src/channels/Produce.kt | 22 +- .../common/src/flow/Builders.kt | 52 +- .../common/src/flow/Channels.kt | 30 +- .../common/src/flow/Flow.kt | 22 +- .../common/src/flow/Migration.kt | 59 +- .../common/src/flow/SharedFlow.kt | 659 +++++++++++++++ .../common/src/flow/SharingStarted.kt | 216 +++++ .../common/src/flow/StateFlow.kt | 263 +++--- .../src/flow/internal/AbstractSharedFlow.kt | 101 +++ .../common/src/flow/internal/ChannelFlow.kt | 122 ++- .../common/src/flow/internal/Merge.kt | 30 +- .../common/src/flow/operators/Context.kt | 85 +- .../common/src/flow/operators/Distinct.kt | 52 +- .../common/src/flow/operators/Emitters.kt | 11 +- .../common/src/flow/operators/Lint.kt | 34 +- .../common/src/flow/operators/Share.kt | 412 +++++++++ .../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/channels/TestChannelKind.kt | 2 +- .../common/test/flow/VirtualTime.kt | 14 +- .../flow/operators/BufferConflationTest.kt | 146 ++++ .../common/test/flow/operators/BufferTest.kt | 33 +- .../operators/DistinctUntilChangedTest.kt | 29 + .../test/flow/sharing/ShareInBufferTest.kt | 98 +++ .../flow/sharing/ShareInConflationTest.kt | 162 ++++ .../test/flow/sharing/ShareInFusionTest.kt | 56 ++ .../common/test/flow/sharing/ShareInTest.kt | 215 +++++ .../flow/sharing/SharedFlowScenarioTest.kt | 331 ++++++++ .../test/flow/sharing/SharedFlowTest.kt | 798 ++++++++++++++++++ .../test/flow/sharing/SharingStartedTest.kt | 183 ++++ .../SharingStartedWhileSubscribedTest.kt | 42 + .../test/flow/{ => sharing}/StateFlowTest.kt | 85 +- .../common/test/flow/sharing/StateInTest.kt | 78 ++ kotlinx-coroutines-core/jvm/test/TestBase.kt | 2 +- .../jvm/test/flow/SharingStressTest.kt | 193 +++++ .../test/flow/StateFlowCancellabilityTest.kt | 56 ++ .../test/TestRunBlockingOrderTest.kt | 4 +- .../kotlinx-coroutines-reactive/README.md | 4 +- .../build.gradle.kts | 2 + .../src/ReactiveFlow.kt | 33 +- .../test/PublisherAsFlowTest.kt | 82 ++ ui/coroutines-guide-ui.md | 2 +- .../test/ordered/tests/TestComponent.kt | 2 +- 58 files changed, 4879 insertions(+), 422 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/channels/BufferOverflow.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/AbstractSharedFlow.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 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 rename kotlinx-coroutines-core/common/test/flow/{ => sharing}/StateFlowTest.kt (52%) create mode 100644 kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/SharingStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/StateFlowCancellabilityTest.kt diff --git a/README.md b/README.md index 7f71d288d4..3baf5b8e30 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 @@ -302,7 +302,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 4ccd340f52..bb1c0f36ab 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -578,6 +578,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 @@ -611,9 +619,9 @@ public final class kotlinx/coroutines/channels/ChannelIterator$DefaultImpls { public final class kotlinx/coroutines/channels/ChannelKt { public static final synthetic fun Channel (I)Lkotlinx/coroutines/channels/Channel; - public static final fun Channel (ILkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/channels/Channel; + public static final fun Channel (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/channels/Channel; public static synthetic fun Channel$default (IILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; - public static synthetic fun Channel$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; + public static synthetic fun Channel$default (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel; } public final class kotlinx/coroutines/channels/ChannelsKt { @@ -868,7 +876,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 +903,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 +1001,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 +1021,15 @@ 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;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; 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 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 @@ -1031,19 +1053,61 @@ public final class kotlinx/coroutines/flow/FlowKt { public final class kotlinx/coroutines/flow/LintKt { public static final fun cancel (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;)V public static synthetic fun cancel$default (Lkotlinx/coroutines/flow/FlowCollector;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V + 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 static final fun getCoroutineContext (Lkotlinx/coroutines/flow/FlowCollector;)Lkotlin/coroutines/CoroutineContext; public static final fun isActive (Lkotlinx/coroutines/flow/FlowCollector;)Z } -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; } @@ -1054,13 +1118,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; } @@ -1074,11 +1140,11 @@ public final class kotlinx/coroutines/flow/internal/FlowExceptions_commonKt { } 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/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index adde8e433c..f7948443fa 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -117,7 +117,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/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 3cd6de5f51..53ecf06a2c 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -1019,23 +1019,23 @@ internal val EMPTY = Symbol("EMPTY") // marker for Conflated & Buffered channels @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 8a08106516..80cb8aa011 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -23,14 +23,18 @@ internal open class ArrayChannel( /** * Buffer capacity. */ - val capacity: Int, + private val capacity: Int, + private val onBufferOverflow: BufferOverflow, onUndeliveredElement: OnUndeliveredElement? ) : AbstractChannel(onUndeliveredElement) { init { + // 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() + /* * Guarded by lock. * Allocate minimum of capacity and 16 to avoid excess memory pressure for large channels when it's not necessary. @@ -43,7 +47,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 } @@ -55,31 +59,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) @@ -92,41 +91,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) @@ -137,6 +131,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 25085ed2d2..72c08e1acd 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -481,22 +481,24 @@ 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`. * @@ -504,6 +506,21 @@ public interface ChannelIterator { * 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. + * * ### Prompt cancellation guarantee * * All suspending functions with channels provide **prompt cancellation guarantee**. @@ -573,25 +590,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 @@ -615,25 +633,47 @@ 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). * @param onUndeliveredElement an optional function that is called when element was sent but was not delivered to the consumer. * See "Undelivered elements" section in [Channel] documentation. * @throws IllegalArgumentException when [capacity] < -2 */ -public fun Channel(capacity: Int = RENDEZVOUS, onUndeliveredElement: ((E) -> Unit)? = null): Channel = +public fun Channel( + capacity: Int = RENDEZVOUS, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + onUndeliveredElement: ((E) -> Unit)? = null +): Channel = when (capacity) { - RENDEZVOUS -> RendezvousChannel(onUndeliveredElement) - UNLIMITED -> LinkedListChannel(onUndeliveredElement) - CONFLATED -> ConflatedChannel(onUndeliveredElement) - BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY, onUndeliveredElement) - else -> ArrayChannel(capacity, onUndeliveredElement) + RENDEZVOUS -> { + if (onBufferOverflow == BufferOverflow.SUSPEND) + RendezvousChannel(onUndeliveredElement) // an efficient implementation of rendezvous channel + else + ArrayChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel + } + CONFLATED -> { + require(onBufferOverflow == BufferOverflow.SUSPEND) { + "CONFLATED capacity cannot be used with non-default onBufferOverflow" + } + ConflatedChannel(onUndeliveredElement) + } + UNLIMITED -> LinkedListChannel(onUndeliveredElement) // 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, onUndeliveredElement + ) + else -> { + if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST) + ConflatedChannel(onUndeliveredElement) // conflated implementation is more efficient but appears to work in the same way + else + ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement) + } } -/** - * @suppress Binary compatibility only, should not be documented - */ -// This declaration is hidden since version 1.4.0 -@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility") -public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity, onUndeliveredElement = null) +@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.4.0, binary compatibility with earlier versions") +public fun Channel(capacity: Int = RENDEZVOUS): Channel = Channel(capacity) /** * Indicates an attempt to [send][SendChannel.send] to a [isClosedForSend][SendChannel.isClosedForSend] channel diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index ba2ccea92d..5986dae3d4 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 6e454d4b27..10a15e2a93 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -95,13 +95,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.** @@ -122,8 +117,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/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 8076d2f878..7e47e6947a 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. * @@ -324,10 +334,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) @@ -339,8 +350,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) @@ -360,6 +372,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 c7b9b71f5b..762cdcad1b 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -119,8 +119,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() { @@ -129,8 +130,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 @@ -157,7 +161,7 @@ private class ChannelAsFlow( } } - override fun additionalToStringProps(): String = "channel=$channel, " + override fun additionalToStringProps(): String = "channel=$channel" } /** @@ -184,8 +188,22 @@ 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. + * + * ### 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 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..88dc775842 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -0,0 +1,659 @@ +/* + * 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() // private mutable 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 + * } + * } + * ``` + * + * 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. + * + * 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: + * + * ``` + * 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]. + * + * **Note: This is an experimental api.** This function may be removed or renamed in the future. + */ + @ExperimentalCoroutinesApi + 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, 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 + * [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 = 0, + extraBufferCapacity: Int = 0, + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND +): MutableSharedFlow { + 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 $onBufferOverflow" + } + 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<*>): Array?> { + 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 resumes: Array?> = EMPTY_RESUMES + val emitted = synchronized(this) { + if (tryEmitLocked(value)) { + resumes = findSlotsToResumeLocked() + true + } else { + false + } + } + for (cont in resumes) cont?.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 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) + resumes = 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) resumes = findSlotsToResumeLocked() + } + } + // outside of the lock: register dispose on cancellation + emitter?.let { cont.disposeOnCancellation(it) } + // outside of the lock: resume slots if needed + for (cont in resumes) cont?.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): Array?> { + assert { oldIndex >= minCollectorIndex } + 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 + // 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 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 + 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 resumes: Array?> = EMPTY_RESUMES + val newQueueEndIndex = newBufferEndIndex + queueSize + if (maxResumeCount > 0) { // collect emitters to resume if we have them + 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 + resumes[resumeCount++] = emitter.cont + buffer.setBufferAt(curEmitterIndex, NO_VALUE) // make as canceled if we moved ahead + buffer.setBufferAt(newBufferEndIndex, emitter.value) + newBufferEndIndex++ + if (resumeCount >= 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 resumes + } + + 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 resumes: Array?> = EMPTY_RESUMES + 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 + resumes = updateCollectorIndexLocked(oldIndex) + newValue + } + } + for (resume in resumes) resume?.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(): 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 + if (resumeCount >= resumes.size) resumes = resumes.copyOf(maxOf(2, 2 * resumes.size)) + resumes[resumeCount++] = cont + slot.cont = null // not waiting anymore + } + return resumes + } + + 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..935efdae2b --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -0,0 +1,216 @@ +/* + * 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 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. + */ + START, + + /** + * Stops sharing, cancelling collection of the upstream flow. + */ + STOP, + + /** + * 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`. + */ + 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). + * Use zero value to expire the cache immediately. + * + * 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 [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. + */ +@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 { + 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 = flow { + 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() * 31 + replayExpiration.hashCode() +} diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index b2bbb6d3ae..8587606633 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 = _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,35 @@ 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. + * + * 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 } /** @@ -144,14 +191,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 +206,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<*>): Array?> { _state.value = null // free now + return EMPTY_RESUMES // nothing more to do } @Suppress("UNCHECKED_CAST") @@ -207,72 +253,97 @@ 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, 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 @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 + // always check for cancellation + collectorJob?.ensureActive() // Conflate value emissions using equality - if (prevState == null || newState != prevState) { + if (oldState == null || oldState != newState) { 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 +355,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/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt new file mode 100644 index 0000000000..ccb5343084 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -0,0 +1,101 @@ +/* + * 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.* +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): Array?> // returns continuations 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 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 + if (nCollectors == 0) nextIndex = 0 + (slot as AbstractSharedFlowSlot).freeLocked(this) + } + /* + 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) + } + + 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/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/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 cf9575b078..a6d6b76dae 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 = "Since 1.4.0, binary compatibility with earlier versions") +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 * @@ -198,8 +232,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. */ @@ -215,17 +249,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(private 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..1a34af776f 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Distinct.kt @@ -7,9 +7,10 @@ package kotlinx.coroutines.flow +import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* -import kotlinx.coroutines.flow.internal.unsafeFlow as flow +import kotlin.native.concurrent.* /** * Returns flow where all subsequent repetitions of the same value are filtered out. @@ -17,44 +18,69 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow * 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 StateFlow<*> -> this - else -> distinctUntilChangedBy { it } + 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 = - 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. + * + * 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 = { old, new -> old == new }) + distinctUntilChangedBy(keySelector = keySelector, areEquivalent = defaultAreEquivalent) + +@SharedImmutable +private val defaultKeySelector: (Any?) -> Any? = { it } + +@SharedImmutable +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 -): Flow = - flow { +private fun Flow.distinctUntilChangedBy( + keySelector: (T) -> Any?, + areEquivalent: (old: Any?, new: Any?) -> Boolean +): 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 as K, key)) { + 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/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index fb37da3a83..3ffe5fe943 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 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). * * 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 c2ae6e9d24..7a70fbf7f2 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -10,37 +10,45 @@ 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() 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..4dd89ee4bf --- /dev/null +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -0,0 +1,412 @@ +/* + * 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. 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 + * 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, SharingStarted.Eagerly) + * ``` + * + * 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, SharingStarted.Eagerly) + * ``` + * + * 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, SharingStarted.Eagerly) + * ``` + * + * ### 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, SharingStarted.Eagerly, 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, 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, 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 + * + * 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 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, + started: SharingStarted, + replay: Int = 0 +): 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 + // 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 + } + } + } + } + } + } + } +} + +// -------------------------------- 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, SharingStarted.Eagerly, 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. + * @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, + 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 { + 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, currentCoroutineContext()) + 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..e80309be89 --- /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, null) +} diff --git a/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt b/kotlinx-coroutines-core/common/test/channels/ConflatedChannelTest.kt index eca19458be..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 = Channel(Channel.CONFLATED) + 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/channels/TestChannelKind.kt b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt index 42330f1cae..993be78e17 100644 --- a/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt +++ b/kotlinx-coroutines-core/common/test/channels/TestChannelKind.kt @@ -26,7 +26,7 @@ enum class TestChannelKind( fun create(onUndeliveredElement: ((T) -> Unit)? = null): Channel = when { viaBroadcast && onUndeliveredElement != null -> error("Broadcast channels to do not support onUndeliveredElement") viaBroadcast -> ChannelViaBroadcast(BroadcastChannel(capacity)) - else -> Channel(capacity, onUndeliveredElement) + else -> Channel(capacity, onUndeliveredElement = onUndeliveredElement) } val isConflated get() = capacity == Channel.CONFLATED diff --git a/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt b/kotlinx-coroutines-core/common/test/flow/VirtualTime.kt index a607a4722a..ddb1d88ae2 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, context: CoroutineContext): 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/operators/DistinctUntilChangedTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt index fc03d367c6..68e7f66b9d 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DistinctUntilChangedTest.kt @@ -94,4 +94,33 @@ class DistinctUntilChangedTest : TestBase() { val flow = flowOf(null, 1, null, null).distinctUntilChanged() assertEquals(listOf(null, 1, null), flow.toList()) } + + @Test + fun testRepeatedDistinctFusionDefault() = testRepeatedDistinctFusion { + 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(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(keySelectorTestFun) + } + + 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) + } } 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..9c6aed211a --- /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, SharingStarted.Eagerly) + } + + @Test + fun testReplay1DefaultBuffer() = + checkBuffer(defaultBufferSize) { + shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test // buffer is padded to default size as needed + fun testReplay10DefaultBuffer() = + checkBuffer(maxOf(10, defaultBufferSize)) { + shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test // buffer is padded to default size as needed + fun testReplay100DefaultBuffer() = + checkBuffer( maxOf(100, defaultBufferSize)) { + shareIn(it, SharingStarted.Eagerly, 100) + } + + @Test + fun testDefaultBufferKeepsDefault() = + checkBuffer(defaultBufferSize) { + buffer().shareIn(it, SharingStarted.Eagerly) + } + + @Test + fun testOverrideDefaultBuffer0() = + checkBuffer(0) { + buffer(0).shareIn(it, SharingStarted.Eagerly) + } + + @Test + fun testOverrideDefaultBuffer10() = + checkBuffer(10) { + buffer(10).shareIn(it, SharingStarted.Eagerly) + } + + @Test // buffer and replay sizes add up + fun testBufferReplaySum() = + checkBuffer(41) { + 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 new file mode 100644 index 0000000000..0528e97e7d --- /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, 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, SharingStarted.Eagerly, 0) + } + + @Test + fun testConflateReplay5() = + checkConflation(5) { + conflate().shareIn(it, SharingStarted.Eagerly, 5) + } + + @Test + fun testBufferDropOldestReplay1() = + checkConflation(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 1) + } + + @Test + fun testBufferDropOldestReplay0() = + checkConflation(1) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBufferDropOldestReplay10() = + checkConflation(10) { + buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 10) + } + + @Test + fun testBuffer20DropOldestReplay0() = + checkConflation(20) { + buffer(20, onBufferOverflow = BufferOverflow.DROP_OLDEST).shareIn(it, SharingStarted.Eagerly, 0) + } + + @Test + fun testBuffer7DropOldestReplay11() = + checkConflation(18) { + 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, 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, SharingStarted.Eagerly, 1) + } + + @Test + fun testBufferDropLatestReplay0() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + 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, SharingStarted.Eagerly, 1) + } + + @Test + fun testBufferDropLatestReplay10() = + checkConflation(10, BufferOverflow.DROP_LATEST) { + 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, SharingStarted.Eagerly, 0) + } + + @Test + fun testBuffer0DropLatestReplay1() = + checkConflation(1, BufferOverflow.DROP_LATEST) { + 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, SharingStarted.Eagerly, 10) + } + + @Test + fun testBuffer5DropLatestReplay0() = + checkConflation(5, BufferOverflow.DROP_LATEST) { + 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, 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, 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 new file mode 100644 index 0000000000..371d01472e --- /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, 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)) + 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, SharingStarted.Eagerly, 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, SharingStarted.Eagerly) + 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..9020f5f311 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -0,0 +1,215 @@ +/* + * 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, SharingStarted.Eagerly) + 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, started = SharingStarted.Lazily, replay = replay) + 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, SharingStarted.Eagerly, 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, 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) + } + + @Suppress("TestFunctionName") + 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..32d88f3c99 --- /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() + 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() + 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() + 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() + 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) { + 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() + 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/StateFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt similarity index 52% rename from kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt rename to kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt index a6be97eb97..0a2c0458c4 100644 --- a/kotlinx-coroutines-core/common/test/flow/StateFlowTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateFlowTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* import kotlin.test.* class StateFlowTest : TestBase() { @@ -56,7 +57,7 @@ class StateFlowTest : TestBase() { expect(2) assertFailsWith { state.collect { value -> - when(value.i) { + when (value.i) { 0 -> expect(3) // initial value 2 -> expect(5) 4 -> expect(7) @@ -103,6 +104,7 @@ class StateFlowTest : TestBase() { class CounterModel { // private data flow private val _counter = MutableStateFlow(0) + // publicly exposed as a flow val counter: StateFlow get() = _counter @@ -110,4 +112,85 @@ class StateFlowTest : TestBase() { _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 testResetUnsupported() { + val state = MutableStateFlow(42) + assertFailsWith { state.resetReplayCache() } + 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 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..2a613afaf7 --- /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, SharingStarted.Eagerly, 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 26c176319e..17238e873c 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -109,7 +109,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..7d346bdc33 --- /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, started, replay) + 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/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) + } +} + 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 { 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/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) } diff --git a/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt b/reactive/kotlinx-coroutines-reactive/src/ReactiveFlow.kt index a51f583b77..5834220c40 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,14 @@ 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) + + // 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..04833e9814 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,85 @@ 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) + } } 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 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 f3a9b60580c03d6ab74ccdd4a23bdf7ecc1ac1ab Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 13 Oct 2020 14:32:13 +0300 Subject: [PATCH 133/257] Version 1.4.0-M1 --- CHANGES.md | 31 +++++++++++++++++++ README.md | 16 +++++----- gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 4 +-- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 8 files changed, 46 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 99a600c48c..513c28fb33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,36 @@ # Change log for kotlinx.coroutines +## Version 1.4.0-M1 + +### Breaking changes + +* The concept of atomic cancellation in channels is removed. All operations in channels + and corresponding `Flow` operators are cancellable in non-atomic way (#1813). +* If `CoroutineDispatcher` throws `RejectedExecutionException`, cancel current `Job` and schedule its execution to `Dispatchers.IO` (#2003). +* `CancellableContinuation.invokeOnCancellation` is invoked if the continuation was cancelled while its resume has been dispatched (#1915). +* `Flow.singleOrNull` operator is aligned with standard library and does not longer throw `IllegalStateException` on multiple values (#2289). + +### New experimental features + +* `SharedFlow` primitive for managing hot sources of events with support of various subscription mechanisms, replay logs and buffering (#2034). +* `Flow.shareIn` and `Flow.stateIn` operators to transform cold instances of flow to hot `SharedFlow` and `StateFlow` respectively (#2047). + +### Other + +* Support leak-free closeable resources transfer via `onUndeliveredElement` in channels (#1936). +* Changed ABI in reactive integrations for Java interoperability (#2182). +* Fixed ProGuard rules for `kotlinx-coroutines-core` (#2046, #2266). +* Lint settings were added to `Flow` to avoid accidental capturing of outer `CoroutineScope` for cancellation check (#2038). + +### External contributions + +* Allow nullable types in `Flow.firstOrNull` and `Flow.singleOrNull` by @ansman (#2229). +* Add `Publisher.awaitSingleOrDefault|Null|Else` extensions by @sdeleuze (#1993). +* `awaitCancellation` top-level function by @LouisCAD (#2213). +* Significant part of our Gradle build scripts were migrated to `.kts` by @turansky. + +Thank you for your contributions and participation in the Kotlin community! + ## Version 1.3.9 * Support of `CoroutineContext` in `Flow.asPublisher` and similar reactive builders (#2155). diff --git a/README.md b/README.md index d622b1caa8..1e72cc1b3e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.9) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.9) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0-M1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0-M1) [![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) @@ -86,7 +86,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.3.9 + 1.4.0-M1 ``` @@ -104,7 +104,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1' } ``` @@ -130,7 +130,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") } ``` @@ -152,7 +152,7 @@ In common code that should get compiled for different platforms, you can add dep ```groovy commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") } } ``` @@ -163,7 +163,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1' ``` This gives you access to Android [Dispatchers.Main] @@ -190,7 +190,7 @@ packagingOptions { ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.9/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0-M1/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -198,7 +198,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.9/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0-M1/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/gradle.properties b/gradle.properties index 34909cd790..18b95166d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.3.9-SNAPSHOT +version=1.4.0-M1-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.4.0 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 1278ed132c..5518e00ef3 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -23,7 +23,7 @@ https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.9' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.0-M1' } ``` @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.9.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.0-M1.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index f0c01cc48e..b82fe8577e 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0-M1' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index ede5e118a8..5df8d7f0ed 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 3f4084b7a5..cd34eb1285 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.3.9 +coroutines_version=1.4.0-M1 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 3f4084b7a5..cd34eb1285 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.3.9 +coroutines_version=1.4.0-M1 android.useAndroidX=true android.enableJetifier=true From 863258b613d61435b0ac07b709661fe9fd33177f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 13 Oct 2020 18:46:18 +0300 Subject: [PATCH 134/257] Fixed RejectedExecutionTest on JDK1.6 --- kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt index a6f4dd6b18..7f6d6b661c 100644 --- a/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt +++ b/kotlinx-coroutines-core/jvm/test/RejectedExecutionTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.* import kotlinx.coroutines.scheduling.* import org.junit.* import org.junit.Test @@ -89,6 +90,7 @@ class RejectedExecutionTest : TestBase() { @Test fun testRejectOnDelay() = runTest { + if (!removeFutureOnCancel(executor)) return@runTest // Skip this test on old JDKs expect(1) executor.acceptTasks = 1 // accept one task assertFailsWith { @@ -110,6 +112,7 @@ class RejectedExecutionTest : TestBase() { @Test fun testRejectWithTimeout() = runTest { + if (!removeFutureOnCancel(executor)) return@runTest // Skip this test on old JDKs expect(1) executor.acceptTasks = 1 // accept one task assertFailsWith { From 3f108cbfbc49680a88010b80b477130f67a3e473 Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Fri, 16 Oct 2020 10:54:35 +0330 Subject: [PATCH 135/257] Update shared-mutable-state-and-concurrency.md (#2309) --- docs/shared-mutable-state-and-concurrency.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/shared-mutable-state-and-concurrency.md b/docs/shared-mutable-state-and-concurrency.md index 316d56e5bc..8b83ad0b20 100644 --- a/docs/shared-mutable-state-and-concurrency.md +++ b/docs/shared-mutable-state-and-concurrency.md @@ -24,7 +24,7 @@ but others are unique. ### The problem -Let us launch a hundred coroutines all doing the same action thousand times. +Let us launch a hundred coroutines all doing the same action a thousand times. We'll also measure their completion time for further comparisons:

@@ -102,7 +102,7 @@ increment the `counter` concurrently from multiple threads without any synchroni ### Volatiles are of no help -There is common misconception that making a variable `volatile` solves concurrency problem. Let us try it: +There is a common misconception that making a variable `volatile` solves concurrency problem. Let us try it: @@ -158,7 +158,7 @@ do not provide atomicity of larger actions (increment in our case). ### Thread-safe data structures The general solution that works both for threads and for coroutines is to use a thread-safe (aka synchronized, -linearizable, or atomic) data structure that provides all the necessarily synchronization for the corresponding +linearizable, or atomic) data structure that provides all the necessary synchronization for the corresponding operations that needs to be performed on a shared state. In the case of a simple counter we can use `AtomicInteger` class which has atomic `incrementAndGet` operations: From eb1b5db93b4910668a002a3d13400b931baab596 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 16 Oct 2020 10:45:08 +0300 Subject: [PATCH 136/257] Fix doc references that differ only in case (function vs interface) (#2292) Fixes #2279 --- README.md | 6 +++--- docs/basics.md | 12 ++++++------ docs/composing-suspending-functions.md | 4 ++-- docs/exception-handling.md | 10 +++++----- docs/flow.md | 8 ++++---- gradle.properties | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1e72cc1b3e..24cc7947ec 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ suspend fun main() = coroutineScope { * [delay] and [yield] top-level suspending functions; * [Flow] — cold asynchronous stream with [flow][_flow] builder and comprehensive operator set ([filter], [map], etc); * [Channel], [Mutex], and [Semaphore] communication and synchronization primitives; - * [coroutineScope], [supervisorScope], [withContext], and [withTimeout] scope builders; + * [coroutineScope][_coroutineScope], [supervisorScope][_supervisorScope], [withContext], and [withTimeout] scope builders; * [MainScope()] for Android and UI applications; * [SupervisorJob()] and [CoroutineExceptionHandler] for supervision of coroutines hierarchies; * [select] expression support and more. @@ -227,8 +227,8 @@ See [Contributing Guidelines](CONTRIBUTING.md). [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html [delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html [yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html -[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html -[supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html +[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html +[_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html [withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html [withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html [MainScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html diff --git a/docs/basics.md b/docs/basics.md index cb64328676..8aca23a18c 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -235,12 +235,12 @@ World! ### Scope builder In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using the -[coroutineScope] builder. It creates a coroutine scope and does not complete until all launched children complete. +[coroutineScope][_coroutineScope] builder. It creates a coroutine scope and does not complete until all launched children complete. -[runBlocking] and [coroutineScope] may look similar because they both wait for their body and all its children to complete. +[runBlocking] and [coroutineScope][_coroutineScope] may look similar because they both wait for their body and all its children to complete. The main difference is that the [runBlocking] method _blocks_ the current thread for waiting, -while [coroutineScope] just suspends, releasing the underlying thread for other usages. -Because of that difference, [runBlocking] is a regular function and [coroutineScope] is a suspending function. +while [coroutineScope][_coroutineScope] just suspends, releasing the underlying thread for other usages. +Because of that difference, [runBlocking] is a regular function and [coroutineScope][_coroutineScope] is a suspending function. It can be demonstrated by the following example: @@ -281,7 +281,7 @@ Coroutine scope is over --> Note that right after the "Task from coroutine scope" message (while waiting for nested launch) - "Task from runBlocking" is executed and printed — even though the [coroutineScope] is not completed yet. + "Task from runBlocking" is executed and printed — even though the [coroutineScope][_coroutineScope] is not completed yet. ### Extract function refactoring @@ -403,7 +403,7 @@ Active coroutines that were launched in [GlobalScope] do not keep the process al [runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html -[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html +[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html [CoroutineScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html diff --git a/docs/composing-suspending-functions.md b/docs/composing-suspending-functions.md index 013076a6d8..81b6f53c1f 100644 --- a/docs/composing-suspending-functions.md +++ b/docs/composing-suspending-functions.md @@ -308,7 +308,7 @@ concurrency, as shown in the section below. Let us take the [Concurrent using async](#concurrent-using-async) example and extract a function that concurrently performs `doSomethingUsefulOne` and `doSomethingUsefulTwo` and returns the sum of their results. Because the [async] coroutine builder is defined as an extension on [CoroutineScope], we need to have it in the -scope and that is what the [coroutineScope] function provides: +scope and that is what the [coroutineScope][_coroutineScope] function provides:
@@ -431,5 +431,5 @@ Computation failed with ArithmeticException [Job.start]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html [GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html -[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html +[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html diff --git a/docs/exception-handling.md b/docs/exception-handling.md index d0b6b517a8..a3070213d1 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -413,9 +413,9 @@ The second child is cancelled because the supervisor was cancelled #### Supervision scope -Instead of [coroutineScope], we can use [supervisorScope] for _scoped_ concurrency. It propagates the cancellation +Instead of [coroutineScope][_coroutineScope], we can use [supervisorScope][_supervisorScope] for _scoped_ concurrency. It propagates the cancellation in one direction only and cancels all its children only if it failed itself. It also waits for all children before completion -just like [coroutineScope] does. +just like [coroutineScope][_coroutineScope] does.
@@ -464,7 +464,7 @@ Caught an assertion error Another crucial difference between regular and supervisor jobs is exception handling. Every child should handle its exceptions by itself via the exception handling mechanism. This difference comes from the fact that child's failure does not propagate to the parent. -It means that coroutines launched directly inside the [supervisorScope] _do_ use the [CoroutineExceptionHandler] +It means that coroutines launched directly inside the [supervisorScope][_supervisorScope] _do_ use the [CoroutineExceptionHandler] that is installed in their scope in the same way as root coroutines do (see the [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details). @@ -517,8 +517,8 @@ The scope is completed [runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html [Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job.html -[coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html -[supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html +[_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html +[_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html [actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html [produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html diff --git a/docs/flow.md b/docs/flow.md index 2b1dfd59a9..4374e7aa86 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -203,7 +203,7 @@ I'm not blocked 3 Notice the following differences in the code with the [Flow] from the earlier examples: -* A builder function for [Flow] type is called [flow]. +* A builder function for [Flow] type is called [flow][_flow]. * Code inside the `flow { ... }` builder block can suspend. * The `simple` function is no longer marked with `suspend` modifier. * Values are _emitted_ from the flow using [emit][FlowCollector.emit] function. @@ -214,7 +214,7 @@ thread is blocked in this case. ### Flows are cold -Flows are _cold_ streams similar to sequences — the code inside a [flow] builder does not +Flows are _cold_ streams similar to sequences — the code inside a [flow][_flow] builder does not run until the flow is collected. This becomes clear in the following example:
@@ -1785,7 +1785,7 @@ coroutine only without cancelling the whole scope or to [join][Job.join] it. ### Flow cancellation checks -For convenience, the [flow] builder performs additional [ensureActive] checks for cancellation on each emitted value. +For convenience, the [flow][_flow] builder performs additional [ensureActive] checks for cancellation on each emitted value. It means that a busy loop emitting from a `flow { ... }` is cancellable:
@@ -1944,7 +1944,7 @@ Integration modules include conversions from and to `Flow`, integration with Rea [CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html -[flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html +[_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html [FlowCollector.emit]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html [collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/collect.html [flowOf]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-of.html diff --git a/gradle.properties b/gradle.properties index 18b95166d6..a4057dfcc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin_version=1.4.0 # Dependencies junit_version=4.12 atomicfu_version=0.14.4 -knit_version=0.2.0 +knit_version=0.2.2 html_version=0.6.8 lincheck_version=2.7.1 dokka_version=0.9.16-rdev-2-mpp-hacks From 20ad25fb3b0a2068677091d808c8b732c8b66662 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 16 Oct 2020 11:22:57 +0300 Subject: [PATCH 137/257] Disable SecurityManager when IDEA is active Otherwise, we have to keep our coroutine debugger disabled all the time: it attempts to read system property and fails on start --- kotlinx-coroutines-core/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index f98f6a529c..29784bffcf 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -177,7 +177,9 @@ jvmTest { minHeapSize = '1g' maxHeapSize = '1g' enableAssertions = true - systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' + if (!Idea.active) { // Workaround for KT-42671 + systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' + } // 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true" if (!Idea.active && rootProject.properties['stress'] == null) { exclude '**/*StressTest.*' From 993c1920d17f1072fe17654f9fe553983f58098d Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 19 Oct 2020 03:01:50 -0700 Subject: [PATCH 138/257] =?UTF-8?q?Cleanup=20lazy=20coroutines=20that=20ha?= =?UTF-8?q?ve=20been=20cancelled=20but=20not=20yet=20garbage=20=E2=80=A6?= =?UTF-8?q?=20(#2315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleanup lazy coroutines that have been cancelled but not yet garbage collected Fixes #2294 --- .../jvm/src/debug/internal/DebugProbesImpl.kt | 23 ++++++++++++++++++- .../test/LazyCoroutineTest.kt | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 kotlinx-coroutines-debug/test/LazyCoroutineTest.kt diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 9dd6c5a548..4b7c09b347 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -156,7 +156,11 @@ internal object DebugProbesImpl { // Stable ordering of coroutines by their sequence number .sortedBy { it.info.sequenceNumber } // Leave in the dump only the coroutines that were not collected while we were dumping them - .mapNotNull { owner -> owner.info.context?.let { context -> create(owner, context) } } + .mapNotNull { owner -> + // Fuse map and filter into one operation to save an inline + if (owner.isFinished()) null + else owner.info.context?.let { context -> create(owner, context) } + } } /* @@ -183,10 +187,27 @@ internal object DebugProbesImpl { dumpCoroutinesSynchronized(out) } + /* + * Filters out coroutines that do not call probeCoroutineCompleted, + * are completed, but not yet garbage collected. + * + * Typically, we intercept completion of the coroutine so it invokes "probeCoroutineCompleted", + * but it's not the case for lazy coroutines that get cancelled before start. + */ + private fun CoroutineOwner<*>.isFinished(): Boolean { + // Guarded by lock + val job = info.context?.get(Job) ?: return false + if (!job.isCompleted) return false + capturedCoroutinesMap.remove(this) // Clean it up by the way + return true + } + private fun dumpCoroutinesSynchronized(out: PrintStream): Unit = coroutineStateLock.write { check(isInstalled) { "Debug probes are not installed" } out.print("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}") capturedCoroutines + .asSequence() + .filter { !it.isFinished() } .sortedBy { it.info.sequenceNumber } .forEach { owner -> val info = owner.info diff --git a/kotlinx-coroutines-debug/test/LazyCoroutineTest.kt b/kotlinx-coroutines-debug/test/LazyCoroutineTest.kt new file mode 100644 index 0000000000..c872b6a53d --- /dev/null +++ b/kotlinx-coroutines-debug/test/LazyCoroutineTest.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.* + +class LazyCoroutineTest : DebugTestBase() { + + @Test + fun testLazyCompletedCoroutine() = runTest { + val job = launch(start = CoroutineStart.LAZY) {} + job.invokeOnCompletion { expect(2) } + expect(1) + job.cancelAndJoin() + expect(3) + assertEquals(1, DebugProbes.dumpCoroutinesInfo().size) // Outer runBlocking + verifyPartialDump(1, "BlockingCoroutine{Active}") + finish(4) + } +} From e941da243b9718f9e7d5fa62a765cda9620f3661 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 19 Oct 2020 08:58:27 -0700 Subject: [PATCH 139/257] Remove workaround for KT-42671 that triggers test failures from IDEA and access properties in a safe manner from agent premain instead (#2311) --- kotlinx-coroutines-core/build.gradle | 4 +--- kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 29784bffcf..f98f6a529c 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -177,9 +177,7 @@ jvmTest { minHeapSize = '1g' maxHeapSize = '1g' enableAssertions = true - if (!Idea.active) { // Workaround for KT-42671 - systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' - } + systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' // 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true" if (!Idea.active && rootProject.properties['stress'] == null) { exclude '**/*StressTest.*' diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index df10501bac..5a1a1ed1b7 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -21,9 +21,9 @@ internal object AgentPremain { public var isInstalledStatically = false - private val enableCreationStackTraces = + private val enableCreationStackTraces = runCatching { System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() - ?: DebugProbesImpl.enableCreationStackTraces + }.getOrNull() ?: DebugProbesImpl.enableCreationStackTraces @JvmStatic public fun premain(args: String?, instrumentation: Instrumentation) { From 9eaa9c647c6b10911b34ab2cd10a9aa4bc08b713 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 19 Oct 2020 10:17:16 -0700 Subject: [PATCH 140/257] Introduce CoroutineContext.job extension (#2312) Fixes #2159 --- .../api/kotlinx-coroutines-core.api | 1 + kotlinx-coroutines-core/common/src/Job.kt | 9 +++++++++ .../{EnsureActiveTest.kt => JobExtensionsTest.kt} | 13 ++++++++++++- kotlinx-coroutines-core/jvm/src/Interruptible.kt | 3 +-- .../jvm/src/debug/internal/DebugProbesImpl.kt | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) rename kotlinx-coroutines-core/common/test/{EnsureActiveTest.kt => JobExtensionsTest.kt} (81%) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index bb1c0f36ab..c3eddb98b0 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -394,6 +394,7 @@ public final class kotlinx/coroutines/JobKt { public static final fun cancelFutureOnCompletion (Lkotlinx/coroutines/Job;Ljava/util/concurrent/Future;)Lkotlinx/coroutines/DisposableHandle; public static final fun ensureActive (Lkotlin/coroutines/CoroutineContext;)V public static final fun ensureActive (Lkotlinx/coroutines/Job;)V + public static final fun getJob (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/Job; public static final fun isActive (Lkotlin/coroutines/CoroutineContext;)Z } diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index 754fa43ece..2e05635a29 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -634,6 +634,15 @@ public fun CoroutineContext.cancelChildren(cause: CancellationException? = null) @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun CoroutineContext.cancelChildren(): Unit = cancelChildren(null) +/** + * Retrieves the current [Job] instance from the given [CoroutineContext] or + * throws [IllegalStateException] if no job is present in the context. + * + * This method is a short-cut for `coroutineContext[Job]!!` and should be used only when it is known in advance that + * the context does have instance of the job in it. + */ +public val CoroutineContext.job: Job get() = get(Job) ?: error("Current context doesn't contain Job in it: $this") + /** * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancelChildren]. */ diff --git a/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt b/kotlinx-coroutines-core/common/test/JobExtensionsTest.kt similarity index 81% rename from kotlinx-coroutines-core/common/test/EnsureActiveTest.kt rename to kotlinx-coroutines-core/common/test/JobExtensionsTest.kt index 89e749cae0..b335926b1f 100644 --- a/kotlinx-coroutines-core/common/test/EnsureActiveTest.kt +++ b/kotlinx-coroutines-core/common/test/JobExtensionsTest.kt @@ -4,9 +4,10 @@ package kotlinx.coroutines +import kotlin.coroutines.* import kotlin.test.* -class EnsureActiveTest : TestBase() { +class JobExtensionsTest : TestBase() { private val job = Job() private val scope = CoroutineScope(job + CoroutineExceptionHandler { _, _ -> }) @@ -81,4 +82,14 @@ class EnsureActiveTest : TestBase() { assertTrue(exception is JobCancellationException) assertTrue(exception.cause is TestException) } + + @Test + fun testJobExtension() = runTest { + assertSame(coroutineContext[Job]!!, coroutineContext.job) + assertSame(NonCancellable, NonCancellable.job) + assertSame(job, job.job) + assertFailsWith { EmptyCoroutineContext.job } + assertFailsWith { Dispatchers.Default.job } + assertFailsWith { (Dispatchers.Default + CoroutineName("")).job } + } } diff --git a/kotlinx-coroutines-core/jvm/src/Interruptible.kt b/kotlinx-coroutines-core/jvm/src/Interruptible.kt index f50e0936d5..070aa62497 100644 --- a/kotlinx-coroutines-core/jvm/src/Interruptible.kt +++ b/kotlinx-coroutines-core/jvm/src/Interruptible.kt @@ -40,8 +40,7 @@ public suspend fun runInterruptible( private fun runInterruptibleInExpectedContext(coroutineContext: CoroutineContext, block: () -> T): T { try { - val job = coroutineContext[Job]!! // withContext always creates a job - val threadState = ThreadState(job) + val threadState = ThreadState(coroutineContext.job) threadState.setup() try { return block() diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 4b7c09b347..83bc02c6d6 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -111,7 +111,7 @@ internal object DebugProbesImpl { check(isInstalled) { "Debug probes are not installed" } val jobToStack = capturedCoroutines .filter { it.delegate.context[Job] != null } - .associateBy({ it.delegate.context[Job]!! }, { it.info }) + .associateBy({ it.delegate.context.job }, { it.info }) return buildString { job.build(jobToStack, this, "") } From 9587590fc003bcd8271a5388075e116c61fb26f5 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 20 Oct 2020 02:40:38 -0700 Subject: [PATCH 141/257] Combine and zip rework (#2308) * Rework Flow.zip operator: improve its performance by 40%, collect one of the upstreams in the same coroutine as emitter * Rework Flow.combine * Get rid of two code paths * Get rid of accidental O(N^2) where N is the number of flows that caused #2296 * Get rid of select that hits performance hard, improving performance by 50% in the pessimistic case * Get rid of crossinlines in API and implementation to fix Android issues * Make combine fairer and its results less surprising in sequential scenarios * Improve stacktrace recovery and stackwalking for SafeCollector, flowOn and zip operators * Update JMH Fixes #1743 Fixes #1683 Fixes #2296 --- benchmarks/build.gradle.kts | 30 +-- .../benchmarks/flow/CombineFlowsBenchmark.kt | 34 +++ .../flow/CombineTwoFlowsBenchmark.kt | 47 ++++ .../benchmarks/flow/NumbersBenchmark.kt | 6 +- .../common/src/channels/AbstractChannel.kt | 8 - .../common/src/channels/ChannelCoroutine.kt | 5 - .../common/src/flow/Migration.kt | 14 +- .../common/src/flow/internal/ChannelFlow.kt | 26 ++- .../common/src/flow/internal/Combine.kt | 218 +++++++++--------- .../common/src/flow/internal/NullSurrogate.kt | 9 + .../common/src/flow/operators/Zip.kt | 81 ++++--- .../src/internal/LocalAtomics.common.kt | 21 ++ .../operators/CombineParametersTestBase.kt | 29 +++ .../common/test/flow/operators/CombineTest.kt | 55 +++-- .../common/test/flow/operators/ZipTest.kt | 80 +++++-- .../js/src/internal/LocalAtomics.kt | 15 ++ .../jvm/src/flow/internal/SafeCollector.kt | 7 +- .../jvm/src/internal/LocalAtomics.kt | 8 + .../native/src/internal/LocalAtomics.kt | 20 ++ .../test/WithContextUndispatchedTest.kt | 67 ++++++ settings.gradle | 2 +- 21 files changed, 551 insertions(+), 231 deletions(-) create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/CombineFlowsBenchmark.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/flow/CombineTwoFlowsBenchmark.kt create mode 100644 kotlinx-coroutines-core/common/src/internal/LocalAtomics.common.kt create mode 100644 kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt create mode 100644 kotlinx-coroutines-core/jvm/src/internal/LocalAtomics.kt create mode 100644 kotlinx-coroutines-core/native/src/internal/LocalAtomics.kt create mode 100644 kotlinx-coroutines-debug/test/WithContextUndispatchedTest.kt diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 5da40f261e..b60dcbc8f4 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -31,38 +31,12 @@ tasks.named("compileJmhKotlin") { } } -/* - * Due to a bug in the inliner it sometimes does not remove inlined symbols (that are later renamed) from unused code paths, - * and it breaks JMH that tries to post-process these symbols and fails because they are renamed. - */ -val removeRedundantFiles by tasks.registering(Delete::class) { - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$nBlanks\$1\$\$special\$\$inlined\$map\$1\$2\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$bonusForDoubleLetter\$1\$\$special\$\$inlined\$map\$1\$2\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$score2\$1\$\$special\$\$inlined\$map\$1\$2\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOptKt\$\$special\$\$inlined\$collect\$2\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleOpt\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/FlowPlaysScrabbleBase\$play\$histoOfLetters\$1\$\$special\$\$inlined\$fold\$1\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/scrabble/SaneFlowPlaysScrabble\$play\$buildHistoOnScore\$1\$\$special\$\$inlined\$filter\$1\$1.class") - // Primes - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$2\$1.class") - delete("$buildDir/classes/kotlin/jmh/benchmarks/flow/misc/Numbers\$\$special\$\$inlined\$filter\$1\$1.class") -} - -tasks.named("jmhRunBytecodeGenerator") { - dependsOn(removeRedundantFiles) -} // It is better to use the following to run benchmarks, otherwise you may get unexpected errors: // ./gradlew --no-daemon cleanJmhJar jmh -Pjmh="MyBenchmark" extensions.configure("jmh") { - jmhVersion = "1.21" + jmhVersion = "1.26" duplicateClassesStrategy = DuplicatesStrategy.INCLUDE failOnError = true resultFormat = "CSV" @@ -80,7 +54,7 @@ tasks.named("jmhJar") { } dependencies { - compile("org.openjdk.jmh:jmh-core:1.21") + compile("org.openjdk.jmh:jmh-core:1.26") compile("io.projectreactor:reactor-core:${version("reactor")}") compile("io.reactivex.rxjava2:rxjava:2.1.9") compile("com.github.akarnokd:rxjava2-extensions:0.20.8") diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/CombineFlowsBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/CombineFlowsBenchmark.kt new file mode 100644 index 0000000000..4725ceda91 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/CombineFlowsBenchmark.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.openjdk.jmh.annotations.* +import java.util.concurrent.* + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class CombineFlowsBenchmark { + + @Param("10", "100", "1000") + private var size = 10 + + @Benchmark + fun combine() = runBlocking { + combine((1 until size).map { flowOf(it) }) { a -> a}.collect() + } + + @Benchmark + fun combineTransform() = runBlocking { + val list = (1 until size).map { flowOf(it) }.toList() + combineTransform((1 until size).map { flowOf(it) }) { emit(it) }.collect() + } +} + diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/CombineTwoFlowsBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/CombineTwoFlowsBenchmark.kt new file mode 100644 index 0000000000..f7fbc6cf23 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/CombineTwoFlowsBenchmark.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.internal.* +import org.openjdk.jmh.annotations.* +import java.util.concurrent.* + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +open class CombineTwoFlowsBenchmark { + + @Param("100", "100000", "1000000") + private var size = 100000 + + @Benchmark + fun combinePlain() = runBlocking { + val flow = (1 until size.toLong()).asFlow() + flow.combine(flow) { a, b -> a + b }.collect() + } + + @Benchmark + fun combineTransform() = runBlocking { + val flow = (1 until size.toLong()).asFlow() + flow.combineTransform(flow) { a, b -> emit(a + b) }.collect() + } + + @Benchmark + fun combineVararg() = runBlocking { + val flow = (1 until size.toLong()).asFlow() + combine(listOf(flow, flow)) { arr -> arr[0] + arr[1] }.collect() + } + + @Benchmark + fun combineTransformVararg() = runBlocking { + val flow = (1 until size.toLong()).asFlow() + combineTransform(listOf(flow, flow)) { arr -> emit(arr[0] + arr[1]) }.collect() + } +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt index 4ebb3d07ff..8453f5c7f9 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt @@ -77,14 +77,14 @@ open class NumbersBenchmark { @Benchmark fun zipRx() { - val numbers = rxNumbers().take(natural.toLong()) + val numbers = rxNumbers().take(natural) val first = numbers .filter { it % 2L != 0L } .map { it * it } val second = numbers .filter { it % 2L == 0L } .map { it * it } - first.zipWith(second, BiFunction { v1, v2 -> v1 + v2 }).filter { it % 3 == 0L }.count() + first.zipWith(second, { v1, v2 -> v1 + v2 }).filter { it % 3 == 0L }.count() .blockingGet() } @@ -98,7 +98,7 @@ open class NumbersBenchmark { @Benchmark fun transformationsRx(): Long { - return rxNumbers().take(natural.toLong()) + return rxNumbers().take(natural) .filter { it % 2L != 0L } .map { it * it } .filter { (it + 1) % 3 == 0L }.count() diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 53ecf06a2c..8edd2b310c 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -137,14 +137,6 @@ internal abstract class AbstractSendChannel( return sendSuspend(element) } - internal suspend fun sendFair(element: E) { - if (offerInternal(element) === OFFER_SUCCESS) { - yield() // Works only on fast path to properly work in sequential use-cases - return - } - return sendSuspend(element) - } - public final override fun offer(element: E): Boolean { val result = offerInternal(element) return when { diff --git a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt index 3f53b48c53..a75d466199 100644 --- a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt @@ -34,9 +34,4 @@ internal open class ChannelCoroutine( _channel.cancel(exception) // cancel the channel cancelCoroutine(exception) // cancel the job } - - @Suppress("UNCHECKED_CAST") - suspend fun sendFair(element: E) { - (_channel as AbstractSendChannel).sendFair(element) - } } diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 59873eba5f..490e88265c 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -367,10 +367,10 @@ public fun Flow.combineLatest(other: Flow, transform: suspen message = "Flow analogue of 'combineLatest' is 'combine'", replaceWith = ReplaceWith("combine(this, other, other2, transform)") ) -public inline fun Flow.combineLatest( +public fun Flow.combineLatest( other: Flow, other2: Flow, - crossinline transform: suspend (T1, T2, T3) -> R + transform: suspend (T1, T2, T3) -> R ) = combine(this, other, other2, transform) @Deprecated( @@ -378,11 +378,11 @@ public inline fun Flow.combineLatest( message = "Flow analogue of 'combineLatest' is 'combine'", replaceWith = ReplaceWith("combine(this, other, other2, other3, transform)") ) -public inline fun Flow.combineLatest( +public fun Flow.combineLatest( other: Flow, other2: Flow, other3: Flow, - crossinline transform: suspend (T1, T2, T3, T4) -> R + transform: suspend (T1, T2, T3, T4) -> R ) = combine(this, other, other2, other3, transform) @Deprecated( @@ -390,12 +390,12 @@ public inline fun Flow.combineLatest( message = "Flow analogue of 'combineLatest' is 'combine'", replaceWith = ReplaceWith("combine(this, other, other2, other3, transform)") ) -public inline fun Flow.combineLatest( +public fun Flow.combineLatest( other: Flow, other2: Flow, other3: Flow, other4: Flow, - crossinline transform: suspend (T1, T2, T3, T4, T5) -> R + transform: suspend (T1, T2, T3, T4, T5) -> R ): Flow = combine(this, other, other2, other3, other4, transform) /** @@ -482,4 +482,4 @@ public fun Flow.replay(bufferSize: Int): Flow = noImpl() 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 +public fun Flow.cache(): Flow = noImpl() diff --git a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt index e53ef35c45..f3730cc7fa 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/ChannelFlow.kt @@ -224,19 +224,33 @@ private class UndispatchedContextCollector( private val emitRef: suspend (T) -> Unit = { downstream.emit(it) } // allocate suspend function ref once on creation override suspend fun emit(value: T): Unit = - withContextUndispatched(emitContext, countOrElement, emitRef, value) + withContextUndispatched(emitContext, value, countOrElement, emitRef) } // Efficiently computes block(value) in the newContext -private suspend fun withContextUndispatched( +internal suspend fun withContextUndispatched( newContext: CoroutineContext, + value: V, countOrElement: Any = threadContextElements(newContext), // can be precomputed for speed - block: suspend (V) -> T, value: V + block: suspend (V) -> T ): T = suspendCoroutineUninterceptedOrReturn { uCont -> withCoroutineContext(newContext, countOrElement) { - block.startCoroutineUninterceptedOrReturn(value, Continuation(newContext) { - uCont.resumeWith(it) - }) + block.startCoroutineUninterceptedOrReturn(value, StackFrameContinuation(uCont, newContext)) } } + +// Continuation that links the caller with uCont with walkable CoroutineStackFrame +private class StackFrameContinuation( + private val uCont: Continuation, override val context: CoroutineContext +) : Continuation, CoroutineStackFrame { + + override val callerFrame: CoroutineStackFrame? + get() = uCont as? CoroutineStackFrame + + override fun resumeWith(result: Result) { + uCont.resumeWith(result) + } + + override fun getStackTraceElement(): StackTraceElement? = null +} diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt index 67da32c9f9..bbdebd08b9 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt @@ -9,133 +9,135 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.internal.* -import kotlinx.coroutines.selects.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* -internal fun getNull(): Symbol = NULL // Workaround for JS BE bug - -internal suspend fun FlowCollector.combineTransformInternal( - first: Flow, second: Flow, - transform: suspend FlowCollector.(a: T1, b: T2) -> Unit -) { - coroutineScope { - val firstChannel = asFairChannel(first) - val secondChannel = asFairChannel(second) - var firstValue: Any? = null - var secondValue: Any? = null - var firstIsClosed = false - var secondIsClosed = false - while (!firstIsClosed || !secondIsClosed) { - select { - onReceive(firstIsClosed, firstChannel, { firstIsClosed = true }) { value -> - firstValue = value - if (secondValue !== null) { - transform(getNull().unbox(firstValue), getNull().unbox(secondValue) as T2) - } - } - - onReceive(secondIsClosed, secondChannel, { secondIsClosed = true }) { value -> - secondValue = value - if (firstValue !== null) { - transform(getNull().unbox(firstValue) as T1, getNull().unbox(secondValue) as T2) - } - } - } - } - } -} +private typealias Update = IndexedValue @PublishedApi internal suspend fun FlowCollector.combineInternal( flows: Array>, - arrayFactory: () -> Array, + arrayFactory: () -> Array?, // Array factory is required to workaround array typing on JVM transform: suspend FlowCollector.(Array) -> Unit -): Unit = coroutineScope { +): Unit = flowScope { // flow scope so any cancellation within the source flow will cancel the whole scope val size = flows.size - val channels = Array(size) { asFairChannel(flows[it]) } + if (size == 0) return@flowScope // bail-out for empty input val latestValues = arrayOfNulls(size) - val isClosed = Array(size) { false } - var nonClosed = size - var remainingNulls = size - // See flow.combine(other) for explanation. - while (nonClosed != 0) { - select { - for (i in 0 until size) { - onReceive(isClosed[i], channels[i], { isClosed[i] = true; --nonClosed }) { value -> - if (latestValues[i] == null) --remainingNulls - latestValues[i] = value - if (remainingNulls != 0) return@onReceive - val arguments = arrayFactory() - for (index in 0 until size) { - arguments[index] = NULL.unbox(latestValues[index]) - } - transform(arguments as Array) + latestValues.fill(UNINITIALIZED) // Smaller bytecode & faster that Array(size) { UNINITIALIZED } + val resultChannel = Channel(size) + val nonClosed = LocalAtomicInt(size) + var remainingAbsentValues = size + for (i in 0 until size) { + // Coroutine per flow that keeps track of its value and sends result to downstream + launch { + try { + flows[i].collect { value -> + resultChannel.send(Update(i, value)) + yield() // Emulate fairness, giving each flow chance to emit + } + } finally { + // Close the channel when there is no more flows + if (nonClosed.decrementAndGet() == 0) { + resultChannel.close() } } } } -} -private inline fun SelectBuilder.onReceive( - isClosed: Boolean, - channel: ReceiveChannel, - crossinline onClosed: () -> Unit, - noinline onReceive: suspend (value: Any) -> Unit -) { - if (isClosed) return - @Suppress("DEPRECATION") - channel.onReceiveOrNull { - // TODO onReceiveOrClosed when boxing issues are fixed - if (it === null) onClosed() - else onReceive(it) - } -} + /* + * Batch-receive optimization: read updates in batches, but bail-out + * as soon as we encountered two values from the same source + */ + val lastReceivedEpoch = ByteArray(size) + var currentEpoch: Byte = 0 + while (true) { + ++currentEpoch + // Start batch + // The very first receive in epoch should be suspending + var element = resultChannel.receiveOrNull() ?: break // Channel is closed, nothing to do here + while (true) { + val index = element.index + // Update values + val previous = latestValues[index] + latestValues[index] = element.value + if (previous === UNINITIALIZED) --remainingAbsentValues + // Check epoch + // Received the second value from the same flow in the same epoch -- bail out + if (lastReceivedEpoch[index] == currentEpoch) break + lastReceivedEpoch[index] = currentEpoch + element = resultChannel.poll() ?: break + } -// Channel has any type due to onReceiveOrNull. This will be fixed after receiveOrClosed -private fun CoroutineScope.asFairChannel(flow: Flow<*>): ReceiveChannel = produce { - val channel = channel as ChannelCoroutine - flow.collect { value -> - return@collect channel.sendFair(value ?: NULL) + // Process batch result if there is enough data + if (remainingAbsentValues == 0) { + /* + * If arrayFactory returns null, then we can avoid array copy because + * it's our own safe transformer that immediately deconstructs the array + */ + val results = arrayFactory() + if (results == null) { + transform(latestValues as Array) + } else { + (latestValues as Array).copyInto(results) + transform(results as Array) + } + } } } -internal fun zipImpl(flow: Flow, flow2: Flow, transform: suspend (T1, T2) -> R): Flow = unsafeFlow { - coroutineScope { - val first = asChannel(flow) - val second = asChannel(flow2) - /* - * This approach only works with rendezvous channel and is required to enforce correctness - * in the following scenario: - * ``` - * val f1 = flow { emit(1); delay(Long.MAX_VALUE) } - * val f2 = flowOf(1) - * f1.zip(f2) { ... } - * ``` - * - * Invariant: this clause is invoked only when all elements from the channel were processed (=> rendezvous restriction). - */ - (second as SendChannel<*>).invokeOnClose { - if (!first.isClosedForReceive) first.cancel(AbortFlowException(this@unsafeFlow)) - } +internal fun zipImpl(flow: Flow, flow2: Flow, transform: suspend (T1, T2) -> R): Flow = + unsafeFlow { + coroutineScope { + val second = produce { + flow2.collect { value -> + return@collect channel.send(value ?: NULL) + } + } - val otherIterator = second.iterator() - try { - first.consumeEach { value -> - if (!otherIterator.hasNext()) { - return@consumeEach + /* + * This approach only works with rendezvous channel and is required to enforce correctness + * in the following scenario: + * ``` + * val f1 = flow { emit(1); delay(Long.MAX_VALUE) } + * val f2 = flowOf(1) + * f1.zip(f2) { ... } + * ``` + * + * Invariant: this clause is invoked only when all elements from the channel were processed (=> rendezvous restriction). + */ + val collectJob = Job() + (second as SendChannel<*>).invokeOnClose { + // Optimization to avoid AFE allocation when the other flow is done + if (collectJob.isActive) collectJob.cancel(AbortFlowException(this@unsafeFlow)) + } + + try { + /* + * Non-trivial undispatched (because we are in the right context and there is no structured concurrency) + * hierarchy: + * -Outer coroutineScope that owns the whole zip process + * - First flow is collected by the child of coroutineScope, collectJob. + * So it can be safely cancelled as soon as the second flow is done + * - **But** the downstream MUST NOT be cancelled when the second flow is done, + * so we emit to downstream from coroutineScope job. + * Typically, such hierarchy requires coroutine for collector that communicates + * with coroutines scope via a channel, but it's way too expensive, so + * we are using this trick instead. + */ + val scopeContext = coroutineContext + val cnt = threadContextElements(scopeContext) + withContextUndispatched(coroutineContext + collectJob, Unit) { + flow.collect { value -> + withContextUndispatched(scopeContext, Unit, cnt) { + val otherValue = second.receiveOrNull() ?: throw AbortFlowException(this@unsafeFlow) + emit(transform(value, NULL.unbox(otherValue))) + } + } } - emit(transform(NULL.unbox(value), NULL.unbox(otherIterator.next()))) + } catch (e: AbortFlowException) { + e.checkOwnership(owner = this@unsafeFlow) + } finally { + if (!second.isClosedForReceive) second.cancel() } - } catch (e: AbortFlowException) { - e.checkOwnership(owner = this@unsafeFlow) - } finally { - if (!second.isClosedForReceive) second.cancel(AbortFlowException(this@unsafeFlow)) } } -} - -// Channel has any type due to onReceiveOrNull. This will be fixed after receiveOrClosed -private fun CoroutineScope.asChannel(flow: Flow<*>): ReceiveChannel = produce { - flow.collect { value -> - return@collect channel.send(value ?: NULL) - } -} diff --git a/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt b/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt index 22e1957419..f20deb2d38 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/NullSurrogate.kt @@ -11,11 +11,20 @@ import kotlin.native.concurrent.* /** * This value is used a a surrogate `null` value when needed. * It should never leak to the outside world. + * Its usage typically are paired with [Symbol.unbox] usages. */ @JvmField @SharedImmutable internal val NULL = Symbol("NULL") +/** + * Symbol to indicate that the value is not yet initialized. + * It should never leak to the outside world. + */ +@JvmField +@SharedImmutable +internal val UNINITIALIZED = Symbol("UNINITIALIZED") + /* * Symbol used to indicate that the flow is complete. * It should never leak to the outside world. diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt index ec661819f2..790c0895e4 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Zip.kt @@ -8,7 +8,6 @@ package kotlinx.coroutines.flow -import kotlinx.coroutines.* import kotlinx.coroutines.flow.internal.* import kotlin.jvm.* import kotlinx.coroutines.flow.flow as safeFlow @@ -31,9 +30,7 @@ import kotlinx.coroutines.flow.internal.unsafeFlow as flow */ @JvmName("flowCombine") public fun Flow.combine(flow: Flow, transform: suspend (a: T1, b: T2) -> R): Flow = flow { - combineTransformInternal(this@combine, flow) { a, b -> - emit(transform(a, b)) - } + combineInternal(arrayOf(this@combine, flow), nullArrayFactory(), { emit(transform(it[0] as T1, it[1] as T2)) }) } /** @@ -75,10 +72,11 @@ public fun combine(flow: Flow, flow2: Flow, transform: suspe public fun Flow.combineTransform( flow: Flow, @BuilderInference transform: suspend FlowCollector.(a: T1, b: T2) -> Unit -): Flow = safeFlow { - combineTransformInternal(this@combineTransform, flow) { a, b -> - transform(a, b) - } +): Flow = combineTransformUnsafe(this, flow) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2 + ) } /** @@ -102,7 +100,7 @@ public fun combineTransform( flow: Flow, flow2: Flow, @BuilderInference transform: suspend FlowCollector.(a: T1, b: T2) -> Unit -): Flow = combineTransform(flow, flow2) { args: Array<*> -> +): Flow = combineTransformUnsafe(flow, flow2) { args: Array<*> -> transform( args[0] as T1, args[1] as T2 @@ -113,12 +111,12 @@ public fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -public inline fun combine( +public fun combine( flow: Flow, flow2: Flow, flow3: Flow, - @BuilderInference crossinline transform: suspend (T1, T2, T3) -> R -): Flow = combine(flow, flow2, flow3) { args: Array<*> -> + @BuilderInference transform: suspend (T1, T2, T3) -> R +): Flow = combineUnsafe(flow, flow2, flow3) { args: Array<*> -> transform( args[0] as T1, args[1] as T2, @@ -132,12 +130,12 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -public inline fun combineTransform( +public fun combineTransform( flow: Flow, flow2: Flow, flow3: Flow, - @BuilderInference crossinline transform: suspend FlowCollector.(T1, T2, T3) -> Unit -): Flow = combineTransform(flow, flow2, flow3) { args: Array<*> -> + @BuilderInference transform: suspend FlowCollector.(T1, T2, T3) -> Unit +): Flow = combineTransformUnsafe(flow, flow2, flow3) { args: Array<*> -> transform( args[0] as T1, args[1] as T2, @@ -149,12 +147,12 @@ public inline fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -public inline fun combine( +public fun combine( flow: Flow, flow2: Flow, flow3: Flow, flow4: Flow, - crossinline transform: suspend (T1, T2, T3, T4) -> R + transform: suspend (T1, T2, T3, T4) -> R ): Flow = combine(flow, flow2, flow3, flow4) { args: Array<*> -> transform( args[0] as T1, @@ -170,13 +168,13 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -public inline fun combineTransform( +public fun combineTransform( flow: Flow, flow2: Flow, flow3: Flow, flow4: Flow, - @BuilderInference crossinline transform: suspend FlowCollector.(T1, T2, T3, T4) -> Unit -): Flow = combineTransform(flow, flow2, flow3, flow4) { args: Array<*> -> + @BuilderInference transform: suspend FlowCollector.(T1, T2, T3, T4) -> Unit +): Flow = combineTransformUnsafe(flow, flow2, flow3, flow4) { args: Array<*> -> transform( args[0] as T1, args[1] as T2, @@ -189,14 +187,14 @@ public inline fun combineTransform( * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. */ -public inline fun combine( +public fun combine( flow: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, - crossinline transform: suspend (T1, T2, T3, T4, T5) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> + transform: suspend (T1, T2, T3, T4, T5) -> R +): Flow = combineUnsafe(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> transform( args[0] as T1, args[1] as T2, @@ -212,14 +210,14 @@ public inline fun combine( * The receiver of the [transform] is [FlowCollector] and thus `transform` is a * generic function that may transform emitted element, skip it or emit it multiple times. */ -public inline fun combineTransform( +public fun combineTransform( flow: Flow, flow2: Flow, flow3: Flow, flow4: Flow, flow5: Flow, - @BuilderInference crossinline transform: suspend FlowCollector.(T1, T2, T3, T4, T5) -> Unit -): Flow = combineTransform(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> + @BuilderInference transform: suspend FlowCollector.(T1, T2, T3, T4, T5) -> Unit +): Flow = combineTransformUnsafe(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> transform( args[0] as T1, args[1] as T2, @@ -253,6 +251,31 @@ public inline fun combineTransform( combineInternal(flows, { arrayOfNulls(flows.size) }, { transform(it) }) } +/* + * Same as combine, but does not copy array each time, deconstructing existing + * array each time. Used in overloads that accept FunctionN instead of Function> + */ +private inline fun combineUnsafe( + vararg flows: Flow, + crossinline transform: suspend (Array) -> R +): Flow = flow { + combineInternal(flows, nullArrayFactory(), { emit(transform(it)) }) +} + +/* + * Same as combineTransform, but does not copy array each time, deconstructing existing + * array each time. Used in overloads that accept FunctionN instead of Function> + */ +private inline fun combineTransformUnsafe( + vararg flows: Flow, + @BuilderInference crossinline transform: suspend FlowCollector.(Array) -> Unit +): Flow = safeFlow { + combineInternal(flows, nullArrayFactory(), { transform(it) }) +} + +// Saves bunch of anonymous classes +private fun nullArrayFactory(): () -> Array? = { null } + /** * Returns a [Flow] whose values are generated with [transform] function by combining * the most recently emitted values by each flow. @@ -298,5 +321,11 @@ public inline fun combineTransform( * println(it) // Will print "1a 2b 3c" * } * ``` + * + * ### Buffering + * + * The upstream flow is collected sequentially in the same coroutine without any buffering, while the + * [other] flow is collected concurrently as if `buffer(0)` is used. See documentation in the [buffer] operator + * for explanation. You can use additional calls to the [buffer] operator as needed for more concurrency. */ public fun Flow.zip(other: Flow, transform: suspend (T1, T2) -> R): Flow = zipImpl(this, other, transform) diff --git a/kotlinx-coroutines-core/common/src/internal/LocalAtomics.common.kt b/kotlinx-coroutines-core/common/src/internal/LocalAtomics.common.kt new file mode 100644 index 0000000000..bcfb932de3 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/LocalAtomics.common.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +/* + * These are atomics that are used as local variables + * where atomicfu doesn't support its tranformations. + * + * Have `Local` prefix to avoid AFU clashes during star-imports + */ +internal expect class LocalAtomicInt(value: Int) { + fun get(): Int + fun set(value: Int) + fun decrementAndGet(): Int +} + +internal inline var LocalAtomicInt.value + get() = get() + set(value) = set(value) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt index b51197e395..8c65ea4fe7 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt @@ -161,4 +161,33 @@ class CombineParametersTest : TestBase() { }.singleOrNull() assertNull(value) } + + @Test + fun testFairnessInVariousConfigurations() = runTest { + // Test various configurations + for (flowsCount in 2..5) { + for (flowSize in 1..5) { + val flows = List(flowsCount) { (1..flowSize).asFlow() } + val combined = combine(flows) { it.joinToString(separator = "") }.toList() + val expected = List(flowSize) { (it + 1).toString().repeat(flowsCount) } + assertEquals(expected, combined, "Count: $flowsCount, size: $flowSize") + } + } + } + + @Test + fun testEpochOverflow() = runTest { + val flow = (0..1023).asFlow() + val result = flow.combine(flow) { a, b -> a + b }.toList() + assertEquals(List(1024) { it * 2 } , result) + } + + @Test + fun testArrayType() = runTest { + val arr = flowOf(1) + combine(listOf(arr, arr)) { + println(it[0]) + it[0] + }.toList().also { println(it) } + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt index 2893321998..5e2926d082 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineTest.kt @@ -1,10 +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.flow +@file:Suppress("UNCHECKED_CAST") +package kotlinx.coroutines.flow.operators import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlin.test.* import kotlinx.coroutines.flow.combine as combineOriginal import kotlinx.coroutines.flow.combineTransform as combineTransformOriginal @@ -21,28 +22,28 @@ abstract class CombineTestBase : TestBase() { val flow = flowOf("a", "b", "c") val flow2 = flowOf(1, 2, 3) val list = flow.combineLatest(flow2) { i, j -> i + j }.toList() - assertEquals(listOf("a1", "b1", "b2", "c2", "c3"), list) + assertEquals(listOf("a1", "b2", "c3"), list) } @Test fun testNulls() = runTest { val flow = flowOf("a", null, null) val flow2 = flowOf(1, 2, 3) - val list = flow.combineLatest(flow2, { i, j -> i + j }).toList() - assertEquals(listOf("a1", "null1", "null2", "null2", "null3"), list) + val list = flow.combineLatest(flow2) { i, j -> i + j }.toList() + assertEquals(listOf("a1", "null2", "null3"), list) } @Test fun testNullsOther() = runTest { val flow = flowOf("a", "b", "c") val flow2 = flowOf(null, 2, null) - val list = flow.combineLatest(flow2, { i, j -> i + j }).toList() - assertEquals(listOf("anull", "bnull", "b2", "c2", "cnull"), list) + val list = flow.combineLatest(flow2) { i, j -> i + j }.toList() + assertEquals(listOf("anull", "b2", "cnull"), list) } @Test fun testEmptyFlow() = runTest { - val flow = emptyFlow().combineLatest(emptyFlow(), { i, j -> i + j }) + val flow = emptyFlow().combineLatest(emptyFlow()) { i, j -> i + j } assertNull(flow.singleOrNull()) } @@ -208,12 +209,12 @@ abstract class CombineTestBase : TestBase() { } val f2 = flow { emit(1) - hang { expect(3) } + expectUnreached() } - val flow = f1.combineLatest(f2, { _, _ -> 1 }).onEach { expect(2) } + val flow = f1.combineLatest(f2) { _, _ -> 1 }.onEach { expect(2) } assertFailsWith(flow) - finish(4) + finish(3) } @Test @@ -229,7 +230,7 @@ abstract class CombineTestBase : TestBase() { hang { expect(6) } } - val flow = f1.combineLatest(f2, { _, _ -> 1 }).onEach { + val flow = f1.combineLatest(f2) { _, _ -> 1 }.onEach { expect(1) yield() expect(4) @@ -248,7 +249,7 @@ abstract class CombineTestBase : TestBase() { emit(Unit) // emit } cancel() // cancel the scope - flow.combineLatest(flow) { u, _ -> u }.collect { + flow.combineLatest(flow) { _, _ -> }.collect { // should not be reached, because cancelled before it runs expectUnreached() } @@ -265,15 +266,26 @@ class CombineTransformTest : CombineTestBase() { emit(transform(a, b)) } } +// Array null-out is an additional test for our array elimination optimization class CombineVarargAdapterTest : CombineTestBase() { override fun Flow.combineLatest(other: Flow, transform: suspend (T1, T2) -> R): Flow = - combineOriginal(this, other) { args: Array -> transform(args[0] as T1, args[1] as T2) } + combineOriginal(this, other) { args: Array -> + transform(args[0] as T1, args[1] as T2).also { + args[0] = null + args[1] = null + } + } } class CombineIterableTest : CombineTestBase() { override fun Flow.combineLatest(other: Flow, transform: suspend (T1, T2) -> R): Flow = - combineOriginal(listOf(this, other)) { args -> transform(args[0] as T1, args[1] as T2) } + combineOriginal(listOf(this, other)) { args -> + transform(args[0] as T1, args[1] as T2).also { + args[0] = null + args[1] = null + } + } } class CombineTransformAdapterTest : CombineTestBase() { @@ -283,11 +295,20 @@ class CombineTransformAdapterTest : CombineTestBase() { class CombineTransformVarargAdapterTest : CombineTestBase() { override fun Flow.combineLatest(other: Flow, transform: suspend (T1, T2) -> R): Flow = - combineTransformOriginal(this, other) { args: Array -> emit(transform(args[0] as T1, args[1] as T2)) } + combineTransformOriginal(this, other) { args: Array -> + emit(transform(args[0] as T1, args[1] as T2)) // Mess up with array + args[0] = null + args[1] = null + } } class CombineTransformIterableTest : CombineTestBase() { override fun Flow.combineLatest(other: Flow, transform: suspend (T1, T2) -> R): Flow = - combineTransformOriginal(listOf(this, other)) { args -> emit(transform(args[0] as T1, args[1] as T2)) } + combineTransformOriginal(listOf(this, other)) { args -> + emit(transform(args[0] as T1, args[1] as T2)) + // Mess up with array + args[0] = null + args[1] = null + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt index 5f2b5a74cd..02dbfc40d9 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/ZipTest.kt @@ -23,7 +23,7 @@ class ZipTest : TestBase() { fun testUnevenZip() = runTest { val f1 = flowOf("a", "b", "c", "d", "e") val f2 = flowOf(1, 2, 3) - assertEquals(listOf("a1", "b2", "c3"), f1.zip(f2, { i, j -> i + j }).toList()) + assertEquals(listOf("a1", "b2", "c3"), f1.zip(f2) { i, j -> i + j }.toList()) assertEquals(listOf("a1", "b2", "c3"), f2.zip(f1) { i, j -> j + i }.toList()) } @@ -67,14 +67,35 @@ class ZipTest : TestBase() { val f1 = flow { emit("1") emit("2") - expectUnreached() // the above emit will get cancelled because f2 ends } - val f2 = flowOf("a", "b") + val f2 = flow { + emit("a") + emit("b") + expectUnreached() + } assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) finish(1) } + @Test + fun testCancelWhenFlowIsDone2() = runTest { + val f1 = flow { + emit("1") + emit("2") + try { + emit("3") + expectUnreached() + } finally { + expect(1) + } + } + + val f2 = flowOf("a", "b") + assertEquals(listOf("1a", "2b"), f1.zip(f2) { s1, s2 -> s1 + s2 }.toList()) + finish(2) + } + @Test fun testCancelWhenFlowIsDoneReversed() = runTest { val f1 = flow { @@ -85,7 +106,12 @@ class ZipTest : TestBase() { } } - val f2 = flowOf("a", "b") + val f2 = flow { + emit("a") + emit("b") + yield() + } + assertEquals(listOf("a1", "b2"), f2.zip(f1) { s1, s2 -> s1 + s2 }.toList()) finish(2) } @@ -95,19 +121,19 @@ class ZipTest : TestBase() { val f1 = flow { emit("a") assertEquals("first", NamedDispatchers.name()) - expect(1) + expect(3) }.flowOn(NamedDispatchers("first")).onEach { assertEquals("with", NamedDispatchers.name()) - expect(2) + expect(4) }.flowOn(NamedDispatchers("with")) val f2 = flow { emit(1) assertEquals("second", NamedDispatchers.name()) - expect(3) + expect(1) }.flowOn(NamedDispatchers("second")).onEach { assertEquals("nested", NamedDispatchers.name()) - expect(4) + expect(2) }.flowOn(NamedDispatchers("nested")) val value = withContext(NamedDispatchers("main")) { @@ -127,14 +153,14 @@ class ZipTest : TestBase() { val f1 = flow { emit("a") hang { - expect(2) + expect(3) } }.flowOn(NamedDispatchers("first")) val f2 = flow { emit(1) hang { - expect(3) + expect(2) } }.flowOn(NamedDispatchers("second")) @@ -174,19 +200,18 @@ class ZipTest : TestBase() { val f1 = flow { expect(1) emit(1) - yield() - expect(4) + expect(5) throw CancellationException("") } val f2 = flow { expect(2) emit(1) - expect(5) + expect(3) hang { expect(6) } } - val flow = f1.zip(f2, { _, _ -> 1 }).onEach { expect(3) } + val flow = f1.zip(f2) { _, _ -> 1 }.onEach { expect(4) } assertFailsWith(flow) finish(7) } @@ -196,24 +221,37 @@ class ZipTest : TestBase() { val f1 = flow { expect(1) emit(1) - yield() - expect(4) - hang { expect(6) } + expectUnreached() // Will throw CE } val f2 = flow { expect(2) emit(1) - expect(5) - hang { expect(7) } + expect(3) + hang { expect(5) } } val flow = f1.zip(f2, { _, _ -> 1 }).onEach { - expect(3) + expect(4) yield() throw CancellationException("") } assertFailsWith(flow) - finish(8) + finish(6) + } + + @Test + fun testCancellationOfCollector() = runTest { + val f1 = flow { + emit("1") + awaitCancellation() + } + + val f2 = flow { + emit("2") + yield() + } + + f1.zip(f2) { a, b -> a + b }.collect { } } } diff --git a/kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt b/kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt new file mode 100644 index 0000000000..fffd76c452 --- /dev/null +++ b/kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +internal actual class LocalAtomicInt actual constructor(private var value: Int) { + actual fun set(value: Int) { + this.value = value + } + + actual fun get(): Int = value + + actual fun decrementAndGet(): Int = --value +} diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt index a8e04f0f16..b275a481cc 100644 --- a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt +++ b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt @@ -21,7 +21,11 @@ private val emitFun = internal actual class SafeCollector actual constructor( @JvmField internal actual val collector: FlowCollector, @JvmField internal actual val collectContext: CoroutineContext -) : FlowCollector, ContinuationImpl(NoOpContinuation, EmptyCoroutineContext) { +) : FlowCollector, ContinuationImpl(NoOpContinuation, EmptyCoroutineContext), CoroutineStackFrame { + + override val callerFrame: CoroutineStackFrame? get() = completion as? CoroutineStackFrame + + override fun getStackTraceElement(): StackTraceElement? = null @JvmField // Note, it is non-capturing lambda, so no extra allocation during init of SafeCollector internal actual val collectContextSize = collectContext.fold(0) { count, _ -> count + 1 } @@ -51,6 +55,7 @@ internal actual class SafeCollector actual constructor( */ override suspend fun emit(value: T) { return suspendCoroutineUninterceptedOrReturn sc@{ uCont -> + // Update information about caller for stackwalking try { emit(uCont, value) } catch (e: Throwable) { diff --git a/kotlinx-coroutines-core/jvm/src/internal/LocalAtomics.kt b/kotlinx-coroutines-core/jvm/src/internal/LocalAtomics.kt new file mode 100644 index 0000000000..f508749ed0 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/internal/LocalAtomics.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias LocalAtomicInt = java.util.concurrent.atomic.AtomicInteger diff --git a/kotlinx-coroutines-core/native/src/internal/LocalAtomics.kt b/kotlinx-coroutines-core/native/src/internal/LocalAtomics.kt new file mode 100644 index 0000000000..398cb63bc2 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/internal/LocalAtomics.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.* + +internal actual class LocalAtomicInt actual constructor(value: Int) { + + private val iRef = atomic(value) + + actual fun set(value: Int) { + iRef.value = value + } + + actual fun get(): Int = iRef.value + + actual fun decrementAndGet(): Int = iRef.decrementAndGet() +} diff --git a/kotlinx-coroutines-debug/test/WithContextUndispatchedTest.kt b/kotlinx-coroutines-debug/test/WithContextUndispatchedTest.kt new file mode 100644 index 0000000000..e803c980cf --- /dev/null +++ b/kotlinx-coroutines-debug/test/WithContextUndispatchedTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.* + +// Test four our internal optimization "withContextUndispatched" +class WithContextUndispatchedTest : DebugTestBase() { + + @Test + fun testZip() = runTest { + val f1 = flowOf("a") + val f2 = flow { + nestedEmit() + yield() + } + f1.zip(f2) { i, j -> i + j }.collect { + bar(false) + } + } + + private suspend fun FlowCollector.nestedEmit() { + emit(1) + emit(2) + } + + @Test + fun testUndispatchedFlowOn() = runTest { + val flow = flowOf(1, 2, 3).flowOn(CoroutineName("...")) + flow.collect { + bar(true) + } + } + + @Test + fun testUndispatchedFlowOnWithNestedCaller() = runTest { + val flow = flow { + nestedEmit() + }.flowOn(CoroutineName("...")) + flow.collect { + bar(true) + } + } + + private suspend fun bar(forFlowOn: Boolean) { + yield() + if (forFlowOn) { + verifyFlowOn() + } else { + verifyZip() + } + yield() + } + + private suspend fun verifyFlowOn() { + yield() // suspend + verifyPartialDump(1, "verifyFlowOn", "bar") + } + + private suspend fun verifyZip() { + yield() // suspend + verifyPartialDump(2, "verifyZip", "bar", "nestedEmit") + } +} diff --git a/settings.gradle b/settings.gradle index d22d65fd25..3a07891799 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,7 @@ pluginManagement { // JMH id "net.ltgt.apt" version "0.21" - id "me.champeau.gradle.jmh" version "0.5.0" + id "me.champeau.gradle.jmh" version "0.5.2" } } From 768e92bec37fa86026237e5d9780a85b4df7790d Mon Sep 17 00:00:00 2001 From: Lalit Behera Date: Tue, 20 Oct 2020 17:26:26 +0530 Subject: [PATCH 142/257] Grammar correction on the Contribution.md file (#2321) Co-authored-by: lalitbehera --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93f1330943..7737062fa3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ so do familiarize yourself with the following guidelines. Use 4 spaces for indentation. * [Build the project](#building) to make sure it all works and passes the tests. * If you fix a bug: - * Write the test the reproduces the bug. + * Write the test that reproduces the bug. * Fixes without tests are accepted only in exceptional circumstances if it can be shown that writing the corresponding test is too hard or otherwise impractical. * Follow the style of writing tests that is used in this project: From 6843648f14867ea52837fb5d71a9c2a3407f8e75 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 22 Oct 2020 04:09:45 -0700 Subject: [PATCH 143/257] Release intercepted SafeCollector when onCompletion block is done (#2323) * Do not use invokeSafely in onCompletion Co-authored-by: Roman Elizarov --- .../common/src/flow/operators/Emitters.kt | 7 ++- .../jvm/src/flow/internal/SafeCollector.kt | 1 - .../OnCompletionInterceptedReleaseTest.kt | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/flow/OnCompletionInterceptedReleaseTest.kt diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 3ffe5fe943..8be19f08e0 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -158,7 +158,12 @@ public fun Flow.onCompletion( throw e } // Normal completion - SafeCollector(this, currentCoroutineContext()).invokeSafely(action, null) + val sc = SafeCollector(this, currentCoroutineContext()) + try { + sc.action(null) + } finally { + sc.releaseIntercepted() + } } /** diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt index b275a481cc..ab42b6345f 100644 --- a/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt +++ b/kotlinx-coroutines-core/jvm/src/flow/internal/SafeCollector.kt @@ -55,7 +55,6 @@ internal actual class SafeCollector actual constructor( */ override suspend fun emit(value: T) { return suspendCoroutineUninterceptedOrReturn sc@{ uCont -> - // Update information about caller for stackwalking try { emit(uCont, value) } catch (e: Throwable) { diff --git a/kotlinx-coroutines-core/jvm/test/flow/OnCompletionInterceptedReleaseTest.kt b/kotlinx-coroutines-core/jvm/test/flow/OnCompletionInterceptedReleaseTest.kt new file mode 100644 index 0000000000..a6268b5156 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/OnCompletionInterceptedReleaseTest.kt @@ -0,0 +1,45 @@ +/* + * 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.Test +import kotlin.coroutines.* +import kotlin.test.* + +class OnCompletionInterceptedReleaseTest : TestBase() { + @Test + fun testLeak() = runTest { + expect(1) + var cont: Continuation? = null + val interceptor = CountingInterceptor() + val job = launch(interceptor, start = CoroutineStart.UNDISPATCHED) { + emptyFlow() + .onCompletion { emit(1) } + .collect { value -> + expect(2) + assertEquals(1, value) + suspendCoroutine { cont = it } + } + } + cont!!.resume(Unit) + assertTrue(job.isCompleted) + assertEquals(interceptor.intercepted, interceptor.released) + finish(3) + } + + class CountingInterceptor : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { + var intercepted = 0 + var released = 0 + override fun interceptContinuation(continuation: Continuation): Continuation { + intercepted++ + return Continuation(continuation.context) { continuation.resumeWith(it) } + } + + override fun releaseInterceptedContinuation(continuation: Continuation<*>) { + released++ + } + } +} \ No newline at end of file From 3275d222813a58f3ccf6e176cdecab75a7d88f2a Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 22 Oct 2020 06:55:00 -0700 Subject: [PATCH 144/257] Propagate exception from stateIn to the caller when the upstream failed to produce initial value (#2329) --- .../common/src/flow/operators/Share.kt | 17 +++++++++---- .../common/test/flow/sharing/StateInTest.kt | 25 +++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt index 4dd89ee4bf..6351d4a89b 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -330,13 +330,20 @@ private fun CoroutineScope.launchSharingDeferred( 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()) + try { + var state: MutableStateFlow? = null + upstream.collect { value -> + state?.let { it.value = value } ?: run { + state = MutableStateFlow(value).also { + result.complete(it.asStateFlow()) + } } } + } catch (e: Throwable) { + // Notify the waiter that the flow has failed + result.completeExceptionally(e) + // But still cancel the scope where state was (not) produced + throw e } } } diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt index 2a613afaf7..c90626deb8 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt @@ -30,21 +30,21 @@ class StateInTest : TestBase() { @Test fun testUpstreamCompletedNoInitialValue() = - testUpstreamCompletedOrFailedReset(failed = false, iv = false) + testUpstreamCompletedOrFailedReset(failed = false, withInitialValue = false) @Test fun testUpstreamFailedNoInitialValue() = - testUpstreamCompletedOrFailedReset(failed = true, iv = false) + testUpstreamCompletedOrFailedReset(failed = true, withInitialValue = false) @Test fun testUpstreamCompletedWithInitialValue() = - testUpstreamCompletedOrFailedReset(failed = false, iv = true) + testUpstreamCompletedOrFailedReset(failed = false, withInitialValue = true) @Test fun testUpstreamFailedWithInitialValue() = - testUpstreamCompletedOrFailedReset(failed = true, iv = true) + testUpstreamCompletedOrFailedReset(failed = true, withInitialValue = true) - private fun testUpstreamCompletedOrFailedReset(failed: Boolean, iv: Boolean) = runTest { + private fun testUpstreamCompletedOrFailedReset(failed: Boolean, withInitialValue: Boolean) = runTest { val emitted = Job() val terminate = Job() val sharingJob = CompletableDeferred() @@ -56,7 +56,7 @@ class StateInTest : TestBase() { } val scope = this + sharingJob val shared: StateFlow - if (iv) { + if (withInitialValue) { shared = upstream.stateIn(scope, SharingStarted.Eagerly, null) assertEquals(null, shared.value) } else { @@ -75,4 +75,15 @@ class StateInTest : TestBase() { assertNull(sharingJob.getCompletionExceptionOrNull()) } } -} \ No newline at end of file + + @Test + fun testUpstreamFailedIMmediatelyWithInitialValue() = runTest { + val ceh = CoroutineExceptionHandler { _, _ -> expect(2) } + val flow = flow { + expect(1) + throw TestException() + } + assertFailsWith { flow.stateIn(CoroutineScope(ceh)) } + finish(3) + } +} From 22c4301a93313f45c8b6128414fac87909bd3a94 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 23 Oct 2020 07:32:51 -0700 Subject: [PATCH 145/257] Propagate kotlin_snapshot_version to buildSrc and kts files (#2332) --- buildSrc/build.gradle.kts | 15 +++++++++++++-- buildSrc/settings.gradle.kts | 7 ++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 96b17a3d99..adcbd90fe1 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,6 +9,7 @@ plugins { } val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true +val buildSnapshotTrain = properties["build_snapshot_train"]?.toString()?.toBoolean() == true repositories { if (cacheRedirectorEnabled) { @@ -20,6 +21,10 @@ repositories { maven("https://dl.bintray.com/kotlin/kotlin-eap") maven("https://dl.bintray.com/kotlin/kotlin-dev") } + + if (buildSnapshotTrain) { + mavenLocal() + } } kotlinDslPluginOptions { @@ -30,8 +35,14 @@ val props = Properties().apply { file("../gradle.properties").inputStream().use { load(it) } } -fun version(target: String): String = - props.getProperty("${target}_version") +fun version(target: String): String { + // Intercept reading from properties file + if (target == "kotlin") { + val snapshotVersion = properties["kotlin_snapshot_version"] + if (snapshotVersion != null) return snapshotVersion.toString() + } + return props.getProperty("${target}_version") +} dependencies { implementation(kotlin("gradle-plugin", version("kotlin"))) diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index e5267ea3e2..a6da8fdbc1 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -1,17 +1,18 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - pluginManagement { + val build_snapshot_train: String? by settings repositories { val cacheRedirectorEnabled = System.getenv("CACHE_REDIRECTOR")?.toBoolean() == true - if (cacheRedirectorEnabled) { println("Redirecting repositories for buildSrc buildscript") - maven("https://cache-redirector.jetbrains.com/plugins.gradle.org/m2") } else { maven("https://plugins.gradle.org/m2") } + if (build_snapshot_train?.toBoolean() == true) { + mavenLocal() + } } } From 8df6f5ac43da556b74afffa05dcb5a67d2954c51 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 23 Oct 2020 17:43:54 +0300 Subject: [PATCH 146/257] Fix race condition in testUpstreamFailedImmediatelyWithInitialValue --- .../common/test/flow/sharing/StateInTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt index c90626deb8..d0e76c461e 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/StateInTest.kt @@ -77,13 +77,13 @@ class StateInTest : TestBase() { } @Test - fun testUpstreamFailedIMmediatelyWithInitialValue() = runTest { + fun testUpstreamFailedImmediatelyWithInitialValue() = runTest { val ceh = CoroutineExceptionHandler { _, _ -> expect(2) } val flow = flow { expect(1) throw TestException() } - assertFailsWith { flow.stateIn(CoroutineScope(ceh)) } + assertFailsWith { flow.stateIn(CoroutineScope(currentCoroutineContext() + Job() + ceh)) } finish(3) } } From ee78090b2805b38f28f6565108ea90429568ccfc Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 23 Oct 2020 08:20:31 -0700 Subject: [PATCH 147/257] Fix potential crash in Rx2 and Rx3 asFlow extension (#2333) Fixes #2104 Fixes #2299 Co-authored-by: Louis CAD --- .../kotlinx-coroutines-rx2/src/RxConvert.kt | 8 ++++- .../test/ObservableSourceAsFlowStressTest.kt | 35 ++++++++++++++++++ .../kotlinx-coroutines-rx3/src/RxConvert.kt | 8 ++++- .../test/ObservableSourceAsFlowStressTest.kt | 36 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 reactive/kotlinx-coroutines-rx2/test/ObservableSourceAsFlowStressTest.kt create mode 100644 reactive/kotlinx-coroutines-rx3/test/ObservableSourceAsFlowStressTest.kt diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index cf73ef2ea8..41c82ed0e8 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -81,7 +81,13 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { val observer = object : Observer { override fun onComplete() { close() } override fun onSubscribe(d: Disposable) { if (!disposableRef.compareAndSet(null, d)) d.dispose() } - override fun onNext(t: T) { sendBlocking(t) } + override fun onNext(t: T) { + try { + sendBlocking(t) + } catch (ignored: Throwable) { // TODO: Replace when this issue is fixed: https://github.com/Kotlin/kotlinx.coroutines/issues/974 + // Is handled by the downstream flow + } + } override fun onError(e: Throwable) { close(e) } } diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableSourceAsFlowStressTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableSourceAsFlowStressTest.kt new file mode 100644 index 0000000000..159f3729c8 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableSourceAsFlowStressTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx2 + +import io.reactivex.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import org.junit.* +import java.util.concurrent.* + +class ObservableSourceAsFlowStressTest : TestBase() { + + private val iterations = 100 * stressTestMultiplierSqrt + + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testAsFlowCancellation() = runTest { + repeat(iterations) { + val latch = Channel(1) + var i = 0 + val observable = Observable.interval(100L, TimeUnit.MICROSECONDS) + .doOnNext { if (++i > 100) latch.offer(Unit) } + val job = observable.asFlow().launchIn(CoroutineScope(Dispatchers.Default)) + latch.receive() + job.cancelAndJoin() + } + } +} diff --git a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt index 9bb38c088f..0978423ac9 100644 --- a/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx3/src/RxConvert.kt @@ -81,7 +81,13 @@ public fun ObservableSource.asFlow(): Flow = callbackFlow { val observer = object : Observer { override fun onComplete() { close() } override fun onSubscribe(d: Disposable) { if (!disposableRef.compareAndSet(null, d)) d.dispose() } - override fun onNext(t: T) { sendBlocking(t) } + override fun onNext(t: T) { + try { + sendBlocking(t) + } catch (ignored: Throwable) { // TODO: Replace when this issue is fixed: https://github.com/Kotlin/kotlinx.coroutines/issues/974 + // Is handled by the downstream flow + } + } override fun onError(e: Throwable) { close(e) } } diff --git a/reactive/kotlinx-coroutines-rx3/test/ObservableSourceAsFlowStressTest.kt b/reactive/kotlinx-coroutines-rx3/test/ObservableSourceAsFlowStressTest.kt new file mode 100644 index 0000000000..431a7a789e --- /dev/null +++ b/reactive/kotlinx-coroutines-rx3/test/ObservableSourceAsFlowStressTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.rx3 + +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.exceptions.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import org.junit.* +import java.util.concurrent.* + +class ObservableSourceAsFlowStressTest : TestBase() { + + private val iterations = 100 * stressTestMultiplierSqrt + + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + + @Test + fun testAsFlowCancellation() = runTest { + repeat(iterations) { + val latch = Channel(1) + var i = 0 + val observable = Observable.interval(100L, TimeUnit.MICROSECONDS) + .doOnNext { if (++i > 100) latch.offer(Unit) } + val job = observable.asFlow().launchIn(CoroutineScope(Dispatchers.Default)) + latch.receive() + job.cancelAndJoin() + } + } +} From 45ba58e1a5fac434c3c8d9c5e18eb6191b161a34 Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Mon, 26 Oct 2020 11:40:38 +0300 Subject: [PATCH 148/257] Fix BlockHound false positives (#2331) Fixes https://github.com/Kotlin/kotlinx.coroutines/issues/2302 Fixes https://github.com/Kotlin/kotlinx.coroutines/issues/2190 Partially fixes https://github.com/Kotlin/kotlinx.coroutines/issues/2303 --- .../src/CoroutinesBlockHoundIntegration.kt | 162 +++++++++++++++++- .../test/BlockHoundTest.kt | 22 +++ 2 files changed, 179 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt index f89d2be23f..091e8eb16e 100644 --- a/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt +++ b/kotlinx-coroutines-debug/src/CoroutinesBlockHoundIntegration.kt @@ -1,16 +1,168 @@ @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + package kotlinx.coroutines.debug -import reactor.blockhound.BlockHound import kotlinx.coroutines.scheduling.* +import reactor.blockhound.* import reactor.blockhound.integration.* @Suppress("UNUSED") -public class CoroutinesBlockHoundIntegration: BlockHoundIntegration { +public class CoroutinesBlockHoundIntegration : BlockHoundIntegration { + + override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) { + allowBlockingCallsInPrimitiveImplementations() + allowBlockingWhenEnqueuingTasks() + allowServiceLoaderInvocationsOnInit() + allowBlockingCallsInReflectionImpl() + /* The predicates that define that BlockHound should only report blocking calls from threads that are part of + the coroutine thread pool and currently execute a CPU-bound coroutine computation. */ + addDynamicThreadPredicate { isSchedulerWorker(it) } + nonBlockingThreadPredicate { p -> p.or { mayNotBlock(it) } } + } + + /** + * Allows blocking calls in various coroutine structures, such as flows and channels. + * + * They use locks in implementations, though only for protecting short pieces of fast and well-understood code, so + * locking in such places doesn't affect the program liveness. + */ + private fun BlockHound.Builder.allowBlockingCallsInPrimitiveImplementations() { + allowBlockingCallsInJobSupport() + allowBlockingCallsInThreadSafeHeap() + allowBlockingCallsInFlow() + allowBlockingCallsInChannels() + } + + /** + * Allows blocking inside [kotlinx.coroutines.JobSupport]. + */ + private fun BlockHound.Builder.allowBlockingCallsInJobSupport() { + for (method in listOf("finalizeFinishingState", "invokeOnCompletion", "makeCancelling", + "tryMakeCompleting")) + { + allowBlockingCallsInside("kotlinx.coroutines.JobSupport", method) + } + } + + /** + * Allows blocking inside [kotlinx.coroutines.internal.ThreadSafeHeap]. + */ + private fun BlockHound.Builder.allowBlockingCallsInThreadSafeHeap() { + for (method in listOf("clear", "peek", "removeFirstOrNull", "addLast")) { + allowBlockingCallsInside("kotlinx.coroutines.internal.ThreadSafeHeap", method) + } + // [addLastIf] is only used in [EventLoop.common]. Users of [removeFirstIf]: + allowBlockingCallsInside("kotlinx.coroutines.test.TestCoroutineDispatcher", "doActionsUntil") + allowBlockingCallsInside("kotlinx.coroutines.test.TestCoroutineContext", "triggerActions") + } + + private fun BlockHound.Builder.allowBlockingCallsInFlow() { + allowBlockingCallsInsideStateFlow() + allowBlockingCallsInsideSharedFlow() + } + + /** + * Allows blocking inside the implementation of [kotlinx.coroutines.flow.StateFlow]. + */ + private fun BlockHound.Builder.allowBlockingCallsInsideStateFlow() { + allowBlockingCallsInside("kotlinx.coroutines.flow.StateFlowImpl", "updateState") + } + + /** + * Allows blocking inside the implementation of [kotlinx.coroutines.flow.SharedFlow]. + */ + private fun BlockHound.Builder.allowBlockingCallsInsideSharedFlow() { + for (method in listOf("emitSuspend", "awaitValue", "getReplayCache", "tryEmit", "cancelEmitter", + "tryTakeValue", "resetReplayCache")) + { + allowBlockingCallsInside("kotlinx.coroutines.flow.SharedFlowImpl", method) + } + for (method in listOf("getSubscriptionCount", "allocateSlot", "freeSlot")) { + allowBlockingCallsInside("kotlinx.coroutines.flow.internal.AbstractSharedFlow", method) + } + } + + private fun BlockHound.Builder.allowBlockingCallsInChannels() { + allowBlockingCallsInArrayChannel() + allowBlockingCallsInBroadcastChannel() + allowBlockingCallsInConflatedChannel() + } + + /** + * Allows blocking inside [kotlinx.coroutines.channels.ArrayChannel]. + */ + private fun BlockHound.Builder.allowBlockingCallsInArrayChannel() { + for (method in listOf( + "pollInternal", "isEmpty", "isFull", "isClosedForReceive", "offerInternal", "offerSelectInternal", + "enqueueSend", "pollInternal", "pollSelectInternal", "enqueueReceiveInternal", "onCancelIdempotent")) + { + allowBlockingCallsInside("kotlinx.coroutines.channels.ArrayChannel", method) + } + } + + /** + * Allows blocking inside [kotlinx.coroutines.channels.ArrayBroadcastChannel]. + */ + private fun BlockHound.Builder.allowBlockingCallsInBroadcastChannel() { + for (method in listOf("offerInternal", "offerSelectInternal", "updateHead")) { + allowBlockingCallsInside("kotlinx.coroutines.channels.ArrayBroadcastChannel", method) + } + for (method in listOf("checkOffer", "pollInternal", "pollSelectInternal")) { + allowBlockingCallsInside("kotlinx.coroutines.channels.ArrayBroadcastChannel\$Subscriber", method) + } + } + + /** + * Allows blocking inside [kotlinx.coroutines.channels.ConflatedChannel]. + */ + private fun BlockHound.Builder.allowBlockingCallsInConflatedChannel() { + for (method in listOf("offerInternal", "offerSelectInternal", "pollInternal", "pollSelectInternal", + "onCancelIdempotent")) + { + allowBlockingCallsInside("kotlinx.coroutines.channels.ConflatedChannel", method) + } + } + + /** + * Allows blocking when enqueuing tasks into a thread pool. + * + * Without this, the following code breaks: + * ``` + * withContext(Dispatchers.Default) { + * withContext(newSingleThreadContext("singleThreadedContext")) { + * } + * } + * ``` + */ + private fun BlockHound.Builder.allowBlockingWhenEnqueuingTasks() { + /* This method may block as part of its implementation, but is probably safe. */ + allowBlockingCallsInside("java.util.concurrent.ScheduledThreadPoolExecutor", "execute") + } + + /** + * Allows instances of [java.util.ServiceLoader] being called. + * + * Each instance is listed separately; another approach could be to generally allow the operations performed by + * service loaders, as they can generally be considered safe. This was not done here because ServiceLoader has a + * large API surface, with some methods being hidden as implementation details (in particular, the implementation of + * its iterator is completely opaque). Relying on particular names being used in ServiceLoader's implementation + * would be brittle, so here we only provide clearance rules for some specific instances. + */ + private fun BlockHound.Builder.allowServiceLoaderInvocationsOnInit() { + allowBlockingCallsInside("kotlinx.coroutines.reactive.ReactiveFlowKt", "") + allowBlockingCallsInside("kotlinx.coroutines.CoroutineExceptionHandlerImplKt", "") + // not part of the coroutines library, but it would be nice if reflection also wasn't considered blocking + allowBlockingCallsInside("kotlin.reflect.jvm.internal.impl.resolve.OverridingUtil", "") + } - override fun applyTo(builder: BlockHound.Builder) { - builder.addDynamicThreadPredicate { isSchedulerWorker(it) } - builder.nonBlockingThreadPredicate { p -> p.or { mayNotBlock(it) } } + /** + * Allows some blocking calls from the reflection API. + * + * The API is big, so surely some other blocking calls will show up, but with these rules in place, at least some + * simple examples work without problems. + */ + private fun BlockHound.Builder.allowBlockingCallsInReflectionImpl() { + allowBlockingCallsInside("kotlin.reflect.jvm.internal.impl.builtins.jvm.JvmBuiltInsPackageFragmentProvider", "findPackage") } } diff --git a/kotlinx-coroutines-debug/test/BlockHoundTest.kt b/kotlinx-coroutines-debug/test/BlockHoundTest.kt index ff5c95cdb1..571daca12f 100644 --- a/kotlinx-coroutines-debug/test/BlockHoundTest.kt +++ b/kotlinx-coroutines-debug/test/BlockHoundTest.kt @@ -1,5 +1,6 @@ package kotlinx.coroutines.debug import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* import org.junit.* import reactor.blockhound.* @@ -52,6 +53,27 @@ class BlockHoundTest : TestBase() { } } + @Test + fun testChannelsNotBeingConsideredBlocking() = runTest { + withContext(Dispatchers.Default) { + // Copy of kotlinx.coroutines.channels.ArrayChannelTest.testSimple + val q = Channel(1) + check(q.isEmpty) + check(!q.isClosedForReceive) + check(!q.isClosedForSend) + val sender = launch { + q.send(1) + q.send(2) + } + val receiver = launch { + q.receive() == 1 + q.receive() == 2 + } + sender.join() + receiver.join() + } + } + @Test(expected = BlockingOperationError::class) fun testReusingThreadsFailure() = runTest { val n = 100 From 53f007ff12b9bb72961b908c3a88e1f8127713fc Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 26 Oct 2020 13:11:01 +0300 Subject: [PATCH 149/257] Fix SharedFlow with replay for subscribers working at different speed (#2325) Problematic scenario: * Emitter suspends because there is a slow subscriber * Fast subscriber collects all the values and suspend * Slow subscriber resumes, collects value, causes emitter to be resume * Fast subscribers must be resumed in this case, too Fixes #2320 --- .../common/src/flow/SharedFlow.kt | 14 ++++--- .../flow/sharing/SharedFlowScenarioTest.kt | 38 ++++++++++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 88dc775842..7167971429 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -326,7 +326,7 @@ private class SharedFlowImpl( var resumes: Array?> = EMPTY_RESUMES val emitted = synchronized(this) { if (tryEmitLocked(value)) { - resumes = findSlotsToResumeLocked() + resumes = findSlotsToResumeLocked(resumes) true } else { false @@ -422,7 +422,7 @@ private class SharedFlowImpl( // recheck buffer under lock again (make sure it is really full) if (tryEmitLocked(value)) { cont.resume(Unit) - resumes = findSlotsToResumeLocked() + resumes = findSlotsToResumeLocked(resumes) return@lock null } // add suspended emitter to the buffer @@ -430,7 +430,7 @@ private class SharedFlowImpl( enqueueLocked(it) queueSize++ // added to queue of waiting emitters // synchronous shared flow might rendezvous with waiting emitter - if (bufferCapacity == 0) resumes = findSlotsToResumeLocked() + if (bufferCapacity == 0) resumes = findSlotsToResumeLocked(resumes) } } // outside of the lock: register dispose on cancellation @@ -512,6 +512,8 @@ 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() + // We need to waken up suspended collectors if any emitters were resumed here + if (resumes.isNotEmpty()) resumes = findSlotsToResumeLocked(resumes) return resumes } @@ -598,9 +600,9 @@ private class SharedFlowImpl( } } - private fun findSlotsToResumeLocked(): Array?> { - var resumes: Array?> = EMPTY_RESUMES - var resumeCount = 0 + private fun findSlotsToResumeLocked(resumesIn: Array?>): Array?> { + var resumes: Array?> = resumesIn + var resumeCount = resumesIn.size 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 diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt index f716389fb7..c3eb2dac04 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt @@ -176,6 +176,31 @@ class SharedFlowScenarioTest : TestBase() { collect(b, 15) } + @Test // https://github.com/Kotlin/kotlinx.coroutines/issues/2320 + fun testResumeFastSubscriberOnResumedEmitter() = + testSharedFlow(MutableSharedFlow(1)) { + // create two subscribers and start collecting + val s1 = subscribe("s1"); resumeCollecting(s1) + val s2 = subscribe("s2"); resumeCollecting(s2) + // now emit 0, make sure it is collected + emitRightNow(0); expectReplayOf(0) + awaitCollected(s1, 0) + awaitCollected(s2, 0) + // now emit 1, and only first subscriber continues and collects it + emitRightNow(1); expectReplayOf(1) + collect(s1, 1) + // now emit 2, it suspend (s2 is blocking it) + val e2 = emitSuspends(2) + resumeCollecting(s1) // resume, but does not collect (e2 is still queued) + collect(s2, 1) // resume + collect next --> resumes emitter, thus resumes s1 + awaitCollected(s1, 2) // <-- S1 collects value from the newly resumed emitter here !!! + emitResumes(e2); expectReplayOf(2) + // now emit 3, it suspends (s2 blocks it) + val e3 = emitSuspends(3) + collect(s2, 2) + emitResumes(e3); expectReplayOf(3) + } + private fun testSharedFlow( sharedFlow: MutableSharedFlow, scenario: suspend ScenarioDsl.() -> Unit @@ -305,14 +330,23 @@ class SharedFlowScenarioTest : TestBase() { return TestJob(job, name) } + // collect ~== resumeCollecting + awaitCollected (for each value) suspend fun collect(job: TestJob, vararg a: T) { for (value in a) { checkReplay() // should not have changed - addAction(ResumeCollecting(job)) - awaitAction(Collected(job, value)) + resumeCollecting(job) + awaitCollected(job, value) } } + suspend fun resumeCollecting(job: TestJob) { + addAction(ResumeCollecting(job)) + } + + suspend fun awaitCollected(job: TestJob, value: T) { + awaitAction(Collected(job, value)) + } + fun stop() { log("--- stop") scope.cancel() From 92db4e1b5d798c9c7a66fcaa6cca50af8d1648ae Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 26 Oct 2020 17:04:15 +0300 Subject: [PATCH 150/257] Add debounce with selector and kotlin.time (#2336) Co-authored-by: Miguel Kano Co-authored-by: Vsevolod Tolstopyatov --- .../api/kotlinx-coroutines-core.api | 2 + .../common/src/flow/operators/Delay.kt | 185 ++++++++++++++---- .../test/flow/operators/DebounceTest.kt | 106 +++++++++- .../jvm/test/examples/example-delay-02.kt | 19 +- .../jvm/test/examples/example-delay-03.kt | 19 ++ .../examples/example-delay-duration-02.kt | 19 +- .../examples/example-delay-duration-03.kt | 21 ++ .../jvm/test/examples/test/FlowDelayTest.kt | 22 ++- 8 files changed, 343 insertions(+), 50 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt create mode 100644 kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index c3eddb98b0..06f4396778 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -942,7 +942,9 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun debounce (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; + public static final fun debounce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun debounce-8GFy2Ro (Lkotlinx/coroutines/flow/Flow;D)Lkotlinx/coroutines/flow/Flow; + public static final fun debounceDuration (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun delayEach (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; public static final fun delayFlow (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow; public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt index aa55fea721..c95b4be940 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Delay.kt @@ -64,37 +64,59 @@ fun main() = runBlocking { */ @FlowPreview public fun Flow.debounce(timeoutMillis: Long): Flow { - require(timeoutMillis > 0) { "Debounce timeout should be positive" } - return scopedFlow { downstream -> - // Actually Any, KT-30796 - val values = produce(capacity = Channel.CONFLATED) { - collect { value -> send(value ?: NULL) } - } - var lastValue: Any? = null - while (lastValue !== DONE) { - select { - // Should be receiveOrClosed when boxing issues are fixed - values.onReceiveOrNull { - if (it == null) { - if (lastValue != null) downstream.emit(NULL.unbox(lastValue)) - lastValue = DONE - } else { - lastValue = it - } - } - - lastValue?.let { value -> - // set timeout when lastValue != null - onTimeout(timeoutMillis) { - lastValue = null // Consume the value - downstream.emit(NULL.unbox(value)) - } - } - } - } - } + require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } + if (timeoutMillis == 0L) return this + return debounceInternal { timeoutMillis } } +/** + * Returns a flow that mirrors the original flow, but filters out values + * that are followed by the newer values within the given [timeout][timeoutMillis]. + * The latest value is always emitted. + * + * A variation of [debounce] that allows specifying the timeout value dynamically. + * + * Example: + * + * ```kotlin + * flow { + * emit(1) + * delay(90) + * emit(2) + * delay(90) + * emit(3) + * delay(1010) + * emit(4) + * delay(1010) + * emit(5) + * }.debounce { + * if (it == 1) { + * 0L + * } else { + * 1000L + * } + * } + * ``` + * + * + * produces the following emissions + * + * ```text + * 1, 3, 4, 5 + * ``` + * + * + * Note that the resulting flow does not emit anything as long as the original flow emits + * items faster than every [timeoutMillis] milliseconds. + * + * @param timeoutMillis [T] is the emitted value and the return value is timeout in milliseconds. + */ +@FlowPreview +@OptIn(kotlin.experimental.ExperimentalTypeInference::class) +@OverloadResolutionByLambdaReturnType +public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = + debounceInternal(timeoutMillis) + /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout]. @@ -129,7 +151,104 @@ public fun Flow.debounce(timeoutMillis: Long): Flow { */ @ExperimentalTime @FlowPreview -public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.toDelayMillis()) +public fun Flow.debounce(timeout: Duration): Flow = + debounce(timeout.toDelayMillis()) + +/** + * Returns a flow that mirrors the original flow, but filters out values + * that are followed by the newer values within the given [timeout]. + * The latest value is always emitted. + * + * A variation of [debounce] that allows specifying the timeout value dynamically. + * + * Example: + * + * ```kotlin + * flow { + * emit(1) + * delay(90.milliseconds) + * emit(2) + * delay(90.milliseconds) + * emit(3) + * delay(1010.milliseconds) + * emit(4) + * delay(1010.milliseconds) + * emit(5) + * }.debounce { + * if (it == 1) { + * 0.milliseconds + * } else { + * 1000.milliseconds + * } + * } + * ``` + * + * + * produces the following emissions + * + * ```text + * 1, 3, 4, 5 + * ``` + * + * + * Note that the resulting flow does not emit anything as long as the original flow emits + * items faster than every [timeout] unit. + * + * @param timeout [T] is the emitted value and the return value is timeout in [Duration]. + */ +@ExperimentalTime +@FlowPreview +@JvmName("debounceDuration") +@OptIn(kotlin.experimental.ExperimentalTypeInference::class) +@OverloadResolutionByLambdaReturnType +public fun Flow.debounce(timeout: (T) -> Duration): Flow = + debounceInternal { emittedItem -> + timeout(emittedItem).toDelayMillis() + } + +private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long) : Flow = + scopedFlow { downstream -> + // Produce the values using the default (rendezvous) channel + // Note: the actual type is Any, KT-30796 + val values = produce { + collect { value -> send(value ?: NULL) } + } + // Now consume the values + var lastValue: Any? = null + while (lastValue !== DONE) { + var timeoutMillis = 0L // will be always computed when lastValue != null + // Compute timeout for this value + if (lastValue != null) { + timeoutMillis = timeoutMillisSelector(NULL.unbox(lastValue)) + require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } + if (timeoutMillis == 0L) { + downstream.emit(NULL.unbox(lastValue)) + lastValue = null // Consume the value + } + } + // assert invariant: lastValue != null implies timeoutMillis > 0 + assert { lastValue == null || timeoutMillis > 0 } + // wait for the next value with timeout + select { + // Set timeout when lastValue exists and is not consumed yet + if (lastValue != null) { + onTimeout(timeoutMillis) { + downstream.emit(NULL.unbox(lastValue)) + lastValue = null // Consume the value + } + } + // Should be receiveOrClosed when boxing issues are fixed + values.onReceiveOrNull { value -> + if (value == null) { + if (lastValue != null) downstream.emit(NULL.unbox(lastValue)) + lastValue = DONE + } else { + lastValue = value + } + } + } + } + } /** * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis]. @@ -144,7 +263,7 @@ public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.t * } * }.sample(200) * ``` - * + * * * produces the following emissions * @@ -152,7 +271,7 @@ public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.t * 1, 3, 5, 7, 9 * ``` * - * + * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @FlowPreview @@ -215,7 +334,7 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil * } * }.sample(200.milliseconds) * ``` - * + * * * produces the following emissions * diff --git a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt index 4065671e3d..ce75e598e9 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt @@ -11,7 +11,7 @@ import kotlin.time.* class DebounceTest : TestBase() { @Test - public fun testBasic() = withVirtualTime { + fun testBasic() = withVirtualTime { expect(1) val flow = flow { expect(3) @@ -159,7 +159,7 @@ class DebounceTest : TestBase() { expect(2) throw TestException() }.flowOn(NamedDispatchers("source")).debounce(Long.MAX_VALUE).map { - expectUnreached() + expectUnreached() } assertFailsWith(flow) finish(3) @@ -175,7 +175,6 @@ class DebounceTest : TestBase() { expect(2) yield() throw TestException() - it } assertFailsWith(flow) @@ -193,7 +192,6 @@ class DebounceTest : TestBase() { expect(2) yield() throw TestException() - it } assertFailsWith(flow) @@ -202,7 +200,7 @@ class DebounceTest : TestBase() { @ExperimentalTime @Test - public fun testDurationBasic() = withVirtualTime { + fun testDurationBasic() = withVirtualTime { expect(1) val flow = flow { expect(3) @@ -223,4 +221,102 @@ class DebounceTest : TestBase() { assertEquals(listOf("A", "D", "E"), result) finish(5) } + + @ExperimentalTime + @Test + fun testDebounceSelectorBasic() = withVirtualTime { + expect(1) + val flow = flow { + expect(3) + emit(1) + delay(90) + emit(2) + delay(90) + emit(3) + delay(1010) + emit(4) + delay(1010) + emit(5) + expect(4) + } + + expect(2) + val result = flow.debounce { + if (it == 1) { + 0 + } else { + 1000 + } + }.toList() + + assertEquals(listOf(1, 3, 4, 5), result) + finish(5) + } + + @Test + fun testZeroDebounceTime() = withVirtualTime { + expect(1) + val flow = flow { + expect(3) + emit("A") + emit("B") + emit("C") + expect(4) + } + + expect(2) + val result = flow.debounce(0).toList() + + assertEquals(listOf("A", "B", "C"), result) + finish(5) + } + + @ExperimentalTime + @Test + fun testZeroDebounceTimeSelector() = withVirtualTime { + expect(1) + val flow = flow { + expect(3) + emit("A") + emit("B") + expect(4) + } + + expect(2) + val result = flow.debounce { 0 }.toList() + + assertEquals(listOf("A", "B"), result) + finish(5) + } + + @ExperimentalTime + @Test + fun testDebounceDurationSelectorBasic() = withVirtualTime { + expect(1) + val flow = flow { + expect(3) + emit("A") + delay(1500.milliseconds) + emit("B") + delay(500.milliseconds) + emit("C") + delay(250.milliseconds) + emit("D") + delay(2000.milliseconds) + emit("E") + expect(4) + } + + expect(2) + val result = flow.debounce { + if (it == "C") { + 0.milliseconds + } else { + 1000.milliseconds + } + }.toList() + + assertEquals(listOf("A", "C", "D", "E"), result) + finish(5) + } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt index 1b6b12f041..f74422e6b4 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-02.kt @@ -11,9 +11,20 @@ import kotlinx.coroutines.flow.* fun main() = runBlocking { flow { - repeat(10) { - emit(it) - delay(110) + emit(1) + delay(90) + emit(2) + delay(90) + emit(3) + delay(1010) + emit(4) + delay(1010) + emit(5) +}.debounce { + if (it == 1) { + 0L + } else { + 1000L } -}.sample(200) +} .toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt new file mode 100644 index 0000000000..edaea74258 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-03.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelay03 + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + repeat(10) { + emit(it) + delay(110) + } +}.sample(200) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt index e43dfd1e05..10ba88a54d 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-02.kt @@ -13,9 +13,20 @@ import kotlinx.coroutines.flow.* fun main() = runBlocking { flow { - repeat(10) { - emit(it) - delay(110.milliseconds) + emit(1) + delay(90.milliseconds) + emit(2) + delay(90.milliseconds) + emit(3) + delay(1010.milliseconds) + emit(4) + delay(1010.milliseconds) + emit(5) +}.debounce { + if (it == 1) { + 0.milliseconds + } else { + 1000.milliseconds } -}.sample(200.milliseconds) +} .toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt new file mode 100644 index 0000000000..5fa980a6f8 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/examples/example-delay-duration-03.kt @@ -0,0 +1,21 @@ +@file:OptIn(ExperimentalTime::class) +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// This file was automatically generated from Delay.kt by Knit tool. Do not edit. +package kotlinx.coroutines.examples.exampleDelayDuration03 + +import kotlin.time.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +fun main() = runBlocking { + +flow { + repeat(10) { + emit(it) + delay(110.milliseconds) + } +}.sample(200.milliseconds) +.toList().joinToString().let { println(it) } } diff --git a/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt index 226d31cc00..99e72eb2c9 100644 --- a/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt +++ b/kotlinx-coroutines-core/jvm/test/examples/test/FlowDelayTest.kt @@ -16,6 +16,13 @@ class FlowDelayTest { ) } + @Test + fun testExampleDelay02() { + test("ExampleDelay02") { kotlinx.coroutines.examples.exampleDelay02.main() }.verifyLines( + "1, 3, 4, 5" + ) + } + @Test fun testExampleDelayDuration01() { test("ExampleDelayDuration01") { kotlinx.coroutines.examples.exampleDelayDuration01.main() }.verifyLines( @@ -24,15 +31,22 @@ class FlowDelayTest { } @Test - fun testExampleDelay02() { - test("ExampleDelay02") { kotlinx.coroutines.examples.exampleDelay02.main() }.verifyLines( + fun testExampleDelayDuration02() { + test("ExampleDelayDuration02") { kotlinx.coroutines.examples.exampleDelayDuration02.main() }.verifyLines( + "1, 3, 4, 5" + ) + } + + @Test + fun testExampleDelay03() { + test("ExampleDelay03") { kotlinx.coroutines.examples.exampleDelay03.main() }.verifyLines( "1, 3, 5, 7, 9" ) } @Test - fun testExampleDelayDuration02() { - test("ExampleDelayDuration02") { kotlinx.coroutines.examples.exampleDelayDuration02.main() }.verifyLines( + fun testExampleDelayDuration03() { + test("ExampleDelayDuration03") { kotlinx.coroutines.examples.exampleDelayDuration03.main() }.verifyLines( "1, 3, 5, 7, 9" ) } From e16eb9d315cbee42bcadb438a8d62b10f65a9aa4 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 26 Oct 2020 07:05:06 -0700 Subject: [PATCH 151/257] Update experimental declarations (#2316) * Gracefully increase deprecation level on Channel operators instead of removing them, a warning was not strict enough * Remove hidden onCompletion from -M release * Promote StateFlow and SharedFlow to stable API * Lift out experimentality where it is applicable * CoroutineDispatcher.invoke * ReceiveChannel.consume and ReceiveChannel.consumeEach * Flow core operators: onStart, onCompletion, onEmpty * CompletableDeferred.completeWith * awaitCancellation * Add experimentality notes where applicable --- .../kotlin/benchmarks/ChannelSinkBenchmark.kt | 18 + .../api/kotlinx-coroutines-core.api | 1 - .../common/src/Builders.common.kt | 1 - .../common/src/CompletableDeferred.kt | 1 - .../common/src/CoroutineStart.kt | 4 +- .../common/src/Debug.common.kt | 2 +- kotlinx-coroutines-core/common/src/Delay.kt | 1 - .../common/src/channels/Broadcast.kt | 2 +- .../common/src/channels/BufferOverflow.kt | 1 - .../common/src/channels/Channels.common.kt | 384 ++++---- .../common/src/flow/Builders.kt | 4 +- .../common/src/flow/Migration.kt | 1 - .../common/src/flow/SharedFlow.kt | 3 - .../common/src/flow/SharingStarted.kt | 6 - .../common/src/flow/StateFlow.kt | 5 +- .../common/src/flow/operators/Emitters.kt | 9 - .../common/src/flow/operators/Share.kt | 6 - .../common/test/channels/ChannelsTest.kt | 524 ---------- .../common/test/channels/ProduceTest.kt | 2 +- .../jvm/test/channels/ChannelsConsumeTest.kt | 908 ------------------ .../jvm/test/channels/ChannelsJvmTest.kt | 4 +- .../kotlinx-coroutines-jdk9/src/Publish.kt | 2 +- .../src/JavaFxConvert.kt | 2 +- 23 files changed, 222 insertions(+), 1669 deletions(-) delete mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ChannelSinkBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/ChannelSinkBenchmark.kt index 9c7f38a6f9..6c5b623191 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/ChannelSinkBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/ChannelSinkBenchmark.kt @@ -50,4 +50,22 @@ open class ChannelSinkBenchmark { for (i in start until (start + count)) send(i) } + + // Migrated from deprecated operators, are good only for stressing channels + + private fun ReceiveChannel.filter(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = + GlobalScope.produce(context, onCompletion = { cancel() }) { + for (e in this@filter) { + if (predicate(e)) send(e) + } + } + + private suspend inline fun ReceiveChannel.fold(initial: R, operation: (acc: R, E) -> R): R { + var accumulator = initial + consumeEach { + accumulator = operation(accumulator, it) + } + return accumulator + } } + diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 06f4396778..b86076fca1 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -992,7 +992,6 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun merge (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun merge ([Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; public static final fun observeOn (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; - public static final synthetic fun onCompletion (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onCompletion (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; public static final fun onEach (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun onEmpty (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index c0924a0238..b7deaccb72 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -175,7 +175,6 @@ public suspend fun withContext( * * This inline function calls [withContext]. */ -@ExperimentalCoroutinesApi public suspend inline operator fun CoroutineDispatcher.invoke( noinline block: suspend CoroutineScope.() -> T ): T = withContext(this, block) diff --git a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt index 0605817afa..2f00847298 100644 --- a/kotlinx-coroutines-core/common/src/CompletableDeferred.kt +++ b/kotlinx-coroutines-core/common/src/CompletableDeferred.kt @@ -57,7 +57,6 @@ public interface CompletableDeferred : Deferred { * This function transitions this deferred in the same ways described by [CompletableDeferred.complete] and * [CompletableDeferred.completeExceptionally]. */ -@ExperimentalCoroutinesApi // since 1.3.2, tentatively until 1.4.0 public fun CompletableDeferred.completeWith(result: Result): Boolean = result.fold({ complete(it) }, { completeExceptionally(it) }) diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 05e80e3ed7..d5791c79fe 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -55,7 +55,7 @@ public enum class CoroutineStart { * Cancellability of coroutine at suspension points depends on the particular implementation details of * suspending functions as in [DEFAULT]. */ - @ExperimentalCoroutinesApi + @ExperimentalCoroutinesApi // Since 1.0.0, no ETA on stability ATOMIC, /** @@ -71,7 +71,7 @@ public enum class CoroutineStart { * * **Note: This is an experimental api.** Execution semantics of coroutines may change in the future when this mode is used. */ - @ExperimentalCoroutinesApi + @ExperimentalCoroutinesApi // Since 1.0.0, no ETA on stability UNDISPATCHED; /** diff --git a/kotlinx-coroutines-core/common/src/Debug.common.kt b/kotlinx-coroutines-core/common/src/Debug.common.kt index 013b983a74..949b05c63f 100644 --- a/kotlinx-coroutines-core/common/src/Debug.common.kt +++ b/kotlinx-coroutines-core/common/src/Debug.common.kt @@ -27,7 +27,7 @@ internal expect fun assert(value: () -> Boolean) * Copy mechanism is used only on JVM, but it might be convenient to implement it in common exceptions, * so on JVM their stacktraces will be properly recovered. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // Since 1.2.0, no ETA on stability public interface CopyableThrowable where T : Throwable, T : CopyableThrowable { /** diff --git a/kotlinx-coroutines-core/common/src/Delay.kt b/kotlinx-coroutines-core/common/src/Delay.kt index f7948443fa..aae623d5df 100644 --- a/kotlinx-coroutines-core/common/src/Delay.kt +++ b/kotlinx-coroutines-core/common/src/Delay.kt @@ -95,7 +95,6 @@ public interface Delay { * } * ``` */ -@ExperimentalCoroutinesApi public suspend fun awaitCancellation(): Nothing = suspendCancellableCoroutine {} /** diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index 790580e0a3..0193ed06b2 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -47,7 +47,7 @@ public fun ReceiveChannel.broadcast( val scope = GlobalScope + Dispatchers.Unconfined + CoroutineExceptionHandler { _, _ -> } // We can run this coroutine in the context that ignores all exceptions, because of `onCompletion = consume()` // which passes all exceptions upstream to the source ReceiveChannel - return scope.broadcast(capacity = capacity, start = start, onCompletion = consumes()) { + return scope.broadcast(capacity = capacity, start = start, onCompletion = { cancelConsumed(it) }) { for (e in this@broadcast) { send(e) } diff --git a/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt index 99994ea81b..a89c633fe6 100644 --- a/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt +++ b/kotlinx-coroutines-core/common/src/channels/BufferOverflow.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.* * * [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. diff --git a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt index d19028bf63..398d5ca44b 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channels.common.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channels.common.kt @@ -3,7 +3,7 @@ */ @file:JvmMultifileClass @file:JvmName("ChannelsKt") -@file:Suppress("DEPRECATION") +@file:Suppress("DEPRECATION_ERROR") package kotlinx.coroutines.channels @@ -52,7 +52,7 @@ public inline fun BroadcastChannel.consume(block: ReceiveChannel.() * to find bugs. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") -@ExperimentalCoroutinesApi // since 1.3.0, tentatively stable in 1.4.0 +@ExperimentalCoroutinesApi // since 1.3.0, tentatively stable in 1.4.x public suspend fun ReceiveChannel.receiveOrNull(): E? { @Suppress("DEPRECATION", "UNCHECKED_CAST") return (this as ReceiveChannel).receiveOrNull() @@ -68,7 +68,7 @@ public suspend fun ReceiveChannel.receiveOrNull(): E? { * these extensions do not accidentally confuse `null` value and a normally closed channel, leading to hard * to find bugs. **/ -@ExperimentalCoroutinesApi // since 1.3.0, tentatively stable in 1.4.0 +@ExperimentalCoroutinesApi // since 1.3.0, tentatively stable in 1.4.x public fun ReceiveChannel.onReceiveOrNull(): SelectClause1 { @Suppress("DEPRECATION", "UNCHECKED_CAST") return (this as ReceiveChannel).onReceiveOrNull @@ -102,8 +102,8 @@ public suspend inline fun BroadcastChannel.consumeEach(action: (E) -> Uni * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel<*>.consumes(): CompletionHandler = { cause: Throwable? -> cancelConsumed(cause) @@ -125,8 +125,8 @@ internal fun ReceiveChannel<*>.cancelConsumed(cause: Throwable?) { * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun consumesAll(vararg channels: ReceiveChannel<*>): CompletionHandler = { cause: Throwable? -> @@ -150,7 +150,6 @@ public fun consumesAll(vararg channels: ReceiveChannel<*>): CompletionHandler = * * The operation is _terminal_. */ -@ExperimentalCoroutinesApi // since 1.3.0, tentatively graduates in 1.4.0 public inline fun ReceiveChannel.consume(block: ReceiveChannel.() -> R): R { var cause: Throwable? = null try { @@ -171,7 +170,6 @@ public inline fun ReceiveChannel.consume(block: ReceiveChannel.() - * The operation is _terminal_. * This function [consumes][ReceiveChannel.consume] all elements of the original [ReceiveChannel]. */ -@ExperimentalCoroutinesApi // since 1.3.0, tentatively graduates in 1.4.0 public suspend inline fun ReceiveChannel.consumeEach(action: (E) -> Unit): Unit = consume { for (e in this) action(e) @@ -187,8 +185,8 @@ public suspend inline fun ReceiveChannel.consumeEach(action: (E) -> Unit) * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.consumeEachIndexed(action: (IndexedValue) -> Unit) { var index = 0 @@ -207,8 +205,8 @@ public suspend inline fun ReceiveChannel.consumeEachIndexed(action: (Inde * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.elementAt(index: Int): E = elementAtOrElse(index) { throw IndexOutOfBoundsException("ReceiveChannel doesn't contain element at index $index.") } @@ -223,8 +221,8 @@ public suspend fun ReceiveChannel.elementAt(index: Int): E = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.elementAtOrElse(index: Int, defaultValue: (Int) -> E): E = consume { @@ -248,8 +246,8 @@ public suspend inline fun ReceiveChannel.elementAtOrElse(index: Int, defa * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.elementAtOrNull(index: Int): E? = consume { @@ -273,8 +271,8 @@ public suspend fun ReceiveChannel.elementAtOrNull(index: Int): E? = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.find(predicate: (E) -> Boolean): E? = firstOrNull(predicate) @@ -289,8 +287,8 @@ public suspend inline fun ReceiveChannel.find(predicate: (E) -> Boolean): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.findLast(predicate: (E) -> Boolean): E? = lastOrNull(predicate) @@ -306,8 +304,8 @@ public suspend inline fun ReceiveChannel.findLast(predicate: (E) -> Boole * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.first(): E = consume { @@ -328,8 +326,8 @@ public suspend fun ReceiveChannel.first(): E = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.first(predicate: (E) -> Boolean): E { consumeEach { @@ -348,8 +346,8 @@ public suspend inline fun ReceiveChannel.first(predicate: (E) -> Boolean) * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.firstOrNull(): E? = consume { @@ -369,8 +367,8 @@ public suspend fun ReceiveChannel.firstOrNull(): E? = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.firstOrNull(predicate: (E) -> Boolean): E? { consumeEach { @@ -389,8 +387,8 @@ public suspend inline fun ReceiveChannel.firstOrNull(predicate: (E) -> Bo * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.indexOf(element: E): Int { var index = 0 @@ -412,8 +410,8 @@ public suspend fun ReceiveChannel.indexOf(element: E): Int { * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.indexOfFirst(predicate: (E) -> Boolean): Int { var index = 0 @@ -435,8 +433,8 @@ public suspend inline fun ReceiveChannel.indexOfFirst(predicate: (E) -> B * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.indexOfLast(predicate: (E) -> Boolean): Int { var lastIndex = -1 @@ -460,8 +458,8 @@ public suspend inline fun ReceiveChannel.indexOfLast(predicate: (E) -> Bo * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.last(): E = consume { @@ -485,8 +483,8 @@ public suspend fun ReceiveChannel.last(): E = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.last(predicate: (E) -> Boolean): E { var last: E? = null @@ -512,8 +510,8 @@ public suspend inline fun ReceiveChannel.last(predicate: (E) -> Boolean): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.lastIndexOf(element: E): Int { var lastIndex = -1 @@ -536,8 +534,8 @@ public suspend fun ReceiveChannel.lastIndexOf(element: E): Int { * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.lastOrNull(): E? = consume { @@ -560,8 +558,8 @@ public suspend fun ReceiveChannel.lastOrNull(): E? = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.lastOrNull(predicate: (E) -> Boolean): E? { var last: E? = null @@ -583,8 +581,8 @@ public suspend inline fun ReceiveChannel.lastOrNull(predicate: (E) -> Boo * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.single(): E = consume { @@ -607,8 +605,8 @@ public suspend fun ReceiveChannel.single(): E = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.single(predicate: (E) -> Boolean): E { var single: E? = null @@ -635,8 +633,8 @@ public suspend inline fun ReceiveChannel.single(predicate: (E) -> Boolean * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.singleOrNull(): E? = consume { @@ -659,8 +657,8 @@ public suspend fun ReceiveChannel.singleOrNull(): E? = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.singleOrNull(predicate: (E) -> Boolean): E? { var single: E? = null @@ -686,8 +684,8 @@ public suspend inline fun ReceiveChannel.singleOrNull(predicate: (E) -> B * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.drop(n: Int, context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -714,8 +712,8 @@ public fun ReceiveChannel.drop(n: Int, context: CoroutineContext = Dispat * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.dropWhile(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -740,8 +738,8 @@ public fun ReceiveChannel.dropWhile(context: CoroutineContext = Dispatche * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.filter(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -762,8 +760,8 @@ public fun ReceiveChannel.filter(context: CoroutineContext = Dispatchers. * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.filterIndexed(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (index: Int, E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -785,8 +783,8 @@ public fun ReceiveChannel.filterIndexed(context: CoroutineContext = Dispa * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.filterIndexedTo(destination: C, predicate: (index: Int, E) -> Boolean): C { consumeEachIndexed { (index, element) -> @@ -807,8 +805,8 @@ public suspend inline fun > ReceiveChannel.fil * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.filterIndexedTo(destination: C, predicate: (index: Int, E) -> Boolean): C { consumeEachIndexed { (index, element) -> @@ -827,8 +825,8 @@ public suspend inline fun > ReceiveChannel.filterIndexe * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.filterNot(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = filter(context) { !predicate(it) } @@ -843,8 +841,8 @@ public fun ReceiveChannel.filterNot(context: CoroutineContext = Dispatche * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) @Suppress("UNCHECKED_CAST") public fun ReceiveChannel.filterNotNull(): ReceiveChannel = @@ -860,8 +858,8 @@ public fun ReceiveChannel.filterNotNull(): ReceiveChannel = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun > ReceiveChannel.filterNotNullTo(destination: C): C { consumeEach { @@ -880,8 +878,8 @@ public suspend fun > ReceiveChannel.fil * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun > ReceiveChannel.filterNotNullTo(destination: C): C { consumeEach { @@ -900,8 +898,8 @@ public suspend fun > ReceiveChannel.filterNotNul * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.filterNotTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { @@ -920,8 +918,8 @@ public suspend inline fun > ReceiveChannel.fil * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.filterNotTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { @@ -940,8 +938,8 @@ public suspend inline fun > ReceiveChannel.filterNotTo( * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.filterTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { @@ -960,8 +958,8 @@ public suspend inline fun > ReceiveChannel.fil * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.filterTo(destination: C, predicate: (E) -> Boolean): C { consumeEach { @@ -980,8 +978,8 @@ public suspend inline fun > ReceiveChannel.filterTo(des * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.take(n: Int, context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1006,8 +1004,8 @@ public fun ReceiveChannel.take(n: Int, context: CoroutineContext = Dispat * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.takeWhile(context: CoroutineContext = Dispatchers.Unconfined, predicate: suspend (E) -> Boolean): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1032,8 +1030,8 @@ public fun ReceiveChannel.takeWhile(context: CoroutineContext = Dispatche * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.associate(transform: (E) -> Pair): Map = associateTo(LinkedHashMap(), transform) @@ -1053,8 +1051,8 @@ public suspend inline fun ReceiveChannel.associate(transform: (E) - * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.associateBy(keySelector: (E) -> K): Map = associateByTo(LinkedHashMap(), keySelector) @@ -1073,8 +1071,8 @@ public suspend inline fun ReceiveChannel.associateBy(keySelector: (E) * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.associateBy(keySelector: (E) -> K, valueTransform: (E) -> V): Map = associateByTo(LinkedHashMap(), keySelector, valueTransform) @@ -1093,8 +1091,8 @@ public suspend inline fun ReceiveChannel.associateBy(keySelector: ( * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.associateByTo(destination: M, keySelector: (E) -> K): M { consumeEach { @@ -1117,8 +1115,8 @@ public suspend inline fun > ReceiveChannel.a * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.associateByTo(destination: M, keySelector: (E) -> K, valueTransform: (E) -> V): M { consumeEach { @@ -1140,8 +1138,8 @@ public suspend inline fun > ReceiveChannel> ReceiveChannel.associateTo(destination: M, transform: (E) -> Pair): M { consumeEach { @@ -1161,8 +1159,8 @@ public suspend inline fun > ReceiveChannel> ReceiveChannel.toChannel(destination: C): C { consumeEach { @@ -1181,8 +1179,8 @@ public suspend fun > ReceiveChannel.toChannel(destinati * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun > ReceiveChannel.toCollection(destination: C): C { consumeEach { @@ -1210,8 +1208,8 @@ public suspend fun ReceiveChannel.toList(): List = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel>.toMap(): Map = toMap(LinkedHashMap()) @@ -1226,8 +1224,8 @@ public suspend fun ReceiveChannel>.toMap(): Map = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun > ReceiveChannel>.toMap(destination: M): M { consumeEach { @@ -1246,8 +1244,8 @@ public suspend fun > ReceiveChannel> * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.toMutableList(): MutableList = toCollection(ArrayList()) @@ -1264,8 +1262,8 @@ public suspend fun ReceiveChannel.toMutableList(): MutableList = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.toSet(): Set = this.toMutableSet() @@ -1280,8 +1278,8 @@ public suspend fun ReceiveChannel.toSet(): Set = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.flatMap(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> ReceiveChannel): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1303,8 +1301,8 @@ public fun ReceiveChannel.flatMap(context: CoroutineContext = Dispatch * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) -> K): Map> = groupByTo(LinkedHashMap(), keySelector) @@ -1323,8 +1321,8 @@ public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) -> K * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) -> K, valueTransform: (E) -> V): Map> = groupByTo(LinkedHashMap(), keySelector, valueTransform) @@ -1342,8 +1340,8 @@ public suspend inline fun ReceiveChannel.groupBy(keySelector: (E) - * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun >> ReceiveChannel.groupByTo(destination: M, keySelector: (E) -> K): M { consumeEach { @@ -1368,8 +1366,8 @@ public suspend inline fun >> ReceiveCh * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun >> ReceiveChannel.groupByTo(destination: M, keySelector: (E) -> K, valueTransform: (E) -> V): M { consumeEach { @@ -1388,8 +1386,8 @@ public suspend inline fun >> Receiv * This function [consumes][ReceiveChannel.consume] all elements of the original [ReceiveChannel]. */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.map(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> R): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1411,8 +1409,8 @@ public fun ReceiveChannel.map(context: CoroutineContext = Dispatchers. * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.mapIndexed(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (index: Int, E) -> R): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1435,8 +1433,8 @@ public fun ReceiveChannel.mapIndexed(context: CoroutineContext = Dispa * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.mapIndexedNotNull(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (index: Int, E) -> R?): ReceiveChannel = mapIndexed(context, transform).filterNotNull() @@ -1454,8 +1452,8 @@ public fun ReceiveChannel.mapIndexedNotNull(context: CoroutineCo * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapIndexedNotNullTo(destination: C, transform: (index: Int, E) -> R?): C { consumeEachIndexed { (index, element) -> @@ -1477,8 +1475,8 @@ public suspend inline fun > ReceiveChann * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapIndexedNotNullTo(destination: C, transform: (index: Int, E) -> R?): C { consumeEachIndexed { (index, element) -> @@ -1500,8 +1498,8 @@ public suspend inline fun > ReceiveChannel.map * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapIndexedTo(destination: C, transform: (index: Int, E) -> R): C { var index = 0 @@ -1524,8 +1522,8 @@ public suspend inline fun > ReceiveChannel. * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapIndexedTo(destination: C, transform: (index: Int, E) -> R): C { var index = 0 @@ -1546,8 +1544,8 @@ public suspend inline fun > ReceiveChannel.mapIndexe * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.mapNotNull(context: CoroutineContext = Dispatchers.Unconfined, transform: suspend (E) -> R?): ReceiveChannel = map(context, transform).filterNotNull() @@ -1563,8 +1561,8 @@ public fun ReceiveChannel.mapNotNull(context: CoroutineContext = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapNotNullTo(destination: C, transform: (E) -> R?): C { consumeEach { @@ -1584,8 +1582,8 @@ public suspend inline fun > ReceiveChann * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapNotNullTo(destination: C, transform: (E) -> R?): C { consumeEach { @@ -1605,8 +1603,8 @@ public suspend inline fun > ReceiveChannel.map * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapTo(destination: C, transform: (E) -> R): C { consumeEach { @@ -1626,8 +1624,8 @@ public suspend inline fun > ReceiveChannel. * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.mapTo(destination: C, transform: (E) -> R): C { consumeEach { @@ -1646,8 +1644,8 @@ public suspend inline fun > ReceiveChannel.mapTo(des * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.withIndex(context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel> = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1669,8 +1667,8 @@ public fun ReceiveChannel.withIndex(context: CoroutineContext = Dispatche * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.distinct(): ReceiveChannel = this.distinctBy { it } @@ -1688,8 +1686,8 @@ public fun ReceiveChannel.distinct(): ReceiveChannel = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.distinctBy(context: CoroutineContext = Dispatchers.Unconfined, selector: suspend (E) -> K): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumes()) { @@ -1715,8 +1713,8 @@ public fun ReceiveChannel.distinctBy(context: CoroutineContext = Dispa * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.toMutableSet(): MutableSet = toCollection(LinkedHashSet()) @@ -1731,8 +1729,8 @@ public suspend fun ReceiveChannel.toMutableSet(): MutableSet = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.all(predicate: (E) -> Boolean): Boolean { consumeEach { @@ -1751,8 +1749,8 @@ public suspend inline fun ReceiveChannel.all(predicate: (E) -> Boolean): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.any(): Boolean = consume { @@ -1769,8 +1767,8 @@ public suspend fun ReceiveChannel.any(): Boolean = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.any(predicate: (E) -> Boolean): Boolean { consumeEach { @@ -1789,8 +1787,8 @@ public suspend inline fun ReceiveChannel.any(predicate: (E) -> Boolean): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.count(): Int { var count = 0 @@ -1808,8 +1806,8 @@ public suspend fun ReceiveChannel.count(): Int { * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.count(predicate: (E) -> Boolean): Int { var count = 0 @@ -1829,8 +1827,8 @@ public suspend inline fun ReceiveChannel.count(predicate: (E) -> Boolean) * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.fold(initial: R, operation: (acc: R, E) -> R): R { var accumulator = initial @@ -1853,8 +1851,8 @@ public suspend inline fun ReceiveChannel.fold(initial: R, operation: ( * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.foldIndexed(initial: R, operation: (index: Int, acc: R, E) -> R): R { var index = 0 @@ -1875,8 +1873,8 @@ public suspend inline fun ReceiveChannel.foldIndexed(initial: R, opera * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.maxBy(selector: (E) -> R): E? = consume { @@ -1905,8 +1903,8 @@ public suspend inline fun > ReceiveChannel.maxBy(selecto * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.maxWith(comparator: Comparator): E? = consume { @@ -1930,8 +1928,8 @@ public suspend fun ReceiveChannel.maxWith(comparator: Comparator): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun > ReceiveChannel.minBy(selector: (E) -> R): E? = consume { @@ -1960,8 +1958,8 @@ public suspend inline fun > ReceiveChannel.minBy(selecto * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.minWith(comparator: Comparator): E? = consume { @@ -1985,8 +1983,8 @@ public suspend fun ReceiveChannel.minWith(comparator: Comparator): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend fun ReceiveChannel.none(): Boolean = consume { @@ -2003,8 +2001,8 @@ public suspend fun ReceiveChannel.none(): Boolean = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.none(predicate: (E) -> Boolean): Boolean { consumeEach { @@ -2023,8 +2021,8 @@ public suspend inline fun ReceiveChannel.none(predicate: (E) -> Boolean): * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.reduce(operation: (acc: S, E) -> S): S = consume { @@ -2050,8 +2048,8 @@ public suspend inline fun ReceiveChannel.reduce(operation: (acc: S * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.reduceIndexed(operation: (index: Int, acc: S, E) -> S): S = consume { @@ -2075,8 +2073,8 @@ public suspend inline fun ReceiveChannel.reduceIndexed(operation: * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.sumBy(selector: (E) -> Int): Int { var sum = 0 @@ -2096,8 +2094,8 @@ public suspend inline fun ReceiveChannel.sumBy(selector: (E) -> Int): Int * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.sumByDouble(selector: (E) -> Double): Double { var sum = 0.0 @@ -2117,8 +2115,8 @@ public suspend inline fun ReceiveChannel.sumByDouble(selector: (E) -> Dou * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.requireNoNulls(): ReceiveChannel = map { it ?: throw IllegalArgumentException("null element found in $this.") } @@ -2135,8 +2133,8 @@ public fun ReceiveChannel.requireNoNulls(): ReceiveChannel = * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public suspend inline fun ReceiveChannel.partition(predicate: (E) -> Boolean): Pair, List> { val first = ArrayList() @@ -2162,8 +2160,8 @@ public suspend inline fun ReceiveChannel.partition(predicate: (E) -> Bool * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public infix fun ReceiveChannel.zip(other: ReceiveChannel): ReceiveChannel> = zip(other) { t1, t2 -> t1 to t2 } @@ -2178,8 +2176,8 @@ public infix fun ReceiveChannel.zip(other: ReceiveChannel): Receive * See [issue #254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). */ @Deprecated( - message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4", - level = DeprecationLevel.WARNING + message = "Channel operators are deprecated in favour of Flow and will be removed in 1.4.x", + level = DeprecationLevel.ERROR ) public fun ReceiveChannel.zip(other: ReceiveChannel, context: CoroutineContext = Dispatchers.Unconfined, transform: (a: E, b: R) -> V): ReceiveChannel = GlobalScope.produce(context, onCompletion = consumesAll(this, other)) { diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 7e47e6947a..7d84cd2105 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -204,8 +204,8 @@ public fun LongRange.asFlow(): Flow = flow { @FlowPreview @Deprecated( message = "Use channelFlow with awaitClose { } instead of flowViaChannel and invokeOnClose { }.", - level = DeprecationLevel.WARNING -) + level = DeprecationLevel.ERROR +) // To be removed in 1.4.x @Suppress("DeprecatedCallableAddReplaceWith") public fun flowViaChannel( bufferSize: Int = BUFFERED, diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 490e88265c..11969a48fa 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -434,7 +434,6 @@ public fun Flow.switchMap(transform: suspend (value: T) -> Flow): F message = "'scanReduce' was renamed to 'runningReduce' to be consistent with Kotlin standard library", replaceWith = ReplaceWith("runningReduce(operation)") ) -@ExperimentalCoroutinesApi public fun Flow.scanReduce(operation: suspend (accumulator: T, value: T) -> T): Flow = runningReduce(operation) @Deprecated( diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 7167971429..427041a7bb 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -108,7 +108,6 @@ import kotlin.native.concurrent.* * 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. @@ -138,7 +137,6 @@ public interface SharedFlow : Flow { * 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 @@ -202,7 +200,6 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { * supported only when `replay > 0` or `extraBufferCapacity > 0`). */ @Suppress("FunctionName", "UNCHECKED_CAST") -@ExperimentalCoroutinesApi public fun MutableSharedFlow( replay: Int = 0, extraBufferCapacity: Int = 0, diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index 935efdae2b..19e5fa36c7 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -12,7 +12,6 @@ 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 sharing, launching collection of the upstream flow. @@ -75,19 +74,16 @@ public enum class SharingCommand { * 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() /** @@ -108,7 +104,6 @@ public interface SharingStarted { * are negative. */ @Suppress("FunctionName") - @ExperimentalCoroutinesApi public fun WhileSubscribed( stopTimeoutMillis: Long = 0, replayExpirationMillis: Long = Long.MAX_VALUE @@ -143,7 +138,6 @@ public interface SharingStarted { */ @Suppress("FunctionName") @ExperimentalTime -@ExperimentalCoroutinesApi public fun SharingStarted.Companion.WhileSubscribed( stopTimeout: Duration = Duration.ZERO, replayExpiration: Duration = Duration.INFINITE diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index 8587606633..a9a4ed3d24 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -135,7 +135,6 @@ import kotlin.native.concurrent.* * might be added to this interface in the future, but is stable for use. * Use the `MutableStateFlow(value)` constructor function to create an implementation. */ -@ExperimentalCoroutinesApi public interface StateFlow : SharedFlow { /** * The current value of this state flow. @@ -156,7 +155,6 @@ public interface StateFlow : SharedFlow { * might be added to this interface in the future, but is stable for use. * Use the `MutableStateFlow()` constructor function to create an implementation. */ -@ExperimentalCoroutinesApi public interface MutableStateFlow : StateFlow, MutableSharedFlow { /** * The current value of this state flow. @@ -180,7 +178,6 @@ public interface MutableStateFlow : StateFlow, MutableSharedFlow { * Creates a [MutableStateFlow] with the given initial [value]. */ @Suppress("FunctionName") -@ExperimentalCoroutinesApi public fun MutableStateFlow(value: T): MutableStateFlow = StateFlowImpl(value ?: NULL) // ------------------------------------ Implementation ------------------------------------ @@ -380,4 +377,4 @@ internal fun StateFlow.fuseStateFlow( return this } return fuseSharedFlow(context, capacity, onBufferOverflow) -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index 8be19f08e0..244af9a7f5 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -71,7 +71,6 @@ internal inline fun Flow.unsafeTransform( * .collect { println(it) } // prints Begin, a, b, c * ``` */ -@ExperimentalCoroutinesApi public fun Flow.onStart( action: suspend FlowCollector.() -> Unit ): Flow = unsafeFlow { // Note: unsafe flow is used here, but safe collector is used to invoke start action @@ -142,7 +141,6 @@ public fun Flow.onStart( * In case of failure or cancellation, any attempt to emit additional elements throws the corresponding exception. * Use [catch] if you need to suppress failure and replace it with emission of elements. */ -@ExperimentalCoroutinesApi public fun Flow.onCompletion( action: suspend FlowCollector.(cause: Throwable?) -> Unit ): Flow = unsafeFlow { // Note: unsafe flow is used here, but safe collector is used to invoke completion action @@ -178,7 +176,6 @@ public fun Flow.onCompletion( * }.collect { println(it) } // prints 1, 2 * ``` */ -@ExperimentalCoroutinesApi public fun Flow.onEmpty( action: suspend FlowCollector.() -> Unit ): Flow = unsafeFlow { @@ -203,12 +200,6 @@ private class ThrowingCollector(private val e: Throwable) : FlowCollector } } -// It was only released in 1.3.0-M2, remove in 1.4.0 -/** @suppress */ -@Deprecated(level = DeprecationLevel.HIDDEN, message = "binary compatibility with a version w/o FlowCollector receiver") -public fun Flow.onCompletion(action: suspend (cause: Throwable?) -> Unit): Flow = - onCompletion { action(it) } - private suspend fun FlowCollector.invokeSafely( action: suspend FlowCollector.(cause: Throwable?) -> Unit, cause: Throwable? diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt index 6351d4a89b..fe737a5bd1 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Share.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Share.kt @@ -132,7 +132,6 @@ import kotlin.jvm.* * @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, started: SharingStarted, @@ -297,7 +296,6 @@ private fun CoroutineScope.launchSharing( * 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, @@ -316,7 +314,6 @@ public fun Flow.stateIn( * * @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>() @@ -353,14 +350,12 @@ private fun CoroutineScope.launchSharingDeferred( /** * 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) @@ -391,7 +386,6 @@ private class ReadonlyStateFlow( * * 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) diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt index ba786d53cc..fb704c5b86 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt @@ -50,123 +50,6 @@ class ChannelsTest: TestBase() { finish(7) } - @Test - fun testAssociate() = runTest { - assertEquals(testList.associate { it * 2 to it * 3 }, - testList.asReceiveChannel().associate { it * 2 to it * 3 }.toMap()) - } - - @Test - fun testAssociateBy() = runTest { - assertEquals(testList.associateBy { it % 2 }, testList.asReceiveChannel().associateBy { it % 2 }) - } - - @Test - fun testAssociateBy2() = runTest { - assertEquals(testList.associateBy({ it * 2}, { it * 3 }), - testList.asReceiveChannel().associateBy({ it * 2}, { it * 3 }).toMap()) - } - - @Test - fun testDistinct() = runTest { - assertEquals(testList.map { it % 2 }.distinct(), testList.asReceiveChannel().map { it % 2 }.distinct().toList()) - } - - @Test - fun testDistinctBy() = runTest { - assertEquals(testList.distinctBy { it % 2 }.toList(), testList.asReceiveChannel().distinctBy { it % 2 }.toList()) - } - - @Test - fun testToCollection() = runTest { - val target = mutableListOf() - testList.asReceiveChannel().toCollection(target) - assertEquals(testList, target) - } - - @Test - fun testDrop() = runTest { - for (i in 0..testList.size) { - assertEquals(testList.drop(i), testList.asReceiveChannel().drop(i).toList(), "Drop $i") - } - } - - @Test - fun testElementAtOrElse() = runTest { - assertEquals(testList.elementAtOrElse(2) { 42 }, testList.asReceiveChannel().elementAtOrElse(2) { 42 }) - assertEquals(testList.elementAtOrElse(9) { 42 }, testList.asReceiveChannel().elementAtOrElse(9) { 42 }) - } - - @Test - fun testFirst() = runTest { - assertEquals(testList.first(), testList.asReceiveChannel().first()) - for (i in testList) { - assertEquals(testList.first { it == i }, testList.asReceiveChannel().first { it == i }) - } - try { - testList.asReceiveChannel().first { it == 9 } - fail() - } catch (nse: NoSuchElementException) { - } - } - - @Test - fun testFirstOrNull() = runTest { - assertEquals(testList.firstOrNull(), testList.asReceiveChannel().firstOrNull()) - assertEquals(testList.firstOrNull { it == 2 }, testList.asReceiveChannel().firstOrNull { it == 2 }) - assertEquals(testList.firstOrNull { it == 9 }, testList.asReceiveChannel().firstOrNull { it == 9 }) - } - - @Test - fun testFlatMap() = runTest { - assertEquals(testList.flatMap { (0..it).toList() }, testList.asReceiveChannel().flatMap { (0..it).asReceiveChannel() }.toList()) - - } - - @Test - fun testFold() = runTest { - assertEquals(testList.fold(mutableListOf(42)) { acc, e -> acc.apply { add(e) } }, - testList.asReceiveChannel().fold(mutableListOf(42)) { acc, e -> acc.apply { add(e) } }.toList()) - } - - @Test - fun testFoldIndexed() = runTest { - assertEquals(testList.foldIndexed(mutableListOf(42)) { index, acc, e -> acc.apply { add(index + e) } }, - testList.asReceiveChannel().foldIndexed(mutableListOf(42)) { index, acc, e -> acc.apply { add(index + e) } }.toList()) - } - - @Test - fun testGroupBy() = runTest { - assertEquals(testList.groupBy { it % 2 }, testList.asReceiveChannel().groupBy { it % 2 }) - } - - @Test - fun testGroupBy2() = runTest { - assertEquals(testList.groupBy({ -it }, { it + 100 }), testList.asReceiveChannel().groupBy({ -it }, { it + 100 }).toMap()) - - } - - @Test - fun testMap() = runTest { - assertEquals(testList.map { it + 10 }, testList.asReceiveChannel().map { it + 10 }.toList()) - - } - - @Test - fun testMapToCollection() = runTest { - val c = mutableListOf() - testList.asReceiveChannel().mapTo(c) { it + 10 } - assertEquals(testList.map { it + 10 }, c) - } - - @Test - fun testMapToSendChannel() = runTest { - val c = produce { - testList.asReceiveChannel().mapTo(channel) { it + 10 } - } - assertEquals(testList.map { it + 10 }, c.toList()) - } - @Test fun testEmptyList() = runTest { assertTrue(emptyList().asReceiveChannel().toList().isEmpty()) @@ -178,413 +61,6 @@ class ChannelsTest: TestBase() { } - @Test - fun testEmptySet() = runTest { - assertTrue(emptyList().asReceiveChannel().toSet().isEmpty()) - - } - - @Test - fun testToSet() = runTest { - assertEquals(testList.toSet(), testList.asReceiveChannel().toSet()) - } - - @Test - fun testToMutableSet() = runTest { - assertEquals(testList.toMutableSet(), testList.asReceiveChannel().toMutableSet()) - } - - @Test - fun testEmptySequence() = runTest { - val channel = Channel() - channel.close() - - assertEquals(emptyList().asReceiveChannel().count(), 0) - } - - @Test - fun testEmptyMap() = runTest { - val channel = Channel>() - channel.close() - - assertTrue(channel.toMap().isEmpty()) - } - - @Test - fun testToMap() = runTest { - val values = testList.map { it to it.toString() } - assertEquals(values.toMap(), values.asReceiveChannel().toMap()) - } - - @Test - fun testReduce() = runTest { - assertEquals(testList.reduce { acc, e -> acc * e }, - testList.asReceiveChannel().reduce { acc, e -> acc * e }) - } - - @Test - fun testReduceIndexed() = runTest { - assertEquals(testList.reduceIndexed { index, acc, e -> index + acc * e }, - testList.asReceiveChannel().reduceIndexed { index, acc, e -> index + acc * e }) - } - - @Test - fun testTake() = runTest { - for (i in 0..testList.size) { - assertEquals(testList.take(i), testList.asReceiveChannel().take(i).toList()) - } - } - - @Test - fun testPartition() = runTest { - assertEquals(testList.partition { it % 2 == 0 }, testList.asReceiveChannel().partition { it % 2 == 0 }) - } - - @Test - fun testZip() = runTest { - val other = listOf("a", "b") - assertEquals(testList.zip(other), testList.asReceiveChannel().zip(other.asReceiveChannel()).toList()) - } - - @Test - fun testElementAt() = runTest { - testList.indices.forEach { i -> - assertEquals(testList[i], testList.asReceiveChannel().elementAt(i)) - } - } - - @Test - fun testElementAtOrNull() = runTest { - testList.indices.forEach { i -> - assertEquals(testList[i], testList.asReceiveChannel().elementAtOrNull(i)) - } - assertNull(testList.asReceiveChannel().elementAtOrNull(-1)) - assertNull(testList.asReceiveChannel().elementAtOrNull(testList.size)) - } - - @Test - fun testFind() = runTest { - repeat(3) { mod -> - assertEquals(testList.find { it % 2 == mod }, - testList.asReceiveChannel().find { it % 2 == mod }) - } - } - - @Test - fun testFindLast() = runTest { - repeat(3) { mod -> - assertEquals(testList.findLast { it % 2 == mod }, testList.asReceiveChannel().findLast { it % 2 == mod }) - } - } - - @Test - fun testIndexOf() = runTest { - repeat(testList.size + 1) { i -> - assertEquals(testList.indexOf(i), testList.asReceiveChannel().indexOf(i)) - } - } - - @Test - fun testLastIndexOf() = runTest { - repeat(testList.size + 1) { i -> - assertEquals(testList.lastIndexOf(i), testList.asReceiveChannel().lastIndexOf(i)) - } - } - - @Test - fun testIndexOfFirst() = runTest { - repeat(3) { mod -> - assertEquals(testList.indexOfFirst { it % 2 == mod }, - testList.asReceiveChannel().indexOfFirst { it % 2 == mod }) - } - } - - @Test - fun testIndexOfLast() = runTest { - repeat(3) { mod -> - assertEquals(testList.indexOfLast { it % 2 != mod }, - testList.asReceiveChannel().indexOfLast { it % 2 != mod }) - } - } - - @Test - fun testLastOrNull() = runTest { - assertEquals(testList.lastOrNull(), testList.asReceiveChannel().lastOrNull()) - assertNull(emptyList().asReceiveChannel().lastOrNull()) - } - - @Test - fun testSingleOrNull() = runTest { - assertEquals(1, listOf(1).asReceiveChannel().singleOrNull()) - assertNull(listOf(1, 2).asReceiveChannel().singleOrNull()) - assertNull(emptyList().asReceiveChannel().singleOrNull()) - repeat(testList.size + 1) { i -> - assertEquals(testList.singleOrNull { it == i }, - testList.asReceiveChannel().singleOrNull { it == i }) - } - repeat(3) { mod -> - assertEquals(testList.singleOrNull { it % 2 == mod }, - testList.asReceiveChannel().singleOrNull { it % 2 == mod }) - } - } - - @Test - fun testDropWhile() = runTest { - repeat(3) { mod -> - assertEquals(testList.dropWhile { it % 2 == mod }, - testList.asReceiveChannel().dropWhile { it % 2 == mod }.toList()) - } - } - - @Test - fun testFilter() = runTest { - repeat(3) { mod -> - assertEquals(testList.filter { it % 2 == mod }, - testList.asReceiveChannel().filter { it % 2 == mod }.toList()) - } - } - - @Test - fun testFilterToCollection() = runTest { - repeat(3) { mod -> - val c = mutableListOf() - testList.asReceiveChannel().filterTo(c) { it % 2 == mod } - assertEquals(testList.filter { it % 2 == mod }, c) - } - } - - @Test - fun testFilterToSendChannel() = runTest { - repeat(3) { mod -> - val c = produce { - testList.asReceiveChannel().filterTo(channel) { it % 2 == mod } - } - assertEquals(testList.filter { it % 2 == mod }, c.toList()) - } - } - - @Test - fun testFilterNot() = runTest { - repeat(3) { mod -> - assertEquals(testList.filterNot { it % 2 == mod }, - testList.asReceiveChannel().filterNot { it % 2 == mod }.toList()) - } - } - - @Test - fun testFilterNotToCollection() = runTest { - repeat(3) { mod -> - val c = mutableListOf() - testList.asReceiveChannel().filterNotTo(c) { it % 2 == mod } - assertEquals(testList.filterNot { it % 2 == mod }, c) - } - } - - @Test - fun testFilterNotToSendChannel() = runTest { - repeat(3) { mod -> - val c = produce { - testList.asReceiveChannel().filterNotTo(channel) { it % 2 == mod } - } - assertEquals(testList.filterNot { it % 2 == mod }, c.toList()) - } - } - - @Test - fun testFilterNotNull() = runTest { - repeat(3) { mod -> - assertEquals( - testList.mapNotNull { it.takeIf { it % 2 == mod } }, - testList.asReceiveChannel().map { it.takeIf { it % 2 == mod } }.filterNotNull().toList()) - } - } - - @Test - fun testFilterNotNullToCollection() = runTest { - repeat(3) { mod -> - val c = mutableListOf() - testList.asReceiveChannel().map { it.takeIf { it % 2 == mod } }.filterNotNullTo(c) - assertEquals(testList.mapNotNull { it.takeIf { it % 2 == mod } }, c) - } - } - - @Test - fun testFilterNotNullToSendChannel() = runTest { - repeat(3) { mod -> - val c = produce { - testList.asReceiveChannel().map { it.takeIf { it % 2 == mod } }.filterNotNullTo(channel) - } - assertEquals(testList.mapNotNull { it.takeIf { it % 2 == mod } }, c.toList()) - } - } - - @Test - fun testFilterIndexed() = runTest { - repeat(3) { mod -> - assertEquals(testList.filterIndexed { index, _ -> index % 2 == mod }, - testList.asReceiveChannel().filterIndexed { index, _ -> index % 2 == mod }.toList()) - } - } - - @Test - fun testFilterIndexedToCollection() = runTest { - repeat(3) { mod -> - val c = mutableListOf() - testList.asReceiveChannel().filterIndexedTo(c) { index, _ -> index % 2 == mod } - assertEquals(testList.filterIndexed { index, _ -> index % 2 == mod }, c) - } - } - - @Test - fun testFilterIndexedToChannel() = runTest { - repeat(3) { mod -> - val c = produce { - testList.asReceiveChannel().filterIndexedTo(channel) { index, _ -> index % 2 == mod } - } - assertEquals(testList.filterIndexed { index, _ -> index % 2 == mod }, c.toList()) - } - } - - @Test - fun testTakeWhile() = runTest { - repeat(3) { mod -> - assertEquals(testList.takeWhile { it % 2 != mod }, - testList.asReceiveChannel().takeWhile { it % 2 != mod }.toList()) - } - } - - @Test - fun testToChannel() = runTest { - val c = produce { - testList.asReceiveChannel().toChannel(channel) - } - assertEquals(testList, c.toList()) - } - - @Test - fun testMapIndexed() = runTest { - assertEquals(testList.mapIndexed { index, i -> index + i }, - testList.asReceiveChannel().mapIndexed { index, i -> index + i }.toList()) - } - - @Test - fun testMapIndexedToCollection() = runTest { - val c = mutableListOf() - testList.asReceiveChannel().mapIndexedTo(c) { index, i -> index + i } - assertEquals(testList.mapIndexed { index, i -> index + i }, c) - } - - @Test - fun testMapIndexedToSendChannel() = runTest { - val c = produce { - testList.asReceiveChannel().mapIndexedTo(channel) { index, i -> index + i } - } - assertEquals(testList.mapIndexed { index, i -> index + i }, c.toList()) - } - - @Test - fun testMapNotNull() = runTest { - repeat(3) { mod -> - assertEquals(testList.mapNotNull { i -> i.takeIf { i % 2 == mod } }, - testList.asReceiveChannel().mapNotNull { i -> i.takeIf { i % 2 == mod } }.toList()) - } - } - - @Test - fun testMapNotNullToCollection() = runTest { - repeat(3) { mod -> - val c = mutableListOf() - testList.asReceiveChannel().mapNotNullTo(c) { i -> i.takeIf { i % 2 == mod } } - assertEquals(testList.mapNotNull { i -> i.takeIf { i % 2 == mod } }, c) - } - } - - @Test - fun testMapNotNullToSendChannel() = runTest { - repeat(3) { mod -> - val c = produce { - testList.asReceiveChannel().mapNotNullTo(channel) { i -> i.takeIf { i % 2 == mod } } - } - assertEquals(testList.mapNotNull { i -> i.takeIf { i % 2 == mod } }, c.toList()) - } - } - - @Test - fun testMapIndexedNotNull() = runTest { - repeat(3) { mod -> - assertEquals(testList.mapIndexedNotNull { index, i -> index.takeIf { i % 2 == mod } }, - testList.asReceiveChannel().mapIndexedNotNull { index, i -> index.takeIf { i % 2 == mod } }.toList()) - } - } - - @Test - fun testMapIndexedNotNullToCollection() = runTest { - repeat(3) { mod -> - val c = mutableListOf() - testList.asReceiveChannel().mapIndexedNotNullTo(c) { index, i -> index.takeIf { i % 2 == mod } } - assertEquals(testList.mapIndexedNotNull { index, i -> index.takeIf { i % 2 == mod } }, c) - } - } - - @Test - fun testMapIndexedNotNullToSendChannel() = runTest { - repeat(3) { mod -> - val c = produce { - testList.asReceiveChannel().mapIndexedNotNullTo(channel) { index, i -> index.takeIf { i % 2 == mod } } - } - assertEquals(testList.mapIndexedNotNull { index, i -> index.takeIf { i % 2 == mod } }, c.toList()) - } - } - - @Test - fun testWithIndex() = runTest { - assertEquals(testList.withIndex().toList(), testList.asReceiveChannel().withIndex().toList()) - } - - @Test - fun testMaxBy() = runTest { - assertEquals(testList.maxBy { 10 - abs(it - 2) }, - testList.asReceiveChannel().maxBy { 10 - abs(it - 2) }) - } - - @Test - fun testMaxWith() = runTest { - val cmp = compareBy { 10 - abs(it - 2) } - assertEquals(testList.maxWith(cmp), - testList.asReceiveChannel().maxWith(cmp)) - } - - @Test - fun testMinBy() = runTest { - assertEquals(testList.minBy { abs(it - 2) }, - testList.asReceiveChannel().minBy { abs(it - 2) }) - } - - @Test - fun testMinWith() = runTest { - val cmp = compareBy { abs(it - 2) } - assertEquals(testList.minWith(cmp), - testList.asReceiveChannel().minWith(cmp)) - } - - @Test - fun testSumBy() = runTest { - assertEquals(testList.sumBy { it * 3 }, - testList.asReceiveChannel().sumBy { it * 3 }) - } - - @Test - fun testSumByDouble() = runTest { - val expected = testList.sumByDouble { it * 3.0 } - val actual = testList.asReceiveChannel().sumByDouble { it * 3.0 } - assertEquals(expected, actual) - } - - @Test - fun testRequireNoNulls() = runTest { - assertEquals(testList.requireNoNulls(), testList.asReceiveChannel().requireNoNulls().toList()) - } - private fun Iterable.asReceiveChannel(context: CoroutineContext = Dispatchers.Unconfined): ReceiveChannel = GlobalScope.produce(context) { for (element in this@asReceiveChannel) diff --git a/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt b/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt index 885f1d6c8f..6ddde001e2 100644 --- a/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ProduceTest.kt @@ -163,7 +163,7 @@ class ProduceTest : TestBase() { private suspend fun cancelOnCompletion(coroutineContext: CoroutineContext) = CoroutineScope(coroutineContext).apply { val source = Channel() expect(1) - val produced = produce(coroutineContext, onCompletion = source.consumes()) { + val produced = produce(coroutineContext, onCompletion = { source.cancelConsumed(it) }) { expect(2) source.receive() } diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt deleted file mode 100644 index cb19b36a13..0000000000 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelsConsumeTest.kt +++ /dev/null @@ -1,908 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("DEPRECATION") - -package kotlinx.coroutines.channels - -import kotlinx.coroutines.* -import kotlin.coroutines.* -import kotlin.test.* - -/** - * Tests that various operators on channels properly consume (close) their source channels. - */ -class ChannelsConsumeTest : TestBase() { - private val sourceList = (1..10).toList() - - // test source with numbers 1..10 - private fun CoroutineScope.testSource() = produce(NonCancellable) { - for (i in sourceList) { - send(i) - } - } - - @Test - fun testConsume() { - checkTerminal { - consume { - assertEquals(1, receive()) - } - } - } - - @Test - fun testConsumeEach() { - checkTerminal { - var sum = 0 - consumeEach { sum += it } - assertEquals(55, sum) - } - } - - @Test - fun testConsumeEachIndexed() { - checkTerminal { - var sum = 0 - consumeEachIndexed { (index, i) -> sum += index * i } - assertEquals(330, sum) - } - } - - @Test - fun testElementAt() { - checkTerminal { - assertEquals(2, elementAt(1)) - } - checkTerminal(expected = { it is IndexOutOfBoundsException }) { - elementAt(10) - } - } - - @Test - fun testElementAtOrElse() { - checkTerminal { - assertEquals(3, elementAtOrElse(2) { error("Cannot happen") }) - } - checkTerminal { - assertEquals(-23, elementAtOrElse(10) { -23 }) - } - } - - @Test - fun testElementOrNull() { - checkTerminal { - assertEquals(4, elementAtOrNull(3)) - } - checkTerminal { - assertNull(elementAtOrNull(10)) - } - } - - @Test - fun testFind() { - checkTerminal { - assertEquals(3, find { it % 3 == 0 }) - } - } - - @Test - fun testFindLast() { - checkTerminal { - assertEquals(9, findLast { it % 3 == 0 }) - } - } - - @Test - fun testFirst() { - checkTerminal { - assertEquals(1, first()) - } - } - - @Test - fun testFirstPredicate() { - checkTerminal { - assertEquals(3, first { it % 3 == 0 }) - } - checkTerminal(expected = { it is NoSuchElementException }) { - first { it > 10 } - } - } - - @Test - fun testFirstOrNull() { - checkTerminal { - assertEquals(1, firstOrNull()) - } - } - - @Test - fun testFirstOrNullPredicate() { - checkTerminal { - assertEquals(3, firstOrNull { it % 3 == 0 }) - } - checkTerminal { - assertNull(firstOrNull { it > 10 }) - } - } - - @Test - fun testIndexOf() { - checkTerminal { - assertEquals(2, indexOf(3)) - } - checkTerminal { - assertEquals(-1, indexOf(11)) - } - } - - @Test - fun testIndexOfFirst() { - checkTerminal { - assertEquals(2, indexOfFirst { it % 3 == 0 }) - } - checkTerminal { - assertEquals(-1, indexOfFirst { it > 10 }) - } - } - - @Test - fun testIndexOfLast() { - checkTerminal { - assertEquals(8, indexOfLast { it % 3 == 0 }) - } - checkTerminal { - assertEquals(-1, indexOfLast { it > 10 }) - } - } - - @Test - fun testLast() { - checkTerminal { - assertEquals(10, last()) - } - } - - @Test - fun testLastPredicate() { - checkTerminal { - assertEquals(9, last { it % 3 == 0 }) - } - checkTerminal(expected = { it is NoSuchElementException }) { - last { it > 10 } - } - } - - @Test - fun testLastIndexOf() { - checkTerminal { - assertEquals(8, lastIndexOf(9)) - } - } - - @Test - fun testLastOrNull() { - checkTerminal { - assertEquals(10, lastOrNull()) - } - } - - @Test - fun testLastOrNullPredicate() { - checkTerminal { - assertEquals(9, lastOrNull { it % 3 == 0 }) - } - checkTerminal { - assertNull(lastOrNull { it > 10 }) - } - } - - @Test - fun testSingle() { - checkTerminal(expected = { it is IllegalArgumentException }) { - single() - } - } - - @Test - fun testSinglePredicate() { - checkTerminal { - assertEquals(7, single { it % 7 == 0 }) - } - checkTerminal(expected = { it is IllegalArgumentException }) { - single { it % 3 == 0 } - } - checkTerminal(expected = { it is NoSuchElementException }) { - single { it > 10 } - } - } - - @Test - fun testSingleOrNull() { - checkTerminal { - assertNull(singleOrNull()) - } - } - - @Test - fun testSingleOrNullPredicate() { - checkTerminal { - assertEquals(7, singleOrNull { it % 7 == 0 }) - } - checkTerminal { - assertNull(singleOrNull { it % 3 == 0 }) - } - checkTerminal { - assertNull(singleOrNull { it > 10 }) - } - } - - @Test - fun testDrop() { - checkTransform(sourceList.drop(3)) { - drop(3) - } - } - - @Test - fun testDropWhile() { - checkTransform(sourceList.dropWhile { it < 4}) { - dropWhile { it < 4 } - } - } - - @Test - fun testFilter() { - checkTransform(sourceList.filter { it % 2 == 0 }) { - filter { it % 2 == 0 } - } - } - - @Test - fun testFilterIndexed() { - checkTransform(sourceList.filterIndexed { index, _ -> index % 2 == 0 }) { - filterIndexed { index, _ -> index % 2 == 0 } - } - } - - @Test - fun testFilterIndexedToCollection() { - checkTerminal { - val list = mutableListOf() - filterIndexedTo(list) { index, _ -> index % 2 == 0 } - assertEquals(listOf(1, 3, 5, 7, 9), list) - } - } - - @Test - fun testFilterIndexedToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - filterIndexedTo(channel) { index, _ -> index % 2 == 0 } - channel.close() - assertEquals(listOf(1, 3, 5, 7, 9), result.await()) - } - } - - @Test - fun testFilterNot() { - checkTransform(sourceList.filterNot { it % 2 == 0 }) { - filterNot { it % 2 == 0 } - } - } - - @Test - fun testFilterNotNullToCollection() { - checkTerminal { - val list = mutableListOf() - filterNotNullTo(list) - assertEquals((1..10).toList(), list) - } - } - - @Test - fun testFilterNotNullToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - filterNotNullTo(channel) - channel.close() - assertEquals((1..10).toList(), result.await()) - } - } - - @Test - fun testFilterNotToCollection() { - checkTerminal { - val list = mutableListOf() - filterNotTo(list) { it % 2 == 0 } - assertEquals(listOf(1, 3, 5, 7, 9), list) - } - } - - @Test - fun testFilterNotToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - filterNotTo(channel) { it % 2 == 0 } - channel.close() - assertEquals(listOf(1, 3, 5, 7, 9), result.await()) - } - } - - @Test - fun testFilterToCollection() { - checkTerminal { - val list = mutableListOf() - filterTo(list) { it % 2 == 0 } - assertEquals(listOf(2, 4, 6, 8, 10), list) - } - } - - @Test - fun testFilterToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - filterTo(channel) { it % 2 == 0 } - channel.close() - assertEquals(listOf(2, 4, 6, 8, 10), result.await()) - } - } - - @Test - fun testTake() { - checkTransform(sourceList.take(3)) { - take(3) - } - } - - @Test - fun testTakeWhile() { - checkTransform(sourceList.takeWhile { it < 4 }) { - takeWhile { it < 4 } - } - } - - @Test - fun testAssociate() { - checkTerminal { - assertEquals(sourceList.associate { it to it.toString() }, associate { it to it.toString() }) - } - } - - @Test - fun testAssociateBy() { - checkTerminal { - assertEquals(sourceList.associateBy { it.toString() }, associateBy { it.toString() }) - } - } - - @Test - fun testAssociateByTwo() { - checkTerminal { - assertEquals(sourceList.associateBy({ it.toString() }, { it + 1}), associateBy({ it.toString() }, { it + 1})) - } - } - - @Test - fun testAssociateByToMap() { - checkTerminal { - val map = mutableMapOf() - associateByTo(map) { it.toString() } - assertEquals(sourceList.associateBy { it.toString() }, map) - } - } - - @Test - fun testAssociateByTwoToMap() { - checkTerminal { - val map = mutableMapOf() - associateByTo(map, { it.toString() }, { it + 1}) - assertEquals(sourceList.associateBy({ it.toString() }, { it + 1}), map) - } - } - - @Test - fun testAssociateToMap() { - checkTerminal { - val map = mutableMapOf() - associateTo(map) { it to it.toString() } - assertEquals(sourceList.associate { it to it.toString() }, map) - } - } - - @Test - fun testToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - toChannel(channel) - channel.close() - assertEquals(sourceList, result.await()) - } - } - - @Test - fun testToCollection() { - checkTerminal { - val list = mutableListOf() - toCollection(list) - assertEquals(sourceList, list) - } - } - - @Test - fun testToList() { - checkTerminal { - val list = toList() - assertEquals(sourceList, list) - } - } - - @Test - fun testToMap() { - checkTerminal { - val map = map { it to it.toString() }.toMap() - assertEquals(sourceList.map { it to it.toString() }.toMap(), map) - } - } - - @Test - fun testToMapWithMap() { - checkTerminal { - val map = mutableMapOf() - map { it to it.toString() }.toMap(map) - assertEquals(sourceList.map { it to it.toString() }.toMap(), map) - } - } - - @Test - fun testToMutableList() { - checkTerminal { - val list = toMutableList() - assertEquals(sourceList, list) - } - } - - @Test - fun testToSet() { - checkTerminal { - val set = toSet() - assertEquals(sourceList.toSet(), set) - } - } - - @Test - fun testFlatMap() { - checkTransform(sourceList.flatMap { listOf("A$it", "B$it") }) { - flatMap { - GlobalScope.produce(coroutineContext) { - send("A$it") - send("B$it") - } - } - } - } - - @Test - fun testGroupBy() { - checkTerminal { - val map = groupBy { it % 2 } - assertEquals(sourceList.groupBy { it % 2 }, map) - } - } - - @Test - fun testGroupByTwo() { - checkTerminal { - val map = groupBy({ it % 2 }, { it.toString() }) - assertEquals(sourceList.groupBy({ it % 2 }, { it.toString() }), map) - } - } - - @Test - fun testGroupByTo() { - checkTerminal { - val map = mutableMapOf>() - groupByTo(map) { it % 2 } - assertEquals(sourceList.groupBy { it % 2 }, map) - } - } - - @Test - fun testGroupByToTwo() { - checkTerminal { - val map = mutableMapOf>() - groupByTo(map, { it % 2 }, { it.toString() }) - assertEquals(sourceList.groupBy({ it % 2 }, { it.toString() }), map) - } - } - - @Test - fun testMap() { - checkTransform(sourceList.map { it.toString() }) { - map { it.toString() } - } - } - - @Test - fun testMapIndexed() { - checkTransform(sourceList.mapIndexed { index, v -> "$index$v" }) { - mapIndexed { index, v -> "$index$v" } - } - } - - @Test - fun testMapIndexedNotNull() { - checkTransform(sourceList.mapIndexedNotNull { index, v -> "$index$v".takeIf { v % 2 == 0 } }) { - mapIndexedNotNull { index, v -> "$index$v".takeIf { v % 2 == 0 } } - } - } - - @Test - fun testMapIndexedNotNullToCollection() { - checkTerminal { - val list = mutableListOf() - mapIndexedNotNullTo(list) { index, v -> "$index$v".takeIf { v % 2 == 0 } } - assertEquals(sourceList.mapIndexedNotNull { index, v -> "$index$v".takeIf { v % 2 == 0 } }, list) - } - } - - @Test - fun testMapIndexedNotNullToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - mapIndexedNotNullTo(channel) { index, v -> "$index$v".takeIf { v % 2 == 0 } } - channel.close() - assertEquals(sourceList.mapIndexedNotNull { index, v -> "$index$v".takeIf { v % 2 == 0 } }, result.await()) - } - } - - @Test - fun testMapIndexedToCollection() { - checkTerminal { - val list = mutableListOf() - mapIndexedTo(list) { index, v -> "$index$v" } - assertEquals(sourceList.mapIndexed { index, v -> "$index$v" }, list) - } - } - - @Test - fun testMapIndexedToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - mapIndexedTo(channel) { index, v -> "$index$v" } - channel.close() - assertEquals(sourceList.mapIndexed { index, v -> "$index$v" }, result.await()) - } - } - - @Test - fun testMapNotNull() { - checkTransform(sourceList.mapNotNull { (it + 3).takeIf { it % 2 == 0 } }) { - mapNotNull { (it + 3).takeIf { it % 2 == 0 } } - } - } - - @Test - fun testMapNotNullToCollection() { - checkTerminal { - val list = mutableListOf() - mapNotNullTo(list) { (it + 3).takeIf { it % 2 == 0 } } - assertEquals(sourceList.mapNotNull { (it + 3).takeIf { it % 2 == 0 } }, list) - } - } - - @Test - fun testMapNotNullToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - mapNotNullTo(channel) { (it + 3).takeIf { it % 2 == 0 } } - channel.close() - assertEquals(sourceList.mapNotNull { (it + 3).takeIf { it % 2 == 0 } }, result.await()) - } - } - - @Test - fun testMapToCollection() { - checkTerminal { - val list = mutableListOf() - mapTo(list) { it + 3 } - assertEquals(sourceList.map { it + 3 }, list) - } - } - - @Test - fun testMapToChannel() { - checkTerminal { - val channel = Channel() - val result = GlobalScope.async { channel.toList() } - mapTo(channel) { it + 3 } - channel.close() - assertEquals(sourceList.map { it + 3 }, result.await()) - } - } - - @Test - fun testWithIndex() { - checkTransform(sourceList.asSequence().withIndex().toList()) { - withIndex() - } - } - - @Test - fun testDistinctBy() { - checkTransform(sourceList.distinctBy { it / 2 }) { - distinctBy { it / 2 } - } - } - - @Test - fun testToMutableSet() { - checkTerminal { - val set = toMutableSet() - assertEquals(sourceList.toSet(), set) - } - } - - @Test - fun testAll() { - checkTerminal { - val all = all { it < 11 } - assertEquals(sourceList.all { it < 11 }, all) - } - } - - @Test - fun testAny() { - checkTerminal { - val any = any() - assertEquals(sourceList.any(), any) - } - } - - @Test - fun testAnyPredicate() { - checkTerminal { - val any = any { it % 3 == 0 } - assertEquals(sourceList.any { it % 3 == 0 }, any) - } - } - - @Test - fun testCount() { - checkTerminal { - val c = count() - assertEquals(sourceList.count(), c) - } - } - - @Test - fun testCountPredicate() { - checkTerminal { - val c = count { it % 3 == 0 } - assertEquals(sourceList.count { it % 3 == 0 }, c) - } - } - - @Test - fun testFold() { - checkTerminal { - val c = fold(1) { a, b -> a + b } - assertEquals(sourceList.fold(1) { a, b -> a + b }, c) - } - } - - @Test - fun testFoldIndexed() { - checkTerminal { - val c = foldIndexed(1) { i, a, b -> i * a + b } - assertEquals(sourceList.foldIndexed(1) { i, a, b -> i * a + b }, c) - } - } - - @Test - fun testMaxBy() { - checkTerminal { - val c = maxBy { it % 3 } - assertEquals(sourceList.maxBy { it % 3 }, c) - } - } - - @Test - fun testMaxWith() { - checkTerminal { - val c = maxWith(compareBy { it % 3 }) - assertEquals(sourceList.maxWith(compareBy { it % 3 }), c) - } - } - - @Test - fun testMinBy() { - checkTerminal { - val c = maxBy { it % 3 } - assertEquals(sourceList.maxBy { it % 3 }, c) - } - } - - @Test - fun testMinWith() { - checkTerminal { - val c = maxWith(compareBy { it % 3 }) - assertEquals(sourceList.maxWith(compareBy { it % 3 }), c) - } - } - - @Test - fun testNone() { - checkTerminal { - val none = none() - assertEquals(sourceList.none(), none) - } - } - - @Test - fun testNonePredicate() { - checkTerminal { - val none = none { it > 10 } - assertEquals(sourceList.none { it > 10 }, none) - } - } - - @Test - fun testReduce() { - checkTerminal { - val c = reduce { a, b -> a + b } - assertEquals(sourceList.reduce { a, b -> a + b }, c) - } - } - - @Test - fun testReduceIndexed() { - checkTerminal { - val c = reduceIndexed { i, a, b -> i * a + b } - assertEquals(sourceList.reduceIndexed { i, a, b -> i * a + b }, c) - } - } - - @Test - fun testSubBy() { - checkTerminal { - val c = sumBy { it } - assertEquals(sourceList.sumBy { it }, c) - } - } - - @Test - fun testSubByDouble() { - checkTerminal { - val c = sumByDouble { it.toDouble() } - assertEquals(sourceList.sumByDouble { it.toDouble() }, c) - } - } - - @Test - fun testPartition() { - checkTerminal { - val pair = partition { it % 2 == 0 } - assertEquals(sourceList.partition { it % 2 == 0 }, pair) - } - } - - @Test - fun testZip() { - val expect = sourceList.zip(sourceList) { a, b -> a + 2 * b } - checkTransform(expect) { - with(CoroutineScope(coroutineContext)) { - zip(testSource()) { a, b -> a + 2*b } - } - } - checkTransform(expect) { - with(CoroutineScope(coroutineContext)) { - testSource().zip(this@checkTransform) { a, b -> a + 2*b } - } - } - } - - // ------------------ - - private fun checkTerminal( - expected: ((Throwable?) -> Unit)? = null, - terminal: suspend ReceiveChannel.() -> Unit - ) { - checkTerminalCompletion(expected, terminal) - checkTerminalCancellation(expected, terminal) - } - - private fun checkTerminalCompletion( - expected: ((Throwable?) -> Unit)? = null, - terminal: suspend ReceiveChannel.() -> Unit - ) { - val src = runBlocking { - val src = testSource() - try { - // terminal operation - terminal(src) - // source must be cancelled at the end of terminal op - if (expected != null) error("Exception was expected") - } catch (e: Throwable) { - if (expected == null) throw e - expected(e) - } - src - } - assertTrue(src.isClosedForReceive, "Source must be closed") - } - - private fun checkTerminalCancellation( - expected: ((Throwable?) -> Unit)? = null, - terminal: suspend ReceiveChannel.() -> Unit - ) { - val src = runBlocking { - val src = testSource() - // terminal operation in a separate async context started until the first suspension - val d = async(NonCancellable, start = CoroutineStart.UNDISPATCHED) { - terminal(src) - } - // then cancel it - d.cancel() - // and try to get it's result - try { - d.await() - } catch (e: CancellationException) { - // ok -- was cancelled - } catch (e: Throwable) { - // if threw a different exception -- must be an expected one - if (expected == null) throw e - expected(e) - } - src - } - // source must be cancelled at the end of terminal op even if it was cancelled while in process - assertTrue(src.isClosedForReceive, "Source must be closed") - } - - private fun checkTransform( - expect: List, - transform: suspend ReceiveChannel.() -> ReceiveChannel - ) { - // check for varying number of received elements from the channel - for (nReceive in 0..expect.size) { - checkTransform(nReceive, expect, transform) - } - } - - private fun checkTransform( - nReceive: Int, - expect: List, - transform: suspend ReceiveChannel.() -> ReceiveChannel - ) { - val src = runBlocking { - val src = testSource() - // transform - val res = transform(src) - // receive nReceive elements from the result - repeat(nReceive) { i -> - assertEquals(expect[i], res.receive()) - } - if (nReceive < expect.size) { - // then cancel - res.cancel() - } else { - // then check that result is closed - assertNull(res.receiveOrNull(), "Result has unexpected values") - } - src - } - // source must be cancelled when runBlocking processes all the scheduled stuff - assertTrue(src.isClosedForReceive, "Source must be closed") - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt index 7613f04d29..da20f0c5ee 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelsJvmTest.kt @@ -14,7 +14,9 @@ class ChannelsJvmTest : TestBase() { fun testBlocking() { val ch = Channel() val sum = GlobalScope.async { - ch.sumBy { it } + var sum = 0 + ch.consumeEach { sum += it } + sum } repeat(10) { ch.sendBlocking(it) diff --git a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt index d274083668..6fd9a5e75b 100644 --- a/reactive/kotlinx-coroutines-jdk9/src/Publish.kt +++ b/reactive/kotlinx-coroutines-jdk9/src/Publish.kt @@ -28,7 +28,7 @@ import org.reactivestreams.FlowAdapters * **Note: This is an experimental api.** Behaviour of publishers that work as children in a parent scope with respect * to cancellation and error handling may change in the future. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // Since 1.3.x public fun flowPublish( context: CoroutineContext = EmptyCoroutineContext, @BuilderInference block: suspend ProducerScope.() -> Unit diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxConvert.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxConvert.kt index 903b60a2cf..c7fcb1c2b6 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxConvert.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxConvert.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.* * Adjacent applications of [flowOn], [buffer], [conflate], and [produceIn] to the result of `asFlow` are fused. * [conflate] has no effect, as this flow is already conflated; one can use [buffer] to change that instead. */ -@ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi // Since 1.3.x public fun ObservableValue.asFlow(): Flow = callbackFlow { val listener = ChangeListener { _, _, newValue -> try { From f1e35a0ff81c5e32dc79e9f9086f41731460c37b Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 26 Oct 2020 17:25:44 +0300 Subject: [PATCH 152/257] Version 1.4.0 --- CHANGES.md | 24 +++++++++++++++++++ README.md | 16 ++++++------- gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 4 ++-- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 513c28fb33..31222b051a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,29 @@ # Change log for kotlinx.coroutines +## Version 1.4.0 + +### Improvements + +* `StateFlow`, `SharedFlow` and corresponding operators are promoted to stable API (#2316). +* `Flow.debounce` operator with timeout selector based on each individual element is added (#1216). +* `CoroutineContext.job` extension property is introduced (#2159). +* `Flow.combine operator` is reworked: + * Complete fairness is maintained for single-threaded dispatchers. + * Its performance is improved, depending on the use-case, by at least 50% (#2296). + * Quadratic complexity depending on the number of upstream flows is eliminated (#2296). + * `crossinline` and `inline`-heavy internals are removed, fixing sporadic SIGSEGV on Mediatek Android devices (#1683, #1743). +* `Flow.zip` operator performance is improved by 40%. +* Various API has been promoted to stable or its deprecation level has been raised (#2316). + +### Bug fixes + +* Suspendable `stateIn` operator propagates exception to the caller when upstream fails to produce initial value (#2329). +* Fix `SharedFlow` with replay for subscribers working at different speed (#2325). +* Do not fail debug agent installation when security manager does not provide access to system properties (#2311). +* Cancelled lazy coroutines are properly cleaned up from debug agent output (#2294). +* `BlockHound` false-positives are correctly filtered out (#2302, #2190, #2303). +* Potential crash during a race between cancellation and upstream in `Observable.asFlow` is fixed (#2104, #2299). + ## Version 1.4.0-M1 ### Breaking changes diff --git a/README.md b/README.md index 24cc7947ec..2bc45cf451 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0-M1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0-M1) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0) [![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) @@ -86,7 +86,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.4.0-M1 + 1.4.0 ``` @@ -104,7 +104,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0' } ``` @@ -130,7 +130,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0") } ``` @@ -152,7 +152,7 @@ In common code that should get compiled for different platforms, you can add dep ```groovy commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0") } } ``` @@ -163,7 +163,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0' ``` This gives you access to Android [Dispatchers.Main] @@ -190,7 +190,7 @@ packagingOptions { ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0-M1/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -198,7 +198,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0-M1/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/gradle.properties b/gradle.properties index a4057dfcc3..75c07d3833 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.4.0-M1-SNAPSHOT +version=1.4.0-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.4.0 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 5518e00ef3..0c01400f1f 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -23,7 +23,7 @@ https://github.com/reactor/BlockHound/blob/1.0.2.RELEASE/docs/quick_start.md). Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.0-M1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.4.0' } ``` @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.0-M1.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.0.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index b82fe8577e..0b1c23938e 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0-M1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 5df8d7f0ed..297b1fbc98 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0-M1" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index cd34eb1285..5ee7794f17 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.4.0-M1 +coroutines_version=1.4.0 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index cd34eb1285..5ee7794f17 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.4.0-M1 +coroutines_version=1.4.0 android.useAndroidX=true android.enableJetifier=true From 00da1ac37958e4ecd15321561d6921c314e70721 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 26 Oct 2020 21:02:03 +0300 Subject: [PATCH 153/257] Acknowledge external contributors --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 31222b051a..bce941c70b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ ### Improvements * `StateFlow`, `SharedFlow` and corresponding operators are promoted to stable API (#2316). -* `Flow.debounce` operator with timeout selector based on each individual element is added (#1216). +* `Flow.debounce` operator with timeout selector based on each individual element is added (#1216, thanks to @mkano9!). * `CoroutineContext.job` extension property is introduced (#2159). * `Flow.combine operator` is reworked: * Complete fairness is maintained for single-threaded dispatchers. @@ -22,7 +22,7 @@ * Do not fail debug agent installation when security manager does not provide access to system properties (#2311). * Cancelled lazy coroutines are properly cleaned up from debug agent output (#2294). * `BlockHound` false-positives are correctly filtered out (#2302, #2190, #2303). -* Potential crash during a race between cancellation and upstream in `Observable.asFlow` is fixed (#2104, #2299). +* Potential crash during a race between cancellation and upstream in `Observable.asFlow` is fixed (#2104, #2299, thanks to @LouisCAD and @drinkthestars). ## Version 1.4.0-M1 From e710048362179c93fdb2dfb2000eaffe5eadfbf3 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 3 Nov 2020 23:04:19 +0300 Subject: [PATCH 154/257] SharedFlow: Fix scenario with concurrent emitters and cancellation of subscriber (#2359) * Added a specific test for a problematic scenario. * Added stress test with concurrent emitters and subscribers that come and go. Fixes #2356 --- .../common/src/flow/SharedFlow.kt | 6 ++ .../flow/sharing/SharedFlowScenarioTest.kt | 42 +++++++++ .../jvm/test/flow/SharedFlowStressTest.kt | 87 +++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index 427041a7bb..feb2749595 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -498,6 +498,12 @@ private class SharedFlowImpl( } // Compute new buffer size -> how many values we now actually have after resume val newBufferSize1 = (newBufferEndIndex - head).toInt() + // Note: When nCollectors == 0 we resume ALL queued emitters and we might have resumed more than bufferCapacity, + // and newMinCollectorIndex might pointing the wrong place because of that. The easiest way to fix it is by + // forcing newMinCollectorIndex = newBufferEndIndex. We do not needed to update newBufferSize1 (which could be + // too big), because the only use of newBufferSize1 in the below code is in the minOf(replay, newBufferSize1) + // expression, which coerces values that are too big anyway. + if (nCollectors == 0) newMinCollectorIndex = newBufferEndIndex // 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) diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt index c3eb2dac04..794553b482 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/SharedFlowScenarioTest.kt @@ -201,6 +201,48 @@ class SharedFlowScenarioTest : TestBase() { emitResumes(e3); expectReplayOf(3) } + @Test + fun testSuspendedConcurrentEmitAndCancelSubscriberReplay1() = + testSharedFlow(MutableSharedFlow(1)) { + val a = subscribe("a"); + emitRightNow(0); expectReplayOf(0) + collect(a, 0) + emitRightNow(1); expectReplayOf(1) + val e2 = emitSuspends(2) // suspends until 1 is collected + val e3 = emitSuspends(3) // suspends until 1 is collected, too + cancel(a) // must resume emitters 2 & 3 + emitResumes(e2) + emitResumes(e3) + expectReplayOf(3) // but replay size is 1 so only 3 should be kept + // Note: originally, SharedFlow was in a broken state here with 3 elements in the buffer + val b = subscribe("b") + collect(b, 3) + emitRightNow(4); expectReplayOf(4) + collect(b, 4) + } + + @Test + fun testSuspendedConcurrentEmitAndCancelSubscriberReplay1ExtraBuffer1() = + testSharedFlow(MutableSharedFlow( replay = 1, extraBufferCapacity = 1)) { + val a = subscribe("a"); + emitRightNow(0); expectReplayOf(0) + collect(a, 0) + emitRightNow(1); expectReplayOf(1) + emitRightNow(2); expectReplayOf(2) + val e3 = emitSuspends(3) // suspends until 1 is collected + val e4 = emitSuspends(4) // suspends until 1 is collected, too + val e5 = emitSuspends(5) // suspends until 1 is collected, too + cancel(a) // must resume emitters 3, 4, 5 + emitResumes(e3) + emitResumes(e4) + emitResumes(e5) + expectReplayOf(5) + val b = subscribe("b") + collect(b, 5) + emitRightNow(6); expectReplayOf(6) + collect(b, 6) + } + private fun testSharedFlow( sharedFlow: MutableSharedFlow, scenario: suspend ScenarioDsl.() -> Unit diff --git a/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt b/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt new file mode 100644 index 0000000000..349b7c8121 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/SharedFlowStressTest.kt @@ -0,0 +1,87 @@ +/* + * 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.atomicfu.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.collections.ArrayList +import kotlin.test.* +import kotlin.time.* + +@ExperimentalTime +class SharedFlowStressTest : TestBase() { + private val nProducers = 5 + private val nConsumers = 3 + private val nSeconds = 3 * stressTestMultiplier + + private lateinit var sf: MutableSharedFlow + private lateinit var view: SharedFlow + + @get:Rule + val producerDispatcher = ExecutorRule(nProducers) + @get:Rule + val consumerDispatcher = ExecutorRule(nConsumers) + + private val totalProduced = atomic(0L) + private val totalConsumed = atomic(0L) + + @Test + fun testStressReplay1() = + testStress(1, 0) + + @Test + fun testStressReplay1ExtraBuffer1() = + testStress(1, 1) + + @Test + fun testStressReplay2ExtraBuffer1() = + testStress(2, 1) + + private fun testStress(replay: Int, extraBufferCapacity: Int) = runTest { + sf = MutableSharedFlow(replay, extraBufferCapacity) + view = sf.asSharedFlow() + val jobs = ArrayList() + jobs += List(nProducers) { producerIndex -> + launch(producerDispatcher) { + var cur = producerIndex.toLong() + while (isActive) { + sf.emit(cur) + totalProduced.incrementAndGet() + cur += nProducers + } + } + } + jobs += List(nConsumers) { consumerIndex -> + launch(consumerDispatcher) { + while (isActive) { + view + .dropWhile { it % nConsumers != consumerIndex.toLong() } + .take(1) + .collect { + check(it % nConsumers == consumerIndex.toLong()) + totalConsumed.incrementAndGet() + } + } + } + } + var lastProduced = 0L + var lastConsumed = 0L + for (sec in 1..nSeconds) { + delay(1.seconds) + val produced = totalProduced.value + val consumed = totalConsumed.value + println("$sec sec: produced = $produced; consumed = $consumed") + assertNotEquals(lastProduced, produced) + assertNotEquals(lastConsumed, consumed) + lastProduced = produced + lastConsumed = consumed + } + jobs.forEach { it.cancel() } + jobs.forEach { it.join() } + println("total: produced = ${totalProduced.value}; consumed = ${totalConsumed.value}") + } +} \ No newline at end of file From c35ce7e509ff284f4b33ed20a0026e5149ca17a9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 3 Nov 2020 23:18:14 +0300 Subject: [PATCH 155/257] Version 1.4.1 --- CHANGES.md | 6 ++++++ README.md | 16 ++++++++-------- RELEASE.md | 16 ++++++++++------ gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 2 +- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 9 files changed, 30 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bce941c70b..baee6c4340 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change log for kotlinx.coroutines +## Version 1.4.1 + +This is a patch release with an important fix to the `SharedFlow` implementation. + +* SharedFlow: Fix scenario with concurrent emitters and cancellation of subscriber (#2359, thanks to @vehovsky for the bug report). + ## Version 1.4.0 ### Improvements diff --git a/README.md b/README.md index 2bc45cf451..7bd8e5a74b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.0) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.0) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.1) [![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) @@ -86,7 +86,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.4.0 + 1.4.1 ``` @@ -104,7 +104,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1' } ``` @@ -130,7 +130,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1") } ``` @@ -152,7 +152,7 @@ In common code that should get compiled for different platforms, you can add dep ```groovy commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1") } } ``` @@ -163,7 +163,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' ``` This gives you access to Android [Dispatchers.Main] @@ -190,7 +190,7 @@ packagingOptions { ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.0/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.1/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -198,7 +198,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.0/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.1/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/RELEASE.md b/RELEASE.md index 22cb61c42f..b2a08b6757 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,11 +12,15 @@ To release new `` of `kotlinx-coroutines`: `git merge origin/master` 4. Search & replace `` with `` across the project files. Should replace in: - * [`README.md`](README.md) (native, core, test, debug, modules) - * [`coroutines-guide.md`](docs/coroutines-guide.md) - * [`gradle.properties`](gradle.properties) - * [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties) - * [`ui/kotlinx-coroutines-android/animation-app/gradle.properties`](ui/kotlinx-coroutines-android/animation-app/gradle.properties) + * Docs + * [`README.md`](README.md) (native, core, test, debug, modules) + * [`kotlinx-coroutines-debug/README.md`](kotlinx-coroutines-debug/README.md) + * [`kotlinx-coroutines-test/README.md`](kotlinx-coroutines-test/README.md) + * [`coroutines-guide-ui.md`](ui/coroutines-guide-ui.md) + * Properties + * [`gradle.properties`](gradle.properties) + * [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties) + * [`ui/kotlinx-coroutines-android/animation-app/gradle.properties`](ui/kotlinx-coroutines-android/animation-app/gradle.properties) * Make sure to **exclude** `CHANGES.md` from replacements. As an alternative approach you can use `./bump-version.sh old_version new_version` @@ -26,7 +30,7 @@ To release new `` of `kotlinx-coroutines`: * Write each change on a single line (don't wrap with CR). * Study commit message from previous release. -6. Create branch for this release: +6. Create the branch for this release: `git checkout -b version-` 7. Commit updated files to a new version branch:
diff --git a/gradle.properties b/gradle.properties index 75c07d3833..1ffa02d1ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.4.0-SNAPSHOT +version=1.4.1-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.4.0 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 0c01400f1f..5525f9129f 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.0.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.1.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 0b1c23938e..afcd4a3b3b 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 297b1fbc98..9c1251fe21 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 5ee7794f17..c4aa67585e 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.4.0 +coroutines_version=1.4.1 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 5ee7794f17..c4aa67585e 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.4.0 +coroutines_version=1.4.1 android.useAndroidX=true android.enableJetifier=true From 865c229de80fd748e2c983d1f887b99c6e66ce64 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 4 Nov 2020 00:42:35 +0300 Subject: [PATCH 156/257] Additional docs on unbuffered shared flow (#2346) --- kotlinx-coroutines-core/common/src/flow/SharedFlow.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index feb2749595..a3075b927a 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -68,6 +68,15 @@ import kotlin.native.concurrent.* * 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. * + * ### Unbuffered shared flow + * + * A default implementation of a shared flow that is created with `MutableSharedFlow()` constructor function + * without parameters has no replay cache nor additional buffer. + * [emit][MutableSharedFlow.emit] call to such a shared flow suspends until all subscribers receive the emitted value + * and returns immediately if there are no subscribers. + * Thus, [tryEmit][MutableSharedFlow.tryEmit] call succeeds and returns `true` only if + * there are no subscribers (in which case the emitted value is immediately lost). + * * ### SharedFlow vs BroadcastChannel * * Conceptually shared flow is similar to [BroadcastChannel][BroadcastChannel] From 937cc0c4912ad707850b0f37417164a77525f4b7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 13 Nov 2020 00:49:56 -0800 Subject: [PATCH 157/257] Fix replacement for deprecated broadcastIn operator (#2382) Fixes #2344 --- kotlinx-coroutines-core/common/src/flow/Channels.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/Channels.kt b/kotlinx-coroutines-core/common/src/flow/Channels.kt index 762cdcad1b..63b285abc3 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -201,7 +201,7 @@ public fun BroadcastChannel.asFlow(): Flow = flow { */ @Deprecated( message = "Use shareIn operator and the resulting SharedFlow as a replacement for BroadcastChannel", - replaceWith = ReplaceWith("shareIn(scope, 0, SharingStarted.Lazily)"), + replaceWith = ReplaceWith("this.shareIn(scope, SharingStarted.Lazily, 0)"), level = DeprecationLevel.WARNING ) public fun Flow.broadcastIn( From addff4b4b7265166c608642a74d21d8fe9a7c1b4 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 13 Nov 2020 03:11:47 -0800 Subject: [PATCH 158/257] Prevent potential re-park when the unparking thread is too slow (#2381) --- .../jvm/src/scheduling/CoroutineScheduler.kt | 14 +++++++++++++- ...ngCoroutineDispatcherMixedStealingStressTest.kt | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 62cf80f7f8..ad61224b52 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -721,7 +721,19 @@ internal class CoroutineScheduler( } assert { localQueue.size == 0 } workerCtl.value = PARKED // Update value once - while (inStack()) { // Prevent spurious wakeups + /* + * inStack() prevents spurious wakeups, while workerCtl.value == PARKED + * prevents the following race: + * + * - T2 scans the queue, adds itself to the stack, goes to rescan + * - T2 suspends in 'workerCtl.value = PARKED' line + * - T1 pops T2 from the stack, claims workerCtl, suspends + * - T2 fails 'while (inStack())' check, goes to full rescan + * - T2 adds itself to the stack, parks + * - T1 unparks T2, bails out with success + * - T2 unparks and loops in 'while (inStack())' + */ + while (inStack() && workerCtl.value == PARKED) { // Prevent spurious wakeups if (isTerminated || state == WorkerState.TERMINATED) break tryReleaseCpu(WorkerState.PARKING) interrupted() // Cleanup interruptions diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt index 1fe0d8386d..3a55f8c4f2 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt @@ -77,4 +77,4 @@ class BlockingCoroutineDispatcherMixedStealingStressTest : SchedulerTestBase() { cpuBlocker.await() } } -} \ No newline at end of file +} From f49fbf61ec2c83f43d9a09fcee078251a086b157 Mon Sep 17 00:00:00 2001 From: Masood Fallahpoor Date: Sat, 14 Nov 2020 18:47:18 +0330 Subject: [PATCH 159/257] Fix a typo in select-expression.md (#2385) --- docs/select-expression.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/select-expression.md b/docs/select-expression.md index f0e5ae4681..43fcfd245b 100644 --- a/docs/select-expression.md +++ b/docs/select-expression.md @@ -222,7 +222,7 @@ fun main() = runBlocking { > You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-02.kt). -The result of this code is quite interesting, so we'll analyze it in mode detail: +The result of this code is quite interesting, so we'll analyze it in more detail: ```text a -> 'Hello 0' From 37b95a94816297667b9d4b537688c3a9e174ca8b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sat, 14 Nov 2020 18:29:03 +0300 Subject: [PATCH 160/257] Simplify JobNode.toString to reduce code and avoid potential StackOverflow (#2377) Fixes #2371 --- kotlinx-coroutines-core/common/src/JobSupport.kt | 13 +------------ .../common/src/selects/Select.kt | 1 - kotlinx-coroutines-core/jvm/src/Future.kt | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 020d00a32c..5f21299e58 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -1151,8 +1151,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren override fun invoke(cause: Throwable?) { parent.continueCompleting(state, child, proposedUpdate) } - override fun toString(): String = - "ChildCompletion[$child, $proposedUpdate]" } private class AwaitContinuation( @@ -1350,6 +1348,7 @@ internal abstract class JobNode( override val isActive: Boolean get() = true override val list: NodeList? get() = null override fun dispose() = (job as JobSupport).removeNode(this) + override fun toString() = "$classSimpleName@$hexAddress[job@${job.hexAddress}]" } internal class NodeList : LockFreeLinkedListHead(), Incomplete { @@ -1384,7 +1383,6 @@ private class InvokeOnCompletion( private val handler: CompletionHandler ) : JobNode(job) { override fun invoke(cause: Throwable?) = handler.invoke(cause) - override fun toString() = "InvokeOnCompletion[$classSimpleName@$hexAddress]" } private class ResumeOnCompletion( @@ -1392,7 +1390,6 @@ private class ResumeOnCompletion( private val continuation: Continuation ) : JobNode(job) { override fun invoke(cause: Throwable?) = continuation.resume(Unit) - override fun toString() = "ResumeOnCompletion[$continuation]" } private class ResumeAwaitOnCompletion( @@ -1411,7 +1408,6 @@ private class ResumeAwaitOnCompletion( continuation.resume(state.unboxState() as T) } } - override fun toString() = "ResumeAwaitOnCompletion[$continuation]" } internal class DisposeOnCompletion( @@ -1419,7 +1415,6 @@ internal class DisposeOnCompletion( private val handle: DisposableHandle ) : JobNode(job) { override fun invoke(cause: Throwable?) = handle.dispose() - override fun toString(): String = "DisposeOnCompletion[$handle]" } private class SelectJoinOnCompletion( @@ -1431,7 +1426,6 @@ private class SelectJoinOnCompletion( if (select.trySelect()) block.startCoroutineCancellable(select.completion) } - override fun toString(): String = "SelectJoinOnCompletion[$select]" } private class SelectAwaitOnCompletion( @@ -1443,7 +1437,6 @@ private class SelectAwaitOnCompletion( if (select.trySelect()) job.selectAwaitCompletion(select, block) } - override fun toString(): String = "SelectAwaitOnCompletion[$select]" } // -------- invokeOnCancellation nodes @@ -1463,7 +1456,6 @@ private class InvokeOnCancelling( override fun invoke(cause: Throwable?) { if (_invoked.compareAndSet(0, 1)) handler.invoke(cause) } - override fun toString() = "InvokeOnCancelling[$classSimpleName@$hexAddress]" } internal class ChildHandleNode( @@ -1472,7 +1464,6 @@ internal class ChildHandleNode( ) : JobCancellingNode(parent), ChildHandle { override fun invoke(cause: Throwable?) = childJob.parentCancelled(job) override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause) - override fun toString(): String = "ChildHandle[$childJob]" } // Same as ChildHandleNode, but for cancellable continuation @@ -1483,7 +1474,5 @@ internal class ChildContinuation( override fun invoke(cause: Throwable?) { child.parentCancelled(child.getContinuationCancellationCause(job)) } - override fun toString(): String = - "ChildContinuation[$child]" } diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 99c54f8417..81d3745e62 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -339,7 +339,6 @@ internal class SelectBuilderImpl( if (trySelect()) resumeSelectWithException(job.getCancellationException()) } - override fun toString(): String = "SelectOnCancelling[${this@SelectBuilderImpl}]" } @PublishedApi diff --git a/kotlinx-coroutines-core/jvm/src/Future.kt b/kotlinx-coroutines-core/jvm/src/Future.kt index bd16f49af0..58792ced31 100644 --- a/kotlinx-coroutines-core/jvm/src/Future.kt +++ b/kotlinx-coroutines-core/jvm/src/Future.kt @@ -41,7 +41,6 @@ private class CancelFutureOnCompletion( // interruption flag and it will cause spurious failures elsewhere future.cancel(false) } - override fun toString() = "CancelFutureOnCompletion[$future]" } private class CancelFutureOnCancel(private val future: Future<*>) : CancelHandler() { From dede17ebea4528679aa9b473af59728734336fb3 Mon Sep 17 00:00:00 2001 From: Will Buck Date: Fri, 13 Nov 2020 20:20:16 -0600 Subject: [PATCH 161/257] Don't unconditionally cast uCont in ScopeCoroutine cast to CoroutineStackFrame Fixes #2386 --- kotlinx-coroutines-core/common/src/internal/Scopes.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index 9bb2ce3d29..f9362cff11 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -16,7 +16,7 @@ internal open class ScopeCoroutine( context: CoroutineContext, @JvmField val uCont: Continuation // unintercepted continuation ) : AbstractCoroutine(context, true), CoroutineStackFrame { - final override val callerFrame: CoroutineStackFrame? get() = uCont as CoroutineStackFrame? + final override val callerFrame: CoroutineStackFrame? get() = uCont as? CoroutineStackFrame final override fun getStackTraceElement(): StackTraceElement? = null final override val isScopedCoroutine: Boolean get() = true From bc553ba7c5aeb3a7fddad23e47b2e11e9de8f297 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 16 Nov 2020 11:23:41 -0800 Subject: [PATCH 162/257] Conditionally create an instance of CancellationException in Channel.cancel() (#2384) Avoid creating costly exception when the channel is cancelled to save a few cycles when it's not necessary. Cancellation is heavy-enough when the channel is open, so the single check won't worsen it. --- kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt | 1 + kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt | 1 + kotlinx-coroutines-core/common/src/flow/internal/Combine.kt | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 8edd2b310c..87bd43714d 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -635,6 +635,7 @@ internal abstract class AbstractChannel( cancelInternal(cause) final override fun cancel(cause: CancellationException?) { + if (isClosedForReceive) return // Do not create an exception if channel is already cancelled cancelInternal(cause ?: CancellationException("$classSimpleName was cancelled")) } diff --git a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt index a75d466199..9ceb77ddc2 100644 --- a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt @@ -26,6 +26,7 @@ internal open class ChannelCoroutine( } final override fun cancel(cause: CancellationException?) { + if (isClosedForReceive) return // Do not create an exception if channel is already cancelled cancelInternal(cause ?: defaultCancellationException()) } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt index bbdebd08b9..d276e5100a 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt @@ -137,7 +137,7 @@ internal fun zipImpl(flow: Flow, flow2: Flow, transform: sus } catch (e: AbortFlowException) { e.checkOwnership(owner = this@unsafeFlow) } finally { - if (!second.isClosedForReceive) second.cancel() + second.cancel() } } } From 598b86151e849f57238c0c6d1e81d314290279be Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 17 Nov 2020 14:44:27 +0300 Subject: [PATCH 163/257] Add lint warnings on SharedFlow operations that never complete (#2376) * Add lint warnings on SharedFlow operations that never complete Fixes #2340 Fixes #2368 * ~ remove awaitCancellation replacements --- .../common/src/flow/operators/Lint.kt | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index 7a70fbf7f2..9aa240d8a9 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -2,12 +2,13 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("unused") +@file:Suppress("unused", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlin.coroutines.* +import kotlin.internal.InlineOnly /** * Applying [cancellable][Flow.cancellable] to a [SharedFlow] has no effect. @@ -79,4 +80,61 @@ public fun FlowCollector<*>.cancel(cause: CancellationException? = null): Unit = replaceWith = ReplaceWith("currentCoroutineContext()") ) public val FlowCollector<*>.coroutineContext: CoroutineContext - get() = noImpl() \ No newline at end of file + get() = noImpl() + +@Deprecated( + message = "SharedFlow never completes, so this operator has no effect.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("this") +) +@InlineOnly +public inline fun SharedFlow.catch(noinline action: suspend FlowCollector.(cause: Throwable) -> Unit): Flow = + (this as Flow).catch(action) + +@Deprecated( + message = "SharedFlow never completes, so this operator has no effect.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("this") +) +@InlineOnly +public inline fun SharedFlow.retry( + retries: Long = Long.MAX_VALUE, + noinline predicate: suspend (cause: Throwable) -> Boolean = { true } +): Flow = + (this as Flow).retry(retries, predicate) + +@Deprecated( + message = "SharedFlow never completes, so this operator has no effect.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("this") +) +@InlineOnly +public inline fun SharedFlow.retryWhen(noinline predicate: suspend FlowCollector.(cause: Throwable, attempt: Long) -> Boolean): Flow = + (this as Flow).retryWhen(predicate) + +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated( + message = "SharedFlow never completes, so this terminal operation never completes.", + level = DeprecationLevel.WARNING +) +@InlineOnly +public suspend inline fun SharedFlow.toList(): List = + (this as Flow).toList() + +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated( + message = "SharedFlow never completes, so this terminal operation never completes.", + level = DeprecationLevel.WARNING +) +@InlineOnly +public suspend inline fun SharedFlow.toSet(): Set = + (this as Flow).toSet() + +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated( + message = "SharedFlow never completes, so this terminal operation never completes.", + level = DeprecationLevel.WARNING +) +@InlineOnly +public suspend inline fun SharedFlow.count(): Int = + (this as Flow).count() From 8ca5296cd19e586d290d0eae7accf904822aae4d Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 17 Nov 2020 22:19:09 +0300 Subject: [PATCH 164/257] Make SharingStarted a fun interface (#2397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was a part of the original design but was forgotten because the prototype was developeв before Kotlin 1.4.0. It makes implementing custom SharingStarted strategies more concise. --- .../common/src/flow/SharingStarted.kt | 12 +++++------- .../common/test/flow/sharing/ShareInTest.kt | 8 +++----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt index 19e5fa36c7..c36d633cbc 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -38,7 +38,7 @@ public enum class SharingCommand { /** * 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 + * This functional 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 @@ -46,11 +46,9 @@ public enum class SharingCommand { * 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 } + * fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int) = + * SharingStarted { subscriptionCount: StateFlow -> + * subscriptionCount.map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } * } * ``` * @@ -74,7 +72,7 @@ public enum class SharingCommand { * 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. */ -public interface SharingStarted { +public fun interface SharingStarted { public companion object { /** * Sharing is started immediately and never stops. diff --git a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt index 9020f5f311..42cdb1e19f 100644 --- a/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/sharing/ShareInTest.kt @@ -187,11 +187,9 @@ class ShareInTest : TestBase() { } @Suppress("TestFunctionName") - 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 fun SharingStarted.Companion.WhileSubscribedAtLeast(threshold: Int) = + SharingStarted { subscriptionCount -> + subscriptionCount.map { if (it >= threshold) SharingCommand.START else SharingCommand.STOP } } private class FlowState { From 4fe809f80a259ff41a77ba0257e44802907ccb80 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 18 Nov 2020 00:40:50 -0800 Subject: [PATCH 165/257] Unlock Mutex and release Semaphore during cancellation on a fast branch of slow-path in Mutex/Semaphore (#2396) Fixes #2390 Co-authored-by: Gareth Pearce --- .../common/src/sync/Mutex.kt | 3 ++- .../common/src/sync/Semaphore.kt | 7 +++---- .../common/test/sync/MutexTest.kt | 3 ++- .../jvm/test/sync/MutexStressTest.kt | 19 ++++++++++++++++++- .../jvm/test/sync/SemaphoreStressTest.kt | 19 ++++++++++++++++++- 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 73aaab5fbf..707c4640bc 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -201,7 +201,8 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { // try lock val update = if (owner == null) EMPTY_LOCKED else Empty(owner) if (_state.compareAndSet(state, update)) { // locked - cont.resume(Unit) + // TODO implement functional type in LockCont as soon as we get rid of legacy JS + cont.resume(Unit) { unlock(owner) } return@sc } } diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index 84b7f4f8a2..c342bb3009 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -172,7 +172,7 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se if (addAcquireToQueue(cont)) return@sc val p = _availablePermits.getAndDecrement() if (p > 0) { // permit acquired - cont.resume(Unit) + cont.resume(Unit, onCancellationRelease) return@sc } } @@ -206,9 +206,8 @@ private class SemaphoreImpl(private val permits: Int, acquiredPermits: Int) : Se // On CAS failure -- the cell must be either PERMIT or BROKEN // If the cell already has PERMIT from tryResumeNextFromQueue, try to grab it if (segment.cas(i, PERMIT, TAKEN)) { // took permit thus eliminating acquire/release pair - // The following resume must always succeed, since continuation was not published yet and we don't have - // to pass onCancellationRelease handle, since the coroutine did not suspend yet and cannot be cancelled - cont.resume(Unit) + /// This continuation is not yet published, but still can be cancelled via outer job + cont.resume(Unit, onCancellationRelease) return true } assert { segment.get(i) === BROKEN } // it must be broken in this case, no other way around it diff --git a/kotlinx-coroutines-core/common/test/sync/MutexTest.kt b/kotlinx-coroutines-core/common/test/sync/MutexTest.kt index c5d0ccf187..4f428bc4b0 100644 --- a/kotlinx-coroutines-core/common/test/sync/MutexTest.kt +++ b/kotlinx-coroutines-core/common/test/sync/MutexTest.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.sync +import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlin.test.* @@ -106,4 +107,4 @@ class MutexTest : TestBase() { assertFalse(mutex.holdsLock(firstOwner)) assertFalse(mutex.holdsLock(secondOwner)) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt index bb713b258d..027f3c514d 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/MutexStressTest.kt @@ -90,4 +90,21 @@ class MutexStressTest : TestBase() { } } } -} \ No newline at end of file + + @Test + fun testShouldBeUnlockedOnCancellation() = runTest { + val mutex = Mutex() + val n = 1000 * stressTestMultiplier + repeat(n) { + val job = launch(Dispatchers.Default) { + mutex.lock() + mutex.unlock() + } + mutex.withLock { + job.cancel() + } + job.join() + assertFalse { mutex.isLocked } + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt index 374a1e3d7c..2ceed64b95 100644 --- a/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/sync/SemaphoreStressTest.kt @@ -2,7 +2,7 @@ package kotlinx.coroutines.sync import kotlinx.coroutines.* import org.junit.Test -import kotlin.test.assertEquals +import kotlin.test.* class SemaphoreStressTest : TestBase() { @Test @@ -90,4 +90,21 @@ class SemaphoreStressTest : TestBase() { } } } + + @Test + fun testShouldBeUnlockedOnCancellation() = runTest { + val semaphore = Semaphore(1) + val n = 1000 * stressTestMultiplier + repeat(n) { + val job = launch(Dispatchers.Default) { + semaphore.acquire() + semaphore.release() + } + semaphore.withPermit { + job.cancel() + } + job.join() + assertTrue { semaphore.availablePermits == 1 } + } + } } From 31a8df0257e584f979f89832fdf90a464626b2fd Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 18 Nov 2020 12:25:41 +0300 Subject: [PATCH 166/257] Clarify thread-safety of SharedFlow methods in docs (#2399) * Clarify thread-safety of SharedFlow methods in docs * Override MutableSharedFlow.emit to attach a more appropriate docs than the one inherited from FlowCollector. * Clarify thread-safety of all the MutableSharedFlow & MutableState "mutating" methods. The latter is needed, because Flows, in general, are sequential, but shared flows provide all the necessarily synchronization themselves, so, to avoid confusion it makes sense to additionally mention thread-safety of shared flows in all the relevant mutating functions. --- .../api/kotlinx-coroutines-core.api | 1 + .../common/src/flow/SharedFlow.kt | 17 +++++++++++++++++ .../common/src/flow/StateFlow.kt | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index b86076fca1..dcd837f7b2 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -1064,6 +1064,7 @@ public final class kotlinx/coroutines/flow/LintKt { } public abstract interface class kotlinx/coroutines/flow/MutableSharedFlow : kotlinx/coroutines/flow/FlowCollector, kotlinx/coroutines/flow/SharedFlow { + public abstract fun emit (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getSubscriptionCount ()Lkotlinx/coroutines/flow/StateFlow; public abstract fun resetReplayCache ()V public abstract fun tryEmit (Ljava/lang/Object;)Z diff --git a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt index a3075b927a..75f9e710f7 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt @@ -147,6 +147,17 @@ public interface SharedFlow : Flow { * Use the `MutableSharedFlow(...)` constructor function to create an implementation. */ public interface MutableSharedFlow : SharedFlow, FlowCollector { + /** + * Emits a [value] to this shared flow, suspending on buffer overflow if the shared flow was created + * with the default [BufferOverflow.SUSPEND] strategy. + * + * See [tryEmit] for a non-suspending variant of this function. + * + * This method is **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. + */ + override suspend fun emit(value: T) + /** * 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] @@ -155,6 +166,9 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { * 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`. + * + * This method is **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. */ public fun tryEmit(value: T): Boolean @@ -190,6 +204,9 @@ public interface MutableSharedFlow : SharedFlow, FlowCollector { * supported, and throws an [UnsupportedOperationException]. To reset a [MutableStateFlow] * to an initial value, just update its [value][MutableStateFlow.value]. * + * This method is **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. + * * **Note: This is an experimental api.** This function may be removed or renamed in the future. */ @ExperimentalCoroutinesApi diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index a9a4ed3d24..45641ca92d 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -160,6 +160,9 @@ 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. + * + * This property is **thread-safe** and can be safely updated from concurrent coroutines without + * external synchronization. */ public override var value: T @@ -170,6 +173,9 @@ public interface MutableStateFlow : StateFlow, MutableSharedFlow { * 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]. + * + * This method is **thread-safe** and can be safely invoked from concurrent coroutines without + * external synchronization. */ public fun compareAndSet(expect: T, update: T): Boolean } From 81577b2af135b4509fdec0737022badce6cc8aeb Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 18 Nov 2020 12:26:13 +0300 Subject: [PATCH 167/257] Clarify withContext(NonCancellable) behavior (#2400) Fixes #2383 --- kotlinx-coroutines-core/common/src/Builders.common.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index b7deaccb72..6ef1a8daea 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -133,6 +133,10 @@ private class LazyDeferredCoroutine( * which means that if the original [coroutineContext], in which `withContext` was invoked, * is cancelled by the time its dispatcher starts to execute the code, * it discards the result of `withContext` and throws [CancellationException]. + * + * The cancellation behaviour described above is enabled if and only if the dispatcher is being changed. + * For example, when using `withContext(NonCancellable) { ... }` there is no change in dispatcher and + * this call will not be cancelled neither on entry to the block inside `withContext` nor on exit from it. */ public suspend fun withContext( context: CoroutineContext, From 179f14219d66b63841bb5ab563ab3fba5fc11a05 Mon Sep 17 00:00:00 2001 From: Alexander Likhachev Date: Wed, 18 Nov 2020 17:54:33 +0300 Subject: [PATCH 168/257] Add build parameter to build coroutines with JVM IR compiler (#2389) * Add build parameters to enable JVM IR and disable native targets * enable_jvm_ir enables JVM IR compiler * disable_native_targets disables native targets in train builds * enable_jvm_ir_api_check enables JVM IR API check (works only if enable_jvm_ir is set) * Fix "Return type must be specified in explicit API mode" in 1.4.20 --- build.gradle | 13 +++++++++++ gradle/compile-jvm-multiplatform.gradle | 8 +++++-- gradle/compile-jvm.gradle | 6 +++++ kotlinx-coroutines-core/build.gradle | 22 +++++++++++++++---- .../common/src/internal/DispatchedTask.kt | 2 +- .../common/src/selects/SelectUnbiased.kt | 2 +- .../jvm/src/internal/LockFreeLinkedList.kt | 6 ++--- kotlinx-coroutines-debug/build.gradle | 10 +++++++++ 8 files changed, 58 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 79c7f3553e..938d42e7a1 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,10 @@ buildscript { throw new IllegalArgumentException("'kotlin_snapshot_version' should be defined when building with snapshot compiler") } } + // These three flags are enabled in train builds for JVM IR compiler testing + ext.jvm_ir_enabled = rootProject.properties['enable_jvm_ir'] != null + ext.jvm_ir_api_check_enabled = rootProject.properties['enable_jvm_ir_api_check'] != null + ext.native_targets_enabled = rootProject.properties['disable_native_targets'] == null // Determine if any project dependency is using a snapshot version ext.using_snapshot_version = build_snapshot_train @@ -323,3 +327,12 @@ knit { } knitPrepare.dependsOn getTasksByName("dokka", true) + +// Disable binary compatibility check for JVM IR compiler output by default +if (jvm_ir_enabled) { + subprojects { project -> + configure(tasks.matching { it.name == "apiCheck" }) { + enabled = enabled && jvm_ir_api_check_enabled + } + } +} \ No newline at end of file diff --git a/gradle/compile-jvm-multiplatform.gradle b/gradle/compile-jvm-multiplatform.gradle index e72d30511e..44b0cbedba 100644 --- a/gradle/compile-jvm-multiplatform.gradle +++ b/gradle/compile-jvm-multiplatform.gradle @@ -6,8 +6,12 @@ sourceCompatibility = 1.6 targetCompatibility = 1.6 kotlin { - targets { - fromPreset(presets.jvm, 'jvm') + jvm { + if (rootProject.ext.jvm_ir_enabled) { + compilations.all { + kotlinOptions.useIR = true + } + } } sourceSets { jvmTest.dependencies { diff --git a/gradle/compile-jvm.gradle b/gradle/compile-jvm.gradle index caa5c45f60..bd2ae14775 100644 --- a/gradle/compile-jvm.gradle +++ b/gradle/compile-jvm.gradle @@ -9,6 +9,12 @@ apply plugin: 'org.jetbrains.kotlin.jvm' sourceCompatibility = 1.6 targetCompatibility = 1.6 +if (rootProject.ext.jvm_ir_enabled) { + kotlin.target.compilations.all { + kotlinOptions.useIR = true + } +} + dependencies { testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Workaround to make addSuppressed work in tests diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index f98f6a529c..314eea350b 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -5,8 +5,12 @@ apply plugin: 'org.jetbrains.kotlin.multiplatform' apply from: rootProject.file("gradle/compile-jvm-multiplatform.gradle") apply from: rootProject.file("gradle/compile-common.gradle") + +if (rootProject.ext.native_targets_enabled) { + apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") +} + apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") -apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") apply from: rootProject.file('gradle/publish-npm-js.gradle') /* ========================================================================== @@ -52,8 +56,11 @@ static boolean isNativeDarwin(String name) { return ["ios", "macos", "tvos", "wa static boolean isNativeOther(String name) { return ["linux", "mingw"].any { name.startsWith(it) } } defineSourceSet("concurrent", ["common"]) { it in ["jvm", "native"] } -defineSourceSet("nativeDarwin", ["native"]) { isNativeDarwin(it) } -defineSourceSet("nativeOther", ["native"]) { isNativeOther(it) } + +if (rootProject.ext.native_targets_enabled) { + defineSourceSet("nativeDarwin", ["native"]) { isNativeDarwin(it) } + defineSourceSet("nativeOther", ["native"]) { isNativeOther(it) } +} /* ========================================================================== */ @@ -129,7 +136,7 @@ def configureNativeSourceSetPreset(name, preset) { } // :KLUDGE: Idea.active: Configure platform libraries for native source sets when working in IDEA -if (Idea.active) { +if (Idea.active && rootProject.ext.native_targets_enabled) { def manager = project.ext.hostManager def linuxPreset = kotlin.presets.linuxX64 def macosPreset = kotlin.presets.macosX64 @@ -183,6 +190,13 @@ jvmTest { exclude '**/*StressTest.*' } systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test + + // TODO: JVM IR generates different stacktrace so temporary disable stacktrace tests + if (rootProject.ext.jvm_ir_enabled) { + filter { + excludeTestsMatching('kotlinx.coroutines.exceptions.StackTraceRecovery*') + } + } } jvmJar { diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 1f4942a358..caf87f143e 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -23,7 +23,7 @@ internal const val MODE_ATOMIC = 0 * **DO NOT CHANGE THE CONSTANT VALUE**. It is being into the user code from [suspendCancellableCoroutine]. */ @PublishedApi -internal const val MODE_CANCELLABLE = 1 +internal const val MODE_CANCELLABLE: Int = 1 /** * Cancellable dispatch mode for [suspendCancellableCoroutineReusable]. diff --git a/kotlinx-coroutines-core/common/src/selects/SelectUnbiased.kt b/kotlinx-coroutines-core/common/src/selects/SelectUnbiased.kt index edcf123b0a..d691c725b5 100644 --- a/kotlinx-coroutines-core/common/src/selects/SelectUnbiased.kt +++ b/kotlinx-coroutines-core/common/src/selects/SelectUnbiased.kt @@ -36,7 +36,7 @@ internal class UnbiasedSelectBuilderImpl(uCont: Continuation) : val clauses = arrayListOf<() -> Unit>() @PublishedApi - internal fun handleBuilderException(e: Throwable) = instance.handleBuilderException(e) + internal fun handleBuilderException(e: Throwable): Unit = instance.handleBuilderException(e) @PublishedApi internal fun initSelectResult(): Any? { diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index 97f9978139..d08f41bf8a 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -11,13 +11,13 @@ import kotlinx.coroutines.* private typealias Node = LockFreeLinkedListNode @PublishedApi -internal const val UNDECIDED = 0 +internal const val UNDECIDED: Int = 0 @PublishedApi -internal const val SUCCESS = 1 +internal const val SUCCESS: Int = 1 @PublishedApi -internal const val FAILURE = 2 +internal const val FAILURE: Int = 2 @PublishedApi internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE") diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle index ab7f28c6a8..2a11bbb38c 100644 --- a/kotlinx-coroutines-debug/build.gradle +++ b/kotlinx-coroutines-debug/build.gradle @@ -28,6 +28,16 @@ dependencies { api "net.java.dev.jna:jna-platform:$jna_version" } +// TODO: JVM IR generates different stacktrace so temporary disable stacktrace tests +if (rootProject.ext.jvm_ir_enabled) { + tasks.named('test', Test) { + filter { +// excludeTest('kotlinx.coroutines.debug.CoroutinesDumpTest', 'testCreationStackTrace') + excludeTestsMatching('kotlinx.coroutines.debug.DebugProbesTest') + } + } +} + jar { manifest { attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain" From e88e58d5c2b2f0f119bdbb6d5e43454fb7226982 Mon Sep 17 00:00:00 2001 From: Louis Wasserman Date: Sat, 21 Nov 2020 01:15:06 -0800 Subject: [PATCH 169/257] Correct a typo in the doc of SharingStarted. (#2403) --- 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 19e5fa36c7..675233765d 100644 --- a/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt +++ b/kotlinx-coroutines-core/common/src/flow/SharingStarted.kt @@ -60,7 +60,7 @@ public enum class SharingCommand { * [`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. + * * [START][SharingCommand.START] — the upstream flow is started. * * [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. From c542b83a07667f50f74d6b8d23975b63a54446f3 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 25 Nov 2020 19:33:31 +0300 Subject: [PATCH 170/257] Fix indents in the documentation --- kotlinx-coroutines-debug/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 5525f9129f..faf55ba61a 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -138,8 +138,8 @@ Coroutine "coroutine#2":DeferredCoroutine{Active}@289d1c02, state: SUSPENDED Dumping only deferred "coroutine#2":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99) - "coroutine#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line ExampleKt.computeOne(Example.kt:14) - "coroutine#4":DeferredCoroutine{Active}, continuation is SUSPENDED at line ExampleKt.computeTwo(Example.kt:19) + "coroutine#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line ExampleKt.computeOne(Example.kt:14) + "coroutine#4":DeferredCoroutine{Active}, continuation is SUSPENDED at line ExampleKt.computeTwo(Example.kt:19) ``` ### Status of the API From 0e5aefdbe0cb565bad289cee4ff550fbe9e36e8c Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 26 Nov 2020 13:46:54 +0300 Subject: [PATCH 171/257] Remove platform-specific dependencies on JavaFx artifacts (#2369) * Remove platform-specific dependencies on JavaFx artifacts Fixes #2360 --- ui/kotlinx-coroutines-javafx/build.gradle.kts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/kotlinx-coroutines-javafx/build.gradle.kts b/ui/kotlinx-coroutines-javafx/build.gradle.kts index 112441e0ed..e850e3940e 100644 --- a/ui/kotlinx-coroutines-javafx/build.gradle.kts +++ b/ui/kotlinx-coroutines-javafx/build.gradle.kts @@ -3,13 +3,20 @@ */ plugins { - id("org.openjfx.javafxplugin") + id("org.openjfx.javafxplugin") version "0.0.9" } javafx { version = version("javafx") modules = listOf("javafx.controls") - configuration = "compile" + configuration = "compileOnly" +} + +sourceSets { + test.configure { + compileClasspath += configurations.compileOnly + runtimeClasspath += configurations.compileOnly + } } val JDK_18: String? by lazy { From c742db28835f82c50a55971cf25ba621795cff34 Mon Sep 17 00:00:00 2001 From: Vadim Semenov <6957841+vadimsemenov@users.noreply.github.com> Date: Thu, 26 Nov 2020 10:52:41 +0000 Subject: [PATCH 172/257] Repair some corner cases in cancellation propagation between coroutines and listenable futures (#2222) * Repair some corner cases in cancellation propagation between coroutines and listenable futures Implement bidirectional cancellation for `future` coroutine builder. This also: * Refactors JobListenableFuture infrastructure so it can be reused in CoroutineScope.future and Deferred.asListenableFuture; * Provides more descriptive `toString` implementation for the returned Future; * Fixes stack traces in thrown exception, so it includes a call to get() that triggered the exception to be thrown; * Hides ListenableFuture.asDeferred return type, so it can't be cast to CompletableDeferred; * Adds more tests to cover fixed corner cases; * Improves documentation; * Suppresses annoying warnings in tests. Fixes #1442 --- .../src/ListenableFuture.kt | 290 ++++++++++-------- .../test/ListenableFutureTest.kt | 163 +++++++++- 2 files changed, 307 insertions(+), 146 deletions(-) diff --git a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt index 974e246283..6d1fab3d69 100644 --- a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt +++ b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt @@ -17,8 +17,11 @@ import kotlin.coroutines.* * The coroutine is immediately started. Passing [CoroutineStart.LAZY] to [start] throws * [IllegalArgumentException], because Futures don't have a way to start lazily. * - * The created coroutine is cancelled when the resulting future completes successfully, fails, or - * is cancelled. + * When the created coroutine [isCompleted][Job.isCompleted], it will try to + * *synchronously* complete the returned Future with the same outcome. This will + * succeed, barring a race with external cancellation of returned [ListenableFuture]. + * + * Cancellation is propagated bidirectionally. * * `CoroutineContext` is inherited from this [CoroutineScope]. Additional context elements can be * added/overlaid by passing [context]. @@ -32,8 +35,10 @@ import kotlin.coroutines.* * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging * facilities. * - * Note that the error and cancellation semantics of [future] are _subtly different_ than - * [asListenableFuture]'s. See [ListenableFutureCoroutine] for details. + * Note that the error and cancellation semantics of [future] are _subtly different_ than [asListenableFuture]'s. + * In particular, any exception that happens in the coroutine after returned future is + * successfully cancelled will be passed to the [CoroutineExceptionHandler] from the [context]. + * See [ListenableFutureCoroutine] for details. * * @param context added overlaying [CoroutineScope.coroutineContext] to form the new context. * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. @@ -46,14 +51,9 @@ public fun CoroutineScope.future( ): ListenableFuture { require(!start.isLazy) { "$start start is not supported" } val newContext = newCoroutineContext(context) - val future = SettableFuture.create() - val coroutine = ListenableFutureCoroutine(newContext, future) - future.addListener( - coroutine, - MoreExecutors.directExecutor()) + val coroutine = ListenableFutureCoroutine(newContext) coroutine.start(start, coroutine, block) - // Return hides the SettableFuture. This should prevent casting. - return object: ListenableFuture by future {} + return coroutine.future } /** @@ -70,7 +70,7 @@ public fun CoroutineScope.future( * When `this` `ListenableFuture` is [successfully cancelled][java.util.concurrent.Future.cancel], * it will cancel the returned `Deferred`. * - * When the returned `Deferred` is [cancelled][Deferred.cancel()], it will try to propagate the + * When the returned `Deferred` is [cancelled][Deferred.cancel], it will try to propagate the * cancellation to `this` `ListenableFuture`. Propagation will succeed, barring a race with the * `ListenableFuture` completing normally. This is the only case in which the returned `Deferred` * will complete with a different outcome than `this` `ListenableFuture`. @@ -152,7 +152,8 @@ public fun ListenableFuture.asDeferred(): Deferred { deferred.invokeOnCompletion { cancel(false) } - return deferred + // Return hides the CompletableDeferred. This should prevent casting. + return object : Deferred by deferred {} } /** @@ -166,7 +167,7 @@ public fun ListenableFuture.asDeferred(): Deferred { * state - a serious fundamental bug. */ private fun ExecutionException.nonNullCause(): Throwable { - return this.cause!! + return this.cause!! } /** @@ -195,13 +196,21 @@ private fun ExecutionException.nonNullCause(): Throwable { * * This is inherently a race. See [Future.cancel] for a description of `Future` cancellation * semantics. See [Job] for a description of coroutine cancellation semantics. See - * [DeferredListenableFuture.cancel] for greater detail on the overlapped cancellation semantics and + * [JobListenableFuture.cancel] for greater detail on the overlapped cancellation semantics and * corner cases of this method. */ public fun Deferred.asListenableFuture(): ListenableFuture { - val outerFuture = OuterFuture(this) - outerFuture.afterInit() - return outerFuture + val listenableFuture = JobListenableFuture(this) + // This invokeOnCompletion completes the JobListenableFuture with the same result as `this` Deferred. + // The JobListenableFuture may have completed earlier if it got cancelled! See JobListenableFuture.cancel(). + invokeOnCompletion { throwable -> + if (throwable == null) { + listenableFuture.complete(getCompleted()) + } else { + listenableFuture.completeExceptionallyOrCancel(throwable) + } + } + return listenableFuture } /** @@ -215,7 +224,6 @@ public fun Deferred.asListenableFuture(): ListenableFuture { * This method is intended to be used with one-shot Futures, so on coroutine cancellation, the Future is cancelled as well. * If cancelling the given future is undesired, use [Futures.nonCancellationPropagating] or * [kotlinx.coroutines.NonCancellable]. - * */ public suspend fun ListenableFuture.await(): T { try { @@ -255,8 +263,7 @@ private class ToContinuation( continuation.cancel() } else { try { - continuation.resumeWith( - Result.success(Uninterruptibles.getUninterruptibly(futureToObserve))) + continuation.resume(Uninterruptibles.getUninterruptibly(futureToObserve)) } catch (e: ExecutionException) { // ExecutionException is the only kind of exception that can be thrown from a gotten // Future. Anything else showing up here indicates a very fundamental bug in a @@ -271,57 +278,46 @@ private class ToContinuation( * An [AbstractCoroutine] intended for use directly creating a [ListenableFuture] handle to * completion. * - * The code in the [Runnable] portion of the class is registered as a [ListenableFuture] callback. - * See [run] for details. Both types are implemented by this object to save an allocation. + * If [future] is successfully cancelled, cancellation is propagated to `this` `Coroutine`. + * By documented contract, a [Future] has been cancelled if + * and only if its `isCancelled()` method returns true. + * + * Any error that occurs after successfully cancelling a [ListenableFuture] will be passed + * to the [CoroutineExceptionHandler] from the context. The contract of [Future] does not permit + * it to return an error after it is successfully cancelled. + * + * By calling [asListenableFuture] on a [Deferred], any error that occurs after successfully + * cancelling the [ListenableFuture] representation of the [Deferred] will _not_ be passed to + * the [CoroutineExceptionHandler]. Cancelling a [Deferred] places that [Deferred] in the + * cancelling/cancelled states defined by [Job], which _can_ show the error. It's assumed that + * the [Deferred] pointing to the task will be used to observe any error outcome occurring after + * cancellation. + * + * This may be counterintuitive, but it maintains the error and cancellation contracts of both + * the [Deferred] and [ListenableFuture] types, while permitting both kinds of promise to point + * to the same running task. */ private class ListenableFutureCoroutine( - context: CoroutineContext, - private val future: SettableFuture -) : AbstractCoroutine(context), Runnable { + context: CoroutineContext +) : AbstractCoroutine(context) { - /** - * When registered as a [ListenableFuture] listener, cancels the returned [Coroutine] if - * [future] is successfully cancelled. By documented contract, a [Future] has been cancelled if - * and only if its `isCancelled()` method returns true. - * - * Any error that occurs after successfully cancelling a [ListenableFuture] - * created by submitting the returned object as a [Runnable] to an `Executor` will be passed - * to the [CoroutineExceptionHandler] from the context. The contract of [Future] does not permit - * it to return an error after it is successfully cancelled. - * - * By calling [asListenableFuture] on a [Deferred], any error that occurs after successfully - * cancelling the [ListenableFuture] representation of the [Deferred] will _not_ be passed to - * the [CoroutineExceptionHandler]. Cancelling a [Deferred] places that [Deferred] in the - * cancelling/cancelled states defined by [Job], which _can_ show the error. It's assumed that - * the [Deferred] pointing to the task will be used to observe any error outcome occurring after - * cancellation. - * - * This may be counterintuitive, but it maintains the error and cancellation contracts of both - * the [Deferred] and [ListenableFuture] types, while permitting both kinds of promise to point - * to the same running task. - */ - override fun run() { - if (future.isCancelled) { - cancel() - } - } + // JobListenableFuture propagates external cancellation to `this` coroutine. See JobListenableFuture. + @JvmField val future = JobListenableFuture(this) override fun onCompleted(value: T) { - future.set(value) + future.complete(value) } - // TODO: This doesn't actually cancel the Future. There doesn't seem to be bidi cancellation? override fun onCancelled(cause: Throwable, handled: Boolean) { - if (!future.setException(cause) && !handled) { - // prevents loss of exception that was not handled by parent & could not be set to SettableFuture + if (!future.completeExceptionallyOrCancel(cause) && !handled) { + // prevents loss of exception that was not handled by parent & could not be set to JobListenableFuture handleCoroutineException(context, cause) } } } /** - * A [ListenableFuture] that delegates to an internal [DeferredListenableFuture], collaborating with - * it. + * A [ListenableFuture] that delegates to an internal [SettableFuture], collaborating with it. * * This setup allows the returned [ListenableFuture] to maintain the following properties: * @@ -333,130 +329,154 @@ private class ListenableFutureCoroutine( * - Fully correct cancellation and listener happens-after obeying [Future] and * [ListenableFuture]'s documented and implicit contracts is surprisingly difficult to achieve. * The best way to be correct, especially given the fun corner cases from - * [AsyncFuture.setAsync], is to just use an [AsyncFuture]. - * - To maintain sanity, this class implements [ListenableFuture] and uses an inner [AsyncFuture] - * around its input [deferred] as a state engine to establish happens-after-completion. This - * could probably be compressed into one subclass of [AsyncFuture] to save an allocation, at the + * [AbstractFuture.setFuture], is to just use an [AbstractFuture]. + * - To maintain sanity, this class implements [ListenableFuture] and uses an auxiliary [SettableFuture] + * around coroutine's result as a state engine to establish happens-after-completion. This + * could probably be compressed into one subclass of [AbstractFuture] to save an allocation, at the * cost of the implementation's readability. */ -private class OuterFuture(private val deferred: Deferred): ListenableFuture { - val innerFuture = DeferredListenableFuture(deferred) +private class JobListenableFuture(private val jobToCancel: Job): ListenableFuture { + /** + * Serves as a state machine for [Future] cancellation. + * + * [AbstractFuture] has a highly-correct atomic implementation of `Future`'s completion and + * cancellation semantics. By using that type, the [JobListenableFuture] can delegate its semantics to + * `auxFuture.get()` the result in such a way that the `Deferred` is always complete when returned. + * + * To preserve Coroutine's [CancellationException], this future points to either `T` or [Cancelled]. + */ + private val auxFuture = SettableFuture.create() - // Adding the listener after initialization resolves partial construction hairpin problem. - // - // This invokeOnCompletion completes the innerFuture as `deferred` does. The innerFuture may - // have completed earlier if it got cancelled! See DeferredListenableFuture. - fun afterInit() { - deferred.invokeOnCompletion { - innerFuture.complete() - } - } + /** + * When the attached coroutine [isCompleted][Job.isCompleted] successfully + * its outcome should be passed to this method. + * + * This should succeed barring a race with external cancellation. + */ + fun complete(result: T): Boolean = auxFuture.set(result) + + /** + * When the attached coroutine [isCompleted][Job.isCompleted] [exceptionally][Job.isCancelled] + * its outcome should be passed to this method. + * + * This method will map coroutine's exception into corresponding Future's exception. + * + * This should succeed barring a race with external cancellation. + */ + // CancellationException is wrapped into `Cancelled` to preserve original cause and message. + // All the other exceptions are delegated to SettableFuture.setException. + fun completeExceptionallyOrCancel(t: Throwable): Boolean = + if (t is CancellationException) auxFuture.set(Cancelled(t)) else auxFuture.setException(t) /** * Returns cancellation _in the sense of [Future]_. This is _not_ equivalent to * [Job.isCancelled]. * - * When done, this Future is cancelled if its innerFuture is cancelled, or if its delegate - * [deferred] is cancelled. Cancellation of [innerFuture] collaborates with this class. + * When done, this Future is cancelled if its [auxFuture] is cancelled, or if [auxFuture] + * contains [CancellationException]. * - * See [DeferredListenableFuture.cancel]. + * See [cancel]. */ override fun isCancelled(): Boolean { // This expression ensures that isCancelled() will *never* return true when isDone() returns false. // In the case that the deferred has completed with cancellation, completing `this`, its // reaching the "cancelled" state with a cause of CancellationException is treated as the - // same thing as innerFuture getting cancelled. If the Job is in the "cancelling" state and + // same thing as auxFuture getting cancelled. If the Job is in the "cancelling" state and // this Future hasn't itself been successfully cancelled, the Future will return // isCancelled() == false. This is the only discovered way to reconcile the two different // cancellation contracts. - return isDone - && (innerFuture.isCancelled - || deferred.getCompletionExceptionOrNull() is kotlinx.coroutines.CancellationException) + return auxFuture.isCancelled || (isDone && Uninterruptibles.getUninterruptibly(auxFuture) is Cancelled) } /** - * Waits for [innerFuture] to complete by blocking, then uses the [deferred] returned by that - * Future to get the `T` value `this` [ListenableFuture] is pointing to. This establishes - * happens-after ordering for completion of the [Deferred] input to [OuterFuture]. + * Waits for [auxFuture] to complete by blocking, then uses its `result` + * to get the `T` value `this` [ListenableFuture] is pointing to or throw a [CancellationException]. + * This establishes happens-after ordering for completion of the entangled coroutine. * - * `innerFuture` _must be complete_ in order for the [isDone] and [isCancelled] happens-after - * contract of [Future] to be correctly followed. If this method were to directly use - * _`this.deferred`_ instead of blocking on its `innerFuture`, the [Deferred] that this - * [ListenableFuture] is created from might be in an incomplete state when used by `get()`. + * [SettableFuture.get] can only throw [CancellationException] if it was cancelled externally. + * Otherwise it returns [Cancelled] that encapsulates outcome of the entangled coroutine. + * + * [auxFuture] _must be complete_ in order for the [isDone] and [isCancelled] happens-after + * contract of [Future] to be correctly followed. */ override fun get(): T { - return getInternal(innerFuture.get()) + return getInternal(auxFuture.get()) } /** See [get()]. */ override fun get(timeout: Long, unit: TimeUnit): T { - return getInternal(innerFuture.get(timeout, unit)) + return getInternal(auxFuture.get(timeout, unit)) } /** See [get()]. */ - private fun getInternal(deferred: Deferred): T { - if (deferred.isCancelled) { - val exception = deferred.getCompletionExceptionOrNull() - if (exception is kotlinx.coroutines.CancellationException) { - throw exception - } else { - throw ExecutionException(exception) - } - } else { - return deferred.getCompleted() - } + private fun getInternal(result: Any): T = if (result is Cancelled) { + throw CancellationException().initCause(result.exception) + } else { + // We know that `auxFuture` can contain either `T` or `Cancelled`. + @Suppress("UNCHECKED_CAST") + result as T } override fun addListener(listener: Runnable, executor: Executor) { - innerFuture.addListener(listener, executor) + auxFuture.addListener(listener, executor) } override fun isDone(): Boolean { - return innerFuture.isDone - } - - override fun cancel(mayInterruptIfRunning: Boolean): Boolean { - return innerFuture.cancel(mayInterruptIfRunning) - } -} - -/** - * Holds a delegate deferred, and serves as a state machine for [Future] cancellation. - * - * [AbstractFuture] has a highly-correct atomic implementation of `Future`'s completion and - * cancellation semantics. By using that type, the [OuterFuture] can delegate its semantics to - * _this_ `Future` `get()` the result in such a way that the `Deferred` is always complete when - * returned. - */ -private class DeferredListenableFuture( - private val deferred: Deferred -) : AbstractFuture>() { - - fun complete() { - set(deferred) + return auxFuture.isDone } /** - * Tries to cancel the task. This is fundamentally racy. + * Tries to cancel [jobToCancel] if `this` future was cancelled. This is fundamentally racy. * - * For any given call to `cancel()`, if [deferred] is already completed, the call will complete - * this Future with it, and fail to cancel. Otherwise, the - * call to `cancel()` will try to cancel this Future: if and only if cancellation of this - * succeeds, [deferred] will have its [Deferred.cancel] called. + * The call to `cancel()` will try to cancel [auxFuture]: if and only if cancellation of [auxFuture] + * succeeds, [jobToCancel] will have its [Job.cancel] called. * - * This arrangement means that [deferred] _might not successfully cancel_, if the race resolves - * in a particular way. [deferred] may also be in its "cancelling" state while this + * This arrangement means that [jobToCancel] _might not successfully cancel_, if the race resolves + * in a particular way. [jobToCancel] may also be in its "cancelling" state while this * ListenableFuture is complete and cancelled. - * - * [OuterFuture] collaborates with this class to present a more cohesive picture and ensure - * that certain combinations of cancelled/cancelling states can't be observed. */ override fun cancel(mayInterruptIfRunning: Boolean): Boolean { - return if (super.cancel(mayInterruptIfRunning)) { - deferred.cancel() + // TODO: call jobToCancel.cancel() _before_ running the listeners. + // `auxFuture.cancel()` will execute auxFuture's listeners. This delays cancellation of + // `jobToCancel` until after auxFuture's listeners have already run. + // Consider moving `jobToCancel.cancel()` into [AbstractFuture.afterDone] when the API is finalized. + return if (auxFuture.cancel(mayInterruptIfRunning)) { + jobToCancel.cancel() true } else { false } } + + override fun toString(): String = buildString { + append(super.toString()) + append("[status=") + if (isDone) { + try { + when (val result = Uninterruptibles.getUninterruptibly(auxFuture)) { + is Cancelled -> append("CANCELLED, cause=[${result.exception}]") + else -> append("SUCCESS, result=[$result") + } + } catch (e: CancellationException) { + // `this` future was cancelled by `Future.cancel`. In this case there's no cause or message. + append("CANCELLED") + } catch (e: ExecutionException) { + append("FAILURE, cause=[${e.cause}]") + } catch (t: Throwable) { + // Violation of Future's contract, should never happen. + append("UNKNOWN, cause=[${t.javaClass} thrown from get()]") + } + } else { + append("PENDING, delegate=[$auxFuture]") + } + } } + +/** + * A wrapper for `Coroutine`'s [CancellationException]. + * + * If the coroutine is _cancelled normally_, we want to show the reason of cancellation to the user. Unfortunately, + * [SettableFuture] can't store the reason of cancellation. To mitigate this, we wrap cancellation exception into this + * class and pass it into [SettableFuture.complete]. See implementation of [JobListenableFuture]. + */ +private class Cancelled(@JvmField val exception: CancellationException) diff --git a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt index a9a7f7ba9d..dc2d99d7f7 100644 --- a/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt +++ b/integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.guava import com.google.common.util.concurrent.* import kotlinx.coroutines.* import org.junit.* +import org.junit.Ignore import org.junit.Test import java.util.concurrent.* import java.util.concurrent.CancellationException @@ -315,6 +316,28 @@ class ListenableFutureTest : TestBase() { finish(4) } + @Test + @Ignore // TODO: propagate cancellation before running listeners. + fun testAsListenableFuturePropagatesCancellationBeforeRunningListeners() = runTest { + expect(1) + val deferred = async(context = Dispatchers.Unconfined) { + try { + delay(Long.MAX_VALUE) + } finally { + expect(3) // Cancelled. + } + } + val asFuture = deferred.asListenableFuture() + asFuture.addListener(Runnable { expect(4) }, MoreExecutors.directExecutor()) + assertFalse(asFuture.isDone) + expect(2) + asFuture.cancel(false) + assertTrue(asFuture.isDone) + assertTrue(asFuture.isCancelled) + assertFailsWith { deferred.await() } + finish(5) + } + @Test fun testFutureCancellation() = runTest { val future = awaitFutureWithCancel(true) @@ -333,15 +356,18 @@ class ListenableFutureTest : TestBase() { val outputCancellationException = assertFailsWith { asFuture.get() } - assertEquals(outputCancellationException.message, "Foobar") - assertTrue(outputCancellationException.cause is OutOfMemoryError) - assertEquals(outputCancellationException.cause?.message, "Foobaz") + val cause = outputCancellationException.cause + assertNotNull(cause) + assertEquals(cause.message, "Foobar") + assertTrue(cause.cause is OutOfMemoryError) + assertEquals(cause.cause?.message, "Foobaz") } @Test fun testNoFutureCancellation() = runTest { val future = awaitFutureWithCancel(false) assertFalse(future.isCancelled) + @Suppress("BlockingMethodInNonBlockingContext") assertEquals(42, future.get()) finish(4) } @@ -354,7 +380,7 @@ class ListenableFutureTest : TestBase() { assertTrue(asDeferredAsFuture.isCancelled) assertFailsWith { - val value: Int = asDeferredAsFuture.await() + asDeferredAsFuture.await() } } @@ -379,7 +405,7 @@ class ListenableFutureTest : TestBase() { assertTrue(asDeferred.isCancelled) assertFailsWith { - val value: Int = asDeferred.await() + asDeferred.await() } } @@ -433,7 +459,10 @@ class ListenableFutureTest : TestBase() { @Test fun testFutureCompletedWithNullFastPathAsDeferred() = runTest { val executor = MoreExecutors.listeningDecorator(ForkJoinPool.commonPool()) - val future = executor.submit(Callable { null }).also { it.get() } + val future = executor.submit(Callable { null }).also { + @Suppress("BlockingMethodInNonBlockingContext") + it.get() + } assertNull(future.asDeferred().await()) } @@ -494,8 +523,10 @@ class ListenableFutureTest : TestBase() { val future = future(Dispatchers.Unconfined) { try { delay(Long.MAX_VALUE) - } finally { + expectUnreached() + } catch (e: CancellationException) { expect(2) + throw e } } @@ -507,17 +538,19 @@ class ListenableFutureTest : TestBase() { @Test fun testExceptionOnExternalCancellation() = runTest(expected = {it is TestException}) { - expect(1) val result = future(Dispatchers.Unconfined) { try { + expect(1) delay(Long.MAX_VALUE) - } finally { - expect(2) + expectUnreached() + } catch (e: CancellationException) { + expect(3) throw TestException() } } + expect(2) result.cancel(true) - finish(3) + finish(4) } @Test @@ -540,12 +573,120 @@ class ListenableFutureTest : TestBase() { finish(3) } + /** This test ensures that we never pass [CancellationException] to [CoroutineExceptionHandler]. */ + @Test + fun testCancellationExceptionOnExternalCancellation() = runTest { + expect(1) + // No parent here (NonCancellable), so nowhere to propagate exception + val result = future(NonCancellable + Dispatchers.Unconfined) { + try { + delay(Long.MAX_VALUE) + } finally { + expect(2) + throw TestCancellationException() // this exception cannot be handled + } + } + assertTrue(result.cancel(true)) + finish(3) + } + + @Test + fun testCancellingFutureContextJobCancelsFuture() = runTest { + expect(1) + val supervisorJob = SupervisorJob() + val future = future(context = supervisorJob) { + expect(2) + try { + delay(Long.MAX_VALUE) + expectUnreached() + } catch (e: CancellationException) { + expect(4) + throw e + } + } + yield() + expect(3) + supervisorJob.cancel(CancellationException("Parent cancelled", TestException())) + supervisorJob.join() + assertTrue(future.isDone) + assertTrue(future.isCancelled) + val thrown = assertFailsWith { future.get() } + val cause = thrown.cause + assertNotNull(cause) + assertTrue(cause is CancellationException) + assertEquals("Parent cancelled", cause.message) + assertTrue(cause.cause is TestException) + finish(5) + } + + @Test + fun testFutureChildException() = runTest { + val future = future(context = NonCancellable + Dispatchers.Unconfined) { + val foo = async { delay(Long.MAX_VALUE); 42 } + val bar = async { throw TestException() } + foo.await() + bar.await() + } + future.checkFutureException() + } + + @Test + fun testFutureIsDoneAfterChildrenCompleted() = runTest { + expect(1) + val testException = TestException() + // Don't propagate exception to the test and use different dispatchers as we are going to block test thread. + val future = future(context = NonCancellable + Dispatchers.Default) { + val foo = async { + try { + delay(Long.MAX_VALUE) + 42 + } finally { + withContext(NonCancellable) { + delay(200) + } + } + } + foo.invokeOnCompletion { + expect(3) + } + val bar = async { throw testException } + foo.await() + bar.await() + } + yield() + expect(2) + // Blocking get should succeed after internal coroutine completes. + val thrown = assertFailsWith { future.get() } + expect(4) + assertEquals(testException, thrown.cause) + finish(5) + } + + @Test + @Ignore // TODO: propagate cancellation before running listeners. + fun testFuturePropagatesCancellationBeforeRunningListeners() = runTest { + expect(1) + val future = future(context = Dispatchers.Unconfined) { + try { + delay(Long.MAX_VALUE) + } finally { + expect(3) // Cancelled. + } + } + future.addListener(Runnable { expect(4) }, MoreExecutors.directExecutor()) + assertFalse(future.isDone) + expect(2) + future.cancel(false) + assertTrue(future.isDone) + assertTrue(future.isCancelled) + finish(5) + } + private inline fun ListenableFuture<*>.checkFutureException() { val e = assertFailsWith { get() } val cause = e.cause!! assertTrue(cause is T) } + @Suppress("SuspendFunctionOnCoroutineScope") private suspend fun CoroutineScope.awaitFutureWithCancel(cancellable: Boolean): ListenableFuture { val latch = CountDownLatch(1) val executor = MoreExecutors.listeningDecorator(ForkJoinPool.commonPool()) From b221094cd8a2c417ab5878a2ab18ed96690eb031 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 26 Nov 2020 16:30:59 +0300 Subject: [PATCH 173/257] Version 1.4.2 --- CHANGES.md | 12 ++++++++++++ README.md | 16 ++++++++-------- gradle.properties | 2 +- kotlinx-coroutines-debug/README.md | 2 +- kotlinx-coroutines-test/README.md | 2 +- ui/coroutines-guide-ui.md | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index baee6c4340..943280e796 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Change log for kotlinx.coroutines +## Version 1.4.2 + +* Fixed `StackOverflowError` in `Job.toString` when `Job` is observed in its intermediate state (#2371). +* Improved liveness and latency of `Dispatchers.Default` and `Dispatchers.IO` in low-loaded mode (#2381). +* Improved performance of consecutive `Channel.cancel` invocations (#2384). +* `SharingStarted` is now `fun` interface (#2397). +* Additional lint settings for `SharedFlow` to catch programmatic errors early (#2376). +* Fixed bug when mutex and semaphore were not released during cancellation (#2390, thanks to @Tilps for reproducing). +* Some corner cases in cancellation propagation between coroutines and listenable futures are repaired (#1442, thanks to @vadimsemenov). +* Fixed unconditional cast to `CoroutineStackFrame` in exception recovery that triggered failures of instrumented code (#2386). +* Platform-specific dependencies are removed from `kotlinx-coroutines-javafx` (#2360). + ## Version 1.4.1 This is a patch release with an important fix to the `SharedFlow` implementation. diff --git a/README.md b/README.md index 7bd8e5a74b..77de32bbdc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![official JetBrains project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.1) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.4.2) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.4.2) [![Kotlin](https://img.shields.io/badge/kotlin-1.4.0-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) @@ -86,7 +86,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.4.1 + 1.4.2 ``` @@ -104,7 +104,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' } ``` @@ -130,7 +130,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") } ``` @@ -152,7 +152,7 @@ In common code that should get compiled for different platforms, you can add dep ```groovy commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") } } ``` @@ -163,7 +163,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as dependency when using `kotlinx.coroutines` on Android: ```groovy -implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' ``` This gives you access to Android [Dispatchers.Main] @@ -190,7 +190,7 @@ packagingOptions { ### JS [Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.1/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.4.2/jar) (follow the link to get the dependency declaration snippet). You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) package via NPM. @@ -198,7 +198,7 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.1/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.4.2/jar) (follow the link to get the dependency declaration snippet). Only single-threaded code (JS-style) on Kotlin/Native is currently supported. diff --git a/gradle.properties b/gradle.properties index 1ffa02d1ae..9163cf5af1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.4.1-SNAPSHOT +version=1.4.2-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.4.0 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index faf55ba61a..fc9637a9d5 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.1.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.4.2.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index afcd4a3b3b..6022955254 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -9,7 +9,7 @@ This package provides testing utilities for effectively testing coroutines. Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 9c1251fe21..c2bbff22a8 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index c4aa67585e..98898bc9dc 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.4.1 +coroutines_version=1.4.2 android.useAndroidX=true android.enableJetifier=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index c4aa67585e..98898bc9dc 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true kotlin_version=1.4.0 -coroutines_version=1.4.1 +coroutines_version=1.4.2 android.useAndroidX=true android.enableJetifier=true From b13018db654d889c1dd5cb652a4664246fa8b9a8 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Sat, 28 Nov 2020 15:02:58 +0300 Subject: [PATCH 174/257] Add the model checking mode in Lincheck tests (#2326) * Add the model checking mode to Lincheck tests and use a newer and faster version of it Co-authored-by: Roman Elizarov --- .gitignore | 1 + .idea/dictionaries/shared.xml | 9 ++++ gradle.properties | 2 +- kotlinx-coroutines-core/build.gradle | 43 +++++++++++++++---- .../common/src/CancellableContinuationImpl.kt | 9 +++- .../common/src/internal/Symbol.kt | 2 +- .../jvm/test/AbstractLincheckTest.kt | 41 ++++++++++++++++++ .../jvm/test/LCStressOptionsDefault.kt | 20 --------- .../ChannelsLincheckTest.kt} | 32 +++++++------- .../LockFreeListLincheckTest.kt} | 13 +++--- .../LockFreeTaskQueueLincheckTest.kt} | 34 +++++++++------ .../MutexLincheckTest.kt} | 19 ++++---- .../SegmentListRemoveLincheckTest.kt} | 20 ++++----- .../SegmentQueueLincheckTest.kt} | 11 +++-- .../test/lincheck/SemaphoreLincheckTest.kt | 35 +++++++++++++++ .../linearizability/SemaphoreLCStressTest.kt | 34 --------------- 16 files changed, 197 insertions(+), 128 deletions(-) create mode 100644 .idea/dictionaries/shared.xml create mode 100644 kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/LCStressOptionsDefault.kt rename kotlinx-coroutines-core/jvm/test/{linearizability/ChannelsLCStressTest.kt => lincheck/ChannelsLincheckTest.kt} (88%) rename kotlinx-coroutines-core/jvm/test/{linearizability/LockFreeListLCStressTest.kt => lincheck/LockFreeListLincheckTest.kt} (83%) rename kotlinx-coroutines-core/jvm/test/{linearizability/LockFreeTaskQueueLCStressTest.kt => lincheck/LockFreeTaskQueueLincheckTest.kt} (50%) rename kotlinx-coroutines-core/jvm/test/{linearizability/MutexLCStressTest.kt => lincheck/MutexLincheckTest.kt} (54%) rename kotlinx-coroutines-core/jvm/test/{linearizability/SegmentListRemoveLCStressTest.kt => lincheck/SegmentListRemoveLincheckTest.kt} (66%) rename kotlinx-coroutines-core/jvm/test/{linearizability/SegmentQueueLCStressTest.kt => lincheck/SegmentQueueLincheckTest.kt} (78%) create mode 100644 kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt diff --git a/.gitignore b/.gitignore index aed7103292..52843ca5b7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ !/.idea/copyright !/.idea/codeStyleSettings.xml !/.idea/codeStyles +!/.idea/dictionaries *.iml .gradle .gradletasknamecache diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml new file mode 100644 index 0000000000..3da8e22952 --- /dev/null +++ b/.idea/dictionaries/shared.xml @@ -0,0 +1,9 @@ + + + + kotlinx + lincheck + redirector + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 9163cf5af1..1ee04710ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junit_version=4.12 atomicfu_version=0.14.4 knit_version=0.2.2 html_version=0.6.8 -lincheck_version=2.7.1 +lincheck_version=2.10 dokka_version=0.9.16-rdev-2-mpp-hacks byte_buddy_version=1.10.9 reactor_version=3.2.5.RELEASE diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 314eea350b..da9cdb4994 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -184,13 +184,22 @@ jvmTest { minHeapSize = '1g' maxHeapSize = '1g' enableAssertions = true - systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' + if (!Idea.active) { + // We should not set this security manager when `jvmTest` + // is invoked by IntelliJ IDEA since we need to pass + // system properties for Lincheck and stress tests. + // TODO Remove once IDEA is smart enough to select between `jvmTest`/`jvmStressTest`/`jvmLincheckTest` #KTIJ-599 + systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' + } // 'stress' is required to be able to run all subpackage tests like ":jvmTests --tests "*channels*" -Pstress=true" if (!Idea.active && rootProject.properties['stress'] == null) { + exclude '**/*LincheckTest.*' exclude '**/*StressTest.*' } - systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test - + if (Idea.active) { + // Configure the IDEA runner for Lincheck + configureJvmForLincheck(jvmTest) + } // TODO: JVM IR generates different stacktrace so temporary disable stacktrace tests if (rootProject.ext.jvm_ir_enabled) { filter { @@ -219,23 +228,41 @@ task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) { systemProperty 'kotlinx.coroutines.semaphore.maxSpinCycles', '10' } +task jvmLincheckTest(type: Test, dependsOn: compileTestKotlinJvm) { + classpath = files { jvmTest.classpath } + testClassesDirs = files { jvmTest.testClassesDirs } + include '**/*LincheckTest.*' + enableAssertions = true + testLogging.showStandardStreams = true + configureJvmForLincheck(jvmLincheckTest) +} + +static void configureJvmForLincheck(task) { + task.minHeapSize = '1g' + task.maxHeapSize = '6g' // we may need more space for building an interleaving tree in the model checking mode + task.jvmArgs = ['--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED', // required for transformation + '--add-exports', 'java.base/jdk.internal.util=ALL-UNNAMED'] // in the model checking mode + task.systemProperty 'kotlinx.coroutines.semaphore.segmentSize', '2' + task.systemProperty 'kotlinx.coroutines.semaphore.maxSpinCycles', '1' // better for the model checking mode +} + task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { classpath = files { jvmTest.classpath } testClassesDirs = files { jvmTest.testClassesDirs } executable = "$System.env.JDK_16/bin/java" exclude '**/*LFStressTest.*' // lock-freedom tests use LockFreedomTestEnvironment which needs JDK8 - exclude '**/*LCStressTest.*' // lin-check tests use LinChecker which needs JDK8 + exclude '**/*LincheckTest.*' // Lincheck tests use LinChecker which needs JDK8 exclude '**/exceptions/**' // exceptions tests check suppressed exception which needs JDK8 exclude '**/ExceptionsGuideTest.*' exclude '**/RunInterruptibleStressTest.*' // fails on JDK 1.6 due to JDK bug } -// Run these tests only during nightly stress test +// Run jdk16Test test only during nightly stress test jdk16Test.onlyIf { project.properties['stressTest'] != null } -// Always run those tests -task moreTest(dependsOn: [jvmStressTest, jdk16Test]) -build.dependsOn moreTest +// Always check additional test sets +task moreTest(dependsOn: [jvmStressTest, jvmLincheckTest, jdk16Test]) +check.dependsOn moreTest task testsJar(type: Jar, dependsOn: jvmTestClasses) { classifier = 'tests' diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index cdb1b78882..a056ef08ed 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -85,6 +85,13 @@ internal open class CancellableContinuationImpl( public override val isCancelled: Boolean get() = state is CancelledContinuation + // We cannot invoke `state.toString()` since it may cause a circular dependency + private val stateDebugRepresentation get() = when(state) { + is NotCompleted -> "Active" + is CancelledContinuation -> "Cancelled" + else -> "Completed" + } + public override fun initCancellability() { setupCancellation() } @@ -503,7 +510,7 @@ internal open class CancellableContinuationImpl( // For nicer debugging public override fun toString(): String = - "${nameString()}(${delegate.toDebugString()}){$state}@$hexAddress" + "${nameString()}(${delegate.toDebugString()}){$stateDebugRepresentation}@$hexAddress" protected open fun nameString(): String = "CancellableContinuation" diff --git a/kotlinx-coroutines-core/common/src/internal/Symbol.kt b/kotlinx-coroutines-core/common/src/internal/Symbol.kt index 4fa8f540af..cd25d3c1af 100644 --- a/kotlinx-coroutines-core/common/src/internal/Symbol.kt +++ b/kotlinx-coroutines-core/common/src/internal/Symbol.kt @@ -10,7 +10,7 @@ package kotlinx.coroutines.internal * @suppress **This is unstable API and it is subject to change.** */ internal class Symbol(val symbol: String) { - override fun toString(): String = symbol + override fun toString(): String = "<$symbol>" @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") inline fun unbox(value: Any?): T = if (value === this) null as T else value as T diff --git a/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt new file mode 100644 index 0000000000..5ba7acf994 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines + +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* +import org.jetbrains.kotlinx.lincheck.strategy.stress.* +import org.jetbrains.kotlinx.lincheck.verifier.* +import org.junit.* + +abstract class AbstractLincheckTest : VerifierState() { + open fun > O.customize(isStressTest: Boolean): O = this + open fun ModelCheckingOptions.customize(isStressTest: Boolean): ModelCheckingOptions = this + open fun StressOptions.customize(isStressTest: Boolean): StressOptions = this + + @Test + fun modelCheckingTest() = ModelCheckingOptions() + .iterations(if (isStressTest) 100 else 20) + .invocationsPerIteration(if (isStressTest) 10_000 else 1_000) + .commonConfiguration() + .customize(isStressTest) + .check(this::class) + + @Test + fun stressTest() = StressOptions() + .iterations(if (isStressTest) 100 else 20) + .invocationsPerIteration(if (isStressTest) 10_000 else 1_000) + .commonConfiguration() + .customize(isStressTest) + .check(this::class) + + private fun > O.commonConfiguration(): O = this + .actorsBefore(if (isStressTest) 3 else 1) + .threads(3) + .actorsPerThread(if (isStressTest) 4 else 2) + .actorsAfter(if (isStressTest) 3 else 0) + .customize(isStressTest) + + override fun extractState(): Any = error("Not implemented") +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/LCStressOptionsDefault.kt b/kotlinx-coroutines-core/jvm/test/LCStressOptionsDefault.kt deleted file mode 100644 index 62ded9f969..0000000000 --- a/kotlinx-coroutines-core/jvm/test/LCStressOptionsDefault.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -package kotlinx.coroutines - -import org.jetbrains.kotlinx.lincheck.* -import org.jetbrains.kotlinx.lincheck.strategy.stress.* -import kotlin.reflect.* - -class LCStressOptionsDefault : StressOptions() { - init { - iterations(100 * stressTestMultiplierCbrt) - invocationsPerIteration(1000 * stressTestMultiplierCbrt) - actorsBefore(if (isStressTest) 3 else 0) - threads(3) - actorsPerThread(if (isStressTest) 3 else 2) - } -} - -fun Options<*,*>.check(testClass: KClass<*>) = LinChecker.check(testClass.java, this) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/ChannelsLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt similarity index 88% rename from kotlinx-coroutines-core/jvm/test/linearizability/ChannelsLCStressTest.kt rename to kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt index 8836fdc7be..fbd5c0d8f3 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/ChannelsLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt @@ -3,7 +3,7 @@ */ @file:Suppress("unused") -package kotlinx.coroutines.linearizability +package kotlinx.coroutines.lincheck import kotlinx.coroutines.* import kotlinx.coroutines.channels.* @@ -11,37 +11,37 @@ import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.RENDEZVOUS import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.selects.* +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.paramgen.* import org.jetbrains.kotlinx.lincheck.verifier.* -import org.junit.* -class RendezvousChannelLCStressTest : ChannelLCStressTestBase( +class RendezvousChannelLincheckTest : ChannelLincheckTestBase( c = Channel(RENDEZVOUS), sequentialSpecification = SequentialRendezvousChannel::class.java ) class SequentialRendezvousChannel : SequentialIntChannelBase(RENDEZVOUS) -class Array1ChannelLCStressTest : ChannelLCStressTestBase( +class Array1ChannelLincheckTest : ChannelLincheckTestBase( c = Channel(1), sequentialSpecification = SequentialArray1RendezvousChannel::class.java ) class SequentialArray1RendezvousChannel : SequentialIntChannelBase(1) -class Array2ChannelLCStressTest : ChannelLCStressTestBase( +class Array2ChannelLincheckTest : ChannelLincheckTestBase( c = Channel(2), sequentialSpecification = SequentialArray2RendezvousChannel::class.java ) class SequentialArray2RendezvousChannel : SequentialIntChannelBase(2) -class UnlimitedChannelLCStressTest : ChannelLCStressTestBase( +class UnlimitedChannelLincheckTest : ChannelLincheckTestBase( c = Channel(UNLIMITED), sequentialSpecification = SequentialUnlimitedChannel::class.java ) class SequentialUnlimitedChannel : SequentialIntChannelBase(UNLIMITED) -class ConflatedChannelLCStressTest : ChannelLCStressTestBase( +class ConflatedChannelLincheckTest : ChannelLincheckTestBase( c = Channel(CONFLATED), sequentialSpecification = SequentialConflatedChannel::class.java ) @@ -51,8 +51,11 @@ class SequentialConflatedChannel : SequentialIntChannelBase(CONFLATED) Param(name = "value", gen = IntGen::class, conf = "1:5"), Param(name = "closeToken", gen = IntGen::class, conf = "1:3") ) -abstract class ChannelLCStressTestBase(private val c: Channel, private val sequentialSpecification: Class<*>) { - @Operation +abstract class ChannelLincheckTestBase( + private val c: Channel, + private val sequentialSpecification: Class<*> +) : AbstractLincheckTest() { + @Operation(promptCancellation = true) suspend fun send(@Param(name = "value") value: Int): Any = try { c.send(value) } catch (e: NumberedCancellationException) { @@ -74,7 +77,7 @@ abstract class ChannelLCStressTestBase(private val c: Channel, private val e.testResult } - @Operation + @Operation(promptCancellation = true) suspend fun receive(): Any = try { c.receive() } catch (e: NumberedCancellationException) { @@ -96,7 +99,7 @@ abstract class ChannelLCStressTestBase(private val c: Channel, private val e.testResult } - @Operation + @Operation(causesBlocking = true) fun close(@Param(name = "closeToken") token: Int): Boolean = c.close(NumberedCancellationException(token)) // TODO: this operation should be (and can be!) linearizable, but is not @@ -113,11 +116,8 @@ abstract class ChannelLCStressTestBase(private val c: Channel, private val // @Operation fun isEmpty() = c.isEmpty - @Test - fun test() = LCStressOptionsDefault() - .actorsBefore(0) - .sequentialSpecification(sequentialSpecification) - .check(this::class) + override fun > O.customize(isStressTest: Boolean): O = + actorsBefore(0).sequentialSpecification(sequentialSpecification) } private class NumberedCancellationException(number: Int) : CancellationException() { diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/LockFreeListLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeListLincheckTest.kt similarity index 83% rename from kotlinx-coroutines-core/jvm/test/linearizability/LockFreeListLCStressTest.kt rename to kotlinx-coroutines-core/jvm/test/lincheck/LockFreeListLincheckTest.kt index 5f91c640a6..4f1bb6ad02 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/LockFreeListLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeListLincheckTest.kt @@ -3,18 +3,17 @@ */ @file:Suppress("unused") -package kotlinx.coroutines.linearizability +package kotlinx.coroutines.lincheck import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.paramgen.* -import org.jetbrains.kotlinx.lincheck.verifier.* -import kotlin.test.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* @Param(name = "value", gen = IntGen::class, conf = "1:5") -class LockFreeListLCStressTest : VerifierState() { +class LockFreeListLincheckTest : AbstractLincheckTest() { class Node(val value: Int): LockFreeLinkedListNode() private val q: LockFreeLinkedListHead = LockFreeLinkedListHead() @@ -43,12 +42,12 @@ class LockFreeListLCStressTest : VerifierState() { private fun Any.isSame(value: Int) = this is Node && this.value == value - @Test - fun testAddRemoveLinearizability() = LCStressOptionsDefault().check(this::class) - override fun extractState(): Any { val elements = ArrayList() q.forEach { elements.add(it.value) } return elements } + + override fun ModelCheckingOptions.customize(isStressTest: Boolean) = + checkObstructionFreedom() } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/LockFreeTaskQueueLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt similarity index 50% rename from kotlinx-coroutines-core/jvm/test/linearizability/LockFreeTaskQueueLCStressTest.kt rename to kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt index de494cc1e6..2a9164e1d7 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/LockFreeTaskQueueLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt @@ -3,19 +3,21 @@ */ @file:Suppress("unused") -package kotlinx.coroutines.linearizability +package kotlinx.coroutines.lincheck import kotlinx.coroutines.* import kotlinx.coroutines.internal.* +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.paramgen.* -import org.jetbrains.kotlinx.lincheck.verifier.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* import org.jetbrains.kotlinx.lincheck.verifier.quiescent.* -import kotlin.test.* @Param(name = "value", gen = IntGen::class, conf = "1:3") -internal abstract class AbstractLockFreeTaskQueueWithoutRemoveLCStressTest protected constructor(singleConsumer: Boolean) : VerifierState() { +internal abstract class AbstractLockFreeTaskQueueWithoutRemoveLincheckTest( + val singleConsumer: Boolean +) : AbstractLincheckTest() { @JvmField protected val q = LockFreeTaskQueue(singleConsumer = singleConsumer) @@ -25,20 +27,24 @@ internal abstract class AbstractLockFreeTaskQueueWithoutRemoveLCStressTest prote @Operation fun addLast(@Param(name = "value") value: Int) = q.addLast(value) - @QuiescentConsistent - @Operation(group = "consumer") - fun removeFirstOrNull() = q.removeFirstOrNull() + override fun > O.customize(isStressTest: Boolean): O = + verifier(QuiescentConsistencyVerifier::class.java) override fun extractState() = q.map { it } to q.isClosed() - @Test - fun testWithRemoveForQuiescentConsistency() = LCStressOptionsDefault() - .verifier(QuiescentConsistencyVerifier::class.java) - .check(this::class) + override fun ModelCheckingOptions.customize(isStressTest: Boolean) = + checkObstructionFreedom() } -@OpGroupConfig(name = "consumer", nonParallel = false) -internal class MCLockFreeTaskQueueWithRemoveLCStressTest : AbstractLockFreeTaskQueueWithoutRemoveLCStressTest(singleConsumer = false) +internal class MCLockFreeTaskQueueWithRemoveLincheckTest : AbstractLockFreeTaskQueueWithoutRemoveLincheckTest(singleConsumer = false) { + @QuiescentConsistent + @Operation(blocking = true) + fun removeFirstOrNull() = q.removeFirstOrNull() +} @OpGroupConfig(name = "consumer", nonParallel = true) -internal class SCLockFreeTaskQueueWithRemoveLCStressTest : AbstractLockFreeTaskQueueWithoutRemoveLCStressTest(singleConsumer = true) \ No newline at end of file +internal class SCLockFreeTaskQueueWithRemoveLincheckTest : AbstractLockFreeTaskQueueWithoutRemoveLincheckTest(singleConsumer = true) { + @QuiescentConsistent + @Operation(group = "consumer") + fun removeFirstOrNull() = q.removeFirstOrNull() +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt similarity index 54% rename from kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt rename to kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt index 9542b5d8de..6e350660ae 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/MutexLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt @@ -2,30 +2,31 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("unused") -package kotlinx.coroutines.linearizability +package kotlinx.coroutines.lincheck import kotlinx.coroutines.* import kotlinx.coroutines.sync.* +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.Operation -import org.jetbrains.kotlinx.lincheck.verifier.* -import org.junit.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -class MutexLCStressTest : VerifierState() { +class MutexLincheckTest : AbstractLincheckTest() { private val mutex = Mutex() @Operation fun tryLock() = mutex.tryLock() - @Operation + @Operation(promptCancellation = true) suspend fun lock() = mutex.lock() @Operation(handleExceptionsAsResult = [IllegalStateException::class]) fun unlock() = mutex.unlock() - @Test - fun test() = LCStressOptionsDefault() - .actorsBefore(0) - .check(this::class) + override fun > O.customize(isStressTest: Boolean): O = + actorsBefore(0) + + override fun ModelCheckingOptions.customize(isStressTest: Boolean) = + checkObstructionFreedom() override fun extractState() = mutex.isLocked } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/SegmentListRemoveLincheckTest.kt similarity index 66% rename from kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt rename to kotlinx-coroutines-core/jvm/test/lincheck/SegmentListRemoveLincheckTest.kt index 5daed99829..5a8d7b475d 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentListRemoveLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/SegmentListRemoveLincheckTest.kt @@ -4,18 +4,16 @@ @file:Suppress("unused") -package kotlinx.coroutines.linearizability +package kotlinx.coroutines.lincheck import kotlinx.coroutines.* import kotlinx.coroutines.internal.* +import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.annotations.* -import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.paramgen.* -import org.jetbrains.kotlinx.lincheck.verifier.* -import org.junit.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* - -class SegmentListRemoveLCStressTest : VerifierState() { +class SegmentListRemoveLincheckTest : AbstractLincheckTest() { private val q = SegmentBasedQueue() private val segments: Array> @@ -29,6 +27,9 @@ class SegmentListRemoveLCStressTest : VerifierState() { segments[index].removeSegment() } + override fun > O.customize(isStressTest: Boolean): O = this + .actorsBefore(0).actorsAfter(0) + override fun extractState() = segments.map { it.logicallyRemoved } @Validate @@ -37,9 +38,6 @@ class SegmentListRemoveLCStressTest : VerifierState() { q.checkAllSegmentsAreNotLogicallyRemoved() } - @Test - fun test() = LCStressOptionsDefault() - .actorsBefore(0) - .actorsAfter(0) - .check(this::class) + override fun ModelCheckingOptions.customize(isStressTest: Boolean) = + checkObstructionFreedom() } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/SegmentQueueLincheckTest.kt similarity index 78% rename from kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt rename to kotlinx-coroutines-core/jvm/test/lincheck/SegmentQueueLincheckTest.kt index 89bf8dfaa4..76a59e39e7 100644 --- a/kotlinx-coroutines-core/jvm/test/linearizability/SegmentQueueLCStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/SegmentQueueLincheckTest.kt @@ -3,18 +3,17 @@ */ @file:Suppress("unused") -package kotlinx.coroutines.linearizability +package kotlinx.coroutines.lincheck import kotlinx.coroutines.* import kotlinx.coroutines.internal.SegmentBasedQueue import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.paramgen.* -import org.jetbrains.kotlinx.lincheck.verifier.* -import org.junit.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* @Param(name = "value", gen = IntGen::class, conf = "1:5") -class SegmentQueueLCStressTest : VerifierState() { +class SegmentQueueLincheckTest : AbstractLincheckTest() { private val q = SegmentBasedQueue() @Operation @@ -40,6 +39,6 @@ class SegmentQueueLCStressTest : VerifierState() { return elements to closed } - @Test - fun test() = LCStressOptionsDefault().check(this::class) + override fun ModelCheckingOptions.customize(isStressTest: Boolean) = + checkObstructionFreedom() } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt new file mode 100644 index 0000000000..84ce773c15 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("unused") +package kotlinx.coroutines.lincheck + +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.* +import org.jetbrains.kotlinx.lincheck.* +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* + +abstract class SemaphoreLincheckTestBase(permits: Int) : AbstractLincheckTest() { + private val semaphore = Semaphore(permits) + + @Operation + fun tryAcquire() = semaphore.tryAcquire() + + @Operation(promptCancellation = true) + suspend fun acquire() = semaphore.acquire() + + @Operation(handleExceptionsAsResult = [IllegalStateException::class]) + fun release() = semaphore.release() + + override fun > O.customize(isStressTest: Boolean): O = + actorsBefore(0) + + override fun extractState() = semaphore.availablePermits + + override fun ModelCheckingOptions.customize(isStressTest: Boolean) = + checkObstructionFreedom() +} + +class Semaphore1LincheckTest : SemaphoreLincheckTestBase(1) +class Semaphore2LincheckTest : SemaphoreLincheckTestBase(2) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt deleted file mode 100644 index 52902f4987..0000000000 --- a/kotlinx-coroutines-core/jvm/test/linearizability/SemaphoreLCStressTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -@file:Suppress("unused") -package kotlinx.coroutines.linearizability - -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.* -import org.jetbrains.kotlinx.lincheck.annotations.Operation -import org.jetbrains.kotlinx.lincheck.verifier.* -import org.junit.* - -abstract class SemaphoreLCStressTestBase(permits: Int) : VerifierState() { - private val semaphore = Semaphore(permits) - - @Operation - fun tryAcquire() = semaphore.tryAcquire() - - @Operation - suspend fun acquire() = semaphore.acquire() - - @Operation(handleExceptionsAsResult = [IllegalStateException::class]) - fun release() = semaphore.release() - - @Test - fun test() = LCStressOptionsDefault() - .actorsBefore(0) - .check(this::class) - - override fun extractState() = semaphore.availablePermits -} - -class Semaphore1LCStressTest : SemaphoreLCStressTestBase(1) -class Semaphore2LCStressTest : SemaphoreLCStressTestBase(2) \ No newline at end of file From 167c44e727688513e56020bb9b3e97f56e70f362 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 30 Nov 2020 05:36:23 -0800 Subject: [PATCH 175/257] Update Knit to 0.2.3 (#2415) --- README.md | 30 +++++++++++++++++++ docs/basics.md | 2 ++ docs/cancellation-and-timeouts.md | 2 ++ docs/channels.md | 6 ++++ docs/compatibility.md | 4 +++ docs/composing-suspending-functions.md | 2 ++ docs/coroutine-context-and-dispatchers.md | 2 ++ docs/debugging.md | 2 ++ docs/exception-handling.md | 4 +++ docs/flow.md | 4 +++ docs/select-expression.md | 6 ++++ docs/shared-mutable-state-and-concurrency.md | 6 ++++ gradle.properties | 2 +- .../kotlinx-coroutines-guava/README.md | 4 +++ integration/kotlinx-coroutines-jdk8/README.md | 4 +++ .../kotlinx-coroutines-slf4j/README.md | 2 ++ kotlinx-coroutines-core/README.md | 8 +++++ kotlinx-coroutines-core/common/README.md | 10 +++++++ kotlinx-coroutines-debug/README.md | 6 ++++ kotlinx-coroutines-test/README.md | 4 +++ .../kotlinx-coroutines-reactive/README.md | 6 ++++ reactive/kotlinx-coroutines-reactor/README.md | 6 ++++ reactive/kotlinx-coroutines-rx2/README.md | 8 +++++ reactive/kotlinx-coroutines-rx3/README.md | 8 +++++ ui/coroutines-guide-ui.md | 6 ++++ 25 files changed, 143 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 77de32bbdc..522c70aa30 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,7 @@ See [Contributing Guidelines](CONTRIBUTING.md). + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html @@ -239,52 +240,81 @@ See [Contributing Guidelines](CONTRIBUTING.md). [Promise.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/kotlin.js.-promise/await.html [promise]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html [Window.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/org.w3c.dom.-window/as-coroutine-dispatcher.html + + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html [_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html [filter]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/filter.html [map]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html + + [Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html + + [select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html + + [Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html [Semaphore]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-semaphore/index.html + + [Dispatchers.setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/kotlinx.coroutines.-dispatchers/set-main.html [TestCoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/index.html + + [DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html + + [CoroutinesTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html + + [MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html + + [CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/await.html + + [ListenableFuture.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/com.google.common.util.concurrent.-listenable-future/await.html + + [Task.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/await.html + + [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 [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 [rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html + + [flux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html [mono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/mono.html + diff --git a/docs/basics.md b/docs/basics.md index 8aca23a18c..bf27c9255f 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -396,6 +396,7 @@ Active coroutines that were launched in [GlobalScope] do not keep the process al + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html @@ -405,6 +406,7 @@ Active coroutines that were launched in [GlobalScope] do not keep the process al [Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html [_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html [CoroutineScope()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html + diff --git a/docs/cancellation-and-timeouts.md b/docs/cancellation-and-timeouts.md index b296bde493..ddd417b911 100644 --- a/docs/cancellation-and-timeouts.md +++ b/docs/cancellation-and-timeouts.md @@ -466,6 +466,7 @@ This example always prints zero. Resources do not leak. + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [cancelAndJoin]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html @@ -479,4 +480,5 @@ This example always prints zero. Resources do not leak. [NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable.html [withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html [withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html + diff --git a/docs/channels.md b/docs/channels.md index d1a4bac05b..608346ab3e 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -682,11 +682,14 @@ delay between elements. + [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [kotlin.coroutines.CoroutineContext.cancelChildren]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/kotlin.coroutines.-coroutine-context/cancel-children.html [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html + + [Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html [SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html [ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html @@ -697,6 +700,9 @@ delay between elements. [ticker]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/ticker.html [ReceiveChannel.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/cancel.html [TickerMode.FIXED_DELAY]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-ticker-mode/-f-i-x-e-d_-d-e-l-a-y.html + + [select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html + diff --git a/docs/compatibility.md b/docs/compatibility.md index 8dafae7293..a9414432ea 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -116,10 +116,14 @@ For the Maven project, a warning can be disabled by passing a compiler flag in y + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html + + [ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html [FlowPreview]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-flow-preview/index.html [ObsoleteCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-obsolete-coroutines-api/index.html [InternalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-internal-coroutines-api/index.html + diff --git a/docs/composing-suspending-functions.md b/docs/composing-suspending-functions.md index 81b6f53c1f..30bcef8bdb 100644 --- a/docs/composing-suspending-functions.md +++ b/docs/composing-suspending-functions.md @@ -422,6 +422,7 @@ Computation failed with ArithmeticException + [async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html @@ -432,4 +433,5 @@ Computation failed with ArithmeticException [GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html + diff --git a/docs/coroutine-context-and-dispatchers.md b/docs/coroutine-context-and-dispatchers.md index 36e049db89..d0c9910432 100644 --- a/docs/coroutine-context-and-dispatchers.md +++ b/docs/coroutine-context-and-dispatchers.md @@ -685,6 +685,7 @@ that should be implemented. + [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html @@ -709,4 +710,5 @@ that should be implemented. [asContextElement]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/java.lang.-thread-local/as-context-element.html [ensurePresent]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/java.lang.-thread-local/ensure-present.html [ThreadContextElement]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/index.html + diff --git a/docs/debugging.md b/docs/debugging.md index 6c846f235d..bffb22cff3 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -98,9 +98,11 @@ For more details see ["Optimization" section for Android](../ui/kotlinx-coroutin + [DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html [CoroutineName]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html [CopyableThrowable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-copyable-throwable/index.html [CopyableThrowable.createCopy]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-copyable-throwable/create-copy.html + diff --git a/docs/exception-handling.md b/docs/exception-handling.md index a3070213d1..b96837bfce 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -505,6 +505,7 @@ The scope is completed + [CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html @@ -519,8 +520,11 @@ The scope is completed [Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job.html [_coroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html [_supervisorScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html + + [actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html [produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html [ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html + diff --git a/docs/flow.md b/docs/flow.md index 4374e7aa86..e119fd418e 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -1929,6 +1929,7 @@ Integration modules include conversions from and to `Flow`, integration with Rea + [delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html [withTimeoutOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout-or-null.html [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html @@ -1942,7 +1943,9 @@ Integration modules include conversions from and to `Flow`, integration with Rea [Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html [ensureActive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html [CancellationException]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html + + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html [_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html [FlowCollector.emit]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow-collector/emit.html @@ -1976,4 +1979,5 @@ Integration modules include conversions from and to `Flow`, integration with Rea [launchIn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.html [IntRange.asFlow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/kotlin.ranges.-int-range/as-flow.html [cancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/cancellable.html + diff --git a/docs/select-expression.md b/docs/select-expression.md index 43fcfd245b..713f5136f7 100644 --- a/docs/select-expression.md +++ b/docs/select-expression.md @@ -537,13 +537,19 @@ Channel was closed + [Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html + + [ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html [ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html [onReceiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/on-receive-or-null.html [SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html [SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html + + [select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html + diff --git a/docs/shared-mutable-state-and-concurrency.md b/docs/shared-mutable-state-and-concurrency.md index 8b83ad0b20..123b6dbdd5 100644 --- a/docs/shared-mutable-state-and-concurrency.md +++ b/docs/shared-mutable-state-and-concurrency.md @@ -525,15 +525,21 @@ have to switch to a different context at all. + [Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html [withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html [CompletableDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-completable-deferred/index.html + + [Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html [Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html [Mutex.unlock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/unlock.html [withLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/with-lock.html + + [actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html [produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html + diff --git a/gradle.properties b/gradle.properties index 1ee04710ca..cb039062f8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin_version=1.4.0 # Dependencies junit_version=4.12 atomicfu_version=0.14.4 -knit_version=0.2.2 +knit_version=0.2.3 html_version=0.6.8 lincheck_version=2.10 dokka_version=0.9.16-rdev-2-mpp-hacks diff --git a/integration/kotlinx-coroutines-guava/README.md b/integration/kotlinx-coroutines-guava/README.md index 4c43317ad6..130cf0a058 100644 --- a/integration/kotlinx-coroutines-guava/README.md +++ b/integration/kotlinx-coroutines-guava/README.md @@ -50,11 +50,15 @@ Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/L + [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html + + [future]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/kotlinx.coroutines.-coroutine-scope/future.html [com.google.common.util.concurrent.ListenableFuture]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/com.google.common.util.concurrent.-listenable-future/index.html [com.google.common.util.concurrent.ListenableFuture.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/com.google.common.util.concurrent.-listenable-future/await.html [kotlinx.coroutines.Deferred.asListenableFuture]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/kotlinx.coroutines.-deferred/as-listenable-future.html + diff --git a/integration/kotlinx-coroutines-jdk8/README.md b/integration/kotlinx-coroutines-jdk8/README.md index 3a204416bf..aebd90f06a 100644 --- a/integration/kotlinx-coroutines-jdk8/README.md +++ b/integration/kotlinx-coroutines-jdk8/README.md @@ -53,12 +53,16 @@ Integration with JDK8 [CompletableFuture] (Android API level 24). + [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html + + [future]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/kotlinx.coroutines.-coroutine-scope/future.html [java.util.concurrent.CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/await.html [java.util.concurrent.CompletionStage.asDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/as-deferred.html [kotlinx.coroutines.Deferred.asCompletableFuture]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/kotlinx.coroutines.-deferred/as-completable-future.html + diff --git a/integration/kotlinx-coroutines-slf4j/README.md b/integration/kotlinx-coroutines-slf4j/README.md index ee5fb320e9..e23d390703 100644 --- a/integration/kotlinx-coroutines-slf4j/README.md +++ b/integration/kotlinx-coroutines-slf4j/README.md @@ -20,5 +20,7 @@ Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html). + [MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html + diff --git a/kotlinx-coroutines-core/README.md b/kotlinx-coroutines-core/README.md index 5fe3298173..bc5587623a 100644 --- a/kotlinx-coroutines-core/README.md +++ b/kotlinx-coroutines-core/README.md @@ -90,6 +90,7 @@ Obsolete and deprecated module to test coroutines. Replaced with `kotlinx-corout + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html @@ -114,12 +115,16 @@ Obsolete and deprecated module to test coroutines. Replaced with `kotlinx-corout [Job.isCompleted]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/is-completed.html [Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html [Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html + + [kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html [kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html [kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html [kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html + + [kotlinx.coroutines.channels.produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html [kotlinx.coroutines.channels.ReceiveChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html [kotlinx.coroutines.channels.ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html @@ -133,8 +138,11 @@ Obsolete and deprecated module to test coroutines. Replaced with `kotlinx-corout [kotlinx.coroutines.channels.ReceiveChannel.poll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/poll.html [kotlinx.coroutines.channels.receiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/receive-or-null.html [kotlinx.coroutines.channels.onReceiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/on-receive-or-null.html + + [kotlinx.coroutines.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html [kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/-select-builder/on-timeout.html + diff --git a/kotlinx-coroutines-core/common/README.md b/kotlinx-coroutines-core/common/README.md index e59392ee66..6712648ae8 100644 --- a/kotlinx-coroutines-core/common/README.md +++ b/kotlinx-coroutines-core/common/README.md @@ -97,6 +97,7 @@ Low-level primitives for finer-grained control of coroutines. + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html @@ -125,12 +126,16 @@ Low-level primitives for finer-grained control of coroutines. [Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html [newCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-coroutine-context.html [DEBUG_PROPERTY_NAME]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-d-e-b-u-g_-p-r-o-p-e-r-t-y_-n-a-m-e.html + + [kotlinx.coroutines.sync.Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html [kotlinx.coroutines.sync.Mutex.lock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/lock.html [kotlinx.coroutines.sync.Mutex.onLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/on-lock.html [kotlinx.coroutines.sync.Mutex.tryLock]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/try-lock.html + + [kotlinx.coroutines.channels.produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html [kotlinx.coroutines.channels.ReceiveChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html [kotlinx.coroutines.channels.ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html @@ -146,9 +151,14 @@ Low-level primitives for finer-grained control of coroutines. [kotlinx.coroutines.channels.ReceiveChannel.poll]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/poll.html [kotlinx.coroutines.channels.receiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/receive-or-null.html [kotlinx.coroutines.channels.onReceiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/on-receive-or-null.html + + [kotlinx.coroutines.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html [kotlinx.coroutines.selects.SelectBuilder.onTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/-select-builder/on-timeout.html + + [kotlinx.coroutines.test.TestCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.test/-test-coroutine-context/index.html + diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index fc9637a9d5..2fd598c6d7 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -264,10 +264,13 @@ More than one file was found with OS independent path 'win32-x86-64/attach_hotsp --> + [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html + + [DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html [DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html [DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html @@ -275,6 +278,9 @@ More than one file was found with OS independent path 'win32-x86-64/attach_hotsp [DebugProbes.printJob]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html [DebugProbes.printScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html [DebugProbes.enableCreationStackTraces]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/enable-creation-stack-traces.html + + [CoroutinesTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html + diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index 6022955254..ee848dbc6e 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -431,6 +431,7 @@ If you have any suggestions for improvements to this experimental API please sha + [Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html @@ -440,8 +441,10 @@ If you have any suggestions for improvements to this experimental API please sha [CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html + + [setMain]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/kotlinx.coroutines.-dispatchers/set-main.html [runBlockingTest]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html [UncompletedCoroutinesError]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-uncompleted-coroutines-error/index.html @@ -454,4 +457,5 @@ If you have any suggestions for improvements to this experimental API please sha [TestCoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-exception-handler/index.html [TestCoroutineScope.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/cleanup-test-coroutines.html [DelayController.cleanupTestCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-delay-controller/cleanup-test-coroutines.html + diff --git a/reactive/kotlinx-coroutines-reactive/README.md b/reactive/kotlinx-coroutines-reactive/README.md index aed262263d..b6466b2da7 100644 --- a/reactive/kotlinx-coroutines-reactive/README.md +++ b/reactive/kotlinx-coroutines-reactive/README.md @@ -32,11 +32,16 @@ Suspending extension functions and suspending iteration: + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html + + [ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.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 @@ -45,6 +50,7 @@ Suspending extension functions and suspending iteration: [org.reactivestreams.Publisher.awaitFirstOrElse]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-first-or-else.html [org.reactivestreams.Publisher.awaitFirstOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-first-or-null.html [org.reactivestreams.Publisher.awaitSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/await-single.html + # Package kotlinx.coroutines.reactive diff --git a/reactive/kotlinx-coroutines-reactor/README.md b/reactive/kotlinx-coroutines-reactor/README.md index cd4a42a34c..7028310621 100644 --- a/reactive/kotlinx-coroutines-reactor/README.md +++ b/reactive/kotlinx-coroutines-reactor/README.md @@ -32,13 +32,18 @@ Conversion functions: + [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html + + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html + + [mono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/mono.html [flux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/flux.html [Flow.asFlux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/kotlinx.coroutines.flow.-flow/as-flux.html @@ -47,6 +52,7 @@ Conversion functions: [kotlinx.coroutines.Deferred.asMono]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/kotlinx.coroutines.-deferred/as-mono.html [kotlinx.coroutines.channels.ReceiveChannel.asFlux]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/kotlinx.coroutines.channels.-receive-channel/as-flux.html [reactor.core.scheduler.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactor/kotlinx.coroutines.reactor/reactor.core.scheduler.-scheduler/as-coroutine-dispatcher.html + # Package kotlinx.coroutines.reactor diff --git a/reactive/kotlinx-coroutines-rx2/README.md b/reactive/kotlinx-coroutines-rx2/README.md index f0fbeb001e..40fe122f89 100644 --- a/reactive/kotlinx-coroutines-rx2/README.md +++ b/reactive/kotlinx-coroutines-rx2/README.md @@ -52,15 +52,22 @@ Conversion functions: + [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html + + [ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html [ReceiveChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/index.html + + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html + + [rxCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-completable.html [rxMaybe]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-maybe.html [rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/rx-single.html @@ -84,6 +91,7 @@ Conversion functions: [kotlinx.coroutines.Deferred.asSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/kotlinx.coroutines.-deferred/as-single.html [kotlinx.coroutines.channels.ReceiveChannel.asObservable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/kotlinx.coroutines.channels.-receive-channel/as-observable.html [io.reactivex.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx2/kotlinx.coroutines.rx2/io.reactivex.-scheduler/as-coroutine-dispatcher.html + # Package kotlinx.coroutines.rx2 diff --git a/reactive/kotlinx-coroutines-rx3/README.md b/reactive/kotlinx-coroutines-rx3/README.md index 3aa73eb969..f9d3c5a86d 100644 --- a/reactive/kotlinx-coroutines-rx3/README.md +++ b/reactive/kotlinx-coroutines-rx3/README.md @@ -49,14 +49,21 @@ Conversion functions: + [CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html [CoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html + + [ProducerScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-producer-scope/index.html + + [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html + + [rxCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-completable.html [rxMaybe]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-maybe.html [rxSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/rx-single.html @@ -77,6 +84,7 @@ Conversion functions: [kotlinx.coroutines.Job.asCompletable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.-job/as-completable.html [kotlinx.coroutines.Deferred.asSingle]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.-deferred/as-single.html [io.reactivex.rxjava3.core.Scheduler.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/io.reactivex.rxjava3.core.-scheduler/as-coroutine-dispatcher.html + # Package kotlinx.coroutines.rx3 diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index c2bbff22a8..60ed3a8afe 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -607,6 +607,7 @@ After delay + [launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html [delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html [Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html @@ -618,15 +619,20 @@ After delay [CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html [async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html [CoroutineStart.UNDISPATCHED]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-u-n-d-i-s-p-a-t-c-h-e-d.html + + [actor]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html [SendChannel.offer]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/offer.html [SendChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/index.html [Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html [Channel.CONFLATED]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/-c-o-n-f-l-a-t-e-d.html + + [kotlinx.coroutines.Dispatchers.JavaFx]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-javafx/kotlinx.coroutines.javafx/kotlinx.coroutines.-dispatchers/-java-fx.html + From 0e67d0a89bc006305e71dbc905cc0a6c4cf562b6 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 30 Nov 2020 07:59:58 -0800 Subject: [PATCH 176/257] Remove outdated Android examples (#2418) These examples are way outdated, unmaintained and cannot be built as is in the most recent Android Studio. There is plenty of high-quality examples in the Web right now, no need to keep (and update Kotlin version) these two --- RELEASE.md | 4 +- bump-version.sh | 2 - .../animation-app/app/build.gradle.kts | 35 ---- .../app/src/main/AndroidManifest.xml | 27 --- .../jetbrains/kotlinx/animation/Animation.kt | 159 --------------- .../kotlinx/animation/MainActivity.kt | 30 --- .../drawable-v24/ic_launcher_foreground.xml | 34 ---- .../res/drawable/ic_launcher_background.xml | 170 ---------------- .../app/src/main/res/layout/activity_main.xml | 47 ----- .../app/src/main/res/layout/content_main.xml | 24 --- .../res/mipmap-anydpi-v26/ic_launcher.xml | 9 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 9 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3056 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5024 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2096 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2858 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4569 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7098 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6464 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10676 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 9250 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 15523 -> 0 bytes .../app/src/main/res/values/colors.xml | 6 - .../app/src/main/res/values/dimens.xml | 3 - .../app/src/main/res/values/strings.xml | 4 - .../app/src/main/res/values/styles.xml | 20 -- .../animation-app/build.gradle.kts | 30 --- .../animation-app/gradle.properties | 28 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 58694 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - .../animation-app/gradlew | 183 ------------------ .../animation-app/gradlew.bat | 103 ---------- .../animation-app/settings.gradle.kts | 5 - .../example-app/.gitignore | 7 - .../example-app/app/build.gradle.kts | 34 ---- .../app/src/main/AndroidManifest.xml | 28 --- .../main/java/com/example/app/MainActivity.kt | 43 ---- .../app/src/main/res/layout/activity_main.xml | 37 ---- .../app/src/main/res/layout/content_main.xml | 25 --- .../app/src/main/res/menu/menu_main.xml | 10 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4208 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2555 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6114 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10056 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10486 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 14696 -> 0 bytes .../app/src/main/res/values/colors.xml | 6 - .../app/src/main/res/values/dimens.xml | 3 - .../app/src/main/res/values/strings.xml | 4 - .../app/src/main/res/values/styles.xml | 20 -- .../example-app/build.gradle.kts | 30 --- .../example-app/gradle.properties | 28 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 58694 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - .../example-app/gradlew | 183 ------------------ .../example-app/gradlew.bat | 103 ---------- .../example-app/settings.gradle.kts | 1 - .../test/examples/swing-example.kt | 39 ---- 62 files changed, 1 insertion(+), 1542 deletions(-) delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/AndroidManifest.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/Animation.kt delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/MainActivity.kt delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/activity_main.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/content_main.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/colors.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/dimens.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/strings.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/styles.xml delete mode 100644 ui/kotlinx-coroutines-android/animation-app/build.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/animation-app/gradle.properties delete mode 100644 ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.jar delete mode 100644 ui/kotlinx-coroutines-android/animation-app/gradle/wrapper/gradle-wrapper.properties delete mode 100755 ui/kotlinx-coroutines-android/animation-app/gradlew delete mode 100644 ui/kotlinx-coroutines-android/animation-app/gradlew.bat delete mode 100644 ui/kotlinx-coroutines-android/animation-app/settings.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/example-app/.gitignore delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/build.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/AndroidManifest.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/java/com/example/app/MainActivity.kt delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/activity_main.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/content_main.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/menu/menu_main.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/values/colors.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/values/dimens.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/values/strings.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/src/main/res/values/styles.xml delete mode 100644 ui/kotlinx-coroutines-android/example-app/build.gradle.kts delete mode 100644 ui/kotlinx-coroutines-android/example-app/gradle.properties delete mode 100644 ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.jar delete mode 100644 ui/kotlinx-coroutines-android/example-app/gradle/wrapper/gradle-wrapper.properties delete mode 100755 ui/kotlinx-coroutines-android/example-app/gradlew delete mode 100644 ui/kotlinx-coroutines-android/example-app/gradlew.bat delete mode 100644 ui/kotlinx-coroutines-android/example-app/settings.gradle.kts delete mode 100644 ui/kotlinx-coroutines-swing/test/examples/swing-example.kt diff --git a/RELEASE.md b/RELEASE.md index b2a08b6757..f6c613a2f8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -18,9 +18,7 @@ To release new `` of `kotlinx-coroutines`: * [`kotlinx-coroutines-test/README.md`](kotlinx-coroutines-test/README.md) * [`coroutines-guide-ui.md`](ui/coroutines-guide-ui.md) * Properties - * [`gradle.properties`](gradle.properties) - * [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties) - * [`ui/kotlinx-coroutines-android/animation-app/gradle.properties`](ui/kotlinx-coroutines-android/animation-app/gradle.properties) + * [`gradle.properties`](gradle.properties) * Make sure to **exclude** `CHANGES.md` from replacements. As an alternative approach you can use `./bump-version.sh old_version new_version` diff --git a/bump-version.sh b/bump-version.sh index 00930cbd49..ae0fc0b02c 100755 --- a/bump-version.sh +++ b/bump-version.sh @@ -20,8 +20,6 @@ update_version "kotlinx-coroutines-core/README.md" update_version "kotlinx-coroutines-debug/README.md" update_version "kotlinx-coroutines-test/README.md" update_version "ui/coroutines-guide-ui.md" -update_version "ui/kotlinx-coroutines-android/example-app/gradle.properties" -update_version "ui/kotlinx-coroutines-android/animation-app/gradle.properties" update_version "gradle.properties" # Escape dots, e.g. 1.0.0 -> 1\.0\.0 diff --git a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts b/ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts deleted file mode 100644 index 517f1f6341..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -plugins { - id("com.android.application") - kotlin("android") - kotlin("android.extensions") -} - -android { - compileSdkVersion = "29" - defaultConfig { - applicationId = "org.jetbrains.kotlinx.animation" - minSdkVersion(14) - targetSdkVersion(29) - versionCode = 1 - versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - implementation("androidx.appcompat:appcompat:1.0.2") - implementation("androidx.constraintlayout:constraintlayout:1.1.3") - implementation("com.google.android.material:material:1.0.0") - implementation("androidx.lifecycle:lifecycle-extensions:2.0.0") - - implementation(kotlin("stdlib-jdk7")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${property("coroutines_version")}") - - testImplementation("junit:junit:4.12") - androidTestImplementation("androidx.test:runner:1.2.0") - androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0") -} diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/AndroidManifest.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/AndroidManifest.xml deleted file mode 100644 index 34d0dd14d5..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/Animation.kt b/ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/Animation.kt deleted file mode 100644 index 88e0baeeb3..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/Animation.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.kotlinx.animation - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.RectF -import android.util.AttributeSet -import android.view.View -import kotlinx.coroutines.* -import kotlinx.coroutines.android.* -import java.util.* - -sealed class AnimatedShape { - var x = 0.5f // 0 .. 1 - var y = 0.5f // 0 .. 1 - var color = Color.BLACK - var r = 0.05f -} - -class AnimatedCircle : AnimatedShape() -class AnimatedSquare : AnimatedShape() - -private val NO_SHAPES = emptySet() - -class AnimationView( - context: Context, attributeSet: AttributeSet -) : View(context, attributeSet), Observer> { - private var shapes = NO_SHAPES - private val paint = Paint() - private val rect = RectF() - - override fun onChanged(shapes: Set?) { - this.shapes = shapes ?: NO_SHAPES - invalidate() - } - - override fun onDraw(canvas: Canvas) { - val scale = minOf(width, height) / 2.0f - shapes.forEach { shape -> - val x = (shape.x - 0.5f) * scale + width / 2 - val y = (shape.y - 0.5f) * scale + height / 2 - val r = shape.r * scale - rect.set(x - r, y - r, x + r, y + r) - paint.color = shape.color - when (shape) { - is AnimatedCircle -> canvas.drawArc(rect, 0.0f, 360.0f, true, paint) - is AnimatedSquare -> canvas.drawRect(rect, paint) - } - } - } -} - -private val rnd = Random() - -class AnimationModel : ViewModel(), CoroutineScope { - - override val coroutineContext = Job() + Dispatchers.Main - - private val shapes = MutableLiveData>() - - fun observe(owner: LifecycleOwner, observer: Observer>) = - shapes.observe(owner, observer) - - fun update(shape: AnimatedShape) { - val old = shapes.value ?: NO_SHAPES - shapes.value = if (shape in old) old else old + shape - } - - fun addAnimation() { - launch { - animateShape(if (rnd.nextBoolean()) AnimatedCircle() else AnimatedSquare()) - } - } - - fun clearAnimations() { - coroutineContext.cancelChildren() - shapes.value = NO_SHAPES - } -} - -private fun norm(x: Float, y: Float) = Math.hypot(x.toDouble(), y.toDouble()).toFloat() - -private const val ACC = 1e-18f -private const val MAX_SPEED = 2e-9f // in screen_fraction/nanos -private const val INIT_POS = 0.8f - -private fun Random.nextColor() = Color.rgb(nextInt(256), nextInt(256), nextInt(256)) -private fun Random.nextPos() = nextFloat() * INIT_POS + (1 - INIT_POS) / 2 -private fun Random.nextSpeed() = nextFloat() * MAX_SPEED - MAX_SPEED / 2 - -suspend fun AnimationModel.animateShape(shape: AnimatedShape) { - shape.x = rnd.nextPos() - shape.y = rnd.nextPos() - shape.color = rnd.nextColor() - var sx = rnd.nextSpeed() - var sy = rnd.nextSpeed() - var time = System.nanoTime() // nanos - var checkTime = time - while (true) { - val dt = time.let { old -> awaitFrame().also { time = it } - old } - if (dt > 0.5e9) continue // don't animate through over a half second lapses - val dx = shape.x - 0.5f - val dy = shape.y - 0.5f - val dn = norm(dx, dy) - sx -= dx / dn * ACC * dt - sy -= dy / dn * ACC * dt - val sn = norm(sx, sy) - val trim = sn.coerceAtMost(MAX_SPEED) - sx = sx / sn * trim - sy = sy / sn * trim - shape.x += sx * dt - shape.y += sy * dt - update(shape) - // check once a second - if (time > checkTime + 1e9) { - checkTime = time - when (rnd.nextInt(20)) { // roll d20 - 0 -> { - animateColor(shape) // wait a second & animate color - time = awaitFrame() // and sync with next frame - } - 1 -> { // random speed change - sx = rnd.nextSpeed() - sy = rnd.nextSpeed() - } - } - } - } -} - -suspend fun AnimationModel.animateColor(shape: AnimatedShape) { - val duration = 1e9f - val startTime = System.nanoTime() - val aColor = shape.color - val bColor = rnd.nextColor() - while (true) { - val time = awaitFrame() - val b = (time - startTime) / duration - if (b >= 1.0f) break - val a = 1 - b - shape.color = Color.rgb( - (Color.red(bColor) * b + Color.red(aColor) * a).toInt(), - (Color.green(bColor) * b + Color.green(aColor) * a).toInt(), - (Color.blue(bColor) * b + Color.blue(aColor) * a).toInt() - ) - update(shape) - } - shape.color = bColor - update(shape) -} diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/MainActivity.kt b/ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/MainActivity.kt deleted file mode 100644 index 756db9bbe6..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/java/org/jetbrains/kotlinx/animation/MainActivity.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.kotlinx.animation - -import androidx.lifecycle.ViewModelProviders -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.content_main.* - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) - - val animationModel = ViewModelProviders.of(this).get(AnimationModel::class.java) - animationModel.observe(this, animationView) - - addButton.setOnClickListener { - animationModel.addAnimation() - } - - removeButton.setOnClickListener { - animationModel.clearAnimations() - } - } -} diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21dbd8..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable/ic_launcher_background.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc538c..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/activity_main.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 8e06e90179..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/content_main.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/content_main.xml deleted file mode 100644 index 2019bb5dc1..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/layout/content_main.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 8bc717e4ad..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 8bc717e4ad..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a2f5908281d070150700378b64a84c7db1f97aa1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 459ca609d3ae0d3943ab44cdc27feef9256dc6d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7098 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 diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8ca12fe024be86e868d14e91120a6902f8e88ac6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6464 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 B0*L?s diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 8e19b410a1b15ff180f3dacac19395fe3046cdec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10676 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 diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 4c19a13c239cb67b8a2134ddd5f325db1d2d5bee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15523 zcmZu&byQSev_3Py&@gnDfPjP`DLFJqiULXtibx~fLnvK>bPOP+(%nO&(%r2fA>H-( zz4z~1>*iYL?tRWZ_k8=?-?=ADTT_`3j}{LAK&YyspmTRd|F`47?v6Thw%7njTB|C^ zKKGc}$-p)u@1g1$=G5ziQhGf`pecnFHQK@{)H)R`NQF;K%92o17K-93yUfN21$b29 zQwz1oFs@r6GO|&!sP_4*_5J}y@1EmX38MLHp9O5Oe0Nc6{^^wzO4l(d z;mtZ_YZu`gPyE@_DZic*_^gGkxh<(}XliiFNpj1&`$dYO3scX$PHr^OPt}D-`w9aR z4}a$o1nmaz>bV)|i2j5($CXJ<=V0%{^_5JXJ2~-Q=5u(R41}kRaj^33P50Hg*ot1f z?w;RDqu}t{QQ%88FhO3t>0-Sy@ck7!K1c53XC+HJeY@B0BH+W}BTA1!ueRG49Clr? z+R!2Jlc`n)zZ?XWaZO0BnqvRN#k{$*;dYA4UO&o_-b>h3>@8fgSjOUsv0wVwlxy0h z{E1|}P_3K!kMbGZt_qQIF~jd+Km4P8D0dwO{+jQ1;}@_Weti;`V}a_?BkaNJA?PXD zNGH$uRwng<4o9{nk4gW z3E-`-*MB=(J%0*&SA1UclA>pLfP4H?eSsQV$G$t!uXTEio7TY9E35&?0M-ERfX4he z{_Hb&AE`T%j8hIZEp@yBVycpvW2!bHrfxbuu6>_i<^9@?ak)9gHU*#bS~}$sGY*Fi z=%P&i3aH%N`b;I~s8{&6uGo$>-`ukQ<8ri(6aH6p_F`Fhdi6HuacwfQn10HVL7Om1 z4aZpjatkbgjp$L5Mceab#G#C)Hr{^W|TJX~?B3@2buj0;kfuNTf4c3*Au~O^aj=W2$j^4okeCxh#lwexN@eam-u4dNz zN2NIuIM4566{T&^k%4ftShcPk#=im-zXm>QWqH^0>A@?MqlDZCZ@8Wi*@tvhn5p<} zRwFm@gz|WZp91S5Z{}tB^e9|FBg(~Ik+?&_53J6ye_QQOSJ*846~H%s#LD}|O9v9H z1fLrrgoPo_&bs}eqEr}2en3iqAcP^>YsKiez$5-6m6(#3ZZ$@M5Ck=_Vv`QA>1A*v z3w-nJ_;5Nc(0_%`kG91#sotIlhO!*5#|yg+Gx{V;0ty`*=Y9=jCh$l*=fE(~t}%R# zc}iNpO)OZX`P=leQY^?^DF1w%FJh>Dkp}-o5Ig|2!6^E>|W|zc~W7gF;MtxX7 zV~UjQNsUC$EYXpN?~o{83D2c*0~7;Tm~%FRTAnnt3ln{?DcLZ=NsBY|JxwUA-6K3V zP&#|9t#a}Q4{Sg{6v-OmjJBkCh>m)8vLNm4lStMUT$)FZeJG05A)px&o3H)5oAl9= z31@?HyCriHcCDnt628BFN+T;U69Wl#itfvqIDBydMvOJO0Zl?go$cfG5>TK75CMj3 zakLaH3=&J0e}Xmqlav$S0>E@_Yo_V~3SiiXrw)$&!XhrHCDQ%P1BHPusuKr0LthAB zg)mDrLy>2*yevMMOQe6fZ|)%PEb!lC^*9yaX9UMy7-v!fSICssTR|wML0Ic2BhKAq z3I1X~ z7^_!M&;6Z9?br3#HU_&kfJ~%botXQkC1v<}ZZxN5q-T)|Sb2cW3WYUBbDZ`TH{!*^ zrmAeRM+(QI>D+?}guZ+dH*X)@^!O|oL69&Avbtw2^M3HP(+2kV{O$^3BN1RLfrC8nwz7=VhBR%>!;7WR<~;34B_j3A{>^@e@H+Q! zL=UNr1(JvKAQLKT0b}EMn|QUWtY>!>8-t@fVj_&`~gGd{_aPy5W>0u5L$zrsU^rBO=i$`#Xd*>kh)lPf}A znNXSEl`+HlhXtylgS9(#N02A=zVV?#OF?)Gr>(HszVa+1*2VG@qYttJuXaBlzP`Pb zX)ueu?s&}R>xI#^*r4gR?tMFi!_eeKlIM5g)Nk)Y^h=ZCR**xY>$E5knctRrq!zw? zX{2|hwR9LXTY1)pTlKg7U4_ej{dcj2{!+1sZ6<@9^?mn)=37V)DIAvS(}S`IgFO!6 zn({?nYw`Z-@jvt@!q|5z?TI3(dx^1szSn%azAwp>N#fk^kt|=MejKtacAs@Rdku#zT>9$s z=m7ek)`=O7hO2n+2Uj$QUs&2EIqycF{(L9Y#^IyxXA%R@ z&j`VAprIV~d!pH-7~zA+bjwVn3kOB3;rlg{nr&wHV12N}g^i>Upls~=z`VX>9HQ#= zTu&luVb@_Lkz63&&^_M!6(-2^0?GCAX9XKp{O={pd|AlIMGriX6s_Jy8_q9|{5jLc zxd1aj_ucE7Vcti#$r!s~w~W=XpaLQ}#mX`apR7^n9-d3?O+adJYr*L;{c)x@REewM@vZN0njS3iE$88KHPWAkWt((OUMherUnPm?i&8@!9E@ zUW^$%CpdruZR0ohzUq-XQ$KEIB8Sjgs1+wKSUH&Y;=ee%E&O$X18{&979d~K2uJW` zd*8awHCXb;Q>4z$B|sPNv+Zd__f6&@KmS+L`z3H1x+x|Xs7-N-iw|1C=QiJdU)f~z z{vO4hpP`0MyqmwIHN=l?jSq>OKG6CEC#O`*blP`?>)CUWj5j1cB>%6N7;`kfZ1iQV zam~SDB?{uyp^=vF_u|=8xn3S)L;wF8ZRZV{bezM-EH;MC91JQZ{KcZZ$IWJUy?SJGeGUWm6PeuO8-K2|hD~p;Ls~9Y-4lE+?|bF)XaNKUNX(K7 zBQk0Z{n>hrH-CA`bTr$6z0n@Cn9EL$XZ3=X7NopjcI=;z<(X7-oEmK}BId=PxX*!b7Q6oL@ufd%eEPc`_la(}WkT zKe?-YJWn^6b$^{dhdJZ)I!Kn6c}iw%o5mLDyvM7qJZbkGG?zLU;M|W;Wis|A;SuY3{_X53`+>9g^B%O4b{;^t$^;{oKHbo*CY%u91 zp#2d8Pg=I0&UX{qwr=y=o_^BLdk=KYH$=Z8+k|p8V5`ph~3b^{^NnL4m_+4zx( zeoTt@f<$DmsB1}o%R1Hx`ToPuBl+P6cb-?uF{1!z-2WvdR4+vJ*SYTic5@gwnzu%e zD!HF^X=$ha^#1hi*@~^nDL!HQ;MC&e+6=onaJgm-J-+|>PpmU=SIe?EQE5vJiqziw z*K=Z%bWZz_we!qiFqE`I?#$yozNxIE7Ei;csv>++r*?)0bozFpF&oLh94u z-2c2L`5BarP7l>87|f)vxaT*9(!Q`2xBMZ&^JVj-|1)Tg!6OW=lk=w zLwVlr!*<(l*L$a?ox3+%!~UIj3Ej@KD;W>1E_c)1szDi93BC;0K?drOQ>@$yi|DtT zSir}!Yx>znf&b0KS;Lk7VKPDF@e>(qQr0%SNcGQd(p9StjqJ`QSW&c{ggF?5{d22w zlkX%JTUq`;(3WSH+)WHl%qlF)iNG_?}K?ZM3cS7#u5v zZ!apx4Apv=PWsn}eD%MI#=KA)OlNy0)l@~D^1;NC5k@|OPW3wt>WNYDN+8~+gM%E! z$ z`Olr0;eytiK&~O*ps%KV?2vq+DhuRh*!6Ilzu>A;iMe9 zI?zug9nT9CI_o)O}KF_I_U z_Cswu{)3pCYgw{eOt#E?UCqBwkAugSl>5 zX?G=Ci(Lo+r3suuJezyQyDvw*<1b{rx*&ZaY2HlJ>k{Qc%IZeU43pQXw4mh!4I5>l zZ@4$uxaPY#!*IhL4Hctn#!n#S+SiPcZP_PTd5fXf1exhFi5zf3kl`UcW2RUk)F2oF z_ogN`{03PiseQR;fa#{Uy;jeNlJ0Sle`~;ZYhLjkuy>a^!Z_nR~`$&F?NVuIE3HX;i zD82snwlwPb`7yE)ZA_Ndmq5zuSO1{{1}(d9u4#!Fl_|eOuxKBwOfQ*tG`VjCV$-WF zxi0c&+w}Z)rqz{%f46@`ADPdGm#x)+zpT+gyfDi;_P zR{#Ta`Mzd=putKO@5lQJO*aNy(i?}Ltwy^Z;69f|eqi#UCI1$vL!+(#mi?dK`OL$! z3jQnx$_$+Li2<__CL@Wuk4^J7-!n3j2I4N8e#=qpir+iEQcrn3`B4yNOd1BBLEni<(tdRWE>m0I^ zt(^*Td+S3}$5rOzXy=MW>%#MN_qy%5St!>HrGZ~Fq1WKw-&kv@2TrCcPCPzY%2aO- zN?7@+$4?&qA|uv{QHuV)O9haZpG7Jx2f%D)7J@oWTxJ#E_YSq_6qT1tomOD?02(1otT{Hk8{?g(944>h4f% zOJ8tzjecV{x2uWde&6oAP)*({ zFkW0Q%gdI*9@W)oKO65DgP<3F_BIKvRXLAR?Z61&0g2TR6mEZ7OZK?dP7zukdg?s_tNZeuOsh^e1Tmdlz5rIg?LcK|%aQ1FsSDv#W0EnHd z9M)p;gAL_R~Z5cojTdwy+qDsd6R01Vtxmq&FhfPz{wxmB$${zW~z@{Ro_ zK#y5^KqIp!#@or>GD`c+aZ(PV1=`Eo1?a55p6a*WepFgxvmp!^2518YEU-;{F}fLr zD~)=S0m=+px3TUN8-El}Xb}{2ET*_i3-|WlY@V7vr6#&cOr*+oS9?GF?@)K6op>>o z4af0@%KwaLr`{3P&)474<3rDMsd!IM-bepWfhfuMmJt}#0%PgDSx*q(s0m%ZFgWTj zwwvH%2!(i9{RHX~FVUB5qHvF{+ZF}+(bZVPG1)a*Ph>KV;cYNK^aB@R#dS~&`^60V zn2Z24Y{{djzK33}t@q%!v5k)u7jAXB_H{#4Ut2 z1}0j5$RXcTyfazqL9=^Qe%GL`G)=!lirv7AgVRf^=XyEM&kiOe_%JD!O?sXK&hrDo zF}m9B68im!oGshuZluy2H#T$`XPZQu@zf;(nBCZB-cjQ&w*p@Tm_$pe^MTN3EauI) zJG&G^H-4S|1OCd#@A6jO+IcAXG#5M-d9E!^YNmV7Z(=F^?8bfrYf&mLMnRd_22&Q} z2*msbLsrI!XPeOK@|V?n>`kNC`8eSFmekELLr|!-wQRltxZnuRedup<7VflowJ+gC z)F}P6lUSsh^B41?=~0*68YA6z63lKG`W$@{GV!cC2FCl0s<7yz6!3JWoBbUDTgpg% z4VNUk%xblMy7PjLF2We*3XY7K*N(*9Yx!_M zjU$&JXLiNxaTzoa&k@NSbzbLJTn$6bu6SPWYx)Zc1Li~Lqj($GuWsA#;zg85eH{yx zz3IIOea3A4QFGmJCfn7N_d$8a77j+T^W}Sr%0XdVLFf&zJ$s^D5Vrc!iV&GXyb5*A z6mG8d*6EDN7a;=dgVjYI--~4@Fe{{fcJ4B|;_Qg~&%6#?I(?X_$S4rDw{=>=8iZS=M^I#EF!m zXn%K_xXWwmm7R40LKXPo6ZzNZfN1-$S6RuVU=JlC|3#Xjo-%ebJvvC4n%IM)Q8NDh zGXd)L;ay_JMozc^mU*Uifnp=#+if>LD*O9MV#@wB1l``z|tlu(7PJqS6rm)0@ zJzP50{0Vpa`_?92oB;*i(?i225a6tZgT+9Dg?vTh)N4OKA~(c8{$8-ZKz=mb@$4IT9g8>;k11WIT+Y=%Z})`y#OJ zK-~rlEy!T%0h!Qo+jjPF2RQz2Z^B;dbvYg2JS`+@D~OWH{2-EEs^BdnuJskh>CKeT z1b;%8dU6QU%i@z?^6Q-{XESe^qRiw`ka+k!d-{c%&lXM}vCX^T=|?|;t6r?N*h-W4 z?o4Hy%BWqW+5=+md#5^8|49zjM zon_Do@rhzZ4XAb}-m|bMH$Vg<;^Bo6A8cfhUQ>|wFk~j(`>1NgD3sTg)He1pWrUj9WZ8R(Wn5Rr zhc&dXvv_m%HrwwHo9l_))NgdVUff%d&@4^$Pc=MDZdZ^xHL$KX^ z7W1{3UJ%>9v$W{Y3>vBvflE-soDj8{`>#F|8Z$EF%lN$NylORTn5JsI4mTMHWd*%- z2sD(RO(H-&i8&Ge)5i12slI5VekYCZ)s8rv&_)194;vKY2m8DIC2{4<&xTM3HHxwT zd(42n)gCJ$O4I|8sJq07#0U7Yk7PjPK&bMdy-5b)OdhSsBo^|IB_H43@&F@tpdJR0 z#~)=UJdP|=)O{0(rVZnjbTtwHV^}&kfLJQP@R6rda;K;O>9J9bnW$BgbzOZ8aO{D8 zPuJ%=Nqg~rdzk-IW0ZC5I%cc;ek5~=lDXl4?gMOQQ!KE5Aq$9qeGFM6jFP;Xy6)%N zjg{q(E6fnF02P3L*tutbHRR-gyYK3g^y9H?GMtIs;ojG zY~3*C>qD)(8jz}89w|xfb7L`^d>AG#%D-uq=qz}(o9kzzrx0LSBX90ykr*5oM+YmoTRWe+Cj6aq^xnWRymLmE>krCpoC9K%2LT0aK0Y< zt@kUUrrj1WL9rmBB8B;WXqg-BztOiUZX-!`*a&-75+!WZ!R0OPiZz?w`Of4q#+(;m z`${Ea6GnTCY3`V2R8w*}knf)*`RA@(8k{Lp4VP;<+ z9O_z0_{3=HcVi z5)&QGEB_&$)mu@)(Z8zuw#>Gc6C>^O-FUZEo;TO1@$>-xu%`v`tMS3V-8R1pb5w&zP%&rAP2*5h z$k{jqReFXCJhJ?-{x(2j5gH_zQ>;#Ec*@bUqF0u}XB09+U-K}+jQd>)k#AOkr6M8x zHyhrfJ`99@Vzr_B@*p@`DxeJ#`jimavZ9ZV%v{mO0!%9$TY(f%_}BU~3R%QxmSdD1 z2Bp45R0C=8qtx-~+oULrzCMHMof!&H<~~>BhOu9t%ti7ERzy&MfeFI`yIK^$C)AW3 zNQRoy0G}{Z0U#b~iYF^Jc^xOlG#4#C=;O>}m0(@{S^B2chkhuBA^ur)c`E;iGC9@z z7%fqif|WXh26-3;GTi8YpXUOSVWuR&C%jb}s5V4o;X~?V>XaR)8gBIQvmh3-xs)|E z8CExUnh>Ngjb^6YLgG<K?>j`V4Zp4G4%h8vUG^ouv)P!AnMkAWurg1zX2{E)hFp5ex ziBTDWLl+>ihx>1Um{+p<{v-zS?fx&Ioeu#9;aON_P4|J-J)gPF2-0?yt=+nHsn^1G z2bM#YbR1hHRbR9Or49U3T&x=1c0%dKX4HI!55MQv`3gt5ENVMAhhgEp@kG2k+qT|<5K~u`9G7x z?eB%b2B#mq)&K}m$lwDv|MU~=Y(D2jO{j*Box$GUn=$90z6O^7F?7pn=P;{r4C8qa zv1n*5N7uIvTn`8$>}(74>Oqk=E7){#pHUFd5XRJ5ObMhqODTa}=V0;+a(7JZR-4<3 zBTvsqRwLh?*ZF)JWsWOkEq7*XMQ!G3Rmkdh7ZbM#v1~?jt((e2y}u}Ky>1qa&Y7m@ zveIzH@?5Gexr79*?sbZGkVS;s1U<7D(%~7HjAmzj$aDYv_FGl5JX@LW8>w=HCDl6W z%?rsr0)bErYJ5G1v&zjr{8=lW)ZYcstgZAuL}!0~8HAcgOm@nJ9cvOOtL@)Fpl2Dr z8876Lt<|1eF88Jx#C*XyGI)C5z_o!Os!t=Xy0$Kj^4fG1pb@16%g z+<)zJ1n1QO78g#$3yHj+(Smv`HW5y_-PP
{h2A1UXMG-c%hMvHLbF6t}G>KA)H# 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 BIP(Ai diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/colors.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/colors.xml deleted file mode 100644 index 9ad7e369a6..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #3f51b5 - #303F9F - #FF4081 - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/dimens.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/dimens.xml deleted file mode 100644 index 59a0b0c4f5..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ - - 16dp - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/strings.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/strings.xml deleted file mode 100644 index cd3f467bcd..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - Animation - Settings - diff --git a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/styles.xml b/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/styles.xml deleted file mode 100644 index 545b9c6d2c..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - -