diff --git a/CHANGES.md b/CHANGES.md index c9ce6b27f8..acf2d2c28b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,19 @@ # Change log for kotlinx.coroutines +## Version 1.7.0-RC + +* Kotlin version is updated to 1.8.20. +* Atomicfu version is updated to 0.20.2. +* `JavaFx` version is updated to 17.0.2 in `kotlinx-coroutines-javafx` (#3671). +* `previous-compilation-data.bin` file is removed from JAR resources (#3668). +* `CoroutineDispatcher.asExecutor()` runs tasks without dispatching if the dispatcher is unconfined (#3683). Thanks @odedniv! +* `SharedFlow.toMutableList` lint overload is undeprecated (#3706). +* `Channel.invokeOnClose` is promoted to stable API (#3358). +* Improved lock contention in `Dispatchers.Default` and `Dispatchers.IO` during the startup phase (#3652). +* Fixed a bug that led to threads oversubscription in `Dispatchers.Default` (#3642). +* Fixed a bug that allowed `limitedParallelism` to perform dispatches even after the underlying dispatcher was closed (#3672). +* Restored binary compatibility of previously experimental `TestScope.runTest(Long)` (#3673). + ## Version 1.7.0-Beta ### Core API significant improvements diff --git a/README.md b/README.md index e709730e19..283afb85ba 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ [![Kotlin Stable](https://kotl.in/badges/stable.svg)](https://kotlinlang.org/docs/components-stability.html) [![JetBrains official 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://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0-Beta)](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0-Beta) -[![Kotlin](https://img.shields.io/badge/kotlin-1.8.10-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0-RC)](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0-RC) +[![Kotlin](https://img.shields.io/badge/kotlin-1.8.20-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 the Kotlin `1.8.10` release. +This is a companion version for the Kotlin `1.8.20` release. ```kotlin suspend fun main() = coroutineScope { @@ -85,7 +85,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.7.0-Beta + 1.7.0-RC ``` @@ -93,7 +93,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.8.10 + 1.8.20 ``` @@ -103,7 +103,7 @@ Add dependencies (you can also add other modules that you need): ```kotlin dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-Beta") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC") } ``` @@ -112,10 +112,10 @@ And make sure that you use the latest Kotlin version: ```kotlin plugins { // For build.gradle.kts (Kotlin DSL) - kotlin("jvm") version "1.8.10" + kotlin("jvm") version "1.8.20" // For build.gradle (Groovy DSL) - id "org.jetbrains.kotlin.jvm" version "1.8.10" + id "org.jetbrains.kotlin.jvm" version "1.8.20" } ``` @@ -133,7 +133,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as a dependency when using `kotlinx.coroutines` on Android: ```kotlin -implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0-Beta") +implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0-RC") ``` This gives you access to the Android [Dispatchers.Main] @@ -168,7 +168,7 @@ In common code that should get compiled for different platforms, you can add a d ```kotlin commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-Beta") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC") } } ``` @@ -180,7 +180,7 @@ Platform-specific dependencies are recommended to be used only for non-multiplat #### JS Kotlin/JS version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.7.0-Beta) +[`kotlinx-coroutines-core-js`](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.7.0-RC) (follow the link to get the dependency declaration snippet) and as [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) NPM package. #### Native diff --git a/build.gradle b/build.gradle index e65558b151..0d17d9dd7b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,10 @@ import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.dokka.gradle.DokkaTaskPartial @@ -361,3 +364,15 @@ allprojects { subProject -> tasks.named("dokkaHtmlMultiModule") { pluginsMapConfiguration.set(["org.jetbrains.dokka.base.DokkaBase": """{ "templatesDir": "${projectDir.toString().replace('\\', '/')}/dokka-templates" }"""]) } + +if (CacheRedirector.enabled) { + def yarnRootExtension = rootProject.extensions.findByType(YarnRootExtension.class) + if (yarnRootExtension != null) { + yarnRootExtension.downloadBaseUrl = CacheRedirector.maybeRedirect(yarnRootExtension.downloadBaseUrl) + } + + def nodeJsExtension = rootProject.extensions.findByType(NodeJsRootExtension.class) + if (nodeJsExtension != null) { + nodeJsExtension.nodeDownloadBaseUrl = CacheRedirector.maybeRedirect(nodeJsExtension.nodeDownloadBaseUrl) + } +} diff --git a/buildSrc/src/main/kotlin/CacheRedirector.kt b/buildSrc/src/main/kotlin/CacheRedirector.kt index bcadd7364e..c0d835159c 100644 --- a/buildSrc/src/main/kotlin/CacheRedirector.kt +++ b/buildSrc/src/main/kotlin/CacheRedirector.kt @@ -26,31 +26,33 @@ private val mirroredUrls = listOf( "https://dl.google.com/dl/android/studio/ide-zips", "https://dl.google.com/go", "https://download.jetbrains.com", + "https://github.com/yarnpkg/yarn/releases/download", "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/dev", "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/eap", + "https://nodejs.org/dist", "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://repo1.maven.org/maven2", "https://services.gradle.org", "https://www.exasol.com/artifactory/exasol-releases", + "https://www.jetbrains.com/intellij-repository/nightly", + "https://www.jetbrains.com/intellij-repository/releases", + "https://www.jetbrains.com/intellij-repository/snapshots", "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( @@ -115,4 +117,13 @@ object CacheRedirector { fun Project.configure() { checkRedirect(repositories, displayName) } + + @JvmStatic + fun maybeRedirect(url: String): String { + if (!cacheRedirectorEnabled) return url + return URI(url).maybeRedirect()?.toString() ?: url + } + + @JvmStatic + val isEnabled get() = cacheRedirectorEnabled } diff --git a/buildSrc/src/main/kotlin/Java9Modularity.kt b/buildSrc/src/main/kotlin/Java9Modularity.kt index 27f1bd38cc..ccd7ef33dd 100644 --- a/buildSrc/src/main/kotlin/Java9Modularity.kt +++ b/buildSrc/src/main/kotlin/Java9Modularity.kt @@ -143,6 +143,8 @@ object Java9Modularity { attributes("Multi-Release" to true) } from(compileJavaModuleInfo) { + // Include **only** file we are interested in as JavaCompile output also contains some tmp files + include("module-info.class") into("META-INF/versions/9/") } } diff --git a/gradle.properties b/gradle.properties index d31f646d0e..966a90daf6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,14 @@ # # Kotlin -version=1.7.0-Beta-SNAPSHOT +version=1.7.0-RC-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.8.10 +kotlin_version=1.8.20 # Dependencies junit_version=4.12 junit5_version=5.7.0 -atomicfu_version=0.20.0 +atomicfu_version=0.20.2 knit_version=0.4.0 html_version=0.7.2 lincheck_version=2.16 @@ -20,7 +20,7 @@ reactor_version=3.4.1 reactive_streams_version=1.0.3 rxjava2_version=2.2.8 rxjava3_version=3.0.2 -javafx_version=11.0.2 +javafx_version=17.0.2 javafx_plugin_version=0.0.8 binary_compatibility_validator_version=0.12.0 kover_version=0.6.1 @@ -30,7 +30,7 @@ jna_version=5.9.0 # Android versions android_version=4.1.1.4 androidx_annotation_version=1.1.0 -robolectric_version=4.4 +robolectric_version=4.9 baksmali_version=2.2.7 # JS diff --git a/integration-testing/gradle.properties b/integration-testing/gradle.properties index b8485eaeb2..4644d2346a 100644 --- a/integration-testing/gradle.properties +++ b/integration-testing/gradle.properties @@ -1,5 +1,5 @@ -kotlin_version=1.8.10 -coroutines_version=1.7.0-Beta-SNAPSHOT +kotlin_version=1.8.20 +coroutines_version=1.7.0-RC-SNAPSHOT asm_version=9.3 kotlin.code.style=official diff --git a/integration-testing/src/mavenTest/kotlin/MavenPublicationMetaInfValidator.kt b/integration-testing/src/mavenTest/kotlin/MavenPublicationMetaInfValidator.kt new file mode 100644 index 0000000000..8ed2b823f7 --- /dev/null +++ b/integration-testing/src/mavenTest/kotlin/MavenPublicationMetaInfValidator.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.validator + +import org.junit.Test +import org.objectweb.asm.* +import org.objectweb.asm.ClassReader.* +import org.objectweb.asm.ClassWriter.* +import org.objectweb.asm.Opcodes.* +import java.util.jar.* +import kotlin.test.* + +class MavenPublicationMetaInfValidator { + + @Test + fun testMetaInfCoreStructure() { + val clazz = Class.forName("kotlinx.coroutines.Job") + JarFile(clazz.protectionDomain.codeSource.location.file).checkMetaInfStructure( + setOf( + "MANIFEST.MF", + "kotlinx-coroutines-core.kotlin_module", + "com.android.tools/proguard/coroutines.pro", + "com.android.tools/r8/coroutines.pro", + "proguard/coroutines.pro", + "versions/9/module-info.class", + "kotlinx_coroutines_core.version" + ) + ) + } + + @Test + fun testMetaInfAndroidStructure() { + val clazz = Class.forName("kotlinx.coroutines.android.HandlerDispatcher") + JarFile(clazz.protectionDomain.codeSource.location.file).checkMetaInfStructure( + setOf( + "MANIFEST.MF", + "kotlinx-coroutines-android.kotlin_module", + "services/kotlinx.coroutines.CoroutineExceptionHandler", + "services/kotlinx.coroutines.internal.MainDispatcherFactory", + "com.android.tools/r8-from-1.6.0/coroutines.pro", + "com.android.tools/r8-upto-3.0.0/coroutines.pro", + "com.android.tools/proguard/coroutines.pro", + "proguard/coroutines.pro", + "versions/9/module-info.class", + "kotlinx_coroutines_android.version" + ) + ) + } + + private fun JarFile.checkMetaInfStructure(expected: Set) { + val actual = HashSet() + for (e in entries()) { + if (e.isDirectory() || !e.realName.contains("META-INF")) { + continue + } + val partialName = e.realName.substringAfter("META-INF/") + actual.add(partialName) + } + + if (actual != expected) { + val intersection = actual.intersect(expected) + val mismatch = actual.subtract(intersection) + expected.subtract(intersection) + fail("Mismatched files: " + mismatch) + } + + close() + } +} diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 2a6dbbc64f..7693966a46 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -312,12 +312,6 @@ static void configureJvmForLincheck(task, additional = false) { task moreTest(dependsOn: [jvmStressTest, jvmLincheckTest, jvmLincheckTestAdditional]) check.dependsOn moreTest -tasks.jvmLincheckTest { - kover { - enabled = false // Always disabled, lincheck doesn't really support coverage - } -} - def commonKoverExcludes = ["kotlinx.coroutines.debug.*", // Tested by debug module "kotlinx.coroutines.channels.ChannelsKt__DeprecatedKt.*", // Deprecated @@ -326,6 +320,9 @@ def commonKoverExcludes = ] kover { + instrumentation { + excludeTasks.add("jvmLincheckTest") // Always disabled, lincheck doesn't really support coverage + } filters { classes { excludes.addAll(commonKoverExcludes) diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 2950ed9814..00c0ec870f 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -208,7 +208,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren assert { state.isCompleting } // consistency check -- must be marked as completing val proposedException = (proposedUpdate as? CompletedExceptionally)?.cause // Create the final exception and seal the state so that no more exceptions can be added - var wasCancelling = false // KLUDGE: we cannot have contract for our own expect fun synchronized + val wasCancelling: Boolean val finalException = synchronized(state) { wasCancelling = state.isCancelling val exceptions = state.sealLocked(proposedException) diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index d6f752c740..b14e61bbd2 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -105,33 +105,40 @@ public interface SendChannel { * If the channel is closed already, the handler is invoked immediately. * * The meaning of `cause` that is passed to the handler: - * * `null` if the channel was closed or cancelled without the corresponding argument - * * the cause of `close` or `cancel` otherwise. + * - `null` if the channel was closed normally without the corresponding argument. + * - Instance of [CancellationException] if the channel was cancelled normally without the corresponding argument. + * - The cause of `close` or `cancel` otherwise. * - * Example of usage (exception handling is omitted): + * ### Execution context and exception safety * + * The [handler] is executed as part of the closing or cancelling operation, and only after the channel reaches its final state. + * This means that if the handler throws an exception or hangs, the channel will still be successfully closed or cancelled. + * Unhandled exceptions from [handler] are propagated to the closing or cancelling operation's caller. + * + * Example of usage: * ``` - * val events = Channel(UNLIMITED) + * val events = Channel(UNLIMITED) * callbackBasedApi.registerCallback { event -> * events.trySend(event) + * .onClosed { /* channel is already closed, but the callback hasn't stopped yet */ } * } * - * val uiUpdater = launch(Dispatchers.Main, parent = UILifecycle) { - * events.consume {} - * events.cancel() + * val uiUpdater = uiScope.launch(Dispatchers.Main) { + * events.consume { /* handle events */ } * } - * + * // Stop the callback after the channel is closed or cancelled * events.invokeOnClose { callbackBasedApi.stop() } * ``` * - * **Note: This is an experimental api.** This function may change its semantics, parameters or return type in the future. + * **Stability note.** This function constitutes a stable API surface, with the only exception being + * that an [IllegalStateException] is thrown when multiple handlers are registered. + * This restriction could be lifted in the future. * - * @throws UnsupportedOperationException if the underlying channel doesn't support [invokeOnClose]. + * @throws UnsupportedOperationException if the underlying channel does not support [invokeOnClose]. * Implementation note: currently, [invokeOnClose] is unsupported only by Rx-like integrations * * @throws IllegalStateException if another handler was already registered */ - @ExperimentalCoroutinesApi public fun invokeOnClose(handler: (cause: Throwable?) -> Unit) /** @@ -388,7 +395,7 @@ public interface ReceiveChannel { * The successful result represents a successful operation with a value of type [T], for example, * the result of [Channel.receiveCatching] operation or a successfully sent element as a result of [Channel.trySend]. * - * The failed result represents a failed operation attempt to a channel, but it doesn't necessary indicate that the channel is failed. + * The failed result represents a failed operation attempt to a channel, but it doesn't necessarily indicate that the channel is failed. * E.g. when the channel is full, [Channel.trySend] returns failed result, but the channel itself is not in the failed state. * * The closed result represents an operation attempt to a closed channel and also implies that the operation has failed. diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index b65340a836..2f3de1565e 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -319,8 +319,8 @@ private class StateFlowImpl( 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 + var curSequence: Int + var curSlots: Array? // benign race, we will not use it synchronized(this) { val oldState = _state.value if (expectedState != null && oldState != expectedState) return false // CAS support diff --git a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt index d263d61227..5dcf0d01ed 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/AbstractSharedFlow.kt @@ -41,7 +41,7 @@ internal abstract class AbstractSharedFlow> : Sync @Suppress("UNCHECKED_CAST") protected fun allocateSlot(): S { // Actually create slot under lock - var subscriptionCount: SubscriptionCountStateFlow? = null + val subscriptionCount: SubscriptionCountStateFlow? val slot = synchronized(this) { val slots = when (val curSlots = slots) { null -> createSlotArray(2).also { slots = it } @@ -72,7 +72,7 @@ internal abstract class AbstractSharedFlow> : Sync @Suppress("UNCHECKED_CAST") protected fun freeSlot(slot: S) { // Release slot under lock - var subscriptionCount: SubscriptionCountStateFlow? = null + val subscriptionCount: SubscriptionCountStateFlow? val resumes = synchronized(this) { nCollectors-- subscriptionCount = _subscriptionCount // retrieve under lock if initialized diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt index f7c7528d43..d7fec3fec0 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Lint.kt @@ -144,9 +144,19 @@ public inline fun SharedFlow.retryWhen(noinline predicate: suspend FlowCo level = DeprecationLevel.WARNING ) @InlineOnly -public suspend inline fun SharedFlow.toList(destination: MutableList = ArrayList()): List = +public suspend inline fun SharedFlow.toList(): List = (this as Flow).toList() +/** + * A specialized version of [Flow.toList] that returns [Nothing] + * to indicate that [SharedFlow] collection never completes. + */ +@InlineOnly +public suspend inline fun SharedFlow.toList(destination: MutableList): Nothing { + (this as Flow).toList(destination) + throw IllegalStateException("this code is supposed to be unreachable") +} + /** * @suppress */ @@ -156,9 +166,19 @@ public suspend inline fun SharedFlow.toList(destination: MutableList = level = DeprecationLevel.WARNING ) @InlineOnly -public suspend inline fun SharedFlow.toSet(destination: MutableSet = LinkedHashSet()): Set = +public suspend inline fun SharedFlow.toSet(): Set = (this as Flow).toSet() +/** + * A specialized version of [Flow.toSet] that returns [Nothing] + * to indicate that [SharedFlow] collection never completes. + */ +@InlineOnly +public suspend inline fun SharedFlow.toSet(destination: MutableSet): Nothing { + (this as Flow).toSet(destination) + throw IllegalStateException("this code is supposed to be unreachable") +} + /** * @suppress */ diff --git a/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt b/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt index 8d814d566d..374e53b7b4 100644 --- a/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlin.coroutines.* -import kotlin.jvm.* /** * The result of .limitedParallelism(x) call, a dispatcher @@ -27,7 +26,7 @@ import kotlin.jvm.* internal class LimitedDispatcher( private val dispatcher: CoroutineDispatcher, private val parallelism: Int -) : CoroutineDispatcher(), Runnable, Delay by (dispatcher as? Delay ?: DefaultDelay) { +) : CoroutineDispatcher(), Delay by (dispatcher as? Delay ?: DefaultDelay) { // Atomic is necessary here for the sake of K/N memory ordering, // there is no need in atomic operations for this property @@ -45,61 +44,37 @@ internal class LimitedDispatcher( return super.limitedParallelism(parallelism) } - override fun run() { - var fairnessCounter = 0 - while (true) { - val task = queue.removeFirstOrNull() - if (task != null) { - try { - task.run() - } catch (e: Throwable) { - handleCoroutineException(EmptyCoroutineContext, e) - } - // 16 is our out-of-thin-air constant to emulate fairness. Used in JS dispatchers as well - if (++fairnessCounter >= 16 && dispatcher.isDispatchNeeded(this)) { - // Do "yield" to let other views to execute their runnable as well - // Note that we do not decrement 'runningWorkers' as we still committed to do our part of work - dispatcher.dispatch(this, this) - return - } - continue - } - - synchronized(workerAllocationLock) { - runningWorkers.decrementAndGet() - if (queue.size == 0) return - runningWorkers.incrementAndGet() - fairnessCounter = 0 - } - } - } - override fun dispatch(context: CoroutineContext, block: Runnable) { - dispatchInternal(block) { - dispatcher.dispatch(this, this) + dispatchInternal(block) { worker -> + dispatcher.dispatch(this, worker) } } @InternalCoroutinesApi override fun dispatchYield(context: CoroutineContext, block: Runnable) { - dispatchInternal(block) { - dispatcher.dispatchYield(this, this) + dispatchInternal(block) { worker -> + dispatcher.dispatchYield(this, worker) } } - private inline fun dispatchInternal(block: Runnable, dispatch: () -> Unit) { + /** + * Tries to dispatch the given [block]. + * If there are not enough workers, it starts a new one via [startWorker]. + */ + private inline fun dispatchInternal(block: Runnable, startWorker: (Worker) -> Unit) { // Add task to queue so running workers will be able to see that - if (addAndTryDispatching(block)) return - /* - * Protect against the race when the number of workers is enough, - * but one (because of synchronized serialization) attempts to complete, - * and we just observed the number of running workers smaller than the actual - * number (hit right between `--runningWorkers` and `++runningWorkers` in `run()`) - */ + queue.addLast(block) + if (runningWorkers.value >= parallelism) return + // allocation may fail if some workers were launched in parallel or a worker temporarily decreased + // `runningWorkers` when they observed an empty queue. if (!tryAllocateWorker()) return - dispatch() + val task = obtainTaskOrDeallocateWorker() ?: return + startWorker(Worker(task)) } + /** + * Tries to obtain the permit to start a new worker. + */ private fun tryAllocateWorker(): Boolean { synchronized(workerAllocationLock) { if (runningWorkers.value >= parallelism) return false @@ -108,9 +83,49 @@ internal class LimitedDispatcher( } } - private fun addAndTryDispatching(block: Runnable): Boolean { - queue.addLast(block) - return runningWorkers.value >= parallelism + /** + * Obtains the next task from the queue, or logically deallocates the worker if the queue is empty. + */ + private fun obtainTaskOrDeallocateWorker(): Runnable? { + while (true) { + when (val nextTask = queue.removeFirstOrNull()) { + null -> synchronized(workerAllocationLock) { + runningWorkers.decrementAndGet() + if (queue.size == 0) return null + runningWorkers.incrementAndGet() + } + else -> return nextTask + } + } + } + + /** + * A worker that polls the queue and runs tasks until there are no more of them. + * + * It always stores the next task to run. This is done in order to prevent the possibility of the fairness + * re-dispatch happening when there are no more tasks in the queue. This is important because, after all the + * actual tasks are done, nothing prevents the user from closing the dispatcher and making it incorrect to + * perform any more dispatches. + */ + private inner class Worker(private var currentTask: Runnable) : Runnable { + override fun run() { + var fairnessCounter = 0 + while (true) { + try { + currentTask.run() + } catch (e: Throwable) { + handleCoroutineException(EmptyCoroutineContext, e) + } + currentTask = obtainTaskOrDeallocateWorker() ?: return + // 16 is our out-of-thin-air constant to emulate fairness. Used in JS dispatchers as well + if (++fairnessCounter >= 16 && dispatcher.isDispatchNeeded(this@LimitedDispatcher)) { + // Do "yield" to let other views execute their runnable as well + // Note that we do not decrement 'runningWorkers' as we are still committed to our part of work + dispatcher.dispatch(this@LimitedDispatcher, this) + return + } + } + } } } diff --git a/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt b/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt index 059b234d8f..7107945a55 100644 --- a/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.internal import kotlinx.coroutines.* +import kotlin.contracts.* /** * @suppress **This an internal API and should not be used from general code.** @@ -16,4 +17,16 @@ public expect open class SynchronizedObject() // marker abstract class * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public expect inline fun synchronized(lock: SynchronizedObject, block: () -> T): T +public expect inline fun synchronizedImpl(lock: SynchronizedObject, block: () -> T): T + +/** + * @suppress **This an internal API and should not be used from general code.** + */ +@OptIn(ExperimentalContracts::class) +@InternalCoroutinesApi +public inline fun synchronized(lock: SynchronizedObject, block: () -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return synchronizedImpl(lock, block) +} diff --git a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt index aeb6199134..1f9b6fa739 100644 --- a/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt @@ -85,6 +85,45 @@ class BasicOperationsTest : TestBase() { } } + @Test + fun testCancelledChannelInvokeOnClose() { + val ch = Channel() + ch.invokeOnClose { assertIs(it) } + ch.cancel() + } + + @Test + fun testCancelledChannelWithCauseInvokeOnClose() { + val ch = Channel() + ch.invokeOnClose { assertIs(it) } + ch.cancel(TimeoutCancellationException("")) + } + + @Test + fun testThrowingInvokeOnClose() = runTest { + val channel = Channel() + channel.invokeOnClose { + assertNull(it) + expect(3) + throw TestException() + } + + launch { + try { + expect(2) + channel.close() + } catch (e: TestException) { + expect(4) + } + } + expect(1) + yield() + assertTrue(channel.isClosedForReceive) + assertTrue(channel.isClosedForSend) + assertFalse(channel.close()) + finish(5) + } + @Suppress("ReplaceAssertBooleanWithAssertEquality") private suspend fun testReceiveCatching(kind: TestChannelKind) = coroutineScope { reset() @@ -124,7 +163,7 @@ class BasicOperationsTest : TestBase() { channel.trySend(2) .onSuccess { expectUnreached() } .onClosed { - assertTrue { it is ClosedSendChannelException} + assertTrue { it is ClosedSendChannelException } if (!kind.isConflated) { assertEquals(42, channel.receive()) } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/LintTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/LintTest.kt new file mode 100644 index 0000000000..9cf6cbb958 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/LintTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2023 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 LintTest: TestBase() { + /** + * Tests that using [SharedFlow.toList] and similar functions by passing a mutable collection does add values + * to the provided collection. + */ + @Test + fun testSharedFlowToCollection() = runTest { + val sharedFlow = MutableSharedFlow() + val list = mutableListOf() + val set = mutableSetOf() + val jobs = listOf(suspend { sharedFlow.toList(list) }, { sharedFlow.toSet(set) }).map { + launch(Dispatchers.Unconfined) { it() } + } + repeat(10) { + sharedFlow.emit(it) + } + jobs.forEach { it.cancelAndJoin() } + assertEquals((0..9).toList(), list) + assertEquals((0..9).toSet(), set) + } +} diff --git a/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt b/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt index 7d82a67baf..49fe93f608 100644 --- a/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/LimitedParallelismConcurrentTest.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.exceptions.* +import kotlin.coroutines.* import kotlin.test.* class LimitedParallelismConcurrentTest : TestBase() { @@ -58,4 +59,37 @@ class LimitedParallelismConcurrentTest : TestBase() { joinAll(j1, j2) executor.close() } + + /** + * Tests that, when no tasks are present, the limited dispatcher does not dispatch any tasks. + * This is important for the case when a dispatcher is closeable and the [CoroutineDispatcher.limitedParallelism] + * machinery could trigger a dispatch after the dispatcher is closed. + */ + @Test + fun testNotDoingDispatchesWhenNoTasksArePresent() = runTest { + class NaggingDispatcher: CoroutineDispatcher() { + val closed = atomic(false) + override fun dispatch(context: CoroutineContext, block: Runnable) { + if (closed.value) + fail("Dispatcher was closed, but still dispatched a task") + Dispatchers.Default.dispatch(context, block) + } + fun close() { + closed.value = true + } + } + repeat(stressTestMultiplier * 500_000) { + val dispatcher = NaggingDispatcher() + val view = dispatcher.limitedParallelism(1) + val deferred = CompletableDeferred() + val job = launch(view) { + deferred.await() + } + launch(Dispatchers.Default) { + deferred.complete(Unit) + } + job.join() + dispatcher.close() + } + } } diff --git a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt b/kotlinx-coroutines-core/js/src/internal/Synchronized.kt index dcbb20217d..05db52854f 100644 --- a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt +++ b/kotlinx-coroutines-core/js/src/internal/Synchronized.kt @@ -16,5 +16,4 @@ public actual typealias SynchronizedObject = Any * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = - block() +public actual inline fun synchronizedImpl(lock: SynchronizedObject, block: () -> T): T = block() diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index 4e98e7bc98..121ba3f443 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -108,7 +108,14 @@ public fun CoroutineDispatcher.asExecutor(): Executor = (this as? ExecutorCoroutineDispatcher)?.executor ?: DispatcherExecutor(this) private class DispatcherExecutor(@JvmField val dispatcher: CoroutineDispatcher) : Executor { - override fun execute(block: Runnable) = dispatcher.dispatch(EmptyCoroutineContext, block) + override fun execute(block: Runnable) { + if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) { + dispatcher.dispatch(EmptyCoroutineContext, block) + } else { + block.run() + } + } + override fun toString(): String = dispatcher.toString() } diff --git a/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt b/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt index f949d9f5ea..6c98a9dad9 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/ResizableAtomicArray.kt @@ -28,11 +28,12 @@ internal class ResizableAtomicArray(initialLength: Int) { val curLen = curArray.length() if (index < curLen) { curArray[index] = value - } else { - val newArray = AtomicReferenceArray((index + 1).coerceAtLeast(2 * curLen)) - for (i in 0 until curLen) newArray[i] = curArray[i] - newArray[index] = value - array = newArray // copy done + return } + // It would be nice to copy array in batch instead of 1 by 1, but it seems like Java has no API for that + val newArray = AtomicReferenceArray((index + 1).coerceAtLeast(2 * curLen)) + for (i in 0 until curLen) newArray[i] = curArray[i] + newArray[index] = value + array = newArray // copy done } } diff --git a/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt b/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt index 6284f3a099..5b12faaf09 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt @@ -16,5 +16,5 @@ public actual typealias SynchronizedObject = Any * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = +public actual inline fun synchronizedImpl(lock: SynchronizedObject, block: () -> T): T = kotlin.synchronized(lock, block) diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index e08f3deedd..5ca3de5726 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -252,22 +252,31 @@ internal class CoroutineScheduler( /** * State of worker threads. - * [workers] is array of lazily created workers up to [maxPoolSize] workers. + * [workers] is a dynamic array of lazily created workers up to [maxPoolSize] workers. * [createdWorkers] is count of already created workers (worker with index lesser than [createdWorkers] exists). - * [blockingTasks] is count of pending (either in the queue or being executed) tasks + * [blockingTasks] is count of pending (either in the queue or being executed) blocking tasks. + * + * Workers array is also used as a lock for workers' creation and termination sequence. * * **NOTE**: `workers[0]` is always `null` (never used, works as sentinel value), so * workers are 1-indexed, code path in [Worker.trySteal] is a bit faster and index swap during termination - * works properly + * works properly. + * + * Initial size is `Dispatchers.Default` size * 2 to prevent unnecessary resizes for slightly or steadily loaded + * applications. */ @JvmField - val workers = ResizableAtomicArray(corePoolSize + 1) + val workers = ResizableAtomicArray((corePoolSize + 1) * 2) /** * The `Long` value describing the state of workers in this pool. - * Currently includes created, CPU-acquired, and blocking workers, each occupying [BLOCKING_SHIFT] bits. + * Currently, includes created, CPU-acquired, and blocking workers, each occupying [BLOCKING_SHIFT] bits. + * + * State layout (highest to lowest bits): + * | --- number of cpu permits, 22 bits --- | --- number of blocking tasks, 21 bits --- | --- number of created threads, 21 bits --- | */ private val controlState = atomic(corePoolSize.toLong() shl CPU_PERMITS_SHIFT) + private val createdWorkers: Int inline get() = (controlState.value and CREATED_MASK).toInt() private val availableCpuPermits: Int inline get() = availableCpuPermits(controlState.value) @@ -383,6 +392,10 @@ internal class CoroutineScheduler( fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) { trackTask() // this is needed for virtual time support val task = createTask(block, taskContext) + val isBlockingTask = task.isBlocking + // Invariant: we increment counter **before** publishing the task + // so executing thread can safely decrement the number of blocking tasks + val stateSnapshot = if (isBlockingTask) incrementBlockingTasks() else 0 // try to submit the task to the local queue and act depending on the result val currentWorker = currentWorker() val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch) @@ -394,12 +407,12 @@ internal class CoroutineScheduler( } val skipUnpark = tailDispatch && currentWorker != null // Checking 'task' instead of 'notAdded' is completely okay - if (task.mode == TASK_NON_BLOCKING) { + if (isBlockingTask) { + // Use state snapshot to better estimate the number of running threads + signalBlockingWork(stateSnapshot, skipUnpark = skipUnpark) + } else { if (skipUnpark) return signalCpuWork() - } else { - // Increment blocking tasks anyway - signalBlockingWork(skipUnpark = skipUnpark) } } @@ -413,11 +426,11 @@ internal class CoroutineScheduler( return TaskImpl(block, nanoTime, taskContext) } - private fun signalBlockingWork(skipUnpark: Boolean) { - // Use state snapshot to avoid thread overprovision - val stateSnapshot = incrementBlockingTasks() + // NB: should only be called from 'dispatch' method due to blocking tasks increment + private fun signalBlockingWork(stateSnapshot: Long, skipUnpark: Boolean) { if (skipUnpark) return if (tryUnpark()) return + // Use state snapshot to avoid accidental thread overprovision if (tryCreateWorker(stateSnapshot)) return tryUnpark() // Try unpark again in case there was race between permit release and parking } @@ -456,12 +469,13 @@ internal class CoroutineScheduler( } } - /* + /** * Returns the number of CPU workers after this function (including new worker) or * 0 if no worker was created. */ private fun createNewWorker(): Int { - synchronized(workers) { + val worker: Worker + return synchronized(workers) { // Make sure we're not trying to resurrect terminated scheduler if (isTerminated) return -1 val state = controlState.value @@ -479,12 +493,11 @@ internal class CoroutineScheduler( * 2) Make it observable by increment created workers count * 3) Only then start the worker, otherwise it may miss its own creation */ - val worker = Worker(newIndex) + worker = Worker(newIndex) workers.setSynchronized(newIndex, worker) require(newIndex == incrementCreatedWorkers()) - worker.start() - return cpuWorkers + 1 - } + cpuWorkers + 1 + }.also { worker.start() } // Start worker when the lock is released to reduce contention, see #3652 } /** diff --git a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt index ebf08a03d0..6e5b18f7e9 100644 --- a/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/ExecutorsTest.kt @@ -81,6 +81,22 @@ class ExecutorsTest : TestBase() { finish(4) } + @Test + fun testCustomDispatcherToExecutorDispatchNotNeeded() { + expect(1) + val dispatcher = object : CoroutineDispatcher() { + override fun isDispatchNeeded(context: CoroutineContext) = false + + override fun dispatch(context: CoroutineContext, block: Runnable) { + fail("should not dispatch") + } + } + dispatcher.asExecutor().execute { + expect(2) + } + finish(3) + } + @Test fun testTwoThreads() { val ctx1 = newSingleThreadContext("Ctx1") @@ -106,4 +122,4 @@ class ExecutorsTest : TestBase() { dispatcher.close() check(executorService.isShutdown) } -} \ 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 6a013fa1da..f9e5466b44 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -97,10 +97,10 @@ public actual open class TestBase(private var disableOutCheck: Boolean) { private fun printError(message: String, cause: Throwable) { setError(cause) - println("$message: $cause") - cause.printStackTrace(System.out) - println("--- Detected at ---") - Throwable().printStackTrace(System.out) + System.err.println("$message: $cause") + cause.printStackTrace(System.err) + System.err.println("--- Detected at ---") + Throwable().printStackTrace(System.err) } /** diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerInternalApiStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerInternalApiStressTest.kt index 22b9b02916..2ec714ffb6 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerInternalApiStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerInternalApiStressTest.kt @@ -80,8 +80,8 @@ class CoroutineSchedulerInternalApiStressTest : TestBase() { } } completionLatch.countDown() -// assertEquals(100, timesHelped) -// assertTrue(Thread.currentThread() in observedDefaultThreads, observedDefaultThreads.toString()) + assertEquals(100, timesHelped) + assertTrue(Thread.currentThread() in observedDefaultThreads, observedDefaultThreads.toString()) } } } diff --git a/kotlinx-coroutines-core/native/src/Debug.kt b/kotlinx-coroutines-core/native/src/Debug.kt index f17c2ed7fc..e208addf88 100644 --- a/kotlinx-coroutines-core/native/src/Debug.kt +++ b/kotlinx-coroutines-core/native/src/Debug.kt @@ -9,6 +9,7 @@ import kotlin.native.* internal actual val DEBUG: Boolean = false +@OptIn(ExperimentalStdlibApi::class) internal actual val Any.hexAddress: String get() = identityHashCode().toUInt().toString(16) internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown" diff --git a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt index edbd3fde0c..8a8ecfe393 100644 --- a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt +++ b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt @@ -17,4 +17,4 @@ public actual typealias SynchronizedObject = kotlinx.atomicfu.locks.Synchronized * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = lock.withLock2(block) +public actual inline fun synchronizedImpl(lock: SynchronizedObject, block: () -> T): T = lock.withLock2(block) diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt index a5588d853d..edc0a13ce8 100644 --- a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt @@ -20,7 +20,7 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = DarwinGloba private object DarwinGlobalQueueDispatcher : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { autoreleasepool { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.convert(), 0)) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.convert(), 0u)) { block.run() } } diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 24d0084be4..ff9ba9fffa 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.7.0-Beta.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.7.0-RC.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 2bdf1d8216..f2805086bb 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -26,7 +26,7 @@ Provided [TestDispatcher] implementations: Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0-Beta' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0-RC' } ``` diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index bcee73e12e..00d9fb659e 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -22,6 +22,7 @@ public final class kotlinx/coroutines/test/TestBuildersKt { public static final fun runTest (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V public static synthetic fun runTest$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public static synthetic fun runTest$default (Lkotlinx/coroutines/test/TestCoroutineScope;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static final synthetic fun runTest$default (Lkotlinx/coroutines/test/TestScope;Ljava/lang/Long;Lkotlin/jvm/functions/Function2;Ljava/lang/Integer;Ljava/lang/Object;)V public static final fun runTest-8Mi8wO0 (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;)V public static final fun runTest-8Mi8wO0 (Lkotlinx/coroutines/test/TestScope;JLkotlin/jvm/functions/Function2;)V public static synthetic fun runTest-8Mi8wO0$default (Lkotlin/coroutines/CoroutineContext;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt index 15cd1fba4a..8ae075a706 100644 --- a/kotlinx-coroutines-test/common/src/TestBuilders.kt +++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt @@ -570,3 +570,16 @@ internal fun throwAll(head: Throwable?, other: List) { } internal expect fun dumpCoroutines() + +@Deprecated( + "This is for binary compatibility with the `runTest` overload that existed at some point", + level = DeprecationLevel.HIDDEN +) +@JvmName("runTest\$default") +@Suppress("DEPRECATION", "UNUSED_PARAMETER") +public fun TestScope.runTestLegacy( + dispatchTimeoutMs: Long?, + testBody: suspend TestScope.() -> Unit, + unused1: Int?, + unused2: Any?, +): TestResult = runTest(dispatchTimeoutMs ?: 60_000, testBody) diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index 127097e702..e00d74edee 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.7.0-Beta" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0-RC" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your