From c8a720af13f482b13b6a1f86153bdab445108ce5 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 4 Sep 2019 23:37:00 +0300 Subject: [PATCH 01/90] Fixed linearizability of Channel.close operation Send operations must ALWAYS help close the channel when they observe that it was closed before throwing an exception. Fixes #1419 --- .../common/src/channels/AbstractChannel.kt | 57 ++++++------- .../common/src/channels/LinkedListChannel.kt | 5 +- .../ChannelCloseLCStressTest.kt | 82 +++++++++++++++++++ 3 files changed, 113 insertions(+), 31 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/linearizability/ChannelCloseLCStressTest.kt diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 7b8f96b6e4..a3be3ba958 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -160,14 +160,24 @@ internal abstract class AbstractSendChannel : SendChannel { val result = offerInternal(element) return when { result === OFFER_SUCCESS -> true - // We should check for closed token on offer as well, otherwise offer won't be linearizable - // in the face of concurrent close() - result === OFFER_FAILED -> throw closedForSend?.sendException?.let { recoverStackTrace(it) } ?: return false - result is Closed<*> -> throw recoverStackTrace(result.sendException) + result === OFFER_FAILED -> { + // 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)) + } + result is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(result)) else -> error("offerInternal returned $result") } } + private fun helpCloseAndGetSendException(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) + return closed.sendException + } + private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutine sc@ { cont -> loop@ while (true) { if (full) { @@ -179,8 +189,7 @@ internal abstract class AbstractSendChannel : SendChannel { return@sc } enqueueResult is Closed<*> -> { - helpClose(enqueueResult) - cont.resumeWithException(enqueueResult.sendException) + cont.helpCloseAndResumeWithSendException(enqueueResult) return@sc } enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead @@ -197,8 +206,7 @@ internal abstract class AbstractSendChannel : SendChannel { } offerResult === OFFER_FAILED -> continue@loop offerResult is Closed<*> -> { - helpClose(offerResult) - cont.resumeWithException(offerResult.sendException) + cont.helpCloseAndResumeWithSendException(offerResult) return@sc } else -> error("offerInternal returned $offerResult") @@ -206,6 +214,11 @@ internal abstract class AbstractSendChannel : SendChannel { } } + private fun Continuation<*>.helpCloseAndResumeWithSendException(closed: Closed<*>) { + helpClose(closed) + resumeWithException(closed.sendException) + } + /** * Result is: * * null -- successfully enqueued @@ -230,23 +243,17 @@ internal abstract class AbstractSendChannel : SendChannel { public override fun close(cause: Throwable?): Boolean { val closed = Closed(cause) - /* * Try to commit close by adding a close token to the end of the queue. * Successful -> we're now responsible for closing receivers * Not successful -> help closing pending receivers to maintain invariant * "if (!close()) next send will throw" */ - val closeAdded = queue.addLastIfPrev(closed, { it !is Closed<*> }) - if (!closeAdded) { - val actualClosed = queue.prevNode as Closed<*> - helpClose(actualClosed) - return false - } - - helpClose(closed) - invokeOnCloseHandler(cause) - return true + val closeAdded = queue.addLastIfPrev(closed) { it !is Closed<*> } + val actuallyClosed = if (closeAdded) closed else queue.prevNode as Closed<*> + helpClose(actuallyClosed) + if (closeAdded) invokeOnCloseHandler(cause) + return closeAdded // true if we have closed } private fun invokeOnCloseHandler(cause: Throwable?) { @@ -370,10 +377,7 @@ internal abstract class AbstractSendChannel : SendChannel { select.disposeOnSelect(node) return } - enqueueResult is Closed<*> -> { - helpClose(enqueueResult) - throw recoverStackTrace(enqueueResult.sendException) - } + enqueueResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(enqueueResult)) enqueueResult === ENQUEUE_FAILED -> {} // try to offer enqueueResult is Receive<*> -> {} // try to offer else -> error("enqueueSend returned $enqueueResult ") @@ -388,10 +392,7 @@ internal abstract class AbstractSendChannel : SendChannel { block.startCoroutineUnintercepted(receiver = this, completion = select.completion) return } - offerResult is Closed<*> -> { - helpClose(offerResult) - throw recoverStackTrace(offerResult.sendException) - } + offerResult is Closed<*> -> throw recoverStackTrace(helpCloseAndGetSendException(offerResult)) else -> error("offerSelectInternal returned $offerResult") } } @@ -432,7 +433,7 @@ internal abstract class AbstractSendChannel : SendChannel { private class SendSelect( override val pollResult: Any?, - @JvmField val channel: SendChannel, + @JvmField val channel: AbstractSendChannel, @JvmField val select: SelectInstance, @JvmField val block: suspend (SendChannel) -> R ) : Send(), DisposableHandle { diff --git a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt index 3afc86c457..2a73930ee9 100644 --- a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -29,8 +29,7 @@ internal open class LinkedListChannel : AbstractChannel() { when { result === OFFER_SUCCESS -> return OFFER_SUCCESS result === OFFER_FAILED -> { // try to buffer - val sendResult = sendBuffered(element) - when (sendResult) { + when (val sendResult = sendBuffered(element)) { null -> return OFFER_SUCCESS is Closed<*> -> return sendResult } diff --git a/kotlinx-coroutines-core/jvm/test/linearizability/ChannelCloseLCStressTest.kt b/kotlinx-coroutines-core/jvm/test/linearizability/ChannelCloseLCStressTest.kt new file mode 100644 index 0000000000..5bdc2841dc --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/linearizability/ChannelCloseLCStressTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2019 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 com.devexperts.dxlab.lincheck.* +import com.devexperts.dxlab.lincheck.annotations.* +import com.devexperts.dxlab.lincheck.paramgen.* +import com.devexperts.dxlab.lincheck.strategy.stress.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import java.io.* + +/** + * This is stress test that is fine-tuned to catch the problem + * [#1419](https://github.com/Kotlin/kotlinx.coroutines/issues/1419) + */ +@Param(name = "value", gen = IntGen::class, conf = "2:2") +@OpGroupConfig.OpGroupConfigs( + OpGroupConfig(name = "send", nonParallel = true), + OpGroupConfig(name = "receive", nonParallel = true), + OpGroupConfig(name = "close", nonParallel = true) +) +class ChannelCloseLCStressTest : TestBase() { + + private companion object { + // Emulating ctor argument for lincheck + var capacity = 0 + } + + private val lt = LinTesting() + private var channel: Channel = Channel(capacity) + + @Operation(runOnce = true, group = "send") + fun send1(@Param(name = "value") value: Int) = lt.run("send1") { channel.send(value) } + + @Operation(runOnce = true, group = "send") + fun send2(@Param(name = "value") value: Int) = lt.run("send2") { channel.send(value) } + + @Operation(runOnce = true, group = "receive") + fun receive1() = lt.run("receive1") { channel.receive() } + + @Operation(runOnce = true, group = "receive") + fun receive2() = lt.run("receive2") { channel.receive() } + + @Operation(runOnce = true, group = "close") + fun close1() = lt.run("close1") { channel.close(IOException("close1")) } + + @Operation(runOnce = true, group = "close") + fun close2() = lt.run("close2") { channel.close(IOException("close2")) } + + @Test + fun testRendezvousChannelLinearizability() { + runTest(0) + } + + @Test + fun testArrayChannelLinearizability() { + for (i in listOf(1, 2, 16)) { + runTest(i) + } + } + + @Test + fun testConflatedChannelLinearizability() = runTest(Channel.CONFLATED) + + @Test + fun testUnlimitedChannelLinearizability() = runTest(Channel.UNLIMITED) + + private fun runTest(capacity: Int) { + ChannelCloseLCStressTest.capacity = capacity + val options = StressOptions() + .iterations(1) // only one iteration -- test scenario is fixed + .invocationsPerIteration(10_000 * stressTestMultiplierSqrt) + .threads(3) + .verifier(LinVerifier::class.java) + LinChecker.check(ChannelCloseLCStressTest::class.java, options) + } +} From 0bef2b97b48aa0612b9b9c1366cba4af00196edb Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 5 Sep 2019 10:13:15 +0300 Subject: [PATCH 02/90] Update example Android apps toolchain (AGP 3.5.0, AndroidX) * Gradle 5.4.1 * AGP 3.5.0 * Migrated to AndroidX * Updated versions of all dependencies to the most recent ones * Removed proguard rules --- .../animation-app/app/build.gradle | 28 +++++++++---------- .../animation-app/app/proguard-rules.pro | 8 ------ .../jetbrains/kotlinx/animation/Animation.kt | 12 +++++--- .../kotlinx/animation/MainActivity.kt | 8 ++++-- .../app/src/main/res/layout/activity_main.xml | 18 +++++++----- .../app/src/main/res/layout/content_main.xml | 8 ++++-- .../animation-app/build.gradle | 6 +++- .../animation-app/gradle.properties | 9 ++++-- .../gradle/wrapper/gradle-wrapper.properties | 6 +++- .../example-app/app/build.gradle | 26 ++++++++--------- .../example-app/app/proguard-rules.pro | 8 ------ .../main/java/com/example/app/MainActivity.kt | 8 ++++-- .../app/src/main/res/layout/activity_main.xml | 16 +++++++---- .../app/src/main/res/layout/content_main.xml | 8 ++++-- .../example-app/build.gradle | 6 +++- .../example-app/gradle.properties | 9 ++++-- .../gradle/wrapper/gradle-wrapper.properties | 6 +++- 17 files changed, 112 insertions(+), 78 deletions(-) delete mode 100644 ui/kotlinx-coroutines-android/animation-app/app/proguard-rules.pro delete mode 100644 ui/kotlinx-coroutines-android/example-app/app/proguard-rules.pro diff --git a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle b/ui/kotlinx-coroutines-android/animation-app/app/build.gradle index b5919bea68..7fd9c8a98d 100644 --- a/ui/kotlinx-coroutines-android/animation-app/app/build.gradle +++ b/ui/kotlinx-coroutines-android/animation-app/app/build.gradle @@ -1,35 +1,33 @@ +/* + * Copyright 2016-2019 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 27 + compileSdkVersion 29 defaultConfig { applicationId "org.jetbrains.kotlinx.animation" minSdkVersion 14 - targetSdkVersion 27 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } dependencies { - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' - implementation 'com.android.support:design:27.1.1' - implementation 'android.arch.lifecycle:extensions:1.1.1' + 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 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + 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/proguard-rules.pro b/ui/kotlinx-coroutines-android/animation-app/app/proguard-rules.pro deleted file mode 100644 index aea920a68b..0000000000 --- a/ui/kotlinx-coroutines-android/animation-app/app/proguard-rules.pro +++ /dev/null @@ -1,8 +0,0 @@ --keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} --keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} --keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} --keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} - --keepclassmembernames class kotlinx.** { - volatile ; -} 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 index dd4aafb1c8..88e0baeeb3 100644 --- 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 @@ -1,9 +1,13 @@ +/* + * 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 android.arch.lifecycle.LifecycleOwner -import android.arch.lifecycle.MutableLiveData -import android.arch.lifecycle.Observer -import android.arch.lifecycle.ViewModel +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 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 index 87a857cc38..756db9bbe6 100644 --- 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 @@ -1,8 +1,12 @@ +/* + * 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 android.arch.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProviders import android.os.Bundle -import android.support.v7.app.AppCompatActivity +import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.content_main.* 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 index cfc022f115..ad11f2aa61 100644 --- 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 @@ -1,28 +1,32 @@ - + + - - - + - - - + 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 index 02058bdee7..30665fe9cf 100644 --- 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 @@ -1,5 +1,9 @@ - + + - + diff --git a/ui/kotlinx-coroutines-android/animation-app/build.gradle b/ui/kotlinx-coroutines-android/animation-app/build.gradle index f512a87311..d198b1ab77 100644 --- a/ui/kotlinx-coroutines-android/animation-app/build.gradle +++ b/ui/kotlinx-coroutines-android/animation-app/build.gradle @@ -1,3 +1,7 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { @@ -6,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 8e119d7159..19a14814c4 100644 --- a/ui/kotlinx-coroutines-android/animation-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/animation-app/gradle.properties @@ -1,3 +1,7 @@ +# +# Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: @@ -16,8 +20,9 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin.coroutines=enable - kotlin_version=1.3.50 coroutines_version=1.3.1 +android.useAndroidX=true +android.enableJetifier=true + 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 caf54fa280..ab5d60b4d9 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,5 +1,9 @@ +# +# Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/ui/kotlinx-coroutines-android/example-app/app/build.gradle b/ui/kotlinx-coroutines-android/example-app/app/build.gradle index 98257d37f9..f970baa6d6 100644 --- a/ui/kotlinx-coroutines-android/example-app/app/build.gradle +++ b/ui/kotlinx-coroutines-android/example-app/app/build.gradle @@ -1,34 +1,32 @@ +/* + * Copyright 2016-2019 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 27 + compileSdkVersion 29 defaultConfig { applicationId "com.example.app" minSdkVersion 14 - targetSdkVersion 27 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } dependencies { - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' - implementation 'com.android.support:design:27.1.1' + 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 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + 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/proguard-rules.pro b/ui/kotlinx-coroutines-android/example-app/app/proguard-rules.pro deleted file mode 100644 index aea920a68b..0000000000 --- a/ui/kotlinx-coroutines-android/example-app/app/proguard-rules.pro +++ /dev/null @@ -1,8 +0,0 @@ --keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} --keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} --keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} --keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} - --keepclassmembernames class kotlinx.** { - volatile ; -} diff --git a/ui/kotlinx-coroutines-android/example-app/app/src/main/java/com/example/app/MainActivity.kt b/ui/kotlinx-coroutines-android/example-app/app/src/main/java/com/example/app/MainActivity.kt index fc1cdbff41..47bd16cc51 100644 --- a/ui/kotlinx-coroutines-android/example-app/app/src/main/java/com/example/app/MainActivity.kt +++ b/ui/kotlinx-coroutines-android/example-app/app/src/main/java/com/example/app/MainActivity.kt @@ -1,8 +1,12 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package com.example.app import android.os.Bundle -import android.support.design.widget.FloatingActionButton -import android.support.v7.app.AppCompatActivity +import com.google.android.material.floatingactionbutton.FloatingActionButton +import androidx.appcompat.app.AppCompatActivity import android.view.Menu import android.view.MenuItem import android.widget.TextView diff --git a/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/activity_main.xml b/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/activity_main.xml index 13d32252c8..b98ce43711 100644 --- a/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/activity_main.xml +++ b/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/activity_main.xml @@ -1,28 +1,32 @@ - + + - - - + - - + diff --git a/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/content_main.xml b/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/content_main.xml index 110dc678ff..6eb08c050e 100644 --- a/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/content_main.xml +++ b/ui/kotlinx-coroutines-android/example-app/app/src/main/res/layout/content_main.xml @@ -1,5 +1,9 @@ - + + - + diff --git a/ui/kotlinx-coroutines-android/example-app/build.gradle b/ui/kotlinx-coroutines-android/example-app/build.gradle index f512a87311..d198b1ab77 100644 --- a/ui/kotlinx-coroutines-android/example-app/build.gradle +++ b/ui/kotlinx-coroutines-android/example-app/build.gradle @@ -1,3 +1,7 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { @@ -6,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 8e119d7159..19a14814c4 100644 --- a/ui/kotlinx-coroutines-android/example-app/gradle.properties +++ b/ui/kotlinx-coroutines-android/example-app/gradle.properties @@ -1,3 +1,7 @@ +# +# Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: @@ -16,8 +20,9 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -kotlin.coroutines=enable - kotlin_version=1.3.50 coroutines_version=1.3.1 +android.useAndroidX=true +android.enableJetifier=true + 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 caf54fa280..ab5d60b4d9 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,5 +1,9 @@ +# +# Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip From a2587d278c1fba41c66e62a8fa867d7d4a905057 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 5 Sep 2019 12:59:11 +0300 Subject: [PATCH 03/90] Robust waiting in CoroutinesDumpTest Fixed incorrect usage of Object.await/notify idiom Fixes #1513 --- .../test/CoroutinesDumpTest.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index ec727014cb..fa7353410b 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.debug @@ -11,6 +11,7 @@ import kotlin.test.* class CoroutinesDumpTest : DebugTestBase() { private val monitor = Any() + private var coroutineStarted = false // guarded by monitor @Test fun testSuspendedCoroutine() = synchronized(monitor) { @@ -130,7 +131,7 @@ class CoroutinesDumpTest : DebugTestBase() { private suspend fun nestedActiveMethod(shouldSuspend: Boolean) { if (shouldSuspend) yield() - notifyTest() + notifyCoroutineStarted() while (coroutineContext[Job]!!.isActive) { Thread.sleep(100) } @@ -143,17 +144,18 @@ class CoroutinesDumpTest : DebugTestBase() { private suspend fun sleepingNestedMethod() { yield() - notifyTest() + notifyCoroutineStarted() delay(Long.MAX_VALUE) } private fun awaitCoroutineStarted() { - (monitor as Object).wait() + while (!coroutineStarted) (monitor as Object).wait() } - private fun notifyTest() { + private fun notifyCoroutineStarted() { synchronized(monitor) { - (monitor as Object).notify() + coroutineStarted = true + (monitor as Object).notifyAll() } } } From 007d8d700288cc6ec495157c2b4cdc5e99eadc58 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 5 Sep 2019 11:56:18 +0300 Subject: [PATCH 04/90] Fixed completion sequence of ChannelLFStressTest A channel must be "cancelled" to abort both working senders & receivers. Fixes #1507 --- .../jvm/test/channels/ChannelLFStressTest.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt index 67bd68ac14..75e34e5a5e 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelLFStressTest.kt @@ -50,9 +50,12 @@ class ChannelLFStressTest : TestBase() { } private fun performLockFreedomTest() { - env.onCompletion { channel.close() } - repeat(2) { env.testThread { sender() } } - repeat(2) { env.testThread { receiver() } } + env.onCompletion { + // We must cancel the channel to abort both senders & receivers + channel.cancel(TestCompleted()) + } + repeat(2) { env.testThread("sender-$it") { sender() } } + repeat(2) { env.testThread("receiver-$it") { receiver() } } env.performTest(nSeconds) { println("Sent: $sendIndex, Received: $receiveCount, dups: $duplicateCount") } @@ -70,7 +73,7 @@ class ChannelLFStressTest : TestBase() { val value = sendIndex.getAndIncrement() try { channel.send(value) - } catch (e: ClosedSendChannelException) { + } catch (e: TestCompleted) { check(env.isCompleted) // expected when test was completed markReceived(value) // fake received (actually failed to send) } @@ -79,7 +82,7 @@ class ChannelLFStressTest : TestBase() { private suspend fun receiver() { val value = try { channel.receive() - } catch (e: ClosedReceiveChannelException) { + } catch (e: TestCompleted) { check(env.isCompleted) // expected when test was completed return } @@ -107,4 +110,6 @@ class ChannelLFStressTest : TestBase() { val bits = receivedBits.get(index) return bits and mask != 0L } + + private class TestCompleted : CancellationException() } From a7afd4657de916978898570d0baf86c0d73906f5 Mon Sep 17 00:00:00 2001 From: Yanis Batura Date: Mon, 5 Aug 2019 20:32:34 +0700 Subject: [PATCH 05/90] Improve docs / fix grammar --- .../common/src/CancellableContinuation.kt | 98 +++---- .../common/src/CoroutineContext.common.kt | 2 +- .../common/src/CoroutineDispatcher.kt | 60 ++-- kotlinx-coroutines-core/common/src/Yield.kt | 8 +- .../common/src/channels/Channel.kt | 266 +++++++++--------- .../common/src/channels/Produce.kt | 46 +-- .../common/src/flow/Builders.kt | 80 +++--- 7 files changed, 284 insertions(+), 276 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 139ef0403d..492e367bb0 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -11,9 +11,9 @@ import kotlin.coroutines.intrinsics.* // --------------- cancellable continuations --------------- /** - * Cancellable continuation. It is _completed_ when it is resumed or cancelled. - * When [cancel] function is explicitly invoked, this continuation immediately resumes with [CancellationException] or - * with the specified cancel cause. + * Cancellable continuation. It is _completed_ when resumed or cancelled. + * When the [cancel] function is explicitly invoked, this continuation immediately resumes with a [CancellationException] or + * the specified cancel cause. * * Cancellable continuation has three states (as subset of [Job] states): * @@ -28,10 +28,10 @@ import kotlin.coroutines.intrinsics.* * * A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted]. * - * Invocation of [resume] or [resumeWithException] in _resumed_ state produces [IllegalStateException]. + * 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 cancellation and cancellation wins). - * Invocation of [resumeWithException] in _cancelled_ state triggers exception handling of passed exception. + * outer job's cancellation, and the cancellation wins). + * Invocation of [resumeWithException] in _cancelled_ state triggers exception handling of the passed exception. * * ``` * +-----------+ resume +---------+ @@ -53,8 +53,8 @@ public interface CancellableContinuation : Continuation { public val isActive: Boolean /** - * Returns `true` when this continuation has completed for any reason. A continuation - * that was cancelled is also considered complete. + * Returns `true` when this continuation has completed for any reason. A cancelled continuation + * is also considered complete. */ public val isCompleted: Boolean @@ -66,11 +66,11 @@ public interface CancellableContinuation : Continuation { public val isCancelled: Boolean /** - * Tries to resume this continuation with a given value and returns non-null object token if it was successful, - * or `null` otherwise (it was already resumed or cancelled). When non-null object was returned, + * Tries to resume this continuation with the specified [value] 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, * [completeResume] must be invoked with it. * - * When [idempotent] is not `null`, this function performs _idempotent_ operation, so that + * When [idempotent] is not `null`, this function performs an _idempotent_ operation, so that * further invocations with the same non-null reference produce the same result. * * @suppress **This is unstable API and it is subject to change.** @@ -79,8 +79,8 @@ public interface CancellableContinuation : Continuation { public fun tryResume(value: T, idempotent: Any? = null): Any? /** - * Tries to resume this continuation with a given exception and returns non-null object token if it was successful, - * or `null` otherwise (it was already resumed or cancelled). When non-null object was returned, + * 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, * [completeResume] must be invoked with it. * * @suppress **This is unstable API and it is subject to change.** @@ -112,34 +112,34 @@ public interface CancellableContinuation : Continuation { public fun initCancellability() /** - * Cancels this continuation with an optional cancellation [cause]. The result is `true` if this continuation was - * cancelled as a result of this invocation and `false` otherwise. + * Cancels this continuation with an optional cancellation `cause`. The result is `true` if this continuation was + * cancelled as a result of this invocation, and `false` otherwise. */ public fun cancel(cause: Throwable? = null): Boolean /** - * Registers handler that is **synchronously** invoked once on cancellation (both regular and exceptional) of this continuation. - * When the continuation is already cancelled, then the handler is immediately invoked - * with cancellation exception. Otherwise, the handler will be invoked once on cancellation if this + * 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 + * with the cancellation exception. Otherwise, the handler will be invoked as soon as this * continuation is cancelled. * - * Installed [handler] should not throw any exceptions. - * If it does, they will get caught, wrapped into [CompletionHandlerException] and - * processed as uncaught exception in the context of the current coroutine + * The installed [handler] should not throw any exceptions. + * If it does, they will get caught, wrapped into a [CompletionHandlerException] and + * processed as an uncaught exception in the context of the current coroutine * (see [CoroutineExceptionHandler]). * - * At most one [handler] can be installed on one continuation. + * At most one [handler] can be installed on a continuation. * * **Note**: Implementation of `CompletionHandler` must be fast, non-blocking, and thread-safe. - * This handler can be invoked concurrently with the surrounding code. - * There is no guarantee on the execution context in which the [handler] is invoked. + * This `handler` can be invoked concurrently with the surrounding code. + * There is no guarantee on the execution context in which the `handler` will be invoked. */ public fun invokeOnCancellation(handler: CompletionHandler) /** - * Resumes this continuation with a given [value] in the invoker thread without going though - * [dispatch][CoroutineDispatcher.dispatch] function of the [CoroutineDispatcher] in the [context]. - * This function is designed to be used only by the [CoroutineDispatcher] implementations themselves. + * Resumes this continuation with the specified [value] in the invoker thread without going through + * the [dispatch][CoroutineDispatcher.dispatch] function of the [CoroutineDispatcher] in the [context]. + * This function is designed to only be used by [CoroutineDispatcher] implementations. * **It should not be used in general code**. * * **Note: This function is experimental.** Its signature general code may be changed in the future. @@ -148,9 +148,9 @@ public interface CancellableContinuation : Continuation { public fun CoroutineDispatcher.resumeUndispatched(value: T) /** - * Resumes this continuation with a given [exception] in the invoker thread without going though - * [dispatch][CoroutineDispatcher.dispatch] function of the [CoroutineDispatcher] in the [context]. - * This function is designed to be used only by the [CoroutineDispatcher] implementations themselves. + * Resumes this continuation with the specified [exception] in the invoker thread without going through + * the [dispatch][CoroutineDispatcher.dispatch] function of the [CoroutineDispatcher] in the [context]. + * This function is designed to only be used by [CoroutineDispatcher] implementations. * **It should not be used in general code**. * * **Note: This function is experimental.** Its signature general code may be changed in the future. @@ -159,19 +159,19 @@ public interface CancellableContinuation : Continuation { public fun CoroutineDispatcher.resumeUndispatchedWithException(exception: Throwable) /** - * Resumes this continuation with a given [value] and calls the specified [onCancellation] - * handler when resumed too late (when continuation was already cancelled) or when resumed - * successfully (before cancellation), but coroutine's job was cancelled before it had a - * chance to run in its dispatcher, so that suspended function threw an exception + * Resumes this continuation with the specified `value` and calls the specified `onCancellation` + * handler when either resumed too late (when continuation was already cancelled) or, although resumed + * successfully (before cancellation), the coroutine's job was cancelled before it had a + * chance to run in its dispatcher, so that the suspended function threw an exception * instead of returning this value. * - * Installed [onCancellation] handler should not throw any exceptions. - * If it does, they will get caught, wrapped into [CompletionHandlerException] and - * processed as uncaught exception in the context of the current coroutine + * The installed [onCancellation] handler should not throw any exceptions. + * If it does, they will get caught, wrapped into a [CompletionHandlerException] and + * processed as an uncaught exception in the context of the current coroutine * (see [CoroutineExceptionHandler]). * - * This function shall be used when resuming with a resource that must be closed by the - * code that had called the corresponding suspending function, e.g.: + * This function shall be used when resuming with a resource that must be closed by + * code that called the corresponding suspending function, e.g.: * * ``` * continuation.resume(resource) { @@ -179,17 +179,17 @@ public interface CancellableContinuation : Continuation { * } * ``` * - * **Note**: Implementation of [onCancellation] handler must be fast, non-blocking, and thread-safe. - * This handler can be invoked concurrently with the surrounding code. - * There is no guarantee on the execution context in which the [onCancellation] handler is invoked. + * **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, tentatively graduates in 1.3.0 public fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) } /** - * Suspends coroutine similar to [suspendCoroutine], but provide an implementation of [CancellableContinuation] to - * the [block]. This function throws [CancellationException] if the coroutine is cancelled or completed while suspended. + * 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. */ public suspend inline fun suspendCancellableCoroutine( crossinline block: (CancellableContinuation) -> Unit @@ -204,9 +204,9 @@ public suspend inline fun suspendCancellableCoroutine( } /** - * Suspends coroutine similar to [suspendCancellableCoroutine], but with *atomic cancellation*. + * Suspends the coroutine like [suspendCancellableCoroutine], but with *atomic cancellation*. * - * When suspended function throws [CancellationException] it means that the continuation was not resumed. + * 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. @@ -238,15 +238,15 @@ public suspend inline fun suspendAtomicCancellableCoroutine( suspendAtomicCancellableCoroutine(block) /** - * Removes a given node on cancellation. + * Removes the specified [node] on cancellation. */ internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) = invokeOnCancellation(handler = RemoveOnCancel(node).asHandler) /** - * Disposes a specified [handle] when this continuation is cancelled. + * Disposes the specified [handle] when this continuation is cancelled. * - * This is a shortcut for the following code with slightly more efficient implementation (one fewer object created). + * This is a shortcut for the following code with slightly more efficient implementation (one fewer object created): * ``` * invokeOnCancellation { handle.dispose() } * ``` diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt index 785e8a7691..a8b5686253 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt @@ -7,7 +7,7 @@ package kotlinx.coroutines import kotlin.coroutines.* /** - * Creates context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher nor + * Creates a context for the new coroutine. It installs [Dispatchers.Default] when no other dispatcher or * [ContinuationInterceptor] is specified, and adds optional support for debugging facilities (when turned on). */ public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 19308c218c..df7a2daac1 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -7,36 +7,36 @@ package kotlinx.coroutines import kotlin.coroutines.* /** - * Base class that shall be extended by all coroutine dispatcher implementations. + * Base class to be extended by all coroutine dispatcher implementations. * * The following standard implementations are provided by `kotlinx.coroutines` as properties on - * [Dispatchers] objects: + * the [Dispatchers] object: * - * * [Dispatchers.Default] -- is used by all standard builder if no dispatcher nor any other [ContinuationInterceptor] + * * [Dispatchers.Default] — is used by all standard builders if no dispatcher or any other [ContinuationInterceptor] * is specified in their context. It uses a common pool of shared background threads. * This is an appropriate choice for compute-intensive coroutines that consume CPU resources. - * * [Dispatchers.IO] -- uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive _blocking_ + * * [Dispatchers.IO] — uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive _blocking_ * operations (like file I/O and blocking socket I/O). - * * [Dispatchers.Unconfined] -- starts coroutine execution in the current call-frame until the first suspension. - * On first suspension the coroutine builder function returns. - * The coroutine resumes in whatever thread that is used by the + * * [Dispatchers.Unconfined] — starts coroutine execution in the current call-frame until the first suspension, + * whereupon the coroutine builder function returns. + * The coroutine will later resume in whatever thread used by the * corresponding suspending function, without confining it to any specific thread or pool. - * **Unconfined dispatcher should not be normally used in code**. + * **The `Unconfined` dispatcher should not normally be used in code**. * * Private thread pools can be created with [newSingleThreadContext] and [newFixedThreadPoolContext]. - * * An arbitrary [Executor][java.util.concurrent.Executor] can be converted to dispatcher with [asCoroutineDispatcher] extension function. + * * An arbitrary [Executor][java.util.concurrent.Executor] can be converted to a dispatcher with the [asCoroutineDispatcher] extension function. * * This class ensures that debugging facilities in [newCoroutineContext] function work properly. */ public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { /** - * Returns `true` if execution shall be dispatched onto another thread. - * The default behaviour for most dispatchers is to return `true`. + * Returns `true` if the execution shall be dispatched onto another thread. + * The default behavior for most dispatchers is to return `true`. * * This method should never be used from general code, it is used only by `kotlinx.coroutines` - * internals and its contract with the rest of API is an implementation detail. + * internals and its contract with the rest of the API is an implementation detail. * - * UI dispatchers _should not_ override `isDispatchNeeded`, but leave a default implementation that + * UI dispatchers _should not_ override `isDispatchNeeded`, but leave the default implementation that * returns `true`. To understand the rationale beyond this recommendation, consider the following code: * * ```kotlin @@ -46,24 +46,24 @@ public abstract class CoroutineDispatcher : * ``` * * When you invoke `asyncUpdateUI` in some background thread, it immediately continues to the next - * line, while UI update happens asynchronously in the UI thread. However, if you invoke - * it in the UI thread itself, it updates UI _synchronously_ if your `isDispatchNeeded` is + * line, while the UI update happens asynchronously in the UI thread. However, if you invoke + * it in the UI thread itself, it will update the UI _synchronously_ if your `isDispatchNeeded` is * overridden with a thread check. Checking if we are already in the UI thread seems more * efficient (and it might indeed save a few CPU cycles), but this subtle and context-sensitive * difference in behavior makes the resulting async code harder to debug. * - * Basically, the choice here is between "JS-style" asynchronous approach (async actions - * are always postponed to be executed later in the even dispatch thread) and "C#-style" approach + * Basically, the choice here is between the "JS-style" asynchronous approach (async actions + * are always postponed to be executed later in the event dispatch thread) and "C#-style" approach * (async actions are executed in the invoker thread until the first suspension point). - * While, C# approach seems to be more efficient, it ends up with recommendations like - * "use `yield` if you need to ....". This is error-prone. JS-style approach is more consistent + * While the C# approach seems to be more efficient, it ends up with recommendations like + * "use `yield` if you need to ....". This is error-prone. The JS-style approach is more consistent * and does not require programmers to think about whether they need to yield or not. * * However, coroutine builders like [launch][CoroutineScope.launch] and [async][CoroutineScope.async] accept an optional [CoroutineStart] - * parameter that allows one to optionally choose C#-style [CoroutineStart.UNDISPATCHED] behaviour + * parameter that allows one to optionally choose the C#-style [CoroutineStart.UNDISPATCHED] behavior * whenever it is needed for efficiency. * - * This method should be generally exception-safe, an exception thrown from this method + * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. * * **Note: This is an experimental api.** Execution semantics of coroutines may change in the future when this function returns `false`. @@ -74,18 +74,18 @@ public abstract class CoroutineDispatcher : /** * Dispatches execution of a runnable [block] onto another thread in the given [context]. * - * This method should be generally exception-safe, an exception thrown from this method + * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. */ public abstract fun dispatch(context: CoroutineContext, block: Runnable) /** - * Dispatches execution of a runnable [block] onto another thread in the given [context] - * with a hint for dispatcher that current dispatch is triggered by [yield] call, so execution of this + * Dispatches execution of a runnable `block` onto another thread in the given `context` + * with a hint for the dispatcher that the current dispatch is triggered by a [yield] call, so that the execution of this * continuation may be delayed in favor of already dispatched coroutines. * - * **Implementation note** though yield marker may be passed as a part of [context], this - * is a separate method for performance reasons + * **Implementation note:** Though the `yield` marker may be passed as a part of [context], this + * is a separate method for performance reasons. * * @suppress **This an internal API and should not be used from general code.** */ @@ -93,9 +93,9 @@ public abstract class CoroutineDispatcher : public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block) /** - * Returns continuation that wraps the original [continuation], thus intercepting all resumptions. + * Returns a continuation that wraps the provided [continuation], thus intercepting all resumptions. * - * This method should be generally exception-safe, an exception thrown from this method + * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. */ public final override fun interceptContinuation(continuation: Continuation): Continuation = @@ -104,13 +104,13 @@ public abstract class CoroutineDispatcher : /** * @suppress **Error**: Operator '+' on two CoroutineDispatcher objects is meaningless. * CoroutineDispatcher is a coroutine context element and `+` is a set-sum operator for coroutine contexts. - * The dispatcher to the right of `+` just replaces the dispatcher the left of `+`. + * The dispatcher to the right of `+` just replaces the dispatcher to the left. */ @Suppress("DeprecatedCallableAddReplaceWith") @Deprecated( message = "Operator '+' on two CoroutineDispatcher objects is meaningless. " + "CoroutineDispatcher is a coroutine context element and `+` is a set-sum operator for coroutine contexts. " + - "The dispatcher to the right of `+` just replaces the dispatcher the left of `+`.", + "The dispatcher to the right of `+` just replaces the dispatcher to the left.", level = DeprecationLevel.ERROR ) public operator fun plus(other: CoroutineDispatcher) = other diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index 78ab27fb87..2272352797 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -8,12 +8,12 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* /** - * Yields a thread (or thread pool) of the current coroutine dispatcher to other coroutines to run. - * If the coroutine dispatcher does not have its own thread pool (like [Dispatchers.Unconfined]) then this - * function does nothing, but checks if the coroutine [Job] was completed. + * Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines to run. + * If the coroutine dispatcher does not have its own thread pool (like [Dispatchers.Unconfined]), this + * function does nothing but check if the coroutine's [Job] was completed. * 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 dispatching, it resumes with [CancellationException]. + * this function is waiting for dispatch, it resumes with a [CancellationException]. */ public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> val context = uCont.context diff --git a/kotlinx-coroutines-core/common/src/channels/Channel.kt b/kotlinx-coroutines-core/common/src/channels/Channel.kt index f13a15c2ec..07e05f07d9 100644 --- a/kotlinx-coroutines-core/common/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/common/src/channels/Channel.kt @@ -22,8 +22,8 @@ import kotlin.internal.* */ public interface SendChannel { /** - * Returns `true` if this channel was closed by invocation of [close] and thus - * the [send] and [offer] attempts throws exception. + * Returns `true` if this channel was closed by an invocation of [close]. This means that + * calling [send] or [offer] will result in an exception. * * **Note: This is an experimental api.** This property may change its semantics and/or name in the future. */ @@ -31,8 +31,8 @@ public interface SendChannel { public val isClosedForSend: Boolean /** - * Returns `true` if the channel is full (out of capacity) and the [send] attempt will suspend. - * This function returns `false` for [isClosedForSend] channel. + * Returns `true` if the channel is full (out of capacity), which means that an attempt to [send] will suspend. + * This function returns `false` if the channel [is closed for `send`][isClosedForSend]. * * @suppress **Will be removed in next releases, no replacement.** */ @@ -41,74 +41,76 @@ public interface SendChannel { public val isFull: Boolean /** - * Adds [element] into to this channel, suspending the caller while the buffer of this channel is full - * or if it does not exist, or throws exception if the channel [isClosedForSend] (see [close] for details). + * 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 had suspended does not cause this suspended send invocation + * Note that closing 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 that are sent over the channel are delivered in first-in first-out order. The element that - * is being sent will get delivered to receivers before a close token. + * 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 [CancellationException]. + * function is suspended, this function immediately resumes with a [CancellationException]. * - * *Cancellation of suspended send is atomic* -- when this function - * throws [CancellationException] it means that the [element] was not sent to this channel. + * *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 + * 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. * * 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. * - * This function can be used in [select] invocation with [onSend] clause. + * This function can be used in [select] invocations with the [onSend] clause. * Use [offer] to try sending to this channel without waiting. */ public suspend fun send(element: E) /** - * Clause for [select] expression of [send] suspending function that selects when the element that is specified - * as parameter is sent to the channel. When the clause is selected the reference to this channel + * Clause for the [select] expression of the [send] suspending function that selects when the element that is specified + * as the parameter is sent to the channel. When the clause is selected, the reference to this channel * is passed into the corresponding block. * - * The [select] invocation fails with exception if the channel [isClosedForSend] (see [close] for details). + * The [select] invocation fails with an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). */ public val onSend: SelectClause2> /** - * Adds [element] into this queue if it is possible to do so immediately without violating capacity restrictions - * and returns `true`. Otherwise, it returns `false` immediately - * or throws exception if the channel [isClosedForSend] (see [close] for details). + * Immediately adds the specified [element] to this channel, if this doesn't violate its capacity restrictions, + * and returns `true`. Otherwise, just returns `false`. This is a synchronous variant of [send] which backs off + * in situations when `send` suspends. + * + * Throws an exception if the channel [is closed for `send`][isClosedForSend] (see [close] for details). */ public fun offer(element: E): Boolean /** * Closes this channel. - * This is an idempotent operation -- subsequent invocations of this function have no effect and return `false`. + * This is an idempotent operation — subsequent invocations of this function have no effect and return `false`. * Conceptually, its sends a special "close token" over this channel. * - * Immediately after invocation of this function + * Immediately after invocation of this function, * [isClosedForSend] starts returning `true`. However, [isClosedForReceive][ReceiveChannel.isClosedForReceive] * on the side of [ReceiveChannel] starts returning `true` only after all previously sent elements * are received. * - * A channel that was closed without a [cause] throws [ClosedSendChannelException] on attempts to send - * and [ClosedReceiveChannelException] on attempts to receive. + * A channel that was closed without a [cause] throws a [ClosedSendChannelException] on attempts to [send] or [offer] + * and [ClosedReceiveChannelException] on attempts to [receive][ReceiveChannel.receive]. * A channel that was closed with non-null [cause] is called a _failed_ channel. Attempts to send or * receive on a failed channel throw the specified [cause] exception. */ public fun close(cause: Throwable? = null): Boolean /** - * Registers handler which is synchronously invoked once the channel is [closed][close] - * or receiving side of this channel is [cancelled][ReceiveChannel.cancel]. - * Only one handler can be attached to the channel during channel's lifetime. - * Handler is invoked when [isClosedForSend] starts to return `true`. - * If channel is already closed, handler is invoked immediately. + * Registers a [handler] which is synchronously invoked once the channel is [closed][close] + * or the receiving side of this channel is [cancelled][ReceiveChannel.cancel]. + * Only one handler can be attached to a channel during its lifetime. + * The `handler` is invoked when [isClosedForSend] starts to return `true`. + * If the channel is closed already, the handler is invoked immediately. * * The meaning of `cause` that is passed to the handler: - * * `null` if channel was closed or cancelled without corresponding argument - * * close or cancel cause otherwise. + * * `null` if the channel was closed or cancelled without the corresponding argument + * * the cause of `close` or `cancel` otherwise. * * Example of usage (exception handling is omitted): * ``` @@ -128,7 +130,7 @@ public interface SendChannel { * * **Note: This is an experimental api.** This function may change its semantics, parameters or return type in the future. * - * @throws UnsupportedOperationException if underlying channel doesn't support [invokeOnClose]. + * @throws UnsupportedOperationException if the underlying channel doesn't support [invokeOnClose]. * Implementation note: currently, [invokeOnClose] is unsupported only by Rx-like integrations * * @throws IllegalStateException if another handler was already registered @@ -143,9 +145,9 @@ public interface SendChannel { public interface ReceiveChannel { /** * Returns `true` if this channel was closed by invocation of [close][SendChannel.close] on the [SendChannel] - * side and all previously sent items were already received, so that the [receive] attempt - * throws [ClosedReceiveChannelException]. If the channel was closed because of the exception, it - * is considered closed, too, but it is called a _failed_ channel. All suspending attempts to receive + * side and all previously sent items were already received. This means that calling [receive] + * will result in a [ClosedReceiveChannelException]. If the channel was closed because of an exception, it + * is considered closed, too, but is called a _failed_ channel. All suspending attempts to receive * an element from a failed channel throw the original [close][SendChannel.close] cause exception. * * **Note: This is an experimental api.** This property may change its semantics and/or name in the future. @@ -154,61 +156,61 @@ public interface ReceiveChannel { public val isClosedForReceive: Boolean /** - * Returns `true` if the channel is empty (contains no elements) and the [receive] attempt will suspend. - * This function returns `false` for [isClosedForReceive] channel. + * Returns `true` if the channel is empty (contains no elements), which means that an attempt to [receive] will suspend. + * This function returns `false` if the channel [is closed for `receive`][isClosedForReceive]. */ @ExperimentalCoroutinesApi public val isEmpty: Boolean /** - * Retrieves and removes the element from this channel suspending the caller while this channel is empty, - * or throws [ClosedReceiveChannelException] if the channel [isClosedForReceive]. - * If the channel was closed because of the exception, it is called a _failed_ channel and this function - * throws the original [close][SendChannel.close] cause exception. + * Retrieves and removes an element from this channel if it's not empty, or suspends the caller while the channel is empty, + * or throws a [ClosedReceiveChannelException] if the channel [is closed for `receive`][isClosedForReceive]. + * If the channel was closed because of an exception, it is called a _failed_ channel and this function + * will throw the original [close][SendChannel.close] cause exception. * * 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]. + * function is suspended, this function immediately resumes with a [CancellationException]. * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. + * *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 + * 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. * * 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. * - * This function can be used in [select] invocation with [onReceive] clause. + * This function can be used in [select] invocations with the [onReceive] clause. * Use [poll] to try receiving from this channel without waiting. */ public suspend fun receive(): E /** - * Clause for [select] expression of [receive] suspending function that selects with the element that - * is received from the channel. - * The [select] invocation fails with exception if the channel - * [isClosedForReceive] (see [close][SendChannel.close] for details). + * Clause for the [select] expression of the [receive] suspending function that selects with the element + * received from the channel. + * The [select] invocation fails with an exception if the channel + * [is closed for `receive`][isClosedForReceive] (see [close][SendChannel.close] for details). */ public val onReceive: SelectClause1 /** - * Retrieves and removes the element from this channel suspending the caller while this channel is empty, - * or returns `null` if the channel is [closed][isClosedForReceive] without cause + * Retrieves and removes an element from this channel if it's not empty, or suspends the caller while the channel is empty, + * or returns `null` if the channel is [closed for `receive`][isClosedForReceive] without cause, * or throws the original [close][SendChannel.close] cause exception if the channel has _failed_. * * 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]. + * function is suspended, this function immediately resumes with a [CancellationException]. * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. + * *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 + * 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. * * 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. * - * This function can be used in [select] invocation with [onReceiveOrNull] clause. + * This function can be used in [select] invocations with the [onReceiveOrNull] clause. * Use [poll] to try receiving from this channel without waiting. * * @suppress **Deprecated**: in favor of receiveOrClosed and receiveOrNull extension. @@ -224,9 +226,9 @@ public interface ReceiveChannel { public suspend fun receiveOrNull(): E? /** - * Clause for [select] expression of [receiveOrNull] suspending function that selects with the element that - * is received from the channel or selects with `null` if the channel - * [isClosedForReceive] without cause. The [select] invocation fails with + * Clause for the [select] expression of the [receiveOrNull] suspending function that selects with the element + * received from the channel or `null` if the channel is + * [closed for `receive`][isClosedForReceive] without a cause. The [select] invocation fails with * the original [close][SendChannel.close] cause exception if the channel has _failed_. * * @suppress **Deprecated**: in favor of onReceiveOrClosed and onReceiveOrNull extension. @@ -242,23 +244,23 @@ public interface ReceiveChannel { public val onReceiveOrNull: SelectClause1 /** - * Retrieves and removes the element from this channel suspending the caller while this channel [isEmpty]. - * This method returns [ValueOrClosed] with a value if element was successfully retrieved from the channel - * or [ValueOrClosed] with close cause if channel was closed. + * Retrieves and removes an element from this channel if it's not empty, or suspends the caller while this channel is empty. + * This method returns [ValueOrClosed] with the value of an element successfully retrieved from the channel + * or the close cause if the channel was closed. * * 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]. + * function is suspended, this function immediately resumes with a [CancellationException]. * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. + * *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. * - * Note, that this function does not check for cancellation when it is not suspended. + * 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. * - * This function can be used in [select] invocation with [onReceiveOrClosed] clause. + * This function can be used in [select] invocations with the [onReceiveOrClosed] clause. * Use [poll] to try receiving from this channel without waiting. * * @suppress *This is an internal API, do not use*: Inline classes ABI is not stable yet and @@ -268,9 +270,9 @@ public interface ReceiveChannel { public suspend fun receiveOrClosed(): ValueOrClosed /** - * Clause for [select] expression of [receiveOrClosed] suspending function that selects with the [ValueOrClosed] with a value - * that is received from the channel or selects with [ValueOrClosed] with a close cause if the channel - * [isClosedForReceive]. + * Clause for the [select] expression of the [receiveOrClosed] suspending function that selects with the [ValueOrClosed] with a value + * that is received from the channel or with a close cause if the channel + * [is closed for `receive`][isClosedForReceive]. * * @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. @@ -279,15 +281,15 @@ public interface ReceiveChannel { public val onReceiveOrClosed: SelectClause1> /** - * Retrieves and removes the element from this channel, or returns `null` if this channel is empty - * or is [isClosedForReceive] without cause. + * Retrieves and removes an element from this channel if its not empty, or returns `null` if the channel is empty + * or is [is closed for `receive`][isClosedForReceive] without a cause. * It throws the original [close][SendChannel.close] cause exception if the channel has _failed_. */ public fun poll(): E? /** - * Returns new iterator to receive elements from this channels using `for` loop. - * Iteration completes normally when the channel is [isClosedForReceive] without cause and + * Returns a new iterator to receive elements from this channel using a `for` loop. + * Iteration completes normally when the channel [is closed for `receive`][isClosedForReceive] without a cause and * throws the original [close][SendChannel.close] cause exception if the channel has _failed_. */ public operator fun iterator(): ChannelIterator @@ -297,14 +299,14 @@ public interface ReceiveChannel { * This function closes the channel and removes all buffered sent elements from it. * * A cause can be used to specify an error message or to provide other details on - * a cancellation reason for debugging purposes. + * the cancellation reason for debugging purposes. * If the cause is not specified, then an instance of [CancellationException] with a * default message is created to [close][SendChannel.close] the channel. * * Immediately after invocation of this function [isClosedForReceive] and * [isClosedForSend][SendChannel.isClosedForSend] - * on the side of [SendChannel] start returning `true`. All attempts to send to this channel - * or receive from this channel will throw [CancellationException]. + * on the side of [SendChannel] start returning `true`. Any attempt to send to or receive from this channel + * will lead to a [CancellationException]. */ public fun cancel(cause: CancellationException? = null) @@ -322,8 +324,8 @@ public interface ReceiveChannel { } /** - * A discriminated union of [ReceiveChannel.receiveOrClosed] result, - * that encapsulates either successfully received element of type [T] from the channel or a close cause. + * A discriminated union of [ReceiveChannel.receiveOrClosed] result + * that encapsulates either an element of type [T] successfully received from the channel or a close cause. * * :todo: Do not make it public before resolving todos in the code of this class. * @@ -335,36 +337,36 @@ public interface ReceiveChannel { public inline class ValueOrClosed internal constructor(private val holder: Any?) { /** - * Returns `true` if this instance represents received element. + * Returns `true` if this instance represents a received element. * In this case [isClosed] returns `false`. * todo: it is commented for now, because it is not used */ //public val isValue: Boolean get() = holder !is Closed /** - * Returns `true` if this instance represents close cause. + * Returns `true` if this instance represents a close cause. * In this case [isValue] returns `false`. */ public val isClosed: Boolean get() = holder is Closed /** - * Returns received value if this instance represents received value or throws [IllegalStateException] otherwise. + * Returns the received value if this instance represents a received value, or throws an [IllegalStateException] otherwise. * - * :todo: Decide if it is needed how it shall be named with relation to [valueOrThrow]: + * :todo: Decide, if it is needed, how it shall be named with relation to [valueOrThrow]: * - * So we have the following methods on ValueOrClosed: `value`, `valueOrNull`, `valueOrThrow`. - * On the other hand, the channel has the following receive variants: + * So we have the following methods on `ValueOrClosed`: `value`, `valueOrNull`, `valueOrThrow`. + * On the other hand, the channel has the following `receive` variants: * * `receive` which corresponds to `receiveOrClosed().valueOrThrow`... huh? * * `receiveOrNull` which corresponds to `receiveOrClosed().valueOrNull` * * `receiveOrClosed` - * For the sake of consider dropping this version of `value` and rename [valueOrThrow] to simply `value`. + * For the sake of simplicity consider dropping this version of `value` and rename [valueOrThrow] to simply `value`. */ @Suppress("UNCHECKED_CAST") public val value: T get() = if (holder is Closed) error(DEFAULT_CLOSE_MESSAGE) else holder as T /** - * Returns received value if this element represents received value or `null` otherwise. + * Returns the received value if this element represents a received value, or `null` otherwise. * :todo: Decide if it shall be made into extension that is available only for non-null T. * Note: it might become inconsistent with kotlin.Result */ @@ -373,9 +375,9 @@ internal constructor(private val holder: Any?) { get() = if (holder is Closed) null else holder as T /** - * :todo: Decide if it is needed how it shall be named with relation to [value]. - * Note, that valueOrThrow rethrows the cause adding no meaningful information about the callsite, - * so if one is sure that ValueOrClosed is always value, this very property should be used. + * :todo: Decide, if it is needed, how it shall be named with relation to [value]. + * Note that `valueOrThrow` rethrows the cause adding no meaningful information about the call site, + * so if one is sure that `ValueOrClosed` always holds a value, this very property should be used. * Otherwise, it could be very hard to locate the source of the exception. * todo: it is commented for now, because it is not used */ @@ -384,8 +386,8 @@ internal constructor(private val holder: Any?) { // get() = if (holder is Closed) throw holder.exception else holder as T /** - * Returns close cause of the channel if this instance represents close cause or throws - * [IllegalStateException] otherwise. + * Returns the close cause of the channel if this instance represents a close cause, or throws + * an [IllegalStateException] otherwise. */ @Suppress("UNCHECKED_CAST") public val closeCause: Throwable? get() = @@ -429,17 +431,17 @@ internal constructor(private val holder: Any?) { public interface ChannelIterator { /** * Returns `true` if the channel has more elements, suspending the caller while this channel is empty, - * or returns `false` if the channel [isClosedForReceive][ReceiveChannel.isClosedForReceive] without cause. + * or returns `false` if the channel [is closed for `receive`][ReceiveChannel.isClosedForReceive] without a cause. * It throws the original [close][SendChannel.close] cause exception if the channel has _failed_. * - * This function retrieves and removes the element from this channel for the subsequent invocation + * This function retrieves and removes an element from this channel for the subsequent invocation * of [next]. * * 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]. + * function is suspended, this function immediately resumes with a [CancellationException]. * - * *Cancellation of suspended receive is atomic* -- when this function - * throws [CancellationException] it means that the element was not retrieved from this channel. + * *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. @@ -463,9 +465,9 @@ public interface ChannelIterator { } /** - * Retrieves the element from the current iterator previously removed from the channel by preceding call to [hasNext] or - * throws [IllegalStateException] if [hasNext] was not invoked. - * [next] should only be used in pair with [hasNext]: + * Retrieves the element removed from the channel by a preceding call to [hasNext], or + * throws an [IllegalStateException] if [hasNext] was not invoked. + * This method should only be used in pair with [hasNext]: * ``` * while (iterator.hasNext()) { * val element = iterator.next() @@ -473,64 +475,64 @@ public interface ChannelIterator { * } * ``` * - * This method throws [ClosedReceiveChannelException] if the channel [isClosedForReceive][ReceiveChannel.isClosedForReceive] without cause. + * This method throws a [ClosedReceiveChannelException] if the channel [is closed for `receive`][ReceiveChannel.isClosedForReceive] without a cause. * It throws the original [close][SendChannel.close] cause exception if the channel has _failed_. */ public operator fun next(): E } /** - * Channel is a non-blocking primitive for communication between sender using [SendChannel] and receiver using [ReceiveChannel]. - * Conceptually, a channel is similar to [BlockingQueue][java.util.concurrent.BlockingQueue], - * but it has suspending operations instead of blocking ones and it can be closed. + * Channel is a non-blocking primitive for communication between a sender (via [SendChannel]) and a receiver (via [ReceiveChannel]). + * 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]. * - * `Channel(capacity)` factory function is used to create channels of different kind depending on - * the value of `capacity` integer: + * 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 `RendezvousChannel`. - * 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 - * until another coroutine invokes [receive] and [receive] suspends until another coroutine invokes [send]. + * * When `capacity` is 0 — it creates a `RendezvousChannel`. + * 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 `LinkedListChannel`. - * This is a channel with linked-list buffer of a unlimited capacity (limited only by available memory). - * Sender to this channel never suspends and [offer] always returns `true`. + * * When `capacity` is [Channel.UNLIMITED] — it creates a `LinkedListChannel`. + * 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 `ConflatedChannel`. + * * When `capacity` is [Channel.CONFLATED] — it creates a `ConflatedChannel`. * This channel buffers at most one element and conflates all subsequent `send` and `offer` invocations, - * so that the receiver always gets the most recently sent element. - * Back-to-send sent elements are _conflated_ -- only the the most recently sent element is received, + * so that the receiver always gets the last element sent. + * Back-to-send sent elements are _conflated_ — only the last sent element is received, * while previously sent elements **are lost**. - * Sender to this channel never suspends and [offer] always returns `true`. + * [Sending][send] to this channel never suspends, and [offer] always returns `true`. * - * * When `capacity` is positive, but less than [UNLIMITED] -- it creates array-based channel with given capacity. + * * 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`. - * Sender suspends only when buffer is full and receiver suspends only when buffer is empty. + * [Sending][send] suspends only when the buffer is full, and [receiving][receive] suspends only when the buffer is empty. */ public interface Channel : SendChannel, ReceiveChannel { /** - * Constants for channel factory function `Channel()`. + * Constants for the channel factory function `Channel()`. */ public companion object Factory { /** - * Requests channel with unlimited capacity buffer in `Channel(...)` factory function + * Requests a channel with an unlimited capacity buffer in the `Channel(...)` factory function */ public const val UNLIMITED = Int.MAX_VALUE /** - * Requests rendezvous channel in `Channel(...)` factory function -- the `RendezvousChannel` gets created. + * Requests a rendezvous channel in the `Channel(...)` factory function — a `RendezvousChannel` gets created. */ public const val RENDEZVOUS = 0 /** - * Requests conflated channel in `Channel(...)` factory function -- the `ConflatedChannel` gets created. + * Requests a conflated channel in the `Channel(...)` factory function — a `ConflatedChannel` gets created. */ public const val CONFLATED = -1 /** - * Requests buffered channel with a default buffer capacity in `Channel(...)` factory function -- - * the `ArrayChannel` gets created with a default capacity. - * This capacity is equal to 64 by default and can be overridden by setting + * 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. */ public const val BUFFERED = -2 @@ -567,18 +569,18 @@ public fun Channel(capacity: Int = RENDEZVOUS): Channel = } /** - * Indicates attempt to [send][SendChannel.send] on [isClosedForSend][SendChannel.isClosedForSend] channel + * 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 * exception on send attempts. * - * This exception is a subclass of [IllegalStateException] because, conceptually, sender is responsible - * for closing the channel and not be trying to send anything after the channel was close. Attempts to - * send into the closed channel indicate logical error in the sender's code. + * This exception is a subclass of [IllegalStateException], because, conceptually, it is the sender's responsibility + * to close the channel and not try to send anything thereafter. Attempts to + * send to a closed channel indicate a logical error in the sender's code. */ public class ClosedSendChannelException(message: String?) : IllegalStateException(message) /** - * Indicates attempt to [receive][ReceiveChannel.receive] on [isClosedForReceive][ReceiveChannel.isClosedForReceive] + * Indicates an attempt to [receive][ReceiveChannel.receive] from a [isClosedForReceive][ReceiveChannel.isClosedForReceive] * channel that was closed without a cause. A _failed_ channel rethrows the original [close][SendChannel.close] cause * exception on receive attempts. * diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index a579d7a247..59ebf52b46 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -8,19 +8,19 @@ import kotlinx.coroutines.* import kotlin.coroutines.* /** - * Scope for [produce][CoroutineScope.produce] coroutine builder. + * Scope for the [produce][CoroutineScope.produce] coroutine builder. * - * **Note: This is an experimental api.** Behaviour of producers that work as children in a parent scope with respect + * **Note: This is an experimental api.** Behavior of producers that work as children in a parent scope with respect * to cancellation and error handling may change in the future. */ @ExperimentalCoroutinesApi public interface ProducerScope : CoroutineScope, SendChannel { /** - * A reference to the channel that this coroutine [sends][send] elements to. + * A reference to the channel this coroutine [sends][send] elements to. * It is provided for convenience, so that the code in the coroutine can refer - * to the channel as `channel` as apposed to `this`. + * to the channel as `channel` as opposed to `this`. * All the [SendChannel] functions on this interface delegate to - * the channel instance returned by this function. + * the channel instance returned by this property. */ val channel: SendChannel } @@ -29,9 +29,9 @@ 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. * - * Note that when producer channel is cancelled this function resumes with cancellation exception, - * so putting the code after calling this function would not lead to its execution in case of cancellation. - * That is why this code takes a lambda parameter. + * 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. + * That's why this function takes a lambda parameter. * * Example of usage: * ``` @@ -43,7 +43,7 @@ public interface ProducerScope : CoroutineScope, SendChannel { */ @ExperimentalCoroutinesApi public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) { - check(kotlin.coroutines.coroutineContext[Job] === this) { "awaitClose() can be invoke only from the producer context" } + check(kotlin.coroutines.coroutineContext[Job] === this) { "awaitClose() can only be invoked from the producer context" } try { suspendCancellableCoroutine { cont -> invokeOnClose { @@ -56,28 +56,28 @@ public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) { } /** - * Launches new coroutine to produce a stream of values by sending them to a channel + * Launches a new coroutine to produce a stream of values by sending them to a channel * and returns a reference to the coroutine as a [ReceiveChannel]. This resulting * object can be used to [receive][ReceiveChannel.receive] elements produced by this coroutine. * - * The scope of the coroutine contains [ProducerScope] interface, which implements - * both [CoroutineScope] and [SendChannel], so that coroutine can invoke + * The scope of the coroutine contains the [ProducerScope] interface, which implements + * both [CoroutineScope] and [SendChannel], so that the coroutine can invoke * [send][SendChannel.send] directly. The channel is [closed][SendChannel.close] * when the coroutine completes. * The running coroutine is cancelled when its receive channel is [cancelled][ReceiveChannel.cancel]. * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [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 [coroutineContext] element. + * The coroutine context is inherited from this [CoroutineScope]. Additional context elements can be specified with the [context] argument. + * If the context does not have any dispatcher or other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * The parent job is inherited from the [CoroutineScope] as well, but it can also be overridden + * with a corresponding [coroutineContext] element. * - * Uncaught exceptions in this coroutine close the channel with this exception as a cause and - * the resulting channel becomes _failed_, so that any attempt to receive from such a channel throws exception. + * Any uncaught exception in this coroutine will close the channel with this exception as the cause and + * the resulting channel will become _failed_, so that any attempt to receive from it thereafter will throw an exception. * * The kind of the resulting channel depends on the specified [capacity] parameter. - * See [Channel] interface documentation for details. + * See the [Channel] interface documentation for details. * - * See [newCoroutineContext] for a description of debugging facilities that are available for newly created coroutine. + * See [newCoroutineContext] for a description of debugging facilities available for newly created coroutines. * * **Note: This is an experimental api.** Behaviour of producers that work as children in a parent scope with respect * to cancellation and error handling may change in the future. @@ -100,9 +100,9 @@ public fun CoroutineScope.produce( } /** - * This an internal API and should not be used from general code.** - * onCompletion parameter will be redesigned. - * If you have to use `onCompletion` operator, please report to https://github.com/Kotlin/kotlinx.coroutines/issues/. + * **This is an internal API and should not be used from general code.** + * The `onCompletion` parameter will be redesigned. + * If you have to use the `onCompletion` operator, please report to https://github.com/Kotlin/kotlinx.coroutines/issues/. * As a temporary solution, [invokeOnCompletion][Job.invokeOnCompletion] can be used instead: * ``` * fun ReceiveChannel.myOperator(): ReceiveChannel = GlobalScope.produce(Dispatchers.Unconfined) { diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 2b03e33160..4b9fa6b7e6 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -20,17 +20,18 @@ import kotlin.jvm.* * * Example of usage: * ``` - * fun fibonacci(): Flow = flow { - * emit(1L) - * var f1 = 1L - * var f2 = 1L - * repeat(100) { - * var tmp = f1 - * f1 = f2 - * f2 += tmp - * emit(f1) + * fun fibonacci(): Flow = flow { + * var x = BigInteger.ZERO + * var y = BigInteger.ONE + * while (true) { + * emit(x) + * x = y.also { + * y += x + * } * } * } + * + * fibonacci().take(100).collect { println(it) } * ``` * * `emit` should happen strictly in the dispatchers of the [block] in order to preserve the flow context. @@ -85,7 +86,7 @@ public fun Iterable.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given iterable. + * Creates a flow that produces values from the given iterator. */ public fun Iterator.asFlow(): Flow = flow { forEach { value -> @@ -103,7 +104,12 @@ public fun Sequence.asFlow(): Flow = flow { } /** - * Creates a flow that produces values from the given array of elements. + * Creates a flow that produces values from the specified `vararg`-arguments. + * + * Example of usage: + * ``` + * flowOf(1, 2, 3) + * ``` */ public fun flowOf(vararg elements: T): Flow = flow { for (element in elements) { @@ -112,12 +118,12 @@ public fun flowOf(vararg elements: T): Flow = flow { } /** - * Creates flow that produces a given [value]. + * Creates flow that produces the given [value]. */ public fun flowOf(value: T): Flow = flow { /* * Implementation note: this is just an "optimized" overload of flowOf(vararg) - * which significantly reduce the footprint of widespread single-value flows. + * which significantly reduces the footprint of widespread single-value flows. */ emit(value) } @@ -141,7 +147,7 @@ public fun Array.asFlow(): Flow = flow { } /** - * Creates flow that produces values from the given array. + * Creates a flow that produces values from the array. */ public fun IntArray.asFlow(): Flow = flow { forEach { value -> @@ -150,7 +156,7 @@ public fun IntArray.asFlow(): Flow = flow { } /** - * Creates flow that produces values from the given array. + * Creates a flow that produces values from the array. */ public fun LongArray.asFlow(): Flow = flow { forEach { value -> @@ -159,7 +165,7 @@ public fun LongArray.asFlow(): Flow = flow { } /** - * Creates flow that produces values from the given range. + * Creates a flow that produces values from the range. */ public fun IntRange.asFlow(): Flow = flow { forEach { value -> @@ -168,7 +174,7 @@ public fun IntRange.asFlow(): Flow = flow { } /** - * Creates flow that produces values from the given range. + * Creates a flow that produces values from the range. */ public fun LongRange.asFlow(): Flow = flow { forEach { value -> @@ -197,20 +203,20 @@ public fun flowViaChannel( /** * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] - * that is provided to the builder's [block] of code via [ProducerScope]. It allows elements to be - * produced by the code that is running in a different context or running concurrently. - * The resulting flow is _cold_, which means that [block] is called on each call of a terminal operator - * on the resulting flow. + * 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 + * is applied to the resulting flow. * * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] can be used * concurrently from different contexts. - * The resulting flow completes as soon as the code in the [block] and all its children complete. + * The resulting flow completes as soon as the code in the [block] and all its children completes. * Use [awaitClose] as the last statement to keep it running. - * For more detailed example please refer to [callbackFlow] documentation. + * A more detailed example is provided in the documentation of [callbackFlow]. * - * A channel with [default][Channel.BUFFERED] buffer size is used. Use [buffer] operator on the - * resulting flow 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. + * 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. * * Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are * always fused so that only one properly configured channel is used for execution. @@ -245,22 +251,22 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() /** * Creates an instance of the cold [Flow] with elements that are sent to a [SendChannel] - * that is provided to the builder's [block] of code via [ProducerScope]. It allows elements to be - * produced by the code that is running in a different context or running concurrently. + * 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 on each call of a terminal operator - * on the resulting flow. + * The resulting flow is _cold_, which means that [block] is called every time a terminal operator + * is applied to the resulting flow. * * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope] can be used - * from any context, e.g. from the callback-based API. - * The resulting flow completes as soon as the code in the [block] and all its children complete. + * from any context, e.g. from a callback-based API. + * The resulting flow completes as soon as the code in the [block] and all its children completes. * Use [awaitClose] as the last statement to keep it running. - * [awaitClose] argument is called when either flow consumer cancels flow collection - * or when callback-based API invokes [SendChannel.close] manually. + * The [awaitClose] argument is called either when a flow consumer cancels the flow collection + * or when a callback-based API invokes [SendChannel.close] manually. * - * A channel with [default][Channel.BUFFERED] buffer size is used. Use [buffer] operator on the - * resulting flow 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. + * 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. * * Adjacent applications of [callbackFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are * always fused so that only one properly configured channel is used for execution. From d02febfcab1106a99e3468f573dbaa34b7bd3fcb Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sat, 7 Sep 2019 19:06:52 +0300 Subject: [PATCH 06/90] Fixes linearizability of Channel.close in advanced receive+send case We cannot resume closed receives until all receivers are removed from the list. Consider channel state: head -> [receive_1] -> [receive_2] -> head - T1 called receive_2, and will call send() when it's receive call resumes - T2 calls close() Now if T2's close resumes T1's receive_2 then it's receive gets "closed for receive" exception, but its subsequent attempt to send successfully rendezvous with receive_1, producing non-linearizable execution. --- .../common/src/channels/AbstractChannel.kt | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index a3be3ba958..b5dfd95cd4 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -287,33 +287,49 @@ internal abstract class AbstractSendChannel : SendChannel { private fun helpClose(closed: Closed<*>) { /* * It's important to traverse list from right to left to avoid races with sender. - * Consider channel state - * head sentinel -> [receiver 1] -> [receiver 2] -> head sentinel - * T1 invokes receive() - * T2 invokes close() - * T3 invokes close() + send(value) + * Consider channel state: head -> [receive_1] -> [receive_2] -> head + * - T1 calls receive() + * - T2 calls close() + * - T3 calls close() + send(value) * * If both will traverse list from left to right, following non-linearizable history is possible: * [close -> false], [send -> transferred 'value' to receiver] + * + * Another problem with linearizability of close is that we cannot resume closed receives until all + * receivers are removed from the list. + * Consider channel state: head -> [receive_1] -> [receive_2] -> head + * - T1 called receive_2, and will call send() when it's receive call resumes + * - T2 calls close() + * + * Now if T2's close resumes T1's receive_2 then it's receive gets "closed for receive" exception, but + * its subsequent attempt to send successfully rendezvous with receive_1, producing non-linearizable execution. */ + var closedNode: Receive? = null // used when one node was closed to avoid extra memory allocation + var closedList: ArrayList>? = null // used when more nodes were closed while (true) { - val previous = closed.prevNode - // Channel is empty or has no receivers - if (previous is LockFreeLinkedListHead || previous !is Receive<*>) { - break - } - + // Break when channel is empty or has no receivers + @Suppress("UNCHECKED_CAST") + val previous = closed.prevNode as? Receive ?: break if (!previous.remove()) { // failed to remove the node (due to race) -- retry finding non-removed prevNode // NOTE: remove() DOES NOT help pending remove operation (that marked next pointer) previous.helpRemove() // make sure remove is complete before continuing continue } - - @Suppress("UNCHECKED_CAST") - previous as Receive // type assertion - previous.resumeReceiveClosed(closed) + // add removed nodes to a separate list + if (closedNode == null) { + closedNode = previous + } else { + val list = closedList ?: ArrayList>().also { closedList = it } + list += previous + } + } + // now notify all removed nodes that the channel was closed + if (closedNode != null) { + closedNode.resumeReceiveClosed(closed) + closedList?.forEach { it.resumeReceiveClosed(closed) } } + // and do other post-processing onClosedIdempotent(closed) } From bf3305286c6942b60ca07cde59632e1237f83671 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 9 Sep 2019 19:43:57 +0300 Subject: [PATCH 07/90] Introduce InlineList to simplify helpClose logic, reverse helpClose resume order --- .../common/src/channels/AbstractChannel.kt | 22 ++++----- .../common/src/internal/InlineList.kt | 46 +++++++++++++++++++ .../common/test/channels/ChannelsTest.kt | 31 +++++++++++++ 3 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 kotlinx-coroutines-core/common/src/internal/InlineList.kt diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index b5dfd95cd4..be18942d9e 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -304,8 +304,7 @@ internal abstract class AbstractSendChannel : SendChannel { * Now if T2's close resumes T1's receive_2 then it's receive gets "closed for receive" exception, but * its subsequent attempt to send successfully rendezvous with receive_1, producing non-linearizable execution. */ - var closedNode: Receive? = null // used when one node was closed to avoid extra memory allocation - var closedList: ArrayList>? = null // used when more nodes were closed + var closedList = InlineList>() while (true) { // Break when channel is empty or has no receivers @Suppress("UNCHECKED_CAST") @@ -316,19 +315,14 @@ internal abstract class AbstractSendChannel : SendChannel { previous.helpRemove() // make sure remove is complete before continuing continue } - // add removed nodes to a separate list - if (closedNode == null) { - closedNode = previous - } else { - val list = closedList ?: ArrayList>().also { closedList = it } - list += previous - } - } - // now notify all removed nodes that the channel was closed - if (closedNode != null) { - closedNode.resumeReceiveClosed(closed) - closedList?.forEach { it.resumeReceiveClosed(closed) } + // add removed nodes to a separate list + closedList += previous } + /* + * Now notify all removed nodes that the channel was closed + * in the order they were added to the channel + */ + closedList.forEachReversed { it.resumeReceiveClosed(closed) } // and do other post-processing onClosedIdempotent(closed) } diff --git a/kotlinx-coroutines-core/common/src/internal/InlineList.kt b/kotlinx-coroutines-core/common/src/internal/InlineList.kt new file mode 100644 index 0000000000..062a9100f9 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/InlineList.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UNCHECKED_CAST") + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.assert + +/* + * Inline class that represents a mutable list, but does not allocate an underlying storage + * for zero and one elements. + * Cannot be parametrized with `List<*>`. + */ +internal inline class InlineList(private val holder: Any? = null) { + public operator fun plus(element: E): InlineList { + assert { element !is List<*> } // Lists are prohibited + return when (holder) { + null -> InlineList(element) + is ArrayList<*> -> { + (holder as ArrayList).add(element) + InlineList(holder) + } + else -> { + val list = ArrayList(4) + list.add(holder as E) + list.add(element) + InlineList(list) + } + } + } + + public inline fun forEachReversed(action: (E) -> Unit) { + when (holder) { + null -> return + !is ArrayList<*> -> action(holder as E) + else -> { + val list = holder as ArrayList + for (i in (list.size - 1) downTo 0) { + action(list[i]) + } + } + } + } +} diff --git a/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt b/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt index 983f353f07..42cc85558b 100644 --- a/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt +++ b/kotlinx-coroutines-core/common/test/channels/ChannelsTest.kt @@ -19,6 +19,37 @@ class ChannelsTest: TestBase() { assertEquals(testList, testList.asReceiveChannel().toList()) } + @Test + fun testCloseWithMultipleWaiters() = runTest { + val channel = Channel() + launch { + try { + expect(2) + channel.receive() + expectUnreached() + } catch (e: ClosedReceiveChannelException) { + expect(5) + } + } + + launch { + try { + expect(3) + channel.receive() + expectUnreached() + } catch (e: ClosedReceiveChannelException) { + expect(6) + } + } + + expect(1) + yield() + expect(4) + channel.close() + yield() + finish(7) + } + @Test fun testAssociate() = runTest { assertEquals(testList.associate { it * 2 to it * 3 }, From 89f8c69032d922c993da2c2132e21665bdc0cde8 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 9 Sep 2019 17:04:27 +0300 Subject: [PATCH 08/90] Minor documentation improvements --- .../common/src/CoroutineScope.kt | 27 +++++++++---------- .../common/src/flow/Builders.kt | 3 +++ kotlinx-coroutines-debug/README.md | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index a9c7fb3a91..b1f31e2f49 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -73,14 +73,13 @@ public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineSco * Example of use: * ``` * class MyAndroidActivity { - * private val scope = MainScope() + * private val scope = MainScope() * - * override fun onDestroy() { - * super.onDestroy() - * scope.cancel() - * } + * override fun onDestroy() { + * super.onDestroy() + * scope.cancel() + * } * } - * * ``` * * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements. @@ -128,7 +127,6 @@ public val CoroutineScope.isActive: Boolean * send(Math.sqrt(number)) * } * } - * * ``` */ public object GlobalScope : CoroutineScope { @@ -151,16 +149,15 @@ public object GlobalScope : CoroutineScope { * * ``` * suspend fun showSomeData() = coroutineScope { - * - * val data = async(Dispatchers.IO) { // <- extension on current scope + * val data = async(Dispatchers.IO) { // <- extension on current scope * ... load some UI data for the Main thread ... - * } + * } * - * withContext(Dispatchers.Main) { - * doSomeWork() - * val result = data.await() - * display(result) - * } + * withContext(Dispatchers.Main) { + * doSomeWork() + * val result = data.await() + * display(result) + * } * } * ``` * diff --git a/kotlinx-coroutines-core/common/src/flow/Builders.kt b/kotlinx-coroutines-core/common/src/flow/Builders.kt index 4b9fa6b7e6..49ad2922e9 100644 --- a/kotlinx-coroutines-core/common/src/flow/Builders.kt +++ b/kotlinx-coroutines-core/common/src/flow/Builders.kt @@ -291,6 +291,9 @@ public fun channelFlow(@BuilderInference block: suspend ProducerScope.() * awaitClose { api.unregister(callback) } * } * ``` + * + * This function is an alias for [channelFlow], it has a separate name to reflect + * the intent of the usage (integration with a callback-based API) better. */ @Suppress("NOTHING_TO_INLINE") @ExperimentalCoroutinesApi diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index ca2a05b628..3c46c31154 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -136,7 +136,7 @@ Do not use this module in production environment and do not rely on the format o Unfortunately, Android runtime does not support Instrument API necessary for `kotlinx-coroutines-debug` to function, triggering `java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;`. -Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support androin-gradle 3.3 +Nevertheless, it will be possible to support debug agent on Android as soon as [GradleAspectJ-Android](https://github.com/Archinamon/android-gradle-aspectj) will support android-gradle 3.3 let's try to select receiving this element from buffer - if (!select.trySelect(null)) { // :todo: move trySelect completion outside of lock + if (!select.trySelect()) { // :todo: move trySelect completion outside of lock this.size = size // restore size buffer[head] = result // restore head return ALREADY_SELECTED diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 61b1f7ae0d..3f15550962 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -273,7 +273,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { } private fun registerSelectSend(select: SelectInstance, element: E, block: suspend (SendChannel) -> R) { - if (!select.trySelect(null)) return + if (!select.trySelect()) return offerInternal(element)?.let { select.resumeSelectCancellableWithException(it.sendException) return diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 21a18832a4..c04ccc4c39 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -85,6 +85,7 @@ internal open class ConflatedChannel : AbstractChannel() { result === ALREADY_SELECTED -> return ALREADY_SELECTED result === OFFER_SUCCESS -> return OFFER_SUCCESS result === OFFER_FAILED -> {} // retry + result === RETRY_ATOMIC -> {} // retry result is Closed<*> -> return result else -> error("Invalid result $result") } diff --git a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt index 2a73930ee9..d925be1c50 100644 --- a/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/LinkedListChannel.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.channels +import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* /** @@ -51,6 +52,7 @@ internal open class LinkedListChannel : AbstractChannel() { result === ALREADY_SELECTED -> return ALREADY_SELECTED result === OFFER_SUCCESS -> return OFFER_SUCCESS result === OFFER_FAILED -> {} // retry + result === RETRY_ATOMIC -> {} // retry result is Closed<*> -> return result else -> error("Invalid result $result") } diff --git a/kotlinx-coroutines-core/common/src/internal/Atomic.kt b/kotlinx-coroutines-core/common/src/internal/Atomic.kt index bc52815361..8a1185ae13 100644 --- a/kotlinx-coroutines-core/common/src/internal/Atomic.kt +++ b/kotlinx-coroutines-core/common/src/internal/Atomic.kt @@ -1,11 +1,12 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.internal import kotlinx.atomicfu.atomic import kotlinx.coroutines.* +import kotlin.jvm.* /** * The most abstract operation that can be in process. Other threads observing an instance of this @@ -19,6 +20,20 @@ public abstract class OpDescriptor { * object that indicates the failure reason. */ abstract fun perform(affected: Any?): Any? + + /** + * Returns reference to atomic operation that this descriptor is a part of or `null` + * if not a part of any [AtomicOp]. + */ + abstract val atomicOp: AtomicOp<*>? + + override fun toString(): String = "$classSimpleName@$hexAddress" // debug + + fun isEarlierThan(that: OpDescriptor): Boolean { + val thisOp = atomicOp ?: return false + val thatOp = that.atomicOp ?: return false + return thisOp.opSequence < thatOp.opSequence + } } @SharedImmutable @@ -40,13 +55,27 @@ public abstract class AtomicOp : OpDescriptor() { val isDecided: Boolean get() = _consensus.value !== NO_DECISION - fun tryDecide(decision: Any?): Boolean { + /** + * Sequence number of this multi-word operation for deadlock resolution. + * An operation with lower number aborts itself with (using [RETRY_ATOMIC] error symbol) if it encounters + * the need to help the operation with higher sequence number and then restarts + * (using higher `opSequence` to ensure progress). + * Simple operations that cannot get into the deadlock always return zero here. + * + * See https://github.com/Kotlin/kotlinx.coroutines/issues/504 + */ + open val opSequence: Long get() = 0L + + override val atomicOp: AtomicOp<*> get() = this + + fun decide(decision: Any?): Any? { assert { decision !== NO_DECISION } - return _consensus.compareAndSet(NO_DECISION, decision) + val current = _consensus.value + if (current !== NO_DECISION) return current + if (_consensus.compareAndSet(NO_DECISION, decision)) return decision + return _consensus.value } - private fun decide(decision: Any?): Any? = if (tryDecide(decision)) decision else _consensus.value - abstract fun prepare(affected: T): Any? // `null` if Ok, or failure reason abstract fun complete(affected: T, failure: Any?) // failure != null if failed to prepare op @@ -59,7 +88,7 @@ public abstract class AtomicOp : OpDescriptor() { if (decision === NO_DECISION) { decision = decide(prepare(affected as T)) } - + // complete operation complete(affected as T, decision) return decision } @@ -71,6 +100,15 @@ public abstract class AtomicOp : OpDescriptor() { * @suppress **This is unstable API and it is subject to change.** */ public abstract class AtomicDesc { + lateinit var atomicOp: AtomicOp<*> // the reference to parent atomicOp, init when AtomicOp is created abstract fun prepare(op: AtomicOp<*>): Any? // returns `null` if prepared successfully abstract fun complete(op: AtomicOp<*>, failure: Any?) // decision == null if success } + +/** + * It is returned as an error by [AtomicOp] implementations when they detect potential deadlock + * using [AtomicOp.opSequence] numbers. + */ +@JvmField +@SharedImmutable +internal val RETRY_ATOMIC: Any = Symbol("RETRY_ATOMIC") diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index fc1c72f067..39dc1d2884 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -4,6 +4,8 @@ package kotlinx.coroutines.internal +import kotlin.jvm.* + /** @suppress **This is unstable API and it is subject to change.** */ public expect open class LockFreeLinkedListNode() { public val isRemoved: Boolean @@ -49,7 +51,7 @@ public expect open class AddLastDesc( ) : AbstractAtomicDesc { val queue: LockFreeLinkedListNode val node: T - protected override fun onPrepare(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode): Any? + override fun finishPrepare(prepareOp: PrepareOp) override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } @@ -57,8 +59,7 @@ public expect open class AddLastDesc( public expect open class RemoveFirstDesc(queue: LockFreeLinkedListNode): AbstractAtomicDesc { val queue: LockFreeLinkedListNode public val result: T - protected open fun validatePrepared(node: T): Boolean - protected final override fun onPrepare(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode): Any? + override fun finishPrepare(prepareOp: PrepareOp) final override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } @@ -68,6 +69,19 @@ public expect abstract class AbstractAtomicDesc : AtomicDesc { final override fun complete(op: AtomicOp<*>, failure: Any?) protected open fun failure(affected: LockFreeLinkedListNode): Any? protected open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean - protected abstract fun onPrepare(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode): Any? // non-null on failure + public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure + public open fun onPrepare(prepareOp: PrepareOp): Any? // non-null on failure protected abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } + +/** @suppress **This is unstable API and it is subject to change.** */ +public expect class PrepareOp: OpDescriptor { + val affected: LockFreeLinkedListNode + override val atomicOp: AtomicOp<*> + val desc: AbstractAtomicDesc + fun finishPrepare() +} + +@JvmField +@SharedImmutable +internal val REMOVE_PREPARED: Any = Symbol("REMOVE_PREPARED") \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 4626fe1d38..eb793f4840 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -87,6 +87,10 @@ public interface SelectClause2 { public fun registerSelectClause2(select: SelectInstance, param: P, block: suspend (Q) -> R) } +@JvmField +@SharedImmutable +internal val SELECT_STARTED: Any = Symbol("SELECT_STARTED") + /** * Internal representation of select instance. This instance is called _selected_ when * the clause to execute is already picked. @@ -101,12 +105,23 @@ public interface SelectInstance { public val isSelected: Boolean /** - * Tries to select this instance. + * Tries to select this instance. Returns `true` on success. + */ + public fun trySelect(): Boolean + + /** + * Tries to select this instance. Returns: + * * [SELECT_STARTED] on success, + * * [RETRY_ATOMIC] on deadlock (needs retry, it is only possible when [otherOp] is not `null`) + * * `null` on failure to select (already selected). + * [otherOp] is not null when trying to rendezvous with this select from inside of another select. + * In this case, [PrepareOp.finishPrepare] must be called before deciding on any value other than [RETRY_ATOMIC]. */ - public fun trySelect(idempotent: Any?): Boolean + public fun trySelectOther(otherOp: PrepareOp?): Any? /** * Performs action atomically with [trySelect]. + * May return [RETRY_ATOMIC], caller shall retry with **fresh instance of desc**. */ public fun performAtomicTrySelect(desc: AtomicDesc): Any? @@ -189,11 +204,22 @@ private val UNDECIDED: Any = Symbol("UNDECIDED") @SharedImmutable private val RESUMED: Any = Symbol("RESUMED") +// Global counter of all atomic select operations for their deadlock resolution +// The separate internal class is work-around for Atomicfu's current implementation that creates public classes +// for static atomics +internal class SeqNumber { + private val number = atomic(1L) + fun next() = number.incrementAndGet() +} + +private val selectOpSequenceNumber = SeqNumber() + @PublishedApi internal class SelectBuilderImpl( private val uCont: Continuation // unintercepted delegate continuation ) : LockFreeLinkedListHead(), SelectBuilder, - SelectInstance, Continuation, CoroutineStackFrame { + SelectInstance, Continuation, CoroutineStackFrame +{ override val callerFrame: CoroutineStackFrame? get() = uCont as? CoroutineStackFrame @@ -234,9 +260,7 @@ internal class SelectBuilderImpl( _result.loop { result -> when { result === UNDECIDED -> if (_result.compareAndSet(UNDECIDED, value())) return - result === COROUTINE_SUSPENDED -> if (_result.compareAndSet(COROUTINE_SUSPENDED, - RESUMED - )) { + result === COROUTINE_SUSPENDED -> if (_result.compareAndSet(COROUTINE_SUSPENDED, RESUMED)) { block() return } @@ -290,29 +314,22 @@ internal class SelectBuilderImpl( private inner class SelectOnCancelling(job: Job) : JobCancellingNode(job) { // Note: may be invoked multiple times, but only the first trySelect succeeds anyway override fun invoke(cause: Throwable?) { - if (trySelect(null)) + if (trySelect()) resumeSelectCancellableWithException(job.getCancellationException()) } override fun toString(): String = "SelectOnCancelling[${this@SelectBuilderImpl}]" } - private val state: Any? get() { - _state.loop { state -> - if (state !is OpDescriptor) return state - state.perform(this) - } - } - @PublishedApi internal fun handleBuilderException(e: Throwable) { - if (trySelect(null)) { + if (trySelect()) { resumeWithException(e) } else if (e !is CancellationException) { /* * Cannot handle this exception -- builder was already resumed with a different exception, * so treat it as "unhandled exception". But only if it is not the completion reason * and it's not the cancellation. Otherwise, in the face of structured concurrency - * the same exception will be reported to theglobal exception handler. + * the same exception will be reported to the global exception handler. */ val result = getResult() if (result !is CompletedExceptionally || unwrap(result.cause) !== unwrap(e)) { @@ -321,7 +338,13 @@ internal class SelectBuilderImpl( } } - override val isSelected: Boolean get() = state !== this + override val isSelected: Boolean get() = _state.loop { state -> + when { + state === this -> return false + state is OpDescriptor -> state.perform(this) // help + else -> return true // already selected + } + } override fun disposeOnSelect(handle: DisposableHandle) { val node = DisposeNode(handle) @@ -342,40 +365,209 @@ internal class SelectBuilderImpl( } } - // it is just like start(), but support idempotent start - override fun trySelect(idempotent: Any?): Boolean { - assert { idempotent !is OpDescriptor } // "cannot use OpDescriptor as idempotent marker" - while (true) { // lock-free loop on state - val state = this.state + override fun trySelect(): Boolean { + val result = trySelectOther(null) + return when { + result === SELECT_STARTED -> true + result == null -> false + else -> error("Unexpected trySelectIdempotent result $result") + } + } + + /* + Diagram for rendezvous between two select operations: + + +---------+ +------------------------+ state(c) + | Channel | | SelectBuilderImpl(1) | -----------------------------------+ + +---------+ +------------------------+ | + | queue ^ | + V | select | + +---------+ next +------------------------+ next +--------------+ | + | LLHead | ------> | Send/ReceiveSelect(3) | -+----> | NextNode ... | | + +---------+ +------------------------+ | +--------------+ | + ^ ^ | next(b) ^ | + | affected | V | | + | +-----------------+ next | V + | | PrepareOp(6) | ----------+ +-----------------+ + | +-----------------+ <-------------------- | PairSelectOp(7) | + | | desc +-----------------+ + | V + | queue +----------------------+ + +------------------------- | TryPoll/OfferDesc(5) | + +----------------------+ + atomicOp | ^ + V | desc + +----------------------+ impl +---------------------+ + | SelectBuilderImpl(2) | <----- | AtomicSelectOp(4) | + +----------------------+ +---------------------+ + | state(a) ^ + | | + +----------------------------+ + + + 0. The first select operation SelectBuilderImpl(1) had already registered Send/ReceiveSelect(3) node + in the channel. + 1. The second select operation SelectBuilderImpl(2) is trying to rendezvous calling + performAtomicTrySelect(TryPoll/TryOfferDesc). + 2. A linked pair of AtomicSelectOp(4) and TryPoll/OfferDesc(5) is created to initiate this operation. + 3. AtomicSelectOp.prepareSelectOp installs a reference to AtomicSelectOp(4) in SelectBuilderImpl(2).state(a) + field. STARTING AT THIS MOMENT CONCURRENT HELPERS CAN DISCOVER AND TRY TO HELP PERFORM THIS OPERATION. + 4. Then TryPoll/OfferDesc.prepare discovers "affectedNode" for this operation as Send/ReceiveSelect(3) and + creates PrepareOp(6) that references it. It installs reference to PrepareOp(6) in Send/ReceiveSelect(3).next(b) + instead of its original next pointer that was stored in PrepareOp(6).next. + 5. PrepareOp(6).perform calls TryPoll/OfferDesc(5).onPrepare which validates that PrepareOp(6).affected node + is of the correct type and tries to secure ability to resume it by calling affected.tryResumeSend/Receive. + Note, that different PrepareOp instances can be repeatedly created for different candidate nodes. If node is + found to be be resumed/selected, then REMOVE_PREPARED result causes Send/ReceiveSelect(3).next change to + undone and new PrepareOp is created with a different candidate node. Different concurrent helpers may end up + creating different PrepareOp instances, so it is important that they ultimately come to consensus about + node on which perform operation upon. + 6. Send/ReceiveSelect(3).affected.tryResumeSend/Receive forwards this call to SelectBuilderImpl.trySelectOther, + passing it a reference to PrepareOp(6) as an indication of the other select instance rendezvous. + 7. SelectBuilderImpl.trySelectOther creates PairSelectOp(7) and installs it as SelectBuilderImpl(1).state(c) + to secure the state of the first builder and commit ability to make it selected for this operation. + 8. NOW THE RENDEZVOUS IS FULLY PREPARED via descriptors installed at + - SelectBuilderImpl(2).state(a) + - Send/ReceiveSelect(3).next(b) + - SelectBuilderImpl(1).state(c) + Any concurrent operation that is trying to access any of the select instances or the queue is going to help. + Any helper that helps AtomicSelectOp(4) calls TryPoll/OfferDesc(5).prepare which tries to determine + "affectedNode" but is bound to discover the same Send/ReceiveSelect(3) node that cannot become + non-first node until this operation completes (there are no insertions to the head of the queue!) + We have not yet decided to complete this operation, but we cannot ever decide to complete this operation + on any other node but Send/ReceiveSelect(3), so it is now safe to perform the next step. + 9. PairSelectOp(7).perform calls PrepareOp(6).finishPrepare which copies PrepareOp(6).affected and PrepareOp(6).next + to the corresponding TryPoll/OfferDesc(5) fields. + 10. PairSelectOp(7).perform calls AtomicSelect(4).decide to reach consensus on successful completion of this + operation. This consensus is important in light of dead-lock resolution algorithm, because a stale helper + could have stumbled upon a higher-numbered atomic operation and had decided to abort this atomic operation, + reaching decision on RETRY_ATOMIC status of it. We cannot proceed with completion in this case and must abort, + all objects including AtomicSelectOp(4) will be dropped, reverting all the three updated pointers to + their original values and atomic operation will retry from scratch. + 11. NOW WITH SUCCESSFUL UPDATE OF AtomicSelectOp(4).consensus to null THE RENDEZVOUS IS COMMITTED. The rest + of the code proceeds to update: + - SelectBuilderImpl(1).state to TryPoll/OfferDesc(5) so that late helpers would know that we have + already successfully completed rendezvous. + - Send/ReceiveSelect(3).next to Removed(next) so that this node becomes marked as removed. + - SelectBuilderImpl(2).state to null to mark this select instance as selected. + + Note, that very late helper may try to perform this AtomicSelectOp(4) when it is already completed. + It can proceed as far as finding affected node, creating PrepareOp, installing this new PrepareOp into the + node's next pointer, but PrepareOp.perform checks that AtomicSelectOp(4) is already decided and undoes all + the preparations. + */ + + // it is just like plain trySelect, but support idempotent start + // Returns SELECT_STARTED | RETRY_ATOMIC | null (when already selected) + override fun trySelectOther(otherOp: PrepareOp?): Any? { + _state.loop { state -> // lock-free loop on state when { + // Found initial state (not selected yet) -- try to make it selected state === this -> { - if (_state.compareAndSet(this, idempotent)) { - doAfterSelect() - return true + if (otherOp == null) { + // regular trySelect -- just mark as select + if (!_state.compareAndSet(this, null)) return@loop + } else { + // Rendezvous with another select instance -- install PairSelectOp + val pairSelectOp = PairSelectOp(otherOp) + if (!_state.compareAndSet(this, pairSelectOp)) return@loop + val decision = pairSelectOp.perform(this) + if (decision !== null) return decision } + doAfterSelect() + return SELECT_STARTED + } + state is OpDescriptor -> { // state is either AtomicSelectOp or PairSelectOp + // Found descriptor of ongoing operation while working in the context of other select operation + if (otherOp != null) { + val otherAtomicOp = otherOp.atomicOp + when { + // It is the same select instance + otherAtomicOp is AtomicSelectOp && otherAtomicOp.impl === this -> { + /* + * We cannot do state.perform(this) here and "help" it since it is the same + * select and we'll get StackOverflowError. + * See https://github.com/Kotlin/kotlinx.coroutines/issues/1411 + * We cannot support this because select { ... } is an expression and its clauses + * have a result that shall be returned from the select. + */ + error("Cannot use matching select clauses on the same object") + } + // The other select (that is trying to proceed) had started earlier + otherAtomicOp.isEarlierThan(state) -> { + /** + * Abort to prevent deadlock by returning a failure to it. + * See https://github.com/Kotlin/kotlinx.coroutines/issues/504 + * The other select operation will receive a failure and will restart itself with a + * larger sequence number. This guarantees obstruction-freedom of this algorithm. + */ + return RETRY_ATOMIC + } + } + } + // Otherwise (not a special descriptor) + state.perform(this) // help it } // otherwise -- already selected - idempotent == null -> return false // already selected - state === idempotent -> return true // was selected with this marker - else -> return false + otherOp == null -> return null // already selected + state === otherOp.desc -> return SELECT_STARTED // was selected with this marker + else -> return null // selected with different marker } } } + // The very last step of rendezvous between two select operations + private class PairSelectOp( + @JvmField val otherOp: PrepareOp + ) : OpDescriptor() { + override fun perform(affected: Any?): Any? { + val impl = affected as SelectBuilderImpl<*> + // here we are definitely not going to RETRY_ATOMIC, so + // 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 + impl._state.compareAndSet(this, update) + return decision + } + + override val atomicOp: AtomicOp<*>? + get() = otherOp.atomicOp + } + override fun performAtomicTrySelect(desc: AtomicDesc): Any? = - AtomicSelectOp(desc).perform(null) + 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})" + } - private inner class AtomicSelectOp( + private class AtomicSelectOp( + @JvmField val impl: SelectBuilderImpl<*>, @JvmField val desc: AtomicDesc ) : AtomicOp() { + // all select operations are totally ordered by their creating time using selectOpSequenceNumber + override val opSequence = selectOpSequenceNumber.next() + + init { + desc.atomicOp = this + } + override fun prepare(affected: Any?): Any? { // only originator of operation makes preparation move of installing descriptor into this selector's state // helpers should never do it, or risk ruining progress when they come late if (affected == null) { // we are originator (affected reference is not null if helping) - prepareIfNotSelected()?.let { return it } + prepareSelectOp()?.let { return it } + } + try { + return desc.prepare(this) + } catch (e: Throwable) { + // undo prepareSelectedOp on crash (for example if IllegalStateException is thrown) + if (affected == null) undoPrepare() + throw e } - return desc.prepare(this) } override fun complete(affected: Any?, failure: Any?) { @@ -383,13 +575,13 @@ internal class SelectBuilderImpl( desc.complete(this, failure) } - fun prepareIfNotSelected(): Any? { - _state.loop { state -> + private fun prepareSelectOp(): Any? { + impl._state.loop { state -> when { - state === this@AtomicSelectOp -> return null // already in progress - state is OpDescriptor -> state.perform(this@SelectBuilderImpl) // help - state === this@SelectBuilderImpl -> { - if (_state.compareAndSet(this@SelectBuilderImpl, this@AtomicSelectOp)) + state === this -> return null // already in progress + state is OpDescriptor -> state.perform(impl) // help + state === impl -> { + if (impl._state.compareAndSet(impl, this)) return null // success } else -> return ALREADY_SELECTED @@ -397,14 +589,21 @@ internal class SelectBuilderImpl( } } + // reverts the change done by prepareSelectedOp + private fun undoPrepare() { + impl._state.compareAndSet(this, impl) + } + private fun completeSelect(failure: Any?) { val selectSuccess = failure == null - val update = if (selectSuccess) null else this@SelectBuilderImpl - if (_state.compareAndSet(this@AtomicSelectOp, update)) { + val update = if (selectSuccess) null else impl + if (impl._state.compareAndSet(this, update)) { if (selectSuccess) - doAfterSelect() + impl.doAfterSelect() } } + + override fun toString(): String = "AtomicSelectOp(sequence=$opSequence)" } override fun SelectClause0.invoke(block: suspend () -> R) { @@ -421,14 +620,14 @@ internal class SelectBuilderImpl( override fun onTimeout(timeMillis: Long, block: suspend () -> R) { if (timeMillis <= 0L) { - if (trySelect(null)) + if (trySelect()) block.startCoroutineUnintercepted(completion) return } val action = Runnable { // todo: we could have replaced startCoroutine with startCoroutineUndispatched // But we need a way to know that Delay.invokeOnTimeout had used the right thread - if (trySelect(null)) + if (trySelect()) block.startCoroutineCancellable(completion) // shall be cancellable while waits for dispatch } disposeOnSelect(context.delay.invokeOnTimeout(timeMillis, action)) diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 3c72915379..f82d6ca8ff 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -240,6 +240,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { } failure === ALREADY_SELECTED -> return // already selected -- bail out failure === LOCK_FAIL -> {} // retry + failure === RETRY_ATOMIC -> {} // retry else -> error("performAtomicTrySelect(TryLockDesc) returned $failure") } } @@ -264,9 +265,9 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { @JvmField val owner: Any? ) : AtomicDesc() { // This is Harris's RDCSS (Restricted Double-Compare Single Swap) operation - private inner class PrepareOp(private val op: AtomicOp<*>) : OpDescriptor() { + private inner class PrepareOp(override val atomicOp: AtomicOp<*>) : OpDescriptor() { override fun perform(affected: Any?): Any? { - val update: Any = if (op.isDecided) EMPTY_UNLOCKED else op // restore if was already decided + val update: Any = if (atomicOp.isDecided) EMPTY_UNLOCKED else atomicOp // restore if was already decided (affected as MutexImpl)._state.compareAndSet(this, update) return null // ok } @@ -367,7 +368,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { @JvmField val select: SelectInstance, @JvmField val block: suspend (Mutex) -> R ) : LockWaiter(owner) { - override fun tryResumeLockWaiter(): Any? = if (select.trySelect(null)) SELECT_SUCCESS else null + 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) @@ -379,6 +380,8 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { 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 diff --git a/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt index ece95db19a..c9747c6fe8 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.selects @@ -385,7 +385,7 @@ class SelectArrayChannelTest : TestBase() { // only for debugging internal fun SelectBuilder.default(block: suspend () -> R) { this as SelectBuilderImpl // type assertion - if (!trySelect(null)) return + if (!trySelect()) return block.startCoroutineUnintercepted(this) } } diff --git a/kotlinx-coroutines-core/common/test/selects/SelectBuilderImplTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectBuilderImplTest.kt deleted file mode 100644 index d231135992..0000000000 --- a/kotlinx-coroutines-core/common/test/selects/SelectBuilderImplTest.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.selects - -import kotlinx.coroutines.* -import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* -import kotlin.test.* - -class SelectBuilderImplTest : TestBase() { - @Test - fun testIdempotentSelectResumeInline() { - var resumed = false - val delegate = object : Continuation { - override val context: CoroutineContext get() = EmptyCoroutineContext - override fun resumeWith(result: Result) { - check(result.getOrNull() == "OK") - resumed = true - } - } - val c = SelectBuilderImpl(delegate) - // still running builder - check(!c.isSelected) - check(c.trySelect("SELECT")) - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - c.completion.resume("OK") - check(!resumed) // still running builder, didn't invoke delegate - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - check(c.getResult() === "OK") // then builder returns - } - - @Test - fun testIdempotentSelectResumeSuspended() { - var resumed = false - val delegate = object : Continuation { - override val context: CoroutineContext get() = EmptyCoroutineContext - override fun resumeWith(result: Result) { - check(result.getOrNull() == "OK") - resumed = true - } - } - val c = SelectBuilderImpl(delegate) - check(c.getResult() === COROUTINE_SUSPENDED) // suspend first - check(!c.isSelected) - check(c.trySelect("SELECT")) - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - check(!resumed) - c.completion.resume("OK") - check(resumed) - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - } - - @Test - fun testIdempotentSelectResumeWithExceptionInline() { - var resumed = false - val delegate = object : Continuation { - override val context: CoroutineContext get() = EmptyCoroutineContext - override fun resumeWith(result: Result) { - check(result.exceptionOrNull() is TestException) - resumed = true - } - } - val c = SelectBuilderImpl(delegate) - // still running builder - check(!c.isSelected) - check(c.trySelect("SELECT")) - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - c.completion.resumeWithException(TestException()) - check(!resumed) // still running builder, didn't invoke delegate - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - try { - c.getResult() // the builder should throw exception - error("Failed") - } catch (e: Throwable) { - check(e is TestException) - } - } - - @Test - fun testIdempotentSelectResumeWithExceptionSuspended() { - var resumed = false - val delegate = object : Continuation { - override val context: CoroutineContext get() = EmptyCoroutineContext - override fun resumeWith(result: Result) { - check(result.exceptionOrNull() is TestException) - resumed = true - } - } - val c = SelectBuilderImpl(delegate) - check(c.getResult() === COROUTINE_SUSPENDED) // suspend first - check(!c.isSelected) - check(c.trySelect("SELECT")) - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - check(!resumed) - c.completion.resumeWithException(TestException()) - check(resumed) - check(c.isSelected) - check(!c.trySelect("OTHER")) - check(c.trySelect("SELECT")) - } -} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt index 6072cc2cbb..e84514ea5b 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.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-2019 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 @@ -429,7 +429,41 @@ class SelectRendezvousChannelTest : TestBase() { // only for debugging internal fun SelectBuilder.default(block: suspend () -> R) { this as SelectBuilderImpl // type assertion - if (!trySelect(null)) return + if (!trySelect()) return block.startCoroutineUnintercepted(this) } + + @Test + fun testSelectSendAndReceive() = runTest { + val c = Channel() + assertFailsWith { + select { + c.onSend(1) { expectUnreached() } + c.onReceive { expectUnreached() } + } + } + checkNotBroken(c) + } + + @Test + fun testSelectReceiveAndSend() = runTest { + val c = Channel() + assertFailsWith { + select { + c.onReceive { expectUnreached() } + c.onSend(1) { expectUnreached() } + } + } + checkNotBroken(c) + } + + // makes sure the channel is not broken + private suspend fun checkNotBroken(c: Channel) { + coroutineScope { + launch { + c.send(42) + } + assertEquals(42, c.receive()) + } + } } diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index 6050901058..7adc7a7865 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -96,42 +96,41 @@ public actual open class AddLastDesc actual constructor( actual val queue: Node, actual val node: T ) : AbstractAtomicDesc() { - protected override val affectedNode: Node get() = queue._prev - protected actual override fun onPrepare(affected: Node, next: Node): Any? = null - protected override fun onComplete() = queue.addLast(node) - protected actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit + override val affectedNode: Node get() = queue._prev + actual override fun finishPrepare(prepareOp: PrepareOp) {} + override fun onComplete() = queue.addLast(node) + actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit } /** @suppress **This is unstable API and it is subject to change.** */ public actual open class RemoveFirstDesc actual constructor( actual val queue: LockFreeLinkedListNode ) : AbstractAtomicDesc() { - @Suppress("UNCHECKED_CAST") - public actual val result: T get() = affectedNode as T - protected override val affectedNode: Node = queue.nextNode - protected actual open fun validatePrepared(node: T): Boolean = true - protected actual final override fun onPrepare(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode): Any? { - @Suppress("UNCHECKED_CAST") - validatePrepared(affectedNode as T) - return null - } - protected override fun onComplete() { queue.removeFirstOrNull() } - protected actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit + actual val result: T get() = affectedNode as T + override val affectedNode: Node = queue.nextNode + actual override fun finishPrepare(prepareOp: PrepareOp) {} + override fun onComplete() { queue.removeFirstOrNull() } + actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit } /** @suppress **This is unstable API and it is subject to change.** */ public actual abstract class AbstractAtomicDesc : AtomicDesc() { protected abstract val affectedNode: Node - protected actual abstract fun onPrepare(affected: Node, next: Node): Any? + actual abstract fun finishPrepare(prepareOp: PrepareOp) protected abstract fun onComplete() + actual open fun onPrepare(prepareOp: PrepareOp): Any? { + finishPrepare(prepareOp) + return null + } + actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode - val next = affected._next val failure = failure(affected) if (failure != null) return failure - return onPrepare(affected, next) + @Suppress("UNCHECKED_CAST") + return onPrepare(PrepareOp(affected, this, op)) } actual final override fun complete(op: AtomicOp<*>, failure: Any?) = onComplete() @@ -140,6 +139,16 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { protected actual abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } +/** @suppress **This is unstable API and it is subject to change.** */ +public actual class PrepareOp( + actual val affected: LockFreeLinkedListNode, + actual val desc: AbstractAtomicDesc, + actual override val atomicOp: AtomicOp<*> +): OpDescriptor() { + override fun perform(affected: Any?): Any? = null + actual fun finishPrepare() {} +} + /** @suppress **This is unstable API and it is subject to change.** */ public open class LinkedListHead : LinkedListNode() { public val isEmpty get() = _next === this diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index d3d168a427..9da237dc5f 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -21,14 +21,9 @@ internal const val FAILURE = 2 @PublishedApi internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE") -@PublishedApi -internal val ALREADY_REMOVED: Any = Symbol("ALREADY_REMOVED") - @PublishedApi internal val LIST_EMPTY: Any = Symbol("LIST_EMPTY") -private val REMOVE_PREPARED: Any = Symbol("REMOVE_PREPARED") - /** @suppress **This is unstable API and it is subject to change.** */ public actual typealias RemoveFirstDesc = LockFreeLinkedListNode.RemoveFirstDesc @@ -38,6 +33,9 @@ public actual typealias AddLastDesc = LockFreeLinkedListNode.AddLastDesc /** @suppress **This is unstable API and it is subject to change.** */ public actual typealias AbstractAtomicDesc = LockFreeLinkedListNode.AbstractAtomicDesc +/** @suppress **This is unstable API and it is subject to change.** */ +public actual typealias PrepareOp = LockFreeLinkedListNode.PrepareOp + /** * Doubly-linked concurrent list node with remove support. * Based on paper @@ -298,13 +296,16 @@ public actual open class LockFreeLinkedListNode { assert { node._next.value === node && node._prev.value === node } } - final override fun takeAffectedNode(op: OpDescriptor): Node { + // Returns null when atomic op got into deadlock trying to help operation that started later + final override fun takeAffectedNode(op: OpDescriptor): Node? { while (true) { val prev = queue._prev.value as Node // this sentinel node is never removed val next = prev._next.value if (next === queue) return prev // all is good -> linked properly if (next === op) return prev // all is good -> our operation descriptor is already there if (next is OpDescriptor) { // some other operation descriptor -> help & retry + if (op.isEarlierThan(next)) + return null // RETRY_ATOMIC next.perform(prev) continue } @@ -321,12 +322,11 @@ public actual open class LockFreeLinkedListNode { override fun retry(affected: Node, next: Any): Boolean = next !== queue - protected override fun onPrepare(affected: Node, next: Node): Any? { + override fun finishPrepare(prepareOp: PrepareOp) { // Note: onPrepare must use CAS to make sure the stale invocation is not // going to overwrite the previous decision on successful preparation. // Result of CAS is irrelevant, but we must ensure that it is set when invoker completes - _affectedNode.compareAndSet(null, affected) - return null // always success + _affectedNode.compareAndSet(null, prepareOp.affected) } override fun updatedNext(affected: Node, next: Node): Any { @@ -351,7 +351,18 @@ public actual open class LockFreeLinkedListNode { @Suppress("UNCHECKED_CAST") public val result: T get() = affectedNode!! as T - final override fun takeAffectedNode(op: OpDescriptor): Node = queue.next as Node + final override fun takeAffectedNode(op: OpDescriptor): Node? { + queue._next.loop { next -> + if (next is OpDescriptor) { + if (op.isEarlierThan(next)) + return null // RETRY_ATOMIC + next.perform(queue) + } else { + return next as Node + } + } + } + final override val affectedNode: Node? get() = _affectedNode.value final override val originalNext: Node? get() = _originalNext.value @@ -359,83 +370,94 @@ public actual open class LockFreeLinkedListNode { protected override fun failure(affected: Node): Any? = if (affected === queue) LIST_EMPTY else null - // validate the resulting node (return false if it should be deleted) - protected open fun validatePrepared(node: T): Boolean = true // false means remove node & retry - final override fun retry(affected: Node, next: Any): Boolean { if (next !is Removed) return false affected.helpDelete() // must help delete, or loose lock-freedom return true } - @Suppress("UNCHECKED_CAST") - final override fun onPrepare(affected: Node, next: Node): Any? { - assert { affected !is LockFreeLinkedListHead } - if (!validatePrepared(affected as T)) return REMOVE_PREPARED - // Note: onPrepare must use CAS to make sure the stale invocation is not + override fun finishPrepare(prepareOp: PrepareOp) { + // Note: finishPrepare must use CAS to make sure the stale invocation is not // going to overwrite the previous decision on successful preparation. // Result of CAS is irrelevant, but we must ensure that it is set when invoker completes - _affectedNode.compareAndSet(null, affected) - _originalNext.compareAndSet(null, next) - return null // ok + _affectedNode.compareAndSet(null, prepareOp.affected) + _originalNext.compareAndSet(null, prepareOp.next) } final override fun updatedNext(affected: Node, next: Node): Any = next.removed() final override fun finishOnSuccess(affected: Node, next: Node) = affected.finishRemove(next) } + // This is Harris's RDCSS (Restricted Double-Compare Single Swap) operation + // It inserts "op" descriptor of when "op" status is still undecided (rolls back otherwise) + public class PrepareOp( + @JvmField val affected: Node, + @JvmField val next: Node, + @JvmField val desc: AbstractAtomicDesc + ) : OpDescriptor() { + override val atomicOp: AtomicOp<*> get() = desc.atomicOp + + // Returns REMOVE_PREPARED or null (it makes decision on any failure) + override fun perform(affected: Any?): Any? { + assert(affected === this.affected) + affected as Node // type assertion + val decision = desc.onPrepare(this) + if (decision === REMOVE_PREPARED) { + // remove element on failure -- do not mark as decided, will try another one + val removed = next.removed() + if (affected._next.compareAndSet(this, removed)) { + affected.helpDelete() + } + return REMOVE_PREPARED + } + val isDecided = 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 + } + val update: Any = if (isDecided) next else atomicOp // restore if decision was already reached + affected._next.compareAndSet(this, update) + return null + } + + public fun finishPrepare() = desc.finishPrepare(this) + + override fun toString(): String = "PrepareOp(op=$atomicOp)" + } + public abstract class AbstractAtomicDesc : AtomicDesc() { protected abstract val affectedNode: Node? protected abstract val originalNext: Node? - protected open fun takeAffectedNode(op: OpDescriptor): Node = affectedNode!! + 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 onPrepare(affected: Node, next: Node): Any? // non-null on failure protected abstract fun updatedNext(affected: Node, next: Node): Any protected abstract fun finishOnSuccess(affected: Node, next: Node) - // This is Harris's RDCSS (Restricted Double-Compare Single Swap) operation - // It inserts "op" descriptor of when "op" status is still undecided (rolls back otherwise) - private class PrepareOp( - @JvmField val next: Node, - @JvmField val op: AtomicOp, - @JvmField val desc: AbstractAtomicDesc - ) : OpDescriptor() { - override fun perform(affected: Any?): Any? { - affected as Node // type assertion - val decision = desc.onPrepare(affected, next) - if (decision != null) { - if (decision === REMOVE_PREPARED) { - // remove element on failure - val removed = next.removed() - if (affected._next.compareAndSet(this, removed)) { - affected.helpDelete() - } - } else { - // some other failure -- mark as decided - op.tryDecide(decision) - // undo preparations - affected._next.compareAndSet(this, next) - } - return decision - } - val update: Any = if (op.isDecided) next else op // restore if decision was already reached - affected._next.compareAndSet(this, update) - return null // ok - } + public abstract fun finishPrepare(prepareOp: PrepareOp) + + // non-null on failure + public open fun onPrepare(prepareOp: PrepareOp): Any? { + finishPrepare(prepareOp) + return null } @Suppress("UNCHECKED_CAST") final override fun prepare(op: AtomicOp<*>): Any? { while (true) { // lock free loop on next - val affected = takeAffectedNode(op) + val affected = takeAffectedNode(op) ?: return RETRY_ATOMIC // read its original next pointer first val next = affected._next.value // then see if already reached consensus on overall operation if (next === op) return null // already in process of operation -- all is good if (op.isDecided) return null // already decided this operation -- go to next desc if (next is OpDescriptor) { - // some other operation is in process -- help it + // some other operation is in process + // if operation in progress (preparing or prepared) has higher sequence number -- abort our preparations + if (op.isEarlierThan(next)) + return RETRY_ATOMIC next.perform(affected) continue // and retry } @@ -443,12 +465,19 @@ public actual open class LockFreeLinkedListNode { val failure = failure(affected) if (failure != null) return failure // signal failure if (retry(affected, next)) continue // retry operation - val prepareOp = PrepareOp(next as Node, op as AtomicOp, this) + val prepareOp = PrepareOp(affected, next as Node, this) if (affected._next.compareAndSet(next, prepareOp)) { // prepared -- complete preparations - val prepFail = prepareOp.perform(affected) - if (prepFail === REMOVE_PREPARED) continue // retry - return prepFail + try { + val prepFail = prepareOp.perform(affected) + if (prepFail === REMOVE_PREPARED) continue // retry + assert { prepFail == null } + return null + } catch (e: Throwable) { + // Crashed during preparation (for example IllegalStateExpception) -- undo & rethrow + affected._next.compareAndSet(prepareOp, next) + throw e + } } } } diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt index af2de24e1a..20c2b5308c 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt @@ -122,6 +122,10 @@ class LockFreeLinkedListAtomicLFStressTest : TestBase() { val add1 = list1.describeAddLast(node1) val add2 = list2.describeAddLast(node2) val op = object : AtomicOp() { + init { + add1.atomicOp = this + add2.atomicOp = this + } override fun prepare(affected: Any?): Any? = add1.prepare(this) ?: add2.prepare(this) @@ -145,6 +149,10 @@ class LockFreeLinkedListAtomicLFStressTest : TestBase() { val remove1 = list1.describeRemoveFirst() val remove2 = list2.describeRemoveFirst() val op = object : AtomicOp() { + init { + remove1.atomicOp = this + remove2.atomicOp = this + } override fun prepare(affected: Any?): Any? = remove1.prepare(this) ?: remove2.prepare(this) diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt index 9238681e8b..9de11f792e 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt @@ -63,6 +63,9 @@ class LockFreeLinkedListTest { private fun single(part: AtomicDesc) { val operation = object : AtomicOp() { + init { + part.atomicOp = this + } override fun prepare(affected: Any?): Any? = part.prepare(this) override fun complete(affected: Any?, failure: Any?) = part.complete(this, failure) } diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt b/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt index 380ec5e84d..200cdc09b0 100644 --- a/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/selects/SelectChannelStressTest.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.selects @@ -70,7 +70,7 @@ class SelectChannelStressTest: TestBase() { internal fun SelectBuilder.default(block: suspend () -> R) { this as SelectBuilderImpl // type assertion - if (!trySelect(null)) return + if (!trySelect()) return block.startCoroutineUnintercepted(this) } } diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockLFStressTest.kt new file mode 100644 index 0000000000..4497bec5b9 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockLFStressTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.selects + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import org.junit.Ignore +import org.junit.Test +import kotlin.math.* +import kotlin.test.* + +/** + * A stress-test on lock-freedom of select sending/receiving into opposite channels. + */ +class SelectDeadlockLFStressTest : TestBase() { + private val env = LockFreedomTestEnvironment("SelectDeadlockLFStressTest", allowSuspendedThreads = 1) + private val nSeconds = 5 * stressTestMultiplier + + private val c1 = Channel() + private val c2 = Channel() + + @Test + fun testLockFreedom() = testScenarios( + "s1r2", + "s2r1", + "r1s2", + "r2s1" + ) + + private fun testScenarios(vararg scenarios: String) { + env.onCompletion { + c1.cancel(TestCompleted()) + c2.cancel(TestCompleted()) + } + val t = scenarios.mapIndexed { i, scenario -> + val idx = i + 1L + TestDef(idx, "$idx [$scenario]", scenario) + } + t.forEach { it.test() } + env.performTest(nSeconds) { + t.forEach { println(it) } + } + } + + private inner class TestDef( + var sendIndex: Long = 0L, + val name: String, + scenario: String + ) { + var receiveIndex = 0L + + val clauses: List.() -> Unit> = ArrayList.() -> Unit>().apply { + require(scenario.length % 2 == 0) + for (i in scenario.indices step 2) { + val ch = when (val c = scenario[i + 1]) { + '1' -> c1 + '2' -> c2 + else -> error("Channel '$c'") + } + val clause = when (val op = scenario[i]) { + 's' -> fun SelectBuilder.() { sendClause(ch) } + 'r' -> fun SelectBuilder.() { receiveClause(ch) } + else -> error("Operation '$op'") + } + add(clause) + } + } + + fun test() = env.testThread(name) { + doSendReceive() + } + + private suspend fun doSendReceive() { + try { + select { + for (clause in clauses) clause() + } + } catch (e: TestCompleted) { + assertTrue(env.isCompleted) + } + } + + private fun SelectBuilder.sendClause(c: Channel) = + c.onSend(sendIndex) { + sendIndex += 4L + } + + private fun SelectBuilder.receiveClause(c: Channel) = + c.onReceive { i -> + receiveIndex = max(i, receiveIndex) + } + + override fun toString(): String = "$name: send=$sendIndex, received=$receiveIndex" + } + + private class TestCompleted : CancellationException() +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockStressTest.kt b/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockStressTest.kt new file mode 100644 index 0000000000..d8d4b228c4 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/selects/SelectDeadlockStressTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.selects + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +/** + * A simple stress-test that does select sending/receiving into opposite channels to ensure that they + * don't deadlock. See https://github.com/Kotlin/kotlinx.coroutines/issues/504 + */ +class SelectDeadlockStressTest : TestBase() { + private val pool = newFixedThreadPoolContext(2, "SelectDeadlockStressTest") + private val nSeconds = 3 * stressTestMultiplier + + @After + fun tearDown() { + pool.close() + } + + @Test + fun testStress() = runTest { + val c1 = Channel() + val c2 = Channel() + val s1 = Stats() + val s2 = Stats() + launchSendReceive(c1, c2, s1) + launchSendReceive(c2, c1, s2) + for (i in 1..nSeconds) { + delay(1000) + println("$i: First: $s1; Second: $s2") + } + coroutineContext.cancelChildren() + } + + private class Stats { + var sendIndex = 0L + var receiveIndex = 0L + + override fun toString(): String = "send=$sendIndex, received=$receiveIndex" + } + + private fun CoroutineScope.launchSendReceive(c1: Channel, c2: Channel, s: Stats) = launch(pool) { + while (true) { + if (s.sendIndex % 1000 == 0L) yield() + select { + c1.onSend(s.sendIndex) { + s.sendIndex++ + } + c2.onReceive { i -> + assertEquals(s.receiveIndex, i) + s.receiveIndex++ + } + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt index 07fe1a06c7..bcdd0e8377 100644 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt @@ -94,42 +94,41 @@ public actual open class AddLastDesc actual constructor( actual val queue: Node, actual val node: T ) : AbstractAtomicDesc() { - protected override val affectedNode: Node get() = queue._prev - protected actual override fun onPrepare(affected: Node, next: Node): Any? = null - protected override fun onComplete() = queue.addLast(node) - protected actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit + override val affectedNode: Node get() = queue._prev + actual override fun finishPrepare(prepareOp: PrepareOp) {} + override fun onComplete() = queue.addLast(node) + actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit } /** @suppress **This is unstable API and it is subject to change.** */ public actual open class RemoveFirstDesc actual constructor( actual val queue: LockFreeLinkedListNode ) : AbstractAtomicDesc() { - @Suppress("UNCHECKED_CAST") - public actual val result: T get() = affectedNode as T - protected override val affectedNode: Node = queue.nextNode - protected actual open fun validatePrepared(node: T): Boolean = true - protected actual final override fun onPrepare(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode): Any? { - @Suppress("UNCHECKED_CAST") - validatePrepared(affectedNode as T) - return null - } - protected override fun onComplete() { queue.removeFirstOrNull() } - protected actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit + actual val result: T get() = affectedNode as T + override val affectedNode: Node = queue.nextNode + actual override fun finishPrepare(prepareOp: PrepareOp) {} + override fun onComplete() { queue.removeFirstOrNull() } + actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit } /** @suppress **This is unstable API and it is subject to change.** */ public actual abstract class AbstractAtomicDesc : AtomicDesc() { protected abstract val affectedNode: Node - protected actual abstract fun onPrepare(affected: Node, next: Node): Any? + actual abstract fun finishPrepare(prepareOp: PrepareOp) protected abstract fun onComplete() + actual open fun onPrepare(prepareOp: PrepareOp): Any? { + finishPrepare(prepareOp) + return null + } + actual final override fun prepare(op: AtomicOp<*>): Any? { val affected = affectedNode - val next = affected._next val failure = failure(affected) if (failure != null) return failure - return onPrepare(affected, next) + @Suppress("UNCHECKED_CAST") + return onPrepare(PrepareOp(affected, this, op)) } actual final override fun complete(op: AtomicOp<*>, failure: Any?) = onComplete() @@ -138,6 +137,16 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { protected actual abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } +/** @suppress **This is unstable API and it is subject to change.** */ +public actual class PrepareOp( + actual val affected: LockFreeLinkedListNode, + actual val desc: AbstractAtomicDesc, + actual override val atomicOp: AtomicOp<*> +): OpDescriptor() { + override fun perform(affected: Any?): Any? = null + actual fun finishPrepare() {} +} + /** @suppress **This is unstable API and it is subject to change.** */ public open class LinkedListHead : LinkedListNode() { public val isEmpty get() = _next === this From 8f39109882a87a1ee2a57c1ec0ae9453041fc0f7 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 24 Sep 2019 10:54:47 +0300 Subject: [PATCH 21/90] Replace all volatiles with atomics in common code In preparation to native multithreading. --- kotlinx-coroutines-core/common/src/Await.kt | 16 +++---- .../common/src/CancellableContinuationImpl.kt | 6 ++- .../common/src/EventLoop.common.kt | 9 ++-- .../common/src/JobSupport.kt | 43 ++++++++++++------- .../src/channels/ArrayBroadcastChannel.kt | 30 ++++++++----- .../common/src/channels/ArrayChannel.kt | 9 ++-- .../common/src/selects/Select.kt | 6 ++- 7 files changed, 74 insertions(+), 45 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Await.kt b/kotlinx-coroutines-core/common/src/Await.kt index b8dc2ac90f..3da0ad5ee9 100644 --- a/kotlinx-coroutines-core/common/src/Await.kt +++ b/kotlinx-coroutines-core/common/src/Await.kt @@ -1,12 +1,11 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 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.* -import kotlin.jvm.* /** * Awaits for completion of given deferred values without blocking a thread and resumes normally with the list of values @@ -62,7 +61,7 @@ private class AwaitAll(private val deferreds: Array>) { suspend fun await(): List = suspendCancellableCoroutine { cont -> // Intricate dance here // Step 1: Create nodes and install them as completion handlers, they may fire! - val nodes = Array(deferreds.size) { i -> + val nodes = Array(deferreds.size) { i -> val deferred = deferreds[i] deferred.start() // To properly await lazily started deferreds AwaitAllNode(cont, deferred).apply { @@ -72,7 +71,7 @@ private class AwaitAll(private val deferreds: Array>) { val disposer = DisposeHandlersOnCancel(nodes) // Step 2: Set disposer to each node nodes.forEach { it.disposer = disposer } - // Here we know that if any code the nodes complete, it will dipsose the rest + // Here we know that if any code the nodes complete, it will dispose the rest // Step 3: Now we can check if continuation is complete if (cont.isCompleted) { // it is already complete while handlers were being installed -- dispose them all @@ -94,8 +93,10 @@ private class AwaitAll(private val deferreds: Array>) { private inner class AwaitAllNode(private val continuation: CancellableContinuation>, job: Job) : JobNode(job) { lateinit var handle: DisposableHandle - @Volatile - var disposer: DisposeHandlersOnCancel? = null + private val _disposer = atomic(null) + var disposer: DisposeHandlersOnCancel? + get() = _disposer.value + set(value) { _disposer.value = value } override fun invoke(cause: Throwable?) { if (cause != null) { @@ -103,9 +104,8 @@ private class AwaitAll(private val deferreds: Array>) { if (token != null) { continuation.completeResume(token) // volatile read of disposer AFTER continuation is complete - val disposer = this.disposer // and if disposer was already set (all handlers where already installed, then dispose them all) - if (disposer != null) disposer.disposeAll() + disposer?.disposeAll() } } else if (notCompletedCount.decrementAndGet() == 0) { continuation.resume(deferreds.map { it.getCompleted() }) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 40344c9c17..bbd2ea74bf 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -63,8 +63,10 @@ internal open class CancellableContinuationImpl( */ private val _state = atomic(Active) - @Volatile - private var parentHandle: DisposableHandle? = null + private val _parentHandle = atomic(null) + private var parentHandle: DisposableHandle? + get() = _parentHandle.value + set(value) { _parentHandle.value = value } internal val state: Any? get() = _state.value diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt index 710705987c..a4984b55e5 100644 --- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt +++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt @@ -182,15 +182,16 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { // Allocated only only once private val _delayed = atomic(null) - @Volatile - private var isCompleted = false + private val _isCompleted = atomic(false) + private var isCompleted + get() = _isCompleted.value + set(value) { _isCompleted.value = value } override val isEmpty: Boolean get() { if (!isUnconfinedQueueEmpty) return false val delayed = _delayed.value if (delayed != null && !delayed.isEmpty) return false - val queue = _queue.value - return when (queue) { + return when (val queue = _queue.value) { null -> true is Queue<*> -> queue.isEmpty else -> queue === CLOSED_EMPTY diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index ca38cc156f..e2fe3697c6 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -127,9 +127,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // Note: use shared objects while we have no listeners private val _state = atomic(if (active) EMPTY_ACTIVE else EMPTY_NEW) - @Volatile - @JvmField - internal var parentHandle: ChildHandle? = null + private val _parentHandle = atomic(null) + internal var parentHandle: ChildHandle? + get() = _parentHandle.value + set(value) { _parentHandle.value = value } // ------------ initialization ------------ @@ -1019,23 +1020,33 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren @Suppress("UNCHECKED_CAST") private class Finishing( override val list: NodeList, - @Volatile - @JvmField var isCompleting: Boolean, - @Volatile - @JvmField var rootCause: Throwable? // NOTE: rootCause is kept even when SEALED + isCompleting: Boolean, + rootCause: Throwable? ) : SynchronizedObject(), Incomplete { - @Volatile - private var _exceptionsHolder: Any? = null // Contains null | Throwable | ArrayList | SEALED + private val _isCompleting = atomic(isCompleting) + var isCompleting: Boolean + get() = _isCompleting.value + set(value) { _isCompleting.value = value } + + private val _rootCause = atomic(rootCause) + var rootCause: Throwable? // NOTE: rootCause is kept even when SEALED + get() = _rootCause.value + set(value) { _rootCause.value = value } + + private val _exceptionsHolder = atomic(null) + private var exceptionsHolder: Any? // Contains null | Throwable | ArrayList | SEALED + get() = _exceptionsHolder.value + set(value) { _exceptionsHolder.value = value } // NotE: cannot be modified when sealed - val isSealed: Boolean get() = _exceptionsHolder === SEALED + val isSealed: Boolean get() = exceptionsHolder === SEALED val isCancelling: Boolean get() = rootCause != null override val isActive: Boolean get() = rootCause == null // !isCancelling // Seals current state and returns list of exceptions // guarded by `synchronized(this)` fun sealLocked(proposedException: Throwable?): List { - val list = when(val eh = _exceptionsHolder) { // volatile read + val list = when(val eh = exceptionsHolder) { // volatile read null -> allocateList() is Throwable -> allocateList().also { it.add(eh) } is ArrayList<*> -> eh as ArrayList @@ -1044,7 +1055,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren val rootCause = this.rootCause // volatile read rootCause?.let { list.add(0, it) } // note -- rootCause goes to the beginning if (proposedException != null && proposedException != rootCause) list.add(proposedException) - _exceptionsHolder = SEALED + exceptionsHolder = SEALED return list } @@ -1056,11 +1067,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren return } if (exception === rootCause) return // nothing to do - when (val eh = _exceptionsHolder) { // volatile read - null -> _exceptionsHolder = exception + when (val eh = exceptionsHolder) { // volatile read + null -> exceptionsHolder = exception is Throwable -> { if (exception === eh) return // nothing to do - _exceptionsHolder = allocateList().apply { + exceptionsHolder = allocateList().apply { add(eh) add(exception) @@ -1074,7 +1085,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren private fun allocateList() = ArrayList(4) override fun toString(): String = - "Finishing[cancelling=$isCancelling, completing=$isCompleting, rootCause=$rootCause, exceptions=$_exceptionsHolder, list=$list]" + "Finishing[cancelling=$isCancelling, completing=$isCompleting, rootCause=$rootCause, exceptions=$exceptionsHolder, list=$list]" } private val Incomplete.isCancelling: Boolean diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index d70a56c33b..c9fb3cc74d 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -4,10 +4,10 @@ package kotlinx.coroutines.channels +import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* -import kotlin.jvm.* /** * Broadcast channel with array buffer of a fixed [capacity]. @@ -44,12 +44,21 @@ internal class ArrayBroadcastChannel( // head & tail are Long (64 bits) and we assume that they never wrap around // head, tail, and size are guarded by bufferLock - @Volatile - private var head: Long = 0 // do modulo on use of head - @Volatile - private var tail: Long = 0 // do modulo on use of tail - @Volatile - private var size: Int = 0 + + private val _head = atomic(0L) + private var head: Long // do modulo on use of head + get() = _head.value + set(value) { _head.value = value } + + private val _tail = atomic(0L) + private var tail: Long // do modulo on use of tail + get() = _tail.value + set(value) { _tail.value = value } + + private val _size = atomic(0) + private var size: Int + get() = _size.value + set(value) { _size.value = value } private val subscribers = subscriberList>() @@ -199,9 +208,10 @@ internal class ArrayBroadcastChannel( ) : AbstractChannel(), ReceiveChannel { private val subLock = ReentrantLock() - @Volatile - @JvmField - var subHead: Long = 0 // guarded by subLock + private val _subHead = atomic(0L) + var subHead: Long // guarded by subLock + get() = _subHead.value + set(value) { _subHead.value = value } override val isBufferAlwaysEmpty: Boolean get() = false override val isBufferEmpty: Boolean get() = subHead >= broadcastChannel.tail diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index d643610375..0b850b27e9 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -4,10 +4,10 @@ package kotlinx.coroutines.channels +import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* -import kotlin.jvm.* import kotlin.math.* /** @@ -36,8 +36,11 @@ internal open class ArrayChannel( */ private var buffer: Array = arrayOfNulls(min(capacity, 8)) private var head: Int = 0 - @Volatile - private var size: Int = 0 // Invariant: size <= capacity + + private val _size = atomic(0) + private var size: Int // Invariant: size <= capacity + get() = _size.value + set(value) { _size.value = value } protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = size == 0 diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index eb793f4840..5318d14757 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -232,8 +232,10 @@ internal class SelectBuilderImpl( private val _result = atomic(UNDECIDED) // cancellability support - @Volatile - private var parentHandle: DisposableHandle? = null + private val _parentHandle = atomic(null) + private var parentHandle: DisposableHandle? + get() = _parentHandle.value + set(value) { _parentHandle.value = value } /* Result state machine From ee04bd2a665f77ac7fc89217aee7df4195f9cab3 Mon Sep 17 00:00:00 2001 From: Tyson Henning Date: Tue, 3 Sep 2019 16:36:40 -0700 Subject: [PATCH 22/90] Hid the SettableFuture of future {}. This should prevent successful casts to type SettableFuture, meaning client code can't access and complete the internal Future without resorting to reflection.. --- integration/kotlinx-coroutines-guava/src/ListenableFuture.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt index d247e82ca5..e502ff464f 100644 --- a/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt +++ b/integration/kotlinx-coroutines-guava/src/ListenableFuture.kt @@ -46,14 +46,14 @@ public fun CoroutineScope.future( ): ListenableFuture { require(!start.isLazy) { "$start start is not supported" } val newContext = newCoroutineContext(context) - // TODO: It'd be nice not to leak this SettableFuture reference, which is easily blind-cast. val future = SettableFuture.create() val coroutine = ListenableFutureCoroutine(newContext, future) future.addListener( coroutine, MoreExecutors.directExecutor()) coroutine.start(start, coroutine, block) - return future + // Return hides the SettableFuture. This should prevent casting. + return object: ListenableFuture by future {} } /** From 946e5789fa982e397d49b788d308afdc5d44ccce Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 25 Sep 2019 18:02:24 +0300 Subject: [PATCH 23/90] Introduce reusable cancellable continuations for hot loops with channels (#1534) * Average performance improvement is around 25% * API is internal and targeted to specific usage * DispatchedTask and DispatchedContinuation are extracted to separate files for better readability and maintainability * Ensure ConsumeAsFlow does not retain reference to the last element of the flow with test --- .../CancellableContinuationBenchmark.kt | 53 ---- .../benchmarks/flow/NumbersBenchmark.kt | 29 +- .../benchmarks/tailcall/SimpleChannel.kt | 98 +++++++ .../tailcall/SimpleChannelBenchmark.kt | 61 ++++ .../common/src/CancellableContinuation.kt | 34 +++ .../common/src/CancellableContinuationImpl.kt | 90 +++++- .../common/src/CoroutineDispatcher.kt | 5 + .../common/src/JobSupport.kt | 2 +- .../common/src/channels/AbstractChannel.kt | 6 +- .../src/internal/DispatchedContinuation.kt | 275 ++++++++++++++++++ .../DispatchedTask.kt} | 254 ++++------------ .../common/src/sync/Mutex.kt | 2 +- .../common/src/sync/Semaphore.kt | 2 +- .../jvm/test/FieldWalker.kt | 115 ++++++++ .../ReusableCancellableContinuationTest.kt | 195 +++++++++++++ .../channels/ChannelAtomicCancelStressTest.kt | 16 +- .../test/channels/ChannelSelectStressTest.kt | 70 +++++ .../test/exceptions/StackTraceRecoveryTest.kt | 3 +- .../jvm/test/flow/ConsumeAsFlowLeakTest.kt | 48 +++ .../test/SanitizedProbesTest.kt | 4 +- 20 files changed, 1052 insertions(+), 310 deletions(-) delete mode 100644 benchmarks/src/jmh/kotlin/benchmarks/CancellableContinuationBenchmark.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt create mode 100644 kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt rename kotlinx-coroutines-core/common/src/{Dispatched.kt => internal/DispatchedTask.kt} (58%) create mode 100644 kotlinx-coroutines-core/jvm/test/FieldWalker.kt create mode 100644 kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/channels/ChannelSelectStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/flow/ConsumeAsFlowLeakTest.kt diff --git a/benchmarks/src/jmh/kotlin/benchmarks/CancellableContinuationBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/CancellableContinuationBenchmark.kt deleted file mode 100644 index 99c0f04902..0000000000 --- a/benchmarks/src/jmh/kotlin/benchmarks/CancellableContinuationBenchmark.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package benchmarks - -import kotlinx.coroutines.* -import org.openjdk.jmh.annotations.* -import java.util.concurrent.* -import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* - -@Warmup(iterations = 5) -@Measurement(iterations = 10) -@BenchmarkMode(Mode.Throughput) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -@State(Scope.Benchmark) -@Fork(2) -open class CancellableContinuationBenchmark { - - @Benchmark - fun awaitWithSuspension(): Int { - val deferred = CompletableDeferred() - return run(allowSuspend = true) { deferred.await() } - } - - @Benchmark - fun awaitNoSuspension(): Int { - val deferred = CompletableDeferred(1) - return run { deferred.await() } - } - - private fun run(allowSuspend: Boolean = false, block: suspend () -> Int): Int { - val value = block.startCoroutineUninterceptedOrReturn(EmptyContinuation) - if (value === COROUTINE_SUSPENDED) { - if (!allowSuspend) { - throw IllegalStateException("Unexpected suspend") - } else { - return -1 - } - } - - return value as Int - } - - object EmptyContinuation : Continuation { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result) { - } - } -} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt index e037069d22..e0bc2fcc48 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/NumbersBenchmark.kt @@ -13,22 +13,6 @@ import kotlinx.coroutines.flow.* import org.openjdk.jmh.annotations.* import java.util.concurrent.* -/* - * Results: - * - * // Throw FlowAborted overhead - * Numbers.primes avgt 7 3039.185 ± 25.598 us/op - * Numbers.primesRx avgt 7 2677.937 ± 17.720 us/op - * - * // On par - * Numbers.transformations avgt 7 16.207 ± 0.133 us/op - * Numbers.transformationsRx avgt 7 19.626 ± 0.135 us/op - * - * // Channels overhead - * Numbers.zip avgt 7 434.160 ± 7.014 us/op - * Numbers.zipRx avgt 7 87.898 ± 5.007 us/op - * - */ @Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1) @@ -39,11 +23,11 @@ open class NumbersBenchmark { companion object { private const val primes = 100 - private const val natural = 1000 + private const val natural = 1000L } - private fun numbers() = flow { - for (i in 2L..Long.MAX_VALUE) emit(i) + private fun numbers(limit: Long = Long.MAX_VALUE) = flow { + for (i in 2L..limit) emit(i) } private fun primesFlow(): Flow = flow { @@ -80,7 +64,7 @@ open class NumbersBenchmark { @Benchmark fun zip() = runBlocking { - val numbers = numbers().take(natural) + val numbers = numbers(natural) val first = numbers .filter { it % 2L != 0L } .map { it * it } @@ -105,8 +89,7 @@ open class NumbersBenchmark { @Benchmark fun transformations(): Int = runBlocking { - numbers() - .take(natural) + numbers(natural) .filter { it % 2L != 0L } .map { it * it } .filter { (it + 1) % 3 == 0L }.count() @@ -120,4 +103,4 @@ open class NumbersBenchmark { .filter { (it + 1) % 3 == 0L }.count() .blockingGet() } -} +} \ No newline at end of file diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt new file mode 100644 index 0000000000..ee1ef724cb --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannel.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.tailcall + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +public abstract class SimpleChannel { + companion object { + const val NULL_SURROGATE: Int = -1 + } + + @JvmField + protected var producer: Continuation? = null + @JvmField + protected var enqueuedValue: Int = NULL_SURROGATE + @JvmField + protected var consumer: Continuation? = null + + suspend fun send(element: Int) { + require(element != NULL_SURROGATE) + if (offer(element)) { + return + } + + return suspendSend(element) + } + + private fun offer(element: Int): Boolean { + if (consumer == null) { + return false + } + + consumer!!.resume(element) + consumer = null + return true + } + + suspend fun receive(): Int { + // Cached value + if (enqueuedValue != NULL_SURROGATE) { + val result = enqueuedValue + enqueuedValue = NULL_SURROGATE + producer!!.resume(Unit) + return result + } + + return suspendReceive() + } + + abstract suspend fun suspendReceive(): Int + abstract suspend fun suspendSend(element: Int) +} + +class NonCancellableChannel : SimpleChannel() { + override suspend fun suspendReceive(): Int = suspendCoroutineUninterceptedOrReturn { + consumer = it.intercepted() + COROUTINE_SUSPENDED + } + + override suspend fun suspendSend(element: Int) = suspendCoroutineUninterceptedOrReturn { + enqueuedValue = element + producer = it.intercepted() + COROUTINE_SUSPENDED + } +} + +class CancellableChannel : SimpleChannel() { + override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutine { + consumer = it.intercepted() + COROUTINE_SUSPENDED + } + + override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutine { + enqueuedValue = element + producer = it.intercepted() + COROUTINE_SUSPENDED + } +} + +class CancellableReusableChannel : SimpleChannel() { + @Suppress("INVISIBLE_MEMBER") + override suspend fun suspendReceive(): Int = suspendAtomicCancellableCoroutineReusable { + consumer = it.intercepted() + COROUTINE_SUSPENDED + } + + @Suppress("INVISIBLE_MEMBER") + override suspend fun suspendSend(element: Int) = suspendAtomicCancellableCoroutineReusable { + enqueuedValue = element + producer = it.intercepted() + COROUTINE_SUSPENDED + } +} \ No newline at end of file diff --git a/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt new file mode 100644 index 0000000000..09ff7697f6 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/tailcall/SimpleChannelBenchmark.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.tailcall + +import kotlinx.coroutines.* +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.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +open class SimpleChannelBenchmark { + + private val iterations = 10_000 + + @Volatile + private var sink: Int = 0 + + @Benchmark + fun cancellable() = runBlocking { + val ch = CancellableChannel() + launch { + repeat(iterations) { ch.send(it) } + } + + launch { + repeat(iterations) { sink = ch.receive() } + } + } + + @Benchmark + fun cancellableReusable() = runBlocking { + val ch = CancellableReusableChannel() + launch { + repeat(iterations) { ch.send(it) } + } + + launch { + repeat(iterations) { sink = ch.receive() } + } + } + + @Benchmark + fun nonCancellable() = runBlocking { + val ch = NonCancellableChannel() + launch { + repeat(iterations) { ch.send(it) } + } + + launch { + repeat(iterations) { + sink = ch.receive() + } + } + } +} diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 492e367bb0..d1e99529a5 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -223,6 +223,40 @@ public suspend inline fun suspendAtomicCancellableCoroutine( cancellable.getResult() } +/** + * Suspends coroutine similar to [suspendAtomicCancellableCoroutine], but an instance of [CancellableContinuationImpl] is reused if possible. + */ +internal suspend inline fun suspendAtomicCancellableCoroutineReusable( + crossinline block: (CancellableContinuation) -> Unit +): T = suspendCoroutineUninterceptedOrReturn { uCont -> + val cancellable = getOrCreateCancellableContinuation(uCont.intercepted()) + block(cancellable) + cancellable.getResult() + } + +internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl { + // If used outside of our dispatcher + if (delegate !is DispatchedContinuation) { + return CancellableContinuationImpl(delegate, resumeMode = MODE_ATOMIC_DEFAULT) + } + /* + * Attempt to claim reusable instance. + * + * suspendAtomicCancellableCoroutineReusable { // <- claimed + * // Any asynchronous cancellation is "postponed" while this block + * // is being executed + * } // postponed cancellation is checked here. + * + * Claim can fail for the following reasons: + * 1) Someone tried to make idempotent resume. + * Idempotent resume is internal (used only by us) and is used only in `select`, + * 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) +} + /** * @suppress **Deprecated** */ diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index bbd2ea74bf..9a19ed61e3 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -80,10 +80,33 @@ internal open class CancellableContinuationImpl( // This method does nothing. Leftover for binary compatibility with old compiled code } - // It is only invoked from an internal getResult function, so we can be sure it is not invoked twice - private fun installParentCancellationHandler() { - if (isCompleted) return // fast path 1 -- don't need to do anything if already completed - val parent = delegate.context[Job] ?: return // fast path 2 -- don't do anything without parent + private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable + + /** + * Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work. + * Invariant: used only by [suspendAtomicCancellableCoroutineReusable] in [REUSABLE_CLAIMED] state. + */ + internal fun resetState(): Boolean { + assert { parentHandle !== NonDisposableHandle } + val state = _state.value + assert { state !is NotCompleted } + if (state is CompletedIdempotentResult) { + detachChild() + return false + } + _decision.value = UNDECIDED + _state.value = Active + return true + } + + /** + * Setups parent cancellation and checks for postponed cancellation in the case of reusable continuations. + * It is only invoked from an internal [getResult] function. + */ + private fun setupCancellation() { + 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, @@ -91,12 +114,25 @@ internal open class CancellableContinuationImpl( ) parentHandle = handle // now check our state _after_ registering (could have completed while we were registering) - if (isCompleted) { + // Also note that we do not dispose parent for reusable continuations, dispatcher will do that for us + if (isCompleted && !isReusable()) { handle.dispose() // it is Ok to call dispose twice -- here and in disposeParentHandle parentHandle = NonDisposableHandle // release it just in case, to aid GC } } + private fun checkCompleted(): Boolean { + val completed = isCompleted + if (resumeMode != MODE_ATOMIC_DEFAULT) 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) { + // Note: this cancel may fail if one more concurrent cancel is currently being invoked + cancel(cause) + } + return true + } + public override val callerFrame: CoroutineStackFrame? get() = delegate as? CoroutineStackFrame @@ -112,6 +148,15 @@ 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 + val dispatched = (delegate as? DispatchedContinuation<*>) ?: return false + return dispatched.postponeCancellation(cause) + } + public override fun cancel(cause: Throwable?): Boolean { _state.loop { state -> if (state !is NotCompleted) return false // false if already complete or cancelling @@ -121,12 +166,19 @@ internal open class CancellableContinuationImpl( // Invoke cancel handler if it was present if (state is CancelHandler) invokeHandlerSafely { state.invoke(cause) } // Complete state update - disposeParentHandle() + detachChildIfNonResuable() dispatchResume(mode = MODE_ATOMIC_DEFAULT) return true } } + internal fun parentCancelled(cause: Throwable) { + if (cancelLater(cause)) return + cancel(cause) + // Even if cancellation has failed, we should detach child to avoid potential leak + detachChildIfNonResuable() + } + private inline fun invokeHandlerSafely(block: () -> Unit) { try { block() @@ -167,7 +219,7 @@ internal open class CancellableContinuationImpl( @PublishedApi internal fun getResult(): Any? { - installParentCancellationHandler() + setupCancellation() if (trySuspend()) return COROUTINE_SUSPENDED // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state val state = this.state @@ -258,7 +310,7 @@ internal open class CancellableContinuationImpl( when (state) { is NotCompleted -> { if (!_state.compareAndSet(state, proposedUpdate)) return@loop // retry on cas failure - disposeParentHandle() + detachChildIfNonResuable() dispatchResume(resumeMode) return null } @@ -280,11 +332,19 @@ internal open class CancellableContinuationImpl( } // Unregister from parent job - private fun disposeParentHandle() { - parentHandle?.let { // volatile read parentHandle (once) - it.dispose() - parentHandle = NonDisposableHandle // release it just in case, to aid GC - } + private fun detachChildIfNonResuable() { + // If instance is reusable, do not detach on every reuse, #releaseInterceptedContinuation will do it for us in the end + if (!isReusable()) detachChild() + } + + /** + * Detaches from the parent. + * Invariant: used used from [CoroutineDispatcher.releaseInterceptedContinuation] iff [isReusable] is `true` + */ + internal fun detachChild() { + val handle = parentHandle + handle?.dispose() + parentHandle = NonDisposableHandle } override fun tryResume(value: T, idempotent: Any?): Any? { @@ -294,7 +354,7 @@ internal open class CancellableContinuationImpl( val update: Any? = if (idempotent == null) value else CompletedIdempotentResult(idempotent, value, state) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - disposeParentHandle() + detachChildIfNonResuable() return state } is CompletedIdempotentResult -> { @@ -316,7 +376,7 @@ internal open class CancellableContinuationImpl( is NotCompleted -> { val update = CompletedExceptionally(exception) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure - disposeParentHandle() + detachChildIfNonResuable() return state } else -> return null // cannot resume -- not active anymore diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index df7a2daac1..f08f8f782f 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -101,6 +101,11 @@ public abstract class CoroutineDispatcher : public final override fun interceptContinuation(continuation: Continuation): Continuation = DispatchedContinuation(this, continuation) + @InternalCoroutinesApi + public override fun releaseInterceptedContinuation(continuation: Continuation<*>) { + (continuation as DispatchedContinuation<*>).reusableCancellableContinuation?.detachChild() + } + /** * @suppress **Error**: Operator '+' on two CoroutineDispatcher objects is meaningless. * CoroutineDispatcher is a coroutine context element and `+` is a set-sum operator for coroutine contexts. diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index e2fe3697c6..74e0133006 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -1426,7 +1426,7 @@ internal class ChildContinuation( @JvmField val child: CancellableContinuationImpl<*> ) : JobCancellingNode(parent) { override fun invoke(cause: Throwable?) { - child.cancel(child.getContinuationCancellationCause(job)) + child.parentCancelled(child.getContinuationCancellationCause(job)) } override fun toString(): String = "ChildContinuation[$child]" diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index a9845c5aa0..22c1971c0a 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -178,7 +178,7 @@ internal abstract class AbstractSendChannel : SendChannel { return closed.sendException } - private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutine sc@ { cont -> + private suspend fun sendSuspend(element: E): Unit = suspendAtomicCancellableCoroutineReusable sc@ { cont -> loop@ while (true) { if (full) { val send = SendElement(element, cont) @@ -559,7 +559,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutine sc@ { cont -> + private suspend fun receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val receive = ReceiveElement(cont as CancellableContinuation, receiveMode) while (true) { if (enqueueReceive(receive)) { @@ -856,7 +856,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel + private suspend fun hasNextSuspend(): Boolean = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val receive = ReceiveHasNext(this, cont) while (true) { if (channel.enqueueReceive(receive)) { diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt new file mode 100644 index 0000000000..bb5e312410 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.atomicfu.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* +import kotlin.jvm.* + +@SharedImmutable +private val UNDEFINED = Symbol("UNDEFINED") +@SharedImmutable +@JvmField +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 { + @JvmField + @Suppress("PropertyName") + internal var _state: Any? = UNDEFINED + override val callerFrame: CoroutineStackFrame? = continuation as? CoroutineStackFrame + override fun getStackTraceElement(): StackTraceElement? = null + @JvmField // pre-cached value to avoid ctx.fold on every resumption + internal val countOrElement = threadContextElements(context) + + /** + * Possible states of reusability: + * + * 1) `null`. Cancellable continuation wasn't yet attempted to be reused or + * was used and then invalidated (e.g. because of the cancellation). + * 2) [CancellableContinuation]. Continuation to be/that is being reused. + * 3) [REUSABLE_CLAIMED]. CC is currently being reused and its owner executes `suspend` block: + * ``` + * // state == null | CC + * suspendAtomicCancellableCoroutineReusable { cont -> + * // state == REUSABLE_CLAIMED + * block(cont) + * } + * // state == CC + * ``` + * 4) [Throwable] continuation was cancelled with this cause while being in [suspendAtomicCancellableCoroutineReusable], + * [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 -> + * val result = pollFastPath() + * if (result != null) cont.resume(result) + * } + * ``` + * always succeeds. + * To make it always successful, we actually postpone "reusable" cancellation + * to this phase and set cancellation only at the moment of instantiation. + */ + private val _reusableCancellableContinuation = atomic(null) + + public val reusableCancellableContinuation: CancellableContinuationImpl<*>? + get() = _reusableCancellableContinuation.value as? CancellableContinuationImpl<*> + + public val isReusable: Boolean + get() = _reusableCancellableContinuation.value != null + + /** + * Claims the continuation for [suspendAtomicCancellableCoroutineReusable] block, + * so all cancellations will be postponed. + */ + @Suppress("UNCHECKED_CAST") + fun claimReusableCancellableContinuation(): CancellableContinuationImpl? { + /* + * Transitions: + * 1) `null` -> claimed, caller will instantiate CC instance + * 2) `CC` -> claimed, caller will reuse CC instance + */ + _reusableCancellableContinuation.loop { state -> + when { + state === null -> { + /* + * null -> CC was not yet published -> we do not compete with cancel + * -> can use plain store instead of CAS + */ + _reusableCancellableContinuation.value = REUSABLE_CLAIMED + return null + } + state is CancellableContinuationImpl<*> -> { + if (_reusableCancellableContinuation.compareAndSet(state, REUSABLE_CLAIMED)) { + return state as CancellableContinuationImpl + } + } + else -> error("Inconsistent state $state") + } + } + } + + /** + * Checks whether there were any attempts to cancel reusable CC while it was in [REUSABLE_CLAIMED] state + * and returns cancellation cause if so, `null` otherwise. + * If continuation was cancelled, it becomes non-reusable. + * + * ``` + * suspendAtomicCancellableCoroutineReusable { // <- claimed + * // Any asynchronous cancellation is "postponed" while this block + * // is being executed + * } // postponed cancellation is checked here in `getResult` + * ``` + * + * See [CancellableContinuationImpl.getResult]. + */ + fun checkPostponedCancellation(continuation: CancellableContinuation<*>): Throwable? { + _reusableCancellableContinuation.loop { state -> + // not when(state) to avoid Intrinsics.equals call + when { + state === REUSABLE_CLAIMED -> { + if (_reusableCancellableContinuation.compareAndSet(REUSABLE_CLAIMED, continuation)) return null + } + state === null -> return null + state is Throwable -> { + require(_reusableCancellableContinuation.compareAndSet(state, null)) + return state + } + else -> error("Inconsistent state $state") + } + } + } + + /** + * Tries to postpone cancellation if reusable CC is currently in [REUSABLE_CLAIMED] state. + * Returns `true` if cancellation is (or previously was) postponed, `false` otherwise. + */ + fun postponeCancellation(cause: Throwable): Boolean { + _reusableCancellableContinuation.loop { state -> + when (state) { + REUSABLE_CLAIMED -> { + if (_reusableCancellableContinuation.compareAndSet(REUSABLE_CLAIMED, cause)) + return true + } + is Throwable -> return true + else -> { + // Invalidate + if (_reusableCancellableContinuation.compareAndSet(state, null)) + return false + } + } + } + } + + override fun takeState(): Any? { + val state = _state + assert { state !== UNDEFINED } // fail-fast if repeatedly invoked + _state = UNDEFINED + return state + } + + override val delegate: Continuation + get() = this + + override fun resumeWith(result: Result) { + val context = continuation.context + val state = result.toState() + if (dispatcher.isDispatchNeeded(context)) { + _state = state + resumeMode = MODE_ATOMIC_DEFAULT + dispatcher.dispatch(context, this) + } else { + executeUnconfined(state, MODE_ATOMIC_DEFAULT) { + withCoroutineContext(this.context, countOrElement) { + continuation.resumeWith(result) + } + } + } + } + + @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack + inline fun resumeCancellable(value: T) { + if (dispatcher.isDispatchNeeded(context)) { + _state = value + resumeMode = MODE_CANCELLABLE + dispatcher.dispatch(context, this) + } else { + executeUnconfined(value, MODE_CANCELLABLE) { + if (!resumeCancelled()) { + resumeUndispatched(value) + } + } + } + } + + @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack + inline fun resumeCancellableWithException(exception: Throwable) { + val context = continuation.context + val state = CompletedExceptionally(exception) + if (dispatcher.isDispatchNeeded(context)) { + _state = CompletedExceptionally(exception) + resumeMode = MODE_CANCELLABLE + dispatcher.dispatch(context, this) + } else { + executeUnconfined(state, MODE_CANCELLABLE) { + if (!resumeCancelled()) { + resumeUndispatchedWithException(exception) + } + } + } + } + + @Suppress("NOTHING_TO_INLINE") + inline fun resumeCancelled(): Boolean { + val job = context[Job] + if (job != null && !job.isActive) { + resumeWithException(job.getCancellationException()) + return true + } + + return false + } + + @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack + inline fun resumeUndispatched(value: T) { + withCoroutineContext(context, countOrElement) { + continuation.resume(value) + } + } + + @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack + inline fun resumeUndispatchedWithException(exception: Throwable) { + withCoroutineContext(context, countOrElement) { + continuation.resumeWithStackTrace(exception) + } + } + + // used by "yield" implementation + internal fun dispatchYield(value: T) { + val context = continuation.context + _state = value + resumeMode = MODE_CANCELLABLE + dispatcher.dispatchYield(context, this) + } + + override fun toString(): String = + "DispatchedContinuation[$dispatcher, ${continuation.toDebugString()}]" +} + +internal fun DispatchedContinuation.yieldUndispatched(): Boolean = + executeUnconfined(Unit, MODE_CANCELLABLE, doYield = true) { + run() + } + +/** + * Executes given [block] as part of current event loop, updating current continuation + * mode and state if continuation is not resumed immediately. + * [doYield] indicates whether current continuation is yielding (to provide fast-path if event-loop is empty). + * Returns `true` if execution of continuation was queued (trampolined) or `false` otherwise. + */ +private inline fun DispatchedContinuation<*>.executeUnconfined( + contState: Any?, mode: Int, doYield: Boolean = false, + block: () -> Unit +): Boolean { + 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 + return if (eventLoop.isUnconfinedLoopActive) { + // When unconfined loop is active -- dispatch continuation for execution to avoid stack overflow + _state = contState + resumeMode = mode + eventLoop.dispatchUnconfined(this) + true // queued into the active loop + } else { + // Was not active -- run event loop until all unconfined tasks are executed + runUnconfinedEventLoop(eventLoop, block = block) + false + } +} diff --git a/kotlinx-coroutines-core/common/src/Dispatched.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt similarity index 58% rename from kotlinx-coroutines-core/common/src/Dispatched.kt rename to kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index a9624bd86e..eb72b5a95a 100644 --- a/kotlinx-coroutines-core/common/src/Dispatched.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -8,198 +8,6 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -@Suppress("PrivatePropertyName") -@SharedImmutable -private val UNDEFINED = Symbol("UNDEFINED") - -/** - * Executes given [block] as part of current event loop, updating current continuation - * mode and state if continuation is not resumed immediately. - * [doYield] indicates whether current continuation is yielding (to provide fast-path if event-loop is empty). - * Returns `true` if execution of continuation was queued (trampolined) or `false` otherwise. - */ -private inline fun DispatchedContinuation<*>.executeUnconfined( - contState: Any?, mode: Int, doYield: Boolean = false, - block: () -> Unit -) : Boolean { - 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 - return if (eventLoop.isUnconfinedLoopActive) { - // When unconfined loop is active -- dispatch continuation for execution to avoid stack overflow - _state = contState - resumeMode = mode - eventLoop.dispatchUnconfined(this) - true // queued into the active loop - } else { - // Was not active -- run event loop until all unconfined tasks are executed - runUnconfinedEventLoop(eventLoop, block = block) - false - } -} - -private fun DispatchedTask<*>.resumeUnconfined() { - val eventLoop = ThreadLocalEventLoop.eventLoop - if (eventLoop.isUnconfinedLoopActive) { - // When unconfined loop is active -- dispatch continuation for execution to avoid stack overflow - eventLoop.dispatchUnconfined(this) - } else { - // Was not active -- run event loop until all unconfined tasks are executed - runUnconfinedEventLoop(eventLoop) { - resume(delegate, MODE_UNDISPATCHED) - } - } -} - -private inline fun DispatchedTask<*>.runUnconfinedEventLoop( - eventLoop: EventLoop, - block: () -> Unit -) { - eventLoop.incrementUseCount(unconfined = true) - try { - block() - while (true) { - // break when all unconfined continuations where executed - if (!eventLoop.processUnconfinedEvent()) break - } - } catch (e: Throwable) { - /* - * This exception doesn't happen normally, only if we have a bug in implementation. - * Report it as a fatal exception. - */ - handleFatalException(e, null) - } finally { - eventLoop.decrementUseCount(unconfined = true) - } -} - -internal class DispatchedContinuation( - @JvmField val dispatcher: CoroutineDispatcher, - @JvmField val continuation: Continuation -) : DispatchedTask(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation by continuation { - @JvmField - @Suppress("PropertyName") - internal var _state: Any? = UNDEFINED - override val callerFrame: CoroutineStackFrame? = continuation as? CoroutineStackFrame - override fun getStackTraceElement(): StackTraceElement? = null - @JvmField // pre-cached value to avoid ctx.fold on every resumption - internal val countOrElement = threadContextElements(context) - - override fun takeState(): Any? { - val state = _state - assert { state !== UNDEFINED } // fail-fast if repeatedly invoked - _state = UNDEFINED - return state - } - - override val delegate: Continuation - get() = this - - override fun resumeWith(result: Result) { - val context = continuation.context - val state = result.toState() - if (dispatcher.isDispatchNeeded(context)) { - _state = state - resumeMode = MODE_ATOMIC_DEFAULT - dispatcher.dispatch(context, this) - } else { - executeUnconfined(state, MODE_ATOMIC_DEFAULT) { - withCoroutineContext(this.context, countOrElement) { - continuation.resumeWith(result) - } - } - } - } - - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeCancellable(value: T) { - if (dispatcher.isDispatchNeeded(context)) { - _state = value - resumeMode = MODE_CANCELLABLE - dispatcher.dispatch(context, this) - } else { - executeUnconfined(value, MODE_CANCELLABLE) { - if (!resumeCancelled()) { - resumeUndispatched(value) - } - } - } - } - - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeCancellableWithException(exception: Throwable) { - val context = continuation.context - val state = CompletedExceptionally(exception) - if (dispatcher.isDispatchNeeded(context)) { - _state = CompletedExceptionally(exception) - resumeMode = MODE_CANCELLABLE - dispatcher.dispatch(context, this) - } else { - executeUnconfined(state, MODE_CANCELLABLE) { - if (!resumeCancelled()) { - resumeUndispatchedWithException(exception) - } - } - } - } - - @Suppress("NOTHING_TO_INLINE") - inline fun resumeCancelled(): Boolean { - val job = context[Job] - if (job != null && !job.isActive) { - resumeWithException(job.getCancellationException()) - return true - } - - return false - } - - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeUndispatched(value: T) { - withCoroutineContext(context, countOrElement) { - continuation.resume(value) - } - } - - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeUndispatchedWithException(exception: Throwable) { - withCoroutineContext(context, countOrElement) { - continuation.resumeWithStackTrace(exception) - } - } - - // used by "yield" implementation - internal fun dispatchYield(value: T) { - val context = continuation.context - _state = value - resumeMode = MODE_CANCELLABLE - dispatcher.dispatchYield(context, this) - } - - override fun toString(): String = - "DispatchedContinuation[$dispatcher, ${continuation.toDebugString()}]" -} - -internal fun Continuation.resumeCancellable(value: T) = when (this) { - is DispatchedContinuation -> resumeCancellable(value) - else -> resume(value) -} - -internal fun Continuation.resumeCancellableWithException(exception: Throwable) = when (this) { - is DispatchedContinuation -> resumeCancellableWithException(exception) - else -> resumeWithStackTrace(exception) -} - -internal fun Continuation.resumeDirect(value: T) = when (this) { - is DispatchedContinuation -> continuation.resume(value) - else -> resume(value) -} - -internal fun Continuation.resumeDirectWithException(exception: Throwable) = when (this) { - is DispatchedContinuation -> continuation.resumeWithStackTrace(exception) - else -> resumeWithStackTrace(exception) -} - internal abstract class DispatchedTask( @JvmField public var resumeMode: Int ) : SchedulerTask() { @@ -281,11 +89,6 @@ internal abstract class DispatchedTask( } } -internal fun DispatchedContinuation.yieldUndispatched(): Boolean = - executeUnconfined(Unit, MODE_CANCELLABLE, doYield = true) { - run() - } - internal fun DispatchedTask.dispatch(mode: Int = MODE_CANCELLABLE) { val delegate = this.delegate if (mode.isDispatchedMode && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { @@ -320,6 +123,61 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In } } +private fun DispatchedTask<*>.resumeUnconfined() { + val eventLoop = ThreadLocalEventLoop.eventLoop + if (eventLoop.isUnconfinedLoopActive) { + // When unconfined loop is active -- dispatch continuation for execution to avoid stack overflow + eventLoop.dispatchUnconfined(this) + } else { + // Was not active -- run event loop until all unconfined tasks are executed + runUnconfinedEventLoop(eventLoop) { + resume(delegate, MODE_UNDISPATCHED) + } + } +} + +internal inline fun DispatchedTask<*>.runUnconfinedEventLoop( + eventLoop: EventLoop, + block: () -> Unit +) { + eventLoop.incrementUseCount(unconfined = true) + try { + block() + while (true) { + // break when all unconfined continuations where executed + if (!eventLoop.processUnconfinedEvent()) break + } + } catch (e: Throwable) { + /* + * This exception doesn't happen normally, only if we have a bug in implementation. + * Report it as a fatal exception. + */ + handleFatalException(e, null) + } finally { + eventLoop.decrementUseCount(unconfined = true) + } +} + + +internal fun Continuation.resumeCancellable(value: T) = when (this) { + is DispatchedContinuation -> resumeCancellable(value) + else -> resume(value) +} + +internal fun Continuation.resumeCancellableWithException(exception: Throwable) = when (this) { + is DispatchedContinuation -> resumeCancellableWithException(exception) + else -> resumeWithStackTrace(exception) +} + +internal fun Continuation.resumeDirect(value: T) = when (this) { + is DispatchedContinuation -> continuation.resume(value) + else -> resume(value) +} + +internal fun Continuation.resumeDirectWithException(exception: Throwable) = when (this) { + is DispatchedContinuation -> continuation.resumeWithStackTrace(exception) + else -> resumeWithStackTrace(exception) +} @Suppress("NOTHING_TO_INLINE") internal inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) { diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index f82d6ca8ff..ea4a510775 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -187,7 +187,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { return lockSuspend(owner) } - private suspend fun lockSuspend(owner: Any?) = suspendAtomicCancellableCoroutine sc@ { cont -> + private suspend fun lockSuspend(owner: Any?) = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val waiter = LockCont(owner, cont) _state.loop { state -> when (state) { diff --git a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt index a9df15cf49..b6ebc501ff 100644 --- a/kotlinx-coroutines-core/common/src/sync/Semaphore.kt +++ b/kotlinx-coroutines-core/common/src/sync/Semaphore.kt @@ -131,7 +131,7 @@ private class SemaphoreImpl( cur + 1 } - private suspend fun addToQueueAndSuspend() = suspendAtomicCancellableCoroutine sc@ { cont -> + private suspend fun addToQueueAndSuspend() = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val last = this.tail val enqIdx = enqIdx.getAndIncrement() val segment = getSegment(last, enqIdx / SEGMENT_SIZE) diff --git a/kotlinx-coroutines-core/jvm/test/FieldWalker.kt b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt new file mode 100644 index 0000000000..bb8b855498 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/FieldWalker.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import java.lang.reflect.* +import java.util.* +import java.util.Collections.* +import kotlin.collections.ArrayList + +object FieldWalker { + + /* + * Reflectively starts to walk through object graph and returns identity set of all reachable objects. + */ + public fun walk(root: Any): Set { + val result = newSetFromMap(IdentityHashMap()) + result.add(root) + val stack = ArrayDeque() + stack.addLast(root) + while (stack.isNotEmpty()) { + val element = stack.removeLast() + val type = element.javaClass + type.visit(element, result, stack) + } + return result + } + + private fun Class<*>.visit( + element: Any, + result: MutableSet, + stack: ArrayDeque + ) { + val fields = fields() + fields.forEach { + it.isAccessible = true + val value = it.get(element) ?: return@forEach + if (result.add(value)) { + stack.addLast(value) + } + } + + if (isArray && !componentType.isPrimitive) { + val array = element as Array + array.filterNotNull().forEach { + if (result.add(it)) { + stack.addLast(it) + } + } + } + } + + private fun Class<*>.fields(): List { + val result = ArrayList() + var type = this + while (type != Any::class.java) { + val fields = type.declaredFields.filter { + !it.type.isPrimitive + && !Modifier.isStatic(it.modifiers) + && !(it.type.isArray && it.type.componentType.isPrimitive) + } + result.addAll(fields) + type = type.superclass + } + + return result + } + + // Debugging-only + @Suppress("UNUSED") + fun printPath(from: Any, to: Any) { + val pathNodes = ArrayList() + val visited = newSetFromMap(IdentityHashMap()) + visited.add(from) + if (findPath(from, to, visited, pathNodes)) { + pathNodes.reverse() + println(pathNodes.joinToString(" -> ", from.javaClass.simpleName + " -> ", "-> " + to.javaClass.simpleName)) + } else { + println("Path from $from to $to not found") + } + } + + private fun findPath(from: Any, to: Any, visited: MutableSet, pathNodes: MutableList): Boolean { + if (from === to) { + return true + } + + val type = from.javaClass + if (type.isArray) { + if (type.componentType.isPrimitive) return false + val array = from as Array + array.filterNotNull().forEach { + if (findPath(it, to, visited, pathNodes)) { + return true + } + } + return false + } + + val fields = type.fields() + fields.forEach { + it.isAccessible = true + val value = it.get(from) ?: return@forEach + if (!visited.add(value)) return@forEach + val found = findPath(value, to, visited, pathNodes) + if (found) { + pathNodes += from.javaClass.simpleName + ":" + it.name + return true + } + } + + return false + } +} diff --git a/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt new file mode 100644 index 0000000000..b324e7ed3a --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/ReusableCancellableContinuationTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2016-2019 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 kotlinx.coroutines.selects.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +class ReusableCancellableContinuationTest : TestBase() { + + @Test + fun testReusable() = runTest { + testContinuationsCount(10, 1, ::suspendAtomicCancellableCoroutineReusable) + } + + @Test + fun testRegular() = runTest { + testContinuationsCount(10, 10, ::suspendAtomicCancellableCoroutine) + } + + private suspend inline fun CoroutineScope.testContinuationsCount( + iterations: Int, + expectedInstances: Int, + suspender: suspend ((CancellableContinuation) -> Unit) -> Unit + ) { + val result = mutableSetOf>() + val job = coroutineContext[Job]!! + val channel = Channel>(1) + launch { + channel.consumeEach { + val f = FieldWalker.walk(job) + result.addAll(f.filterIsInstance>()) + it.resumeWith(Result.success(Unit)) + } + } + + repeat(iterations) { + suspender { + assertTrue(channel.offer(it)) + } + } + channel.close() + assertEquals(expectedInstances, result.size - 1) + } + + @Test + fun testCancelledOnClaimedCancel() = runTest { + expect(1) + try { + suspendAtomicCancellableCoroutineReusable { + it.cancel() + } + expectUnreached() + } catch (e: CancellationException) { + finish(2) + } + } + + @Test + fun testNotCancelledOnClaimedResume() = runTest({ it is CancellationException }) { + expect(1) + // Bind child at first + var continuation: Continuation<*>? = null + suspendAtomicCancellableCoroutineReusable { + expect(2) + continuation = it + launch { // Attach to the parent, avoid fast path + expect(3) + it.resume(Unit) + } + } + expect(4) + ensureActive() + // Verify child was bound + assertNotNull(FieldWalker.walk(coroutineContext[Job]!!).single { it === continuation }) + suspendAtomicCancellableCoroutineReusable { + expect(5) + coroutineContext[Job]!!.cancel() + it.resume(Unit) + } + assertFalse(isActive) + finish(6) + } + + @Test + fun testResumeReusablePreservesReference() = runTest { + expect(1) + var cont: Continuation? = null + launch { + cont!!.resumeWith(Result.success(Unit)) + } + suspendAtomicCancellableCoroutineReusable { + cont = it + } + ensureActive() + assertTrue { FieldWalker.walk(coroutineContext[Job]!!).contains(cont!!) } + finish(2) + } + + @Test + fun testResumeRegularDoesntPreservesReference() = runTest { + expect(1) + var cont: Continuation? = null + launch { // Attach to the parent, avoid fast path + cont!!.resumeWith(Result.success(Unit)) + } + suspendAtomicCancellableCoroutine { + cont = it + } + ensureActive() + assertFalse { FieldWalker.walk(coroutineContext[Job]!!).contains(cont!!) } + finish(2) + } + + @Test + fun testDetachedOnCancel() = runTest { + expect(1) + var cont: Continuation<*>? = null + try { + suspendAtomicCancellableCoroutineReusable { + cont = it + it.cancel() + } + expectUnreached() + } catch (e: CancellationException) { + assertFalse { FieldWalker.walk(coroutineContext[Job]!!).contains(cont!!) } + finish(2) + } + } + + @Test + fun testPropagatedCancel() = runTest({it is CancellationException}) { + val currentJob = coroutineContext[Job]!! + expect(1) + // Bind child at first + suspendAtomicCancellableCoroutineReusable { + expect(2) + // Attach to the parent, avoid fast path + launch { + expect(3) + it.resume(Unit) + } + } + expect(4) + ensureActive() + // Verify child was bound + assertEquals(1, FieldWalker.walk(currentJob).count { it is CancellableContinuation<*> }) + currentJob.cancel() + assertFalse(isActive) + // Child detached + assertEquals(0, FieldWalker.walk(currentJob).count { it is CancellableContinuation<*> }) + suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } + suspendAtomicCancellableCoroutineReusable { it.resume(Unit) } + assertEquals(0, FieldWalker.walk(currentJob).count { it is CancellableContinuation<*> }) + + try { + suspendAtomicCancellableCoroutineReusable {} + } catch (e: CancellationException) { + assertEquals(0, FieldWalker.walk(currentJob).count { it is CancellableContinuation<*> }) + finish(5) + } + } + + @Test + fun testChannelMemoryLeak() = runTest { + val iterations = 100 + val channel = Channel() + launch { + repeat(iterations) { + select { + channel.onSend(Unit) {} + } + } + } + + val receiver = launch { + repeat(iterations) { + channel.receive() + } + expect(2) + val job = coroutineContext[Job]!! + // 1 for reusable CC, another one for outer joiner + assertEquals(2, FieldWalker.walk(job).count { it is CancellableContinuation<*> }) + } + expect(1) + receiver.join() + // Reference should be claimed at this point + assertEquals(0, FieldWalker.walk(receiver).count { it is CancellableContinuation<*> }) + finish(3) + } +} diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt index 6223213d67..5afac37c9c 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelAtomicCancelStressTest.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.channels @@ -10,7 +10,7 @@ import org.junit.* import org.junit.Assert.* import org.junit.runner.* import org.junit.runners.* -import java.util.* +import kotlin.random.Random import java.util.concurrent.atomic.* /** @@ -57,9 +57,6 @@ class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBas private inline fun cancellable(done: Channel, block: () -> Unit) { try { block() - } catch (e: Throwable) { - if (e !is CancellationException) fail(e) - throw e } finally { if (!done.offer(true)) fail(IllegalStateException("failed to offer to done channel")) @@ -72,9 +69,8 @@ class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBas val deadline = System.currentTimeMillis() + TEST_DURATION launchSender() launchReceiver() - val rnd = Random() while (System.currentTimeMillis() < deadline && failed.get() == null) { - when (rnd.nextInt(3)) { + when (Random.nextInt(3)) { 0 -> { // cancel & restart sender stopSender() launchSender() @@ -104,12 +100,11 @@ class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBas private fun launchSender() { sender = scope.launch(start = CoroutineStart.ATOMIC) { - val rnd = Random() cancellable(senderDone) { var counter = 0 while (true) { val trySend = lastSent + 1 - when (rnd.nextInt(2)) { + when (Random.nextInt(2)) { 0 -> channel.send(trySend) 1 -> select { channel.onSend(trySend) {} } else -> error("cannot happen") @@ -134,10 +129,9 @@ class ChannelAtomicCancelStressTest(private val kind: TestChannelKind) : TestBas private fun launchReceiver() { receiver = scope.launch(start = CoroutineStart.ATOMIC) { - val rnd = Random() cancellable(receiverDone) { while (true) { - val received = when (rnd.nextInt(2)) { + val received = when (Random.nextInt(2)) { 0 -> channel.receive() 1 -> select { channel.onReceive { it } } else -> error("cannot happen") diff --git a/kotlinx-coroutines-core/jvm/test/channels/ChannelSelectStressTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ChannelSelectStressTest.kt new file mode 100644 index 0000000000..0fa64276df --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/channels/ChannelSelectStressTest.kt @@ -0,0 +1,70 @@ +/* + * 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.* +import org.junit.Assert.* +import java.util.concurrent.atomic.AtomicLongArray + +class ChannelSelectStressTest : TestBase() { + private val pairedCoroutines = 3 + private val dispatcher = newFixedThreadPoolContext(pairedCoroutines * 2, "ChannelSelectStressTest") + private val scope = CoroutineScope(dispatcher) + private val elementsToSend = 20_000 * Long.SIZE_BITS * stressTestMultiplier + private val sent = atomic(0) + private val received = atomic(0) + private val receivedArray = AtomicLongArray(elementsToSend / Long.SIZE_BITS) + private val channel = Channel() + + @After + fun tearDown() { + dispatcher.close() + } + + @Test + fun testAtomicCancelStress() = runTest { + repeat(pairedCoroutines) { launchSender() } + repeat(pairedCoroutines) { launchReceiver() } + val job = scope.coroutineContext[Job] as CompletableJob + job.complete() + job.join() + + for (i in 0 until receivedArray.length()) { + assertEquals("Missing element detected", 0L.inv(), receivedArray[i]) + } + } + + private fun launchSender() { + scope.launch { + while (sent.value < elementsToSend) { + val element = sent.getAndIncrement() + if (element >= elementsToSend) break + select { channel.onSend(element) {} } + } + channel.close(CancellationException()) + } + } + + private fun launchReceiver() { + scope.launch { + while (received.value != elementsToSend) { + val element = select { channel.onReceive { it } } + received.incrementAndGet() + val index = (element / Long.SIZE_BITS) + val mask = 1L shl (element % Long.SIZE_BITS.toLong()).toInt() + while (true) { + val bits = receivedArray.get(index) + if (bits and mask != 0L) { + error("Detected duplicate") + } + if (receivedArray.compareAndSet(index, bits, bits or mask)) break + } + } + } + } +} diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt index e7b46cd105..0f87960f24 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt @@ -102,8 +102,7 @@ class StackTraceRecoveryTest : TestBase() { "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:89)", "Caused by: java.lang.IllegalArgumentException\n" + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1\$job\$1.invokeSuspend(StackTraceRecoveryTest.kt:93)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + - "\tat kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:152)") + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") expect(3) job.join() finish(4) diff --git a/kotlinx-coroutines-core/jvm/test/flow/ConsumeAsFlowLeakTest.kt b/kotlinx-coroutines-core/jvm/test/flow/ConsumeAsFlowLeakTest.kt new file mode 100644 index 0000000000..3fcceaf1fb --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/flow/ConsumeAsFlowLeakTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2019 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 org.junit.Test +import kotlin.test.* + +class ConsumeAsFlowLeakTest : TestBase() { + + private data class Box(val i: Int) + + // In companion to avoid references through runTest + companion object { + private val first = Box(4) + private val second = Box(5) + } + + // @Test //ignored until KT-33986 + fun testReferenceIsNotRetained() = testReferenceNotRetained(true) + + @Test + fun testReferenceIsNotRetainedNoSuspension() = testReferenceNotRetained(false) + + private fun testReferenceNotRetained(shouldSuspendOnSend: Boolean) = runTest { + val channel = BroadcastChannel(1) + val job = launch { + expect(2) + channel.asFlow().collect { + expect(it.i) + } + } + + expect(1) + yield() + expect(3) + channel.send(first) + if (shouldSuspendOnSend) yield() + channel.send(second) + yield() + assertEquals(0, FieldWalker.walk(channel).count { it === second }) + finish(6) + job.cancelAndJoin() + } +} diff --git a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt index 223a33469f..bf6cbdf10c 100644 --- a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt +++ b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt @@ -68,7 +68,7 @@ class SanitizedProbesTest : DebugTestBase() { "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.access\$createActiveDeferred(SanitizedProbesTest.kt:16)\n" + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testCoroutinesDump\$1.invokeSuspend(SanitizedProbesTest.kt:57)\n" + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + - "\tat kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:237)\n" + + "\tat kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:237)\n" + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testCoroutinesDump(SanitizedProbesTest.kt:56)" ) @@ -96,7 +96,7 @@ class SanitizedProbesTest : DebugTestBase() { "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.access\$launchSelector(SanitizedProbesTest.kt:16)\n" + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testSelectBuilder\$1.invokeSuspend(SanitizedProbesTest.kt:89)\n" + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + - "\tat kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)\n" + + "\tat kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:233)\n" + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:154)\n" + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testSelectBuilder(SanitizedProbesTest.kt:88)") finish(4) From fd27d553580657a9f69087126595444a0ed07263 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 25 Sep 2019 23:49:34 +0300 Subject: [PATCH 24/90] Separate all JVM stress tests into a stressTest gradle task * ./gradlew :kotlinx-coroutines-core:jvmTest becomes really fast, since it does not run any stress tests. * ./gradlew build still runs all the tests. --- kotlinx-coroutines-core/build.gradle | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index c329497f95..3dcec12c77 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -76,16 +76,19 @@ jvmTest { maxHeapSize = '1g' enableAssertions = true systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' - exclude '**/*LFStressTest.*' + exclude '**/*StressTest.*' systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test } -task lockFreedomTest(type: Test, dependsOn: compileTestKotlinJvm) { +task stressTest(type: Test, dependsOn: compileTestKotlinJvm) { classpath = files { jvmTest.classpath } testClassesDirs = files { jvmTest.testClassesDirs } - include '**/*LFStressTest.*' + minHeapSize = '1g' + maxHeapSize = '1g' + include '**/*StressTest.*' enableAssertions = true testLogging.showStandardStreams = true + systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test } task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { @@ -102,7 +105,7 @@ task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { jdk16Test.onlyIf { project.properties['stressTest'] != null } // Always run those tests -task moreTest(dependsOn: [lockFreedomTest, jdk16Test]) +task moreTest(dependsOn: [stressTest, jdk16Test]) build.dependsOn moreTest task testsJar(type: Jar, dependsOn: jvmTestClasses) { From 2f8bff1b0972defc473dd8c857600db481f013a3 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sun, 22 Sep 2019 21:13:12 +0300 Subject: [PATCH 25/90] Fix race condition in pair select This bug was introduced by #1524. The crux of problem is that TryOffer/PollDesc.onPrepare method is no longer allowed to update fields in these classes (like "resumeToken" and "pollResult") after call to tryResumeSend/Receive method, because the latter will complete the ongoing atomic operation and helper method might find it complete and try reading "resumeToken" which was not initialized yet. This change removes "pollResult" field which was not really needed ("result.pollResult" field is used) and removes "resumeToken" by exploiting the fact that current implementation of CancellableContinuationImpl does not need a token anymore. However, CancellableContinuation.tryResume/completeResume ABI is left intact, because it is used by 3rd party code. This fix lead to overall simplification of the code. A number of fields and an auxiliary IdempotentTokenValue class are removed, tokens used to indicate various results are consolidated, so that resume success is now consistently indicated by a single RESUME_TOKEN symbol. Fixes #1561 --- .../common/src/CancellableContinuationImpl.kt | 19 ++-- .../common/src/channels/AbstractChannel.kt | 101 ++++++------------ .../src/channels/ArrayBroadcastChannel.kt | 16 +-- .../common/src/channels/ArrayChannel.kt | 32 +++--- .../common/src/selects/Select.kt | 14 +-- 5 files changed, 74 insertions(+), 108 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 9a19ed61e3..559afb8289 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -14,6 +14,10 @@ private const val UNDECIDED = 0 private const val SUSPENDED = 1 private const val RESUMED = 2 +@JvmField +@SharedImmutable +internal val RESUME_TOKEN = Symbol("RESUME_TOKEN") + /** * @suppress **This is unstable API and it is subject to change.** */ @@ -347,20 +351,21 @@ internal open class CancellableContinuationImpl( parentHandle = NonDisposableHandle } + // 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, state) + CompletedIdempotentResult(idempotent, value) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure detachChildIfNonResuable() - return state + return RESUME_TOKEN } is CompletedIdempotentResult -> { return if (state.idempotentResume === idempotent) { assert { state.result === value } // "Non-idempotent resume" - state.token + RESUME_TOKEN } else { null } @@ -377,15 +382,16 @@ internal open class CancellableContinuationImpl( val update = CompletedExceptionally(exception) if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure detachChildIfNonResuable() - return state + return RESUME_TOKEN } else -> return null // cannot resume -- not active anymore } } } + // note: token is always RESUME_TOKEN override fun completeResume(token: Any) { - // note: We don't actually use token anymore, because handler needs to be invoked on cancellation only + assert { token === RESUME_TOKEN } dispatchResume(resumeMode) } @@ -437,8 +443,7 @@ private class InvokeOnCancel( // Clashes with InvokeOnCancellation private class CompletedIdempotentResult( @JvmField val idempotentResume: Any?, - @JvmField val result: Any?, - @JvmField val token: NotCompleted + @JvmField val result: Any? ) { override fun toString(): String = "CompletedIdempotentResult[$result]" } diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 22c1971c0a..9f672af10b 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -48,7 +48,8 @@ internal abstract class AbstractSendChannel : SendChannel { val receive = takeFirstReceiveOrPeekClosed() ?: return OFFER_FAILED val token = receive.tryResumeReceive(element, null) if (token != null) { - receive.completeResumeReceive(token) + assert { token === RESUME_TOKEN } + receive.completeResumeReceive(element) return receive.offerResult } } @@ -65,7 +66,7 @@ internal abstract class AbstractSendChannel : SendChannel { val failure = select.performAtomicTrySelect(offerOp) if (failure != null) return failure val receive = offerOp.result - receive.completeResumeReceive(offerOp.resumeToken!!) + receive.completeResumeReceive(element) return receive.offerResult } @@ -354,8 +355,6 @@ internal abstract class AbstractSendChannel : SendChannel { @JvmField val element: E, queue: LockFreeLinkedListHead ) : RemoveFirstDesc>(queue) { - @JvmField var resumeToken: Any? = null - override fun failure(affected: LockFreeLinkedListNode): Any? = when (affected) { is Closed<*> -> affected !is ReceiveOrClosed<*> -> OFFER_FAILED @@ -367,7 +366,7 @@ internal abstract class AbstractSendChannel : SendChannel { val affected = prepareOp.affected as ReceiveOrClosed // see "failure" impl val token = affected.tryResumeReceive(element, prepareOp) ?: return REMOVE_PREPARED if (token === RETRY_ATOMIC) return RETRY_ATOMIC - resumeToken = token + assert { token === RESUME_TOKEN } return null } } @@ -454,8 +453,7 @@ internal abstract class AbstractSendChannel : SendChannel { override fun tryResumeSend(otherOp: PrepareOp?): Any? = select.trySelectOther(otherOp) - override fun completeResumeSend(token: Any) { - assert { token === SELECT_STARTED } + override fun completeResumeSend() { block.startCoroutine(receiver = channel, completion = select.completion) } @@ -475,8 +473,8 @@ internal abstract class AbstractSendChannel : SendChannel { @JvmField val element: E ) : Send() { override val pollResult: Any? get() = element - override fun tryResumeSend(otherOp: PrepareOp?): Any? = SEND_RESUMED.also { otherOp?.finishPrepare() } - override fun completeResumeSend(token: Any) { assert { token === SEND_RESUMED } } + override fun tryResumeSend(otherOp: PrepareOp?): Any? = RESUME_TOKEN.also { otherOp?.finishPrepare() } + override fun completeResumeSend() {} override fun resumeSendClosed(closed: Closed<*>) {} } } @@ -511,7 +509,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel(queue: LockFreeLinkedListHead) : RemoveFirstDesc(queue) { - @JvmField var resumeToken: Any? = null - @JvmField var pollResult: E? = null - override fun failure(affected: LockFreeLinkedListNode): Any? = when (affected) { is Closed<*> -> affected !is Send -> POLL_FAILED @@ -687,8 +683,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel) { when { receiveMode == RECEIVE_NULL_ON_CLOSE && closed.closeCause == null -> cont.resume(null) @@ -925,25 +921,16 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel() { override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? { otherOp?.finishPrepare() - val token = cont.tryResume(true, otherOp?.desc) - if (token != null) { - /* - When otherOp != null this invocation can be stale and we cannot directly update iterator.result - Instead, we save both token & result into a temporary IdempotentTokenValue object and - set iterator result only in completeResumeReceive that is going to be invoked just once - */ - if (otherOp != null) return IdempotentTokenValue(token, value) - iterator.result = value - } - return token + return cont.tryResume(true, otherOp?.desc) } - override fun completeResumeReceive(token: Any) { - if (token is IdempotentTokenValue<*>) { - iterator.result = token.value - cont.completeResume(token.token) - } else - cont.completeResume(token) + override fun completeResumeReceive(value: E) { + /* + When otherOp != null invocation of tryResumeReceive can happen multiple times and much later, + but completeResumeReceive is called once so we set iterator result here. + */ + iterator.result = value + cont.completeResume(RESUME_TOKEN) } override fun resumeReceiveClosed(closed: Closed<*>) { @@ -966,14 +953,11 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel R, @JvmField val receiveMode: Int ) : Receive(), DisposableHandle { - override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? { - val result = select.trySelectOther(otherOp) - return if (result === SELECT_STARTED) value ?: NULL_VALUE else result - } + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? = + select.trySelectOther(otherOp) @Suppress("UNCHECKED_CAST") - override fun completeResumeReceive(token: Any) { - val value: E = NULL_VALUE.unbox(token) + override fun completeResumeReceive(value: E) { block.startCoroutine(if (receiveMode == RECEIVE_RESULT) ValueOrClosed.value(value) else value, select.completion) } @@ -997,11 +981,6 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( - @JvmField val token: Any, - @JvmField val value: E - ) } // receiveMode values @@ -1025,18 +1004,6 @@ internal val POLL_FAILED: Any = Symbol("POLL_FAILED") @SharedImmutable internal val ENQUEUE_FAILED: Any = Symbol("ENQUEUE_FAILED") -@JvmField -@SharedImmutable -internal val NULL_VALUE: Symbol = Symbol("NULL_VALUE") - -@JvmField -@SharedImmutable -internal val CLOSE_RESUMED: Any = Symbol("CLOSE_RESUMED") - -@JvmField -@SharedImmutable -internal val SEND_RESUMED: Any = Symbol("SEND_RESUMED") - @JvmField @SharedImmutable internal val HANDLER_INVOKED: Any = Symbol("ON_CLOSE_HANDLER_INVOKED") @@ -1050,10 +1017,10 @@ internal abstract class Send : LockFreeLinkedListNode() { abstract val pollResult: Any? // E | Closed // Returns: null - failure, // RETRY_ATOMIC for retry (only when otherOp != null), - // otherwise token for completeResumeSend + // RESUME_TOKEN on success (call completeResumeSend) // Must call otherOp?.finishPrepare() before deciding on result other than RETRY_ATOMIC abstract fun tryResumeSend(otherOp: PrepareOp?): Any? - abstract fun completeResumeSend(token: Any) + abstract fun completeResumeSend() abstract fun resumeSendClosed(closed: Closed<*>) } @@ -1064,10 +1031,10 @@ internal interface ReceiveOrClosed { val offerResult: Any // OFFER_SUCCESS | Closed // Returns: null - failure, // RETRY_ATOMIC for retry (only when otherOp != null), - // otherwise token for completeResumeReceive + // RESUME_TOKEN on success (call completeResumeReceive) // Must call otherOp?.finishPrepare() before deciding on result other than RETRY_ATOMIC fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? - fun completeResumeReceive(token: Any) + fun completeResumeReceive(value: E) } /** @@ -1082,7 +1049,7 @@ internal class SendElement( otherOp?.finishPrepare() return cont.tryResume(Unit, otherOp?.desc) } - override fun completeResumeSend(token: Any) = cont.completeResume(token) + override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) override fun toString(): String = "SendElement($pollResult)" } @@ -1098,10 +1065,10 @@ internal class Closed( override val offerResult get() = this override val pollResult get() = this - override fun tryResumeSend(otherOp: PrepareOp?): Any? = CLOSE_RESUMED.also { otherOp?.finishPrepare() } - override fun completeResumeSend(token: Any) { assert { token === CLOSE_RESUMED } } - override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? = CLOSE_RESUMED.also { otherOp?.finishPrepare() } - override fun completeResumeReceive(token: Any) { assert { token === CLOSE_RESUMED } } + override fun tryResumeSend(otherOp: PrepareOp?): Any? = RESUME_TOKEN.also { otherOp?.finishPrepare() } + override fun completeResumeSend() {} + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? = RESUME_TOKEN.also { otherOp?.finishPrepare() } + override fun completeResumeReceive(value: E) {} override fun resumeSendClosed(closed: Closed<*>) = assert { false } // "Should be never invoked" override fun toString(): String = "Closed[$closeCause]" } diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index c9fb3cc74d..5f3eb32d8f 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -143,7 +143,6 @@ internal class ArrayBroadcastChannel( private tailrec fun updateHead(addSub: Subscriber? = null, removeSub: Subscriber? = null) { // update head in a tail rec loop var send: Send? = null - var token: Any? = null bufferLock.withLock { if (addSub != null) { addSub.subHead = tail // start from last element @@ -172,8 +171,9 @@ internal class ArrayBroadcastChannel( while (true) { send = takeFirstSendOrPeekClosed() ?: break // when when no sender if (send is Closed<*>) break // break when closed for send - token = send!!.tryResumeSend(null) + val token = send!!.tryResumeSend(null) if (token != null) { + assert { token === RESUME_TOKEN } // put sent element to the buffer buffer[(tail % capacity).toInt()] = (send as Send).pollResult this.size = size + 1 @@ -186,7 +186,7 @@ internal class ArrayBroadcastChannel( return // done updating here -> return } // we only get out of the lock normally when there is a sender to resume - send!!.completeResumeSend(token!!) + send!!.completeResumeSend() // since we've just sent an element, we might need to resume some receivers checkSubOffers() // tailrec call to recheck @@ -239,9 +239,9 @@ internal class ArrayBroadcastChannel( // it means that `checkOffer` must be retried after every `unlock` if (!subLock.tryLock()) break val receive: ReceiveOrClosed? - val token: Any? + var result: Any? try { - val result = peekUnderLock() + result = peekUnderLock() when { result === POLL_FAILED -> continue@loop // must retest `needsToCheckOfferWithoutLock` outside of the lock result is Closed<*> -> { @@ -252,15 +252,15 @@ internal class ArrayBroadcastChannel( // find a receiver for an element receive = takeFirstReceiveOrPeekClosed() ?: break // break when no one's receiving if (receive is Closed<*>) break // noting more to do if this sub already closed - token = receive.tryResumeReceive(result as E, null) - if (token == null) continue // bail out here to next iteration (see for next receiver) + val token = receive.tryResumeReceive(result as E, null) ?: continue + assert { token === RESUME_TOKEN } val subHead = this.subHead this.subHead = subHead + 1 // retrieved element for this subscriber updated = true } finally { subLock.unlock() } - receive!!.completeResumeReceive(token!!) + receive!!.completeResumeReceive(result as E) } // do close outside of lock if needed closed?.also { close(cause = it.closeCause) } diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index 0b850b27e9..f10713d95b 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -50,7 +50,6 @@ internal open class ArrayChannel( // result is `OFFER_SUCCESS | OFFER_FAILED | Closed` protected override fun offerInternal(element: E): Any { var receive: ReceiveOrClosed? = null - var token: Any? = null lock.withLock { val size = this.size closedForSend?.let { return it } @@ -65,8 +64,9 @@ internal open class ArrayChannel( this.size = size // restore size return receive!! } - token = receive!!.tryResumeReceive(element, null) + val token = receive!!.tryResumeReceive(element, null) if (token != null) { + assert { token === RESUME_TOKEN } this.size = size // restore size return@withLock } @@ -80,14 +80,13 @@ internal open class ArrayChannel( return OFFER_FAILED } // breaks here if offer meets receiver - receive!!.completeResumeReceive(token!!) + receive!!.completeResumeReceive(element) return receive!!.offerResult } // result is `ALREADY_SELECTED | OFFER_SUCCESS | OFFER_FAILED | Closed` protected override fun offerSelectInternal(element: E, select: SelectInstance<*>): Any { var receive: ReceiveOrClosed? = null - var token: Any? = null lock.withLock { val size = this.size closedForSend?.let { return it } @@ -103,8 +102,6 @@ internal open class ArrayChannel( failure == null -> { // offered successfully this.size = size // restore size receive = offerOp.result - token = offerOp.resumeToken - assert { token != null } return@withLock } failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer @@ -130,7 +127,7 @@ internal open class ArrayChannel( return OFFER_FAILED } // breaks here if offer meets receiver - receive!!.completeResumeReceive(token!!) + receive!!.completeResumeReceive(element) return receive!!.offerResult } @@ -150,7 +147,7 @@ internal open class ArrayChannel( // result is `E | POLL_FAILED | Closed` protected override fun pollInternal(): Any? { var send: Send? = null - var token: Any? = null + var resumed = false var result: Any? = null lock.withLock { val size = this.size @@ -164,8 +161,10 @@ internal open class ArrayChannel( if (size == capacity) { loop@ while (true) { send = takeFirstSendOrPeekClosed() ?: break - token = send!!.tryResumeSend(null) + val token = send!!.tryResumeSend(null) if (token != null) { + assert { token === RESUME_TOKEN } + resumed = true replacement = send!!.pollResult break@loop } @@ -178,15 +177,15 @@ internal open class ArrayChannel( head = (head + 1) % buffer.size } // complete send the we're taken replacement from - if (token != null) - send!!.completeResumeSend(token!!) + if (resumed) + send!!.completeResumeSend() return result } // result is `ALREADY_SELECTED | E | POLL_FAILED | Closed` protected override fun pollSelectInternal(select: SelectInstance<*>): Any? { var send: Send? = null - var token: Any? = null + var success = false var result: Any? = null lock.withLock { val size = this.size @@ -204,8 +203,7 @@ internal open class ArrayChannel( when { failure == null -> { // polled successfully send = pollOp.result - token = pollOp.resumeToken - assert { token != null } + success = true replacement = send!!.pollResult break@loop } @@ -218,7 +216,7 @@ internal open class ArrayChannel( } failure is Closed<*> -> { send = failure - token = failure.tryResumeSend(null) + success = true replacement = failure break@loop } @@ -240,8 +238,8 @@ internal open class ArrayChannel( head = (head + 1) % buffer.size } // complete send the we're taken replacement from - if (token != null) - send!!.completeResumeSend(token!!) + if (success) + send!!.completeResumeSend() return result } diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 5318d14757..550ea1f59e 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -87,10 +87,6 @@ public interface SelectClause2 { public fun registerSelectClause2(select: SelectInstance, param: P, block: suspend (Q) -> R) } -@JvmField -@SharedImmutable -internal val SELECT_STARTED: Any = Symbol("SELECT_STARTED") - /** * Internal representation of select instance. This instance is called _selected_ when * the clause to execute is already picked. @@ -111,7 +107,7 @@ public interface SelectInstance { /** * Tries to select this instance. Returns: - * * [SELECT_STARTED] on success, + * * [RESUME_TOKEN] on success, * * [RETRY_ATOMIC] on deadlock (needs retry, it is only possible when [otherOp] is not `null`) * * `null` on failure to select (already selected). * [otherOp] is not null when trying to rendezvous with this select from inside of another select. @@ -370,7 +366,7 @@ internal class SelectBuilderImpl( override fun trySelect(): Boolean { val result = trySelectOther(null) return when { - result === SELECT_STARTED -> true + result === RESUME_TOKEN -> true result == null -> false else -> error("Unexpected trySelectIdempotent result $result") } @@ -460,7 +456,7 @@ internal class SelectBuilderImpl( */ // it is just like plain trySelect, but support idempotent start - // Returns SELECT_STARTED | RETRY_ATOMIC | null (when already selected) + // Returns RESUME_TOKEN | RETRY_ATOMIC | null (when already selected) override fun trySelectOther(otherOp: PrepareOp?): Any? { _state.loop { state -> // lock-free loop on state when { @@ -477,7 +473,7 @@ internal class SelectBuilderImpl( if (decision !== null) return decision } doAfterSelect() - return SELECT_STARTED + return RESUME_TOKEN } state is OpDescriptor -> { // state is either AtomicSelectOp or PairSelectOp // Found descriptor of ongoing operation while working in the context of other select operation @@ -512,7 +508,7 @@ internal class SelectBuilderImpl( } // otherwise -- already selected otherOp == null -> return null // already selected - state === otherOp.desc -> return SELECT_STARTED // was selected with this marker + state === otherOp.desc -> return RESUME_TOKEN // was selected with this marker else -> return null // selected with different marker } } From f24b60ce8880a9b1d9a0f809d762406dba9865dc Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 26 Sep 2019 12:47:54 +0300 Subject: [PATCH 26/90] Narrow down return type of tryResumeSend/Receive They return Symbol? --- .../common/src/channels/AbstractChannel.kt | 36 +++++++++++-------- .../common/src/selects/Select.kt | 6 +++- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 9f672af10b..ce0acc0f53 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -450,8 +450,8 @@ internal abstract class AbstractSendChannel : SendChannel { @JvmField val select: SelectInstance, @JvmField val block: suspend (SendChannel) -> R ) : Send(), DisposableHandle { - override fun tryResumeSend(otherOp: PrepareOp?): Any? = - select.trySelectOther(otherOp) + override fun tryResumeSend(otherOp: PrepareOp?): Symbol? = + select.trySelectOther(otherOp) as Symbol? // must return symbol override fun completeResumeSend() { block.startCoroutine(receiver = channel, completion = select.completion) @@ -473,7 +473,7 @@ internal abstract class AbstractSendChannel : SendChannel { @JvmField val element: E ) : Send() { override val pollResult: Any? get() = element - override fun tryResumeSend(otherOp: PrepareOp?): Any? = RESUME_TOKEN.also { otherOp?.finishPrepare() } + override fun tryResumeSend(otherOp: PrepareOp?): Symbol? = RESUME_TOKEN.also { otherOp?.finishPrepare() } override fun completeResumeSend() {} override fun resumeSendClosed(closed: Closed<*>) {} } @@ -898,9 +898,11 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel, @JvmField val cont: CancellableContinuation ) : Receive() { - override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? { + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { otherOp?.finishPrepare() - return cont.tryResume(true, otherOp?.desc) + val token = cont.tryResume(true, otherOp?.desc) ?: return null + assert { token === RESUME_TOKEN } // the only other possible result + return RESUME_TOKEN } override fun completeResumeReceive(value: E) { @@ -953,8 +957,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel R, @JvmField val receiveMode: Int ) : Receive(), DisposableHandle { - override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? = - select.trySelectOther(otherOp) + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? = + select.trySelectOther(otherOp) as Symbol? @Suppress("UNCHECKED_CAST") override fun completeResumeReceive(value: E) { @@ -1019,7 +1023,7 @@ internal abstract class Send : LockFreeLinkedListNode() { // RETRY_ATOMIC for retry (only when otherOp != null), // RESUME_TOKEN on success (call completeResumeSend) // Must call otherOp?.finishPrepare() before deciding on result other than RETRY_ATOMIC - abstract fun tryResumeSend(otherOp: PrepareOp?): Any? + abstract fun tryResumeSend(otherOp: PrepareOp?): Symbol? abstract fun completeResumeSend() abstract fun resumeSendClosed(closed: Closed<*>) } @@ -1033,7 +1037,7 @@ internal interface ReceiveOrClosed { // RETRY_ATOMIC for retry (only when otherOp != null), // RESUME_TOKEN on success (call completeResumeReceive) // Must call otherOp?.finishPrepare() before deciding on result other than RETRY_ATOMIC - fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? + fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? fun completeResumeReceive(value: E) } @@ -1045,9 +1049,11 @@ internal class SendElement( override val pollResult: Any?, @JvmField val cont: CancellableContinuation ) : Send() { - override fun tryResumeSend(otherOp: PrepareOp?): Any? { + override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { otherOp?.finishPrepare() - return cont.tryResume(Unit, otherOp?.desc) + val token = cont.tryResume(Unit, otherOp?.desc) + assert { token === RESUME_TOKEN } // the only other possible result + return RESUME_TOKEN } override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) @@ -1065,9 +1071,9 @@ internal class Closed( override val offerResult get() = this override val pollResult get() = this - override fun tryResumeSend(otherOp: PrepareOp?): Any? = RESUME_TOKEN.also { otherOp?.finishPrepare() } + override fun tryResumeSend(otherOp: PrepareOp?): Symbol? = RESUME_TOKEN.also { otherOp?.finishPrepare() } override fun completeResumeSend() {} - override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Any? = RESUME_TOKEN.also { otherOp?.finishPrepare() } + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? = RESUME_TOKEN.also { otherOp?.finishPrepare() } override fun completeResumeReceive(value: E) {} override fun resumeSendClosed(closed: Closed<*>) = assert { false } // "Should be never invoked" override fun toString(): String = "Closed[$closeCause]" diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 550ea1f59e..372c9e32dc 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -93,7 +93,7 @@ public interface SelectClause2 { * * @suppress **This is unstable API and it is subject to change.** */ -@InternalCoroutinesApi +@InternalCoroutinesApi // todo: sealed interface https://youtrack.jetbrains.com/issue/KT-22286 public interface SelectInstance { /** * Returns `true` when this [select] statement had already picked a clause to execute. @@ -112,6 +112,10 @@ public interface SelectInstance { * * `null` on failure to select (already selected). * [otherOp] is not null when trying to rendezvous with this select from inside of another select. * In this case, [PrepareOp.finishPrepare] must be called before deciding on any value other than [RETRY_ATOMIC]. + * + * Note, that this method's actual return type is `Symbol?` but we cannot declare it as such, because this + * member is public, but [Symbol] is internal. When [SelectInstance] becomes a `sealed interface` + * (see KT-222860) we can declare this method as internal. */ public fun trySelectOther(otherOp: PrepareOp?): Any? From 1b0eca9cd483a63cb5563e1aec50f1d267de9ce7 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 27 Sep 2019 09:31:19 +0300 Subject: [PATCH 27/90] Fixed SendElement.tryResumeSend Was failing with java.lang.AssertionError at kotlinx.coroutines.channels.SendElement.tryResumeSend(AbstractChannel.kt:1055) at kotlinx.coroutines.channels.AbstractChannel.pollInternal(AbstractChannel.kt:510) at kotlinx.coroutines.channels.AbstractChannel.receive(AbstractChannel.kt:548) --- kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index ce0acc0f53..489d24c308 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -1051,7 +1051,7 @@ internal class SendElement( ) : Send() { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { otherOp?.finishPrepare() - val token = cont.tryResume(Unit, otherOp?.desc) + val token = cont.tryResume(Unit, otherOp?.desc) ?: return null assert { token === RESUME_TOKEN } // the only other possible result return RESUME_TOKEN } From 216828a71d2e2a9697dd032ce6bc227f458ba42b Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sat, 28 Sep 2019 00:42:51 +0300 Subject: [PATCH 28/90] Rename stressTest task to jvmStressTest to avoid conflict with prop --- kotlinx-coroutines-core/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 3dcec12c77..7ef0c8df4e 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -80,7 +80,7 @@ jvmTest { systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test } -task stressTest(type: Test, dependsOn: compileTestKotlinJvm) { +task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) { classpath = files { jvmTest.classpath } testClassesDirs = files { jvmTest.testClassesDirs } minHeapSize = '1g' @@ -105,7 +105,7 @@ task jdk16Test(type: Test, dependsOn: [compileTestKotlinJvm, checkJdk16]) { jdk16Test.onlyIf { project.properties['stressTest'] != null } // Always run those tests -task moreTest(dependsOn: [stressTest, jdk16Test]) +task moreTest(dependsOn: [jvmStressTest, jdk16Test]) build.dependsOn moreTest task testsJar(type: Jar, dependsOn: jvmTestClasses) { From 8fa07b548e08cbc5575e7818bba16077b8855068 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Fri, 27 Sep 2019 18:26:03 +0300 Subject: [PATCH 29/90] Make `isStressTest` and the related properties top-level --- .../common/test/TestBase.common.kt | 6 ++--- kotlinx-coroutines-core/js/test/TestBase.kt | 6 ++--- kotlinx-coroutines-core/jvm/test/TestBase.kt | 24 +++++++++---------- .../LockFreeLinkedListAtomicLFStressTest.kt | 4 ++-- .../jvm/test/internal/SegmentQueueTest.kt | 7 +++--- .../native/test/TestBase.kt | 6 ++--- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index ad7b8b1508..64f9f34d39 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.* import kotlin.coroutines.* import kotlin.test.* -public expect open class TestBase constructor() { - public val isStressTest: Boolean - public val stressTestMultiplier: Int +public expect val isStressTest: Boolean +public expect val stressTestMultiplier: Int +public expect open class TestBase constructor() { public fun error(message: Any, cause: Throwable? = null): Nothing public fun expect(index: Int) public fun expectUnreached() diff --git a/kotlinx-coroutines-core/js/test/TestBase.kt b/kotlinx-coroutines-core/js/test/TestBase.kt index 3bf49ef8c9..8b3d69a7f5 100644 --- a/kotlinx-coroutines-core/js/test/TestBase.kt +++ b/kotlinx-coroutines-core/js/test/TestBase.kt @@ -6,10 +6,10 @@ package kotlinx.coroutines import kotlin.js.* -public actual open class TestBase actual constructor() { - public actual val isStressTest: Boolean = false - public actual val stressTestMultiplier: Int = 1 +public actual val isStressTest: Boolean = false +public actual val stressTestMultiplier: Int = 1 +public actual open class TestBase actual constructor() { private var actionIndex = 0 private var finished = false private var error: Throwable? = null diff --git a/kotlinx-coroutines-core/jvm/test/TestBase.kt b/kotlinx-coroutines-core/jvm/test/TestBase.kt index 01daa4a8a8..cc41ff3f62 100644 --- a/kotlinx-coroutines-core/jvm/test/TestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/TestBase.kt @@ -14,6 +14,18 @@ import kotlin.test.* private val VERBOSE = systemProp("test.verbose", false) +/** + * Is `true` when running in a nightly stress test mode. + */ +public actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ?: false + +public val stressTestMultiplierSqrt = if (isStressTest) 5 else 1 + +/** + * Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test. + */ +public actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt + /** * Base class for tests, so that tests for predictable scheduling of actions in multiple coroutines sharing a single * thread can be written. Use it like this: @@ -34,18 +46,6 @@ private val VERBOSE = systemProp("test.verbose", false) * ``` */ public actual open class TestBase actual constructor() { - /** - * Is `true` when running in a nightly stress test mode. - */ - public actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ?: false - - public val stressTestMultiplierSqrt = if (isStressTest) 5 else 1 - - /** - * Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test. - */ - public actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt - private var actionIndex = AtomicInteger() private var finished = AtomicBoolean() private var error = AtomicReference() diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt index 20c2b5308c..e5c4c2c898 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListAtomicLFStressTest.kt @@ -5,7 +5,7 @@ package kotlinx.coroutines.internal import kotlinx.atomicfu.LockFreedomTestEnvironment -import kotlinx.coroutines.TestBase +import kotlinx.coroutines.stressTestMultiplier import org.junit.Assert.* import org.junit.Test import java.util.* @@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicReference * This stress test has 4 threads adding randomly to the list and them immediately undoing * this addition by remove, and 4 threads trying to remove nodes from two lists simultaneously (atomically). */ -class LockFreeLinkedListAtomicLFStressTest : TestBase() { +class LockFreeLinkedListAtomicLFStressTest { private val env = LockFreedomTestEnvironment("LockFreeLinkedListAtomicLFStressTest") data class IntNode(val i: Int) : LockFreeLinkedListNode() diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt index 9a6ee42aa0..6fbe44717d 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt @@ -1,6 +1,6 @@ package kotlinx.coroutines.internal -import kotlinx.coroutines.TestBase +import kotlinx.coroutines.stressTestMultiplier import org.junit.Test import java.util.* import java.util.concurrent.CyclicBarrier @@ -9,12 +9,11 @@ import kotlin.concurrent.thread import kotlin.random.Random import kotlin.test.assertEquals -class SegmentQueueTest : TestBase() { - +class SegmentQueueTest { @Test fun simpleTest() { val q = SegmentBasedQueue() - assertEquals( 1, q.numberOfSegments) + assertEquals(1, q.numberOfSegments) assertEquals(null, q.dequeue()) q.enqueue(1) assertEquals(1, q.numberOfSegments) diff --git a/kotlinx-coroutines-core/native/test/TestBase.kt b/kotlinx-coroutines-core/native/test/TestBase.kt index 4f41faa5a1..890f029ca2 100644 --- a/kotlinx-coroutines-core/native/test/TestBase.kt +++ b/kotlinx-coroutines-core/native/test/TestBase.kt @@ -4,10 +4,10 @@ package kotlinx.coroutines -public actual open class TestBase actual constructor() { - public actual val isStressTest: Boolean = false - public actual val stressTestMultiplier: Int = 1 +public actual val isStressTest: Boolean = false +public actual val stressTestMultiplier: Int = 1 +public actual open class TestBase actual constructor() { private var actionIndex = 0 private var finished = false private var error: Throwable? = null From 58bfd08369ee498a96938aaeb35dcf4bef43fc64 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Tue, 1 Oct 2019 13:57:55 +0300 Subject: [PATCH 30/90] Fix ArrayChannel.isBufferEmpty atomicity Fixes #1526 --- kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index f10713d95b..0135778409 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -43,7 +43,7 @@ internal open class ArrayChannel( set(value) { _size.value = value } protected final override val isBufferAlwaysEmpty: Boolean get() = false - protected final override val isBufferEmpty: Boolean get() = size == 0 + protected final override val isBufferEmpty: Boolean get() = lock.withLock { size == 0 } protected final override val isBufferAlwaysFull: Boolean get() = false protected final override val isBufferFull: Boolean get() = size == capacity From 8248fe4c41570bbde73ca1cbb9d8a7d5546ade22 Mon Sep 17 00:00:00 2001 From: Lukasz Wojtach Date: Wed, 2 Oct 2019 01:05:48 +0200 Subject: [PATCH 31/90] Make MapTest - testErrorCancelsUpstream - actually test map operator --- kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt index c744404d36..8c9398a684 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/MapTest.kt @@ -37,8 +37,9 @@ class MapTest : TestBase() { hang { cancelled = true } } emit(1) + expectUnreached() } - }.onEach { + }.map { latch.receive() throw TestException() }.catch { emit(42) } From c5a42da5318bc468c63ddea270b326d47cb34f09 Mon Sep 17 00:00:00 2001 From: Sergey Shatunov Date: Sun, 22 Sep 2019 19:50:35 +0700 Subject: [PATCH 32/90] Migrate BOM module to java-platform plugin --- build.gradle | 1 - gradle.properties | 1 - gradle/publish-bintray.gradle | 26 ++++++++++++-------------- kotlinx-coroutines-bom/build.gradle | 24 +++++++++++++++++++----- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index b5c3443f9e..8fd5441e5e 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,6 @@ buildscript { classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version" - classpath "io.spring.gradle:dependency-management-plugin:$spring_dependency_management_version" // JMH plugins classpath "com.github.jengelman.gradle.plugins:shadow:5.1.0" diff --git a/gradle.properties b/gradle.properties index 949a2a5223..ed6f22690b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,6 @@ mocha_version=4.1.0 mocha_headless_chrome_version=1.8.2 mocha_teamcity_reporter_version=2.2.2 source_map_support_version=0.5.3 -spring_dependency_management_version=1.0.8.RELEASE # Settings kotlin.incremental.multiplatform=true diff --git a/gradle/publish-bintray.gradle b/gradle/publish-bintray.gradle index dd528fe982..9062896b58 100644 --- a/gradle/publish-bintray.gradle +++ b/gradle/publish-bintray.gradle @@ -6,7 +6,6 @@ apply plugin: 'maven' apply plugin: 'maven-publish' -apply plugin: "com.github.johnrengelman.shadow" apply from: project.rootProject.file('gradle/maven-central.gradle') @@ -15,7 +14,16 @@ apply from: project.rootProject.file('gradle/maven-central.gradle') def isMultiplatform = project.name == "kotlinx-coroutines-core" def isBom = project.name == "kotlinx-coroutines-bom" -if (!isMultiplatform) { +if (!isBom) { + apply plugin: "com.github.johnrengelman.shadow" + + // empty xxx-javadoc.jar + task javadocJar(type: Jar) { + archiveClassifier = 'javadoc' + } +} + +if (!isMultiplatform && !isBom) { // Regular java modules need 'java-library' plugin for proper publication apply plugin: 'java-library' @@ -26,11 +34,6 @@ if (!isMultiplatform) { } } -// empty xxx-javadoc.jar -task javadocJar(type: Jar) { - archiveClassifier = 'javadoc' -} - publishing { repositories { maven { @@ -46,12 +49,7 @@ publishing { } } - if (isBom) { - // Configure mavenBom publication - publications { - mavenBom(MavenPublication) {} - } - } else if (!isMultiplatform) { + if (!isMultiplatform && !isBom) { // Configure java publications for regular non-MPP modules publications { maven(MavenPublication) { @@ -106,4 +104,4 @@ task publishDevelopSnapshot() { } // Compatibility with old TeamCity configurations that perform :kotlinx-coroutines-core:bintrayUpload -task bintrayUpload(dependsOn: publish) \ No newline at end of file +task bintrayUpload(dependsOn: publish) diff --git a/kotlinx-coroutines-bom/build.gradle b/kotlinx-coroutines-bom/build.gradle index c6675dd33a..d78f079e8d 100644 --- a/kotlinx-coroutines-bom/build.gradle +++ b/kotlinx-coroutines-bom/build.gradle @@ -2,23 +2,37 @@ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ plugins { - id 'io.spring.dependency-management' + id 'java-platform' } def name = project.name -dependencyManagement { - dependencies { +dependencies { + constraints { rootProject.subprojects.each { - if (ext.unpublished.contains(it.name)) return + if (rootProject.ext.unpublished.contains(it.name)) return if (it.name == name) return if (!it.plugins.hasPlugin('maven-publish')) return evaluationDependsOn(it.path) it.publishing.publications.all { if (it.artifactId.endsWith("-kotlinMultiplatform")) return if (it.artifactId.endsWith("-metadata")) return - dependency(group: it.groupId, name: it.artifactId, version: it.version) + // Skip platform artifacts (like *-linuxx64, *-macosx64) + // It leads to inconsistent bom when publishing from different platforms + // (e.g. on linux it will include only linuxx64 artifacts and no macosx64) + // It shouldn't be a problem as usually consumers need to use generic *-native artifact + // Gradle will choose correct variant by using metadata attributes + if (it.artifacts.any { it.extension == 'klib' }) return + api(group: it.groupId, name: it.artifactId, version: it.version) } } } } + +publishing { + publications { + mavenBom(MavenPublication) { + from components.javaPlatform + } + } +} From cfc08ee605d94934f2c1d0ef159769689325b619 Mon Sep 17 00:00:00 2001 From: Nikita Koval Date: Wed, 2 Oct 2019 14:54:03 +0300 Subject: [PATCH 33/90] All ArrayChannel.size accesses should be under the channel lock --- .../common/src/channels/ArrayChannel.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index 0135778409..da284be525 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -36,16 +36,12 @@ internal open class ArrayChannel( */ private var buffer: Array = arrayOfNulls(min(capacity, 8)) private var head: Int = 0 - - private val _size = atomic(0) - private var size: Int // Invariant: size <= capacity - get() = _size.value - set(value) { _size.value = value } + private var size = 0 // Invariant: size <= capacity protected final override val isBufferAlwaysEmpty: Boolean get() = false protected final override val isBufferEmpty: Boolean get() = lock.withLock { size == 0 } protected final override val isBufferAlwaysFull: Boolean get() = false - protected final override val isBufferFull: Boolean get() = size == capacity + protected final override val isBufferFull: Boolean get() = lock.withLock { size == capacity } // result is `OFFER_SUCCESS | OFFER_FAILED | Closed` protected override fun offerInternal(element: E): Any { From d9d35747cfa336108734a47f7aac1aa05410389a Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 2 Oct 2019 14:42:30 +0300 Subject: [PATCH 34/90] Fix race in channel select/cancel This bug was introduced by PR #1524. It was reproducing when there is a regular "send" operation on one side of a channel and "select { onReceive }" on another side of the channel and the "send" coroutine gets cancelled. The problem is that SendElement.tryResumeSend implementation was calling finishPrepare before it has successfully resumed continuation, so if that continuation was already cancelled, the code in "finishPrepare" had already stored the wrong affected node which it would later try to call "completeResume" on. This patch also adds hexAddress to the debug toString method of all internal node classes in channel implementation. Fixes #1588 --- .../common/src/channels/AbstractChannel.kt | 26 +++++++++++-------- .../jvm/src/internal/LockFreeLinkedList.kt | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 489d24c308..8aadd26a73 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -466,7 +466,7 @@ internal abstract class AbstractSendChannel : SendChannel { select.resumeSelectCancellableWithException(closed.sendException) } - override fun toString(): String = "SendSelect($pollResult)[$channel, $select]" + override fun toString(): String = "SendSelect@$hexAddress($pollResult)[$channel, $select]" } internal class SendBuffered( @@ -476,6 +476,7 @@ internal abstract class AbstractSendChannel : SendChannel { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? = RESUME_TOKEN.also { otherOp?.finishPrepare() } override fun completeResumeSend() {} override fun resumeSendClosed(closed: Closed<*>) {} + override fun toString(): String = "SendBuffered@$hexAddress($element)" } } @@ -899,9 +900,10 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel cont.resumeWithException(closed.receiveException) } } - override fun toString(): String = "ReceiveElement[receiveMode=$receiveMode]" + override fun toString(): String = "ReceiveElement@$hexAddress[receiveMode=$receiveMode]" } private class ReceiveHasNext( @@ -922,9 +924,10 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel ) : Receive() { override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - otherOp?.finishPrepare() val token = cont.tryResume(true, otherOp?.desc) ?: 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() return RESUME_TOKEN } @@ -948,7 +951,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( @@ -983,7 +986,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) @@ -1036,7 +1039,7 @@ internal interface ReceiveOrClosed { // Returns: null - failure, // RETRY_ATOMIC for retry (only when otherOp != null), // RESUME_TOKEN on success (call completeResumeReceive) - // Must call otherOp?.finishPrepare() before deciding on result other than RETRY_ATOMIC + // Must call otherOp?.finishPrepare() after deciding on result other than RETRY_ATOMIC fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? fun completeResumeReceive(value: E) } @@ -1050,14 +1053,15 @@ internal class SendElement( @JvmField val cont: CancellableContinuation ) : Send() { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { - otherOp?.finishPrepare() val token = cont.tryResume(Unit, otherOp?.desc) ?: 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() // 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($pollResult)" + override fun toString(): String = "SendElement@$hexAddress($pollResult)" } /** @@ -1076,7 +1080,7 @@ internal class Closed( override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? = RESUME_TOKEN.also { otherOp?.finishPrepare() } override fun completeResumeReceive(value: E) {} override fun resumeSendClosed(closed: Closed<*>) = assert { false } // "Should be never invoked" - override fun toString(): String = "Closed[$closeCause]" + override fun toString(): String = "Closed@$hexAddress[$closeCause]" } private abstract class Receive : LockFreeLinkedListNode(), ReceiveOrClosed { diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt index 9da237dc5f..70568aa278 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt @@ -399,7 +399,7 @@ public actual open class LockFreeLinkedListNode { // Returns REMOVE_PREPARED or null (it makes decision on any failure) override fun perform(affected: Any?): Any? { - assert(affected === this.affected) + assert { affected === this.affected } affected as Node // type assertion val decision = desc.onPrepare(this) if (decision === REMOVE_PREPARED) { From 2fc00b89665daaa11abc763ffa7f92e875a8233c Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 4 Oct 2019 12:42:29 +0300 Subject: [PATCH 35/90] Update atomicfu version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ed6f22690b..34c11d5681 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ kotlin_version=1.3.50 # Dependencies junit_version=4.12 -atomicfu_version=0.13.0 +atomicfu_version=0.13.2 html_version=0.6.8 lincheck_version=2.0 dokka_version=0.9.16-rdev-2-mpp-hacks From ff9060c5f0054940e9204049ba30fd6797899ca0 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 26 Sep 2019 00:14:05 +0300 Subject: [PATCH 36/90] Shorten resume call stack Calls of afterCompletionInternal are pulled up the call stack. This is very important, since scoped coroutines do "uCont.resumeWith" from afterCompletionInternal, which makes all the JVM method visible in the debugger call frames and in exceptions. Additionally, this allows for some simplification of the JobSupport code, as a number of methods do not need "mode" parameter anymore. Moreover, the kludge of MODE_IGNORE is no longer needed and is dropped. Make TimeoutCoroutine extends ScopedCoroutines. Fixes #1574 --- .../common/src/AbstractCoroutine.kt | 6 +- .../common/src/JobSupport.kt | 188 +++++++++++------- .../common/src/ResumeMode.kt | 50 +---- kotlinx-coroutines-core/common/src/Timeout.kt | 20 +- .../src/internal/DispatchedContinuation.kt | 41 ++-- .../common/src/internal/DispatchedTask.kt | 41 ++-- .../common/src/internal/Scopes.kt | 18 +- .../common/src/intrinsics/Cancellable.kt | 8 +- .../common/src/intrinsics/Undispatched.kt | 27 +-- .../common/src/selects/Select.kt | 2 +- 10 files changed, 174 insertions(+), 227 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt index 014a74258c..5265d021aa 100644 --- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("DEPRECATION_ERROR") @@ -108,7 +108,9 @@ public abstract class AbstractCoroutine( * Completes execution of this with coroutine with the specified result. */ public final override fun resumeWith(result: Result) { - makeCompletingOnce(result.toState(), defaultResumeMode) + val state = makeCompletingOnce(result.toState()) + if (state === COMPLETING_WAITING_CHILDREN) return + afterCompletionInternal(state, defaultResumeMode) } internal final override fun handleOnCompletionException(exception: Throwable) { diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 74e0133006..670076f635 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -193,7 +193,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // Finalizes Finishing -> Completed (terminal state) transition. // ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method. - private fun tryFinalizeFinishingState(state: Finishing, proposedUpdate: Any?, mode: Int): Boolean { + // Returns final state that was created and updated to + private fun tryFinalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? { /* * Note: proposed state can be Incomplete, e.g. * async { @@ -234,8 +235,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // Then CAS to completed state -> it must succeed require(_state.compareAndSet(state, finalState.boxIncomplete())) { "Unexpected state: ${_state.value}, expected: $state, update: $finalState" } // And process all post-completion actions - completeStateFinalization(state, finalState, mode) - return true + completeStateFinalization(state, finalState) + return finalState } private fun getFinalRootCause(state: Finishing, exceptions: List): Throwable? { @@ -268,18 +269,19 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } // fast-path method to finalize normally completed coroutines without children - private fun tryFinalizeSimpleState(state: Incomplete, update: Any?, mode: Int): Boolean { + // returns true if complete, and afterCompletionInternal(update, mode) shall be called + private fun tryFinalizeSimpleState(state: Incomplete, update: Any?): Boolean { assert { state is Empty || state is JobNode<*> } // only simple state without lists where children can concurrently add assert { update !is CompletedExceptionally } // only for normal completion if (!_state.compareAndSet(state, update.boxIncomplete())) return false onCancelling(null) // simple state is not a failure onCompletionInternal(update) - completeStateFinalization(state, update, mode) + completeStateFinalization(state, update) return true } // suppressed == true when any exceptions were suppressed while building the final completion cause - private fun completeStateFinalization(state: Incomplete, update: Any?, mode: Int) { + private fun completeStateFinalization(state: Incomplete, update: Any?) { /* * Now the job in THE FINAL state. We need to properly handle the resulting state. * Order of various invocations here is important. @@ -304,11 +306,6 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } else { state.list?.notifyCompletion(cause) } - /* - * 3) Resumes the rest of the code in scoped coroutines - * (runBlocking, coroutineScope, withContext, withTimeout, etc) - */ - afterCompletionInternal(update, mode) } private fun notifyCancelling(list: NodeList, cause: Throwable) { @@ -641,28 +638,41 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // cause is Throwable or ParentJob when cancelChild was invoked // returns true is exception was handled, false otherwise internal fun cancelImpl(cause: Any?): Boolean { + var finalState: Any? = COMPLETING_ALREADY if (onCancelComplete) { - // make sure it is completing, if cancelMakeCompleting returns true it means it had make it + // make sure it is completing, if cancelMakeCompleting returns state it means it had make it // completing and had recorded exception - if (cancelMakeCompleting(cause)) return true - // otherwise just record exception via makeCancelling below + finalState = cancelMakeCompleting(cause) + if (finalState === COMPLETING_WAITING_CHILDREN) return true + } + if (finalState === COMPLETING_ALREADY) { + finalState = makeCancelling(cause) + } + return when { + finalState === COMPLETING_ALREADY -> true + finalState === COMPLETING_WAITING_CHILDREN -> true + finalState === TOO_LATE_TO_CANCEL -> false + else -> { + afterCompletionInternal(finalState, MODE_ATOMIC_DEFAULT) + true + } } - return makeCancelling(cause) } // cause is Throwable or ParentJob when cancelChild was invoked - private fun cancelMakeCompleting(cause: Any?): Boolean { + // It contains a loop and never returns COMPLETING_RETRY, can return + // COMPLETING_ALREADY -- if already completed/completing + // COMPLETING_WAITING_CHILDREN -- if started waiting for children + // final state -- when completed, for call to afterCompletionInternal + private fun cancelMakeCompleting(cause: Any?): Any? { loopOnState { state -> if (state !is Incomplete || state is Finishing && state.isCompleting) { - return false // already completed/completing, do not even propose update + // already completed/completing, do not even create exception to propose update + return COMPLETING_ALREADY } val proposedUpdate = CompletedExceptionally(createCauseException(cause)) - when (tryMakeCompleting(state, proposedUpdate, mode = MODE_ATOMIC_DEFAULT)) { - COMPLETING_ALREADY_COMPLETING -> return false - COMPLETING_COMPLETED, COMPLETING_WAITING_CHILDREN -> return true - COMPLETING_RETRY -> return@loopOnState - else -> error("unexpected result") - } + val finalState = tryMakeCompleting(state, proposedUpdate) + if (finalState !== COMPLETING_RETRY) return finalState } } @@ -689,13 +699,18 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // transitions to Cancelling state // cause is Throwable or ParentJob when cancelChild was invoked - private fun makeCancelling(cause: Any?): Boolean { + // It contains a loop and never returns COMPLETING_RETRY, can return + // COMPLETING_ALREADY -- if already completing or successfully made cancelling, added exception + // COMPLETING_WAITING_CHILDREN -- if started waiting for children, added exception + // TOO_LATE_TO_CANCEL -- too late to cancel, did not add exception + // final state -- when completed, for call to afterCompletionInternal + private fun makeCancelling(cause: Any?): Any? { var causeExceptionCache: Throwable? = null // lazily init result of createCauseException(cause) loopOnState { state -> when (state) { is Finishing -> { // already finishing -- collect exceptions val notifyRootCause = synchronized(state) { - if (state.isSealed) return false // too late, already sealed -- cannot add exception nor mark cancelled + if (state.isSealed) return TOO_LATE_TO_CANCEL // already sealed -- cannot add exception nor mark cancelled // add exception, do nothing is parent is cancelling child that is already being cancelled val wasCancelling = state.isCancelling // will notify if was not cancelling // Materialize missing exception if it is the first exception (otherwise -- don't) @@ -707,25 +722,25 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren state.rootCause.takeIf { !wasCancelling } } notifyRootCause?.let { notifyCancelling(state.list, it) } - return true + return COMPLETING_ALREADY } is Incomplete -> { // Not yet finishing -- try to make it cancelling val causeException = causeExceptionCache ?: createCauseException(cause).also { causeExceptionCache = it } if (state.isActive) { // active state becomes cancelling - if (tryMakeCancelling(state, causeException)) return true + if (tryMakeCancelling(state, causeException)) return COMPLETING_ALREADY } else { // non active state starts completing - when (tryMakeCompleting(state, CompletedExceptionally(causeException), mode = MODE_ATOMIC_DEFAULT)) { - COMPLETING_ALREADY_COMPLETING -> error("Cannot happen in $state") - COMPLETING_COMPLETED, COMPLETING_WAITING_CHILDREN -> return true // ok - COMPLETING_RETRY -> return@loopOnState - else -> error("unexpected result") + val finalState = tryMakeCompleting(state, CompletedExceptionally(causeException)) + when { + finalState === COMPLETING_ALREADY -> error("Cannot happen in $state") + finalState === COMPLETING_RETRY -> return@loopOnState + else -> return finalState } } } - else -> return false // already complete + else -> return TOO_LATE_TO_CANCEL // already complete } } } @@ -759,45 +774,55 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } /** - * This function is used by [CompletableDeferred.complete] (and exceptionally) and by [JobImpl.cancel]. - * It returns `false` on repeated invocation (when this job is already completing). - * - * @suppress **This is unstable API and it is subject to change.** + * Completes this job. Used by [CompletableDeferred.complete] (and exceptionally) + * and by [JobImpl.cancel]. It returns `false` on repeated invocation + * (when this job is already completing). */ - internal fun makeCompleting(proposedUpdate: Any?): Boolean = loopOnState { state -> - when (tryMakeCompleting(state, proposedUpdate, mode = MODE_ATOMIC_DEFAULT)) { - COMPLETING_ALREADY_COMPLETING -> return false - COMPLETING_COMPLETED, COMPLETING_WAITING_CHILDREN -> return true - COMPLETING_RETRY -> return@loopOnState - else -> error("unexpected result") + internal fun makeCompleting(proposedUpdate: Any?): Boolean { + loopOnState { state -> + val finalState = tryMakeCompleting(state, proposedUpdate) + when { + finalState === COMPLETING_ALREADY -> return false + finalState === COMPLETING_WAITING_CHILDREN -> return true + finalState === COMPLETING_RETRY -> return@loopOnState + else -> { + afterCompletionInternal(finalState, MODE_ATOMIC_DEFAULT) + return true + } + } } - } + } /** - * This function is used by [AbstractCoroutine.resume]. - * It throws exception on repeated invocation (when this job is already completing). - * + * Completes this job. Used by [AbstractCoroutine.resume]. + * It throws [IllegalStateException] on repeated invocation (when this job is already completing). * Returns: - * * `true` if state was updated to completed/cancelled; - * * `false` if made completing or it is cancelling and is waiting for children. - * - * @throws IllegalStateException if job is already complete or completing - * @suppress **This is unstable API and it is subject to change.** + * * [COMPLETING_WAITING_CHILDREN] if started waiting for children. + * * Final state otherwise (caller should do [afterCompletionInternal]) */ - internal fun makeCompletingOnce(proposedUpdate: Any?, mode: Int): Boolean = loopOnState { state -> - when (tryMakeCompleting(state, proposedUpdate, mode)) { - COMPLETING_ALREADY_COMPLETING -> throw IllegalStateException("Job $this is already complete or completing, " + - "but is being completed with $proposedUpdate", proposedUpdate.exceptionOrNull) - COMPLETING_COMPLETED -> return true - COMPLETING_WAITING_CHILDREN -> return false - COMPLETING_RETRY -> return@loopOnState - else -> error("unexpected result") + internal fun makeCompletingOnce(proposedUpdate: Any?): Any? { + loopOnState { state -> + val finalState = tryMakeCompleting(state, proposedUpdate) + when { + finalState === COMPLETING_ALREADY -> + throw IllegalStateException( + "Job $this is already complete or completing, " + + "but is being completed with $proposedUpdate", proposedUpdate.exceptionOrNull + ) + finalState === COMPLETING_RETRY -> return@loopOnState + else -> return finalState // COMPLETING_WAITING_CHILDREN or final state + } } } - private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?, mode: Int): Int { + // Returns one of COMPLETING symbols or final state: + // COMPLETING_ALREADY -- when already complete or completing + // COMPLETING_RETRY -- when need to retry due to interference + // COMPLETING_WAITING_CHILDREN -- when made completing and is waiting for children + // final state -- when completed, for call to afterCompletionInternal + private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?): Any? { if (state !is Incomplete) - return COMPLETING_ALREADY_COMPLETING + return COMPLETING_ALREADY /* * FAST PATH -- no children to wait for && simple state (no list) && not cancelling => can complete immediately * Cancellation (failures) always have to go through Finishing state to serialize exception handling. @@ -805,14 +830,22 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren * which may miss unhandled exception. */ if ((state is Empty || state is JobNode<*>) && state !is ChildHandleNode && proposedUpdate !is CompletedExceptionally) { - if (!tryFinalizeSimpleState(state, proposedUpdate, mode)) return COMPLETING_RETRY - return COMPLETING_COMPLETED + if (tryFinalizeSimpleState(state, proposedUpdate)) { + // Completed successfully on fast path -- return updated state + return proposedUpdate + } + return COMPLETING_RETRY } // The separate slow-path function to simplify profiling - return tryMakeCompletingSlowPath(state, proposedUpdate, mode) + return tryMakeCompletingSlowPath(state, proposedUpdate) } - private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?, mode: Int): Int { + // Returns one of COMPLETING symbols or final state: + // COMPLETING_ALREADY -- when already complete or completing + // COMPLETING_RETRY -- when need to retry due to interference + // COMPLETING_WAITING_CHILDREN -- when made completing and is waiting for children + // final state -- when completed, for call to afterCompletionInternal + private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? { // get state's list or else promote to list to correctly operate on child lists val list = getOrPromoteCancellingList(state) ?: return COMPLETING_RETRY // promote to Finishing state if we are not in it yet @@ -823,7 +856,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren var notifyRootCause: Throwable? = null synchronized(finishing) { // check if this state is already completing - if (finishing.isCompleting) return COMPLETING_ALREADY_COMPLETING + if (finishing.isCompleting) return COMPLETING_ALREADY // mark as completing finishing.isCompleting = true // if we need to promote to finishing then atomically do it here. @@ -847,10 +880,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (child != null && tryWaitForChild(finishing, child, proposedUpdate)) return COMPLETING_WAITING_CHILDREN // otherwise -- we have not children left (all were already cancelled?) - if (tryFinalizeFinishingState(finishing, proposedUpdate, mode)) - return COMPLETING_COMPLETED - // otherwise retry - return COMPLETING_RETRY + return tryFinalizeFinishingState(finishing, proposedUpdate) } private val Any?.exceptionOrNull: Throwable? @@ -879,7 +909,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // try wait for next child if (waitChild != null && tryWaitForChild(state, waitChild, proposedUpdate)) return // waiting for next child // no more children to wait -- try update state - if (tryFinalizeFinishingState(state, proposedUpdate, MODE_ATOMIC_DEFAULT)) return + val finalState = tryFinalizeFinishingState(state, proposedUpdate) + afterCompletionInternal(finalState, MODE_ATOMIC_DEFAULT) } private fun LockFreeLinkedListNode.nextChild(): ChildHandleNode? { @@ -1233,10 +1264,15 @@ internal fun Any?.unboxState(): Any? = (this as? IncompleteStateBox)?.state ?: t // --------------- helper classes & constants for job implementation -private const val COMPLETING_ALREADY_COMPLETING = 0 -private const val COMPLETING_COMPLETED = 1 -private const val COMPLETING_WAITING_CHILDREN = 2 -private const val COMPLETING_RETRY = 3 +@SharedImmutable +private val COMPLETING_ALREADY = Symbol("COMPLETING_ALREADY") +@JvmField +@SharedImmutable +internal val COMPLETING_WAITING_CHILDREN = Symbol("COMPLETING_WAITING_CHILDREN") +@SharedImmutable +private val COMPLETING_RETRY = Symbol("COMPLETING_RETRY") +@SharedImmutable +private val TOO_LATE_TO_CANCEL = Symbol("TOO_LATE_TO_CANCEL") private const val RETRY = -1 private const val FALSE = 0 diff --git a/kotlinx-coroutines-core/common/src/ResumeMode.kt b/kotlinx-coroutines-core/common/src/ResumeMode.kt index 0afea98c7f..1675ad7e8d 100644 --- a/kotlinx-coroutines-core/common/src/ResumeMode.kt +++ b/kotlinx-coroutines-core/common/src/ResumeMode.kt @@ -1,61 +1,13 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 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.coroutines.intrinsics.* - @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_DIRECT = 2 // when the context is right just invoke the delegate continuation direct @PublishedApi internal const val MODE_UNDISPATCHED = 3 // when the thread is right, but need to mark it with current coroutine -@PublishedApi internal const val MODE_IGNORE = 4 // don't do anything internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE internal val Int.isDispatchedMode get() = this == MODE_ATOMIC_DEFAULT || this == MODE_CANCELLABLE - -internal fun Continuation.resumeMode(value: T, mode: Int) { - when (mode) { - MODE_ATOMIC_DEFAULT -> resume(value) - MODE_CANCELLABLE -> resumeCancellable(value) - MODE_DIRECT -> resumeDirect(value) - MODE_UNDISPATCHED -> (this as DispatchedContinuation).resumeUndispatched(value) - MODE_IGNORE -> {} - else -> error("Invalid mode $mode") - } -} - -internal fun Continuation.resumeWithExceptionMode(exception: Throwable, mode: Int) { - when (mode) { - MODE_ATOMIC_DEFAULT -> resumeWithException(exception) - MODE_CANCELLABLE -> resumeCancellableWithException(exception) - MODE_DIRECT -> resumeDirectWithException(exception) - MODE_UNDISPATCHED -> (this as DispatchedContinuation).resumeUndispatchedWithException(exception) - MODE_IGNORE -> {} - else -> error("Invalid mode $mode") - } -} - -internal fun Continuation.resumeUninterceptedMode(value: T, mode: Int) { - when (mode) { - MODE_ATOMIC_DEFAULT -> intercepted().resume(value) - MODE_CANCELLABLE -> intercepted().resumeCancellable(value) - MODE_DIRECT -> resume(value) - MODE_UNDISPATCHED -> withCoroutineContext(context, null) { resume(value) } - MODE_IGNORE -> {} - else -> error("Invalid mode $mode") - } -} - -internal fun Continuation.resumeUninterceptedWithExceptionMode(exception: Throwable, mode: Int) { - when (mode) { - MODE_ATOMIC_DEFAULT -> intercepted().resumeWithException(exception) - MODE_CANCELLABLE -> intercepted().resumeCancellableWithException(exception) - MODE_DIRECT -> resumeWithException(exception) - MODE_UNDISPATCHED -> withCoroutineContext(context, null) { resumeWithException(exception) } - MODE_IGNORE -> {} - else -> error("Invalid mode $mode") - } -} diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 8bfaf336fe..512c2a55c3 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -80,26 +80,12 @@ private fun setupTimeout( private open class TimeoutCoroutine( @JvmField val time: Long, - @JvmField val uCont: Continuation // unintercepted continuation -) : AbstractCoroutine(uCont.context, active = true), Runnable, Continuation, CoroutineStackFrame { - override val defaultResumeMode: Int get() = MODE_DIRECT - override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame) - override fun getStackTraceElement(): StackTraceElement? = null - override val isScopedCoroutine: Boolean get() = true - - @Suppress("LeakingThis", "Deprecation") + uCont: Continuation // unintercepted continuation +) : ScopeCoroutine(uCont.context, uCont), Runnable { override fun run() { cancelCoroutine(TimeoutCancellationException(time, this)) } - @Suppress("UNCHECKED_CAST") - override fun afterCompletionInternal(state: Any?, mode: Int) { - if (state is CompletedExceptionally) - uCont.resumeUninterceptedWithExceptionMode(state.cause, mode) - else - uCont.resumeUninterceptedMode(state as T, mode) - } - override fun nameString(): String = "${super.nameString()}(timeMillis=$time)" } diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index bb5e312410..0a5553f053 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -175,32 +175,16 @@ internal class DispatchedContinuation( } @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeCancellable(value: T) { - if (dispatcher.isDispatchNeeded(context)) { - _state = value - resumeMode = MODE_CANCELLABLE - dispatcher.dispatch(context, this) - } else { - executeUnconfined(value, MODE_CANCELLABLE) { - if (!resumeCancelled()) { - resumeUndispatched(value) - } - } - } - } - - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeCancellableWithException(exception: Throwable) { - val context = continuation.context - val state = CompletedExceptionally(exception) + inline fun resumeCancellableWith(result: Result) { + val state = result.toState() if (dispatcher.isDispatchNeeded(context)) { - _state = CompletedExceptionally(exception) + _state = state resumeMode = MODE_CANCELLABLE dispatcher.dispatch(context, this) } else { executeUnconfined(state, MODE_CANCELLABLE) { if (!resumeCancelled()) { - resumeUndispatchedWithException(exception) + resumeUndispatchedWith(result) } } } @@ -218,16 +202,9 @@ internal class DispatchedContinuation( } @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeUndispatched(value: T) { - withCoroutineContext(context, countOrElement) { - continuation.resume(value) - } - } - - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack - inline fun resumeUndispatchedWithException(exception: Throwable) { + inline fun resumeUndispatchedWith(result: Result) { withCoroutineContext(context, countOrElement) { - continuation.resumeWithStackTrace(exception) + continuation.resumeWith(result) } } @@ -243,6 +220,12 @@ internal class DispatchedContinuation( "DispatchedContinuation[$dispatcher, ${continuation.toDebugString()}]" } +@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack +internal inline fun Continuation.resumeCancellableWith(result: Result) = when (this) { + is DispatchedContinuation -> resumeCancellableWith(result) + else -> resumeWith(result) +} + internal fun DispatchedContinuation.yieldUndispatched(): Boolean = executeUnconfined(Unit, MODE_CANCELLABLE, doYield = true) { run() diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index eb72b5a95a..0bc0dfa15f 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -105,21 +105,29 @@ internal fun DispatchedTask.dispatch(mode: Int = MODE_CANCELLABLE) { } } +@Suppress("UNCHECKED_CAST") internal fun DispatchedTask.resume(delegate: Continuation, useMode: Int) { // slow-path - use delegate val state = takeState() - val exception = getExceptionalResult(state) - if (exception != null) { + val exception = getExceptionalResult(state)?.let { /* * Recover stacktrace for non-dispatched tasks. * We usually do not recover stacktrace in a `resume` as all resumes go through `DispatchedTask.run` * and we recover stacktraces there, but this is not the case for a `suspend fun main()` that knows nothing about * kotlinx.coroutines and DispatchedTask */ - val recovered = if (delegate is DispatchedTask<*>) exception else recoverStackTrace(exception, delegate) - delegate.resumeWithExceptionMode(recovered, useMode) - } else { - delegate.resumeMode(getSuccessfulResult(state), useMode) + if (delegate is DispatchedTask<*>) it else recoverStackTrace(it, delegate) + } + val result = if (exception != null) + Result.failure(exception) + else + Result.success(state as T) + when (useMode) { + MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) + MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) + MODE_DIRECT -> ((delegate as? DispatchedContinuation)?.continuation ?: delegate).resumeWith(result) + MODE_UNDISPATCHED -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) + else -> error("Invalid mode $useMode") } } @@ -158,27 +166,6 @@ internal inline fun DispatchedTask<*>.runUnconfinedEventLoop( } } - -internal fun Continuation.resumeCancellable(value: T) = when (this) { - is DispatchedContinuation -> resumeCancellable(value) - else -> resume(value) -} - -internal fun Continuation.resumeCancellableWithException(exception: Throwable) = when (this) { - is DispatchedContinuation -> resumeCancellableWithException(exception) - else -> resumeWithStackTrace(exception) -} - -internal fun Continuation.resumeDirect(value: T) = when (this) { - is DispatchedContinuation -> continuation.resume(value) - else -> resume(value) -} - -internal fun Continuation.resumeDirectWithException(exception: Throwable) = when (this) { - is DispatchedContinuation -> continuation.resumeWithStackTrace(exception) - else -> resumeWithStackTrace(exception) -} - @Suppress("NOTHING_TO_INLINE") internal inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) { resumeWith(Result.failure(recoverStackTrace(exception, this))) diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index 9197ec83c0..aefffdb252 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -1,11 +1,12 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 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.* +import kotlin.coroutines.intrinsics.* import kotlin.jvm.* /** @@ -25,11 +26,16 @@ internal open class ScopeCoroutine( @Suppress("UNCHECKED_CAST") override fun afterCompletionInternal(state: Any?, mode: Int) { - if (state is CompletedExceptionally) { - val exception = if (mode == MODE_IGNORE) state.cause else recoverStackTrace(state.cause, uCont) - uCont.resumeUninterceptedWithExceptionMode(exception, mode) - } else { - uCont.resumeUninterceptedMode(state as T, mode) + val result = if (state is CompletedExceptionally) + Result.failure(recoverStackTrace(state.cause, uCont)) + else + Result.success(state as T) + when (mode) { + MODE_ATOMIC_DEFAULT -> uCont.intercepted().resumeWith(result) + MODE_CANCELLABLE -> uCont.intercepted().resumeCancellableWith(result) + MODE_DIRECT -> uCont.resumeWith(result) + MODE_UNDISPATCHED -> withCoroutineContext(uCont.context, null) { uCont.resumeWith(result) } + else -> error("Invalid mode $mode") } } } diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt index ca0ab18d8a..2027d9bd50 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Cancellable.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.intrinsics @@ -14,7 +14,7 @@ import kotlin.coroutines.intrinsics.* */ @InternalCoroutinesApi public fun (suspend () -> T).startCoroutineCancellable(completion: Continuation) = runSafely(completion) { - createCoroutineUnintercepted(completion).intercepted().resumeCancellable(Unit) + createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit)) } /** @@ -23,7 +23,7 @@ public fun (suspend () -> T).startCoroutineCancellable(completion: Continuat */ internal fun (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation) = runSafely(completion) { - createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit) + createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit)) } /** @@ -32,7 +32,7 @@ internal fun (suspend (R) -> T).startCoroutineCancellable(receiver: R, co */ internal fun Continuation.startCoroutineCancellable(fatalCompletion: Continuation<*>) = runSafely(fatalCompletion) { - intercepted().resumeCancellable(Unit) + intercepted().resumeCancellableWith(Result.success(Unit)) } /** diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt index 0da91b9bec..4850bd619b 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.intrinsics @@ -112,7 +112,6 @@ private inline fun AbstractCoroutine.undispatchedResult( } catch (e: Throwable) { CompletedExceptionally(e) } - /* * We're trying to complete our undispatched block here and have three code-paths: * 1) Suspended. @@ -127,20 +126,16 @@ private inline fun AbstractCoroutine.undispatchedResult( * If timeout is exceeded, but withTimeout() block was not suspended, we would like to return block value, * not a timeout exception. */ - return when { - result === COROUTINE_SUSPENDED -> COROUTINE_SUSPENDED - makeCompletingOnce(result, MODE_IGNORE) -> { - val state = state - if (state is CompletedExceptionally) { - when { - shouldThrow(state.cause) -> throw tryRecover(state.cause) - result is CompletedExceptionally -> throw tryRecover(result.cause) - else -> result - } - } else { - state.unboxState() - } + if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED + val state = makeCompletingOnce(result) + if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED + return if (state is CompletedExceptionally) { + when { + shouldThrow(state.cause) -> throw tryRecover(state.cause) + result is CompletedExceptionally -> throw tryRecover(result.cause) + else -> result } - else -> COROUTINE_SUSPENDED + } else { + state.unboxState() } } diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 372c9e32dc..29d31b9cb1 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -285,7 +285,7 @@ internal class SelectBuilderImpl( // Resumes in MODE_CANCELLABLE override fun resumeSelectCancellableWithException(exception: Throwable) { doResume({ CompletedExceptionally(exception) }) { - uCont.intercepted().resumeCancellableWithException(exception) + uCont.intercepted().resumeCancellableWith(Result.failure(exception)) } } From 5f2413a54e475c73add8298f2970d6bde8a95077 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 26 Sep 2019 14:11:15 +0300 Subject: [PATCH 37/90] Get rid of AbstractCoroutine.defaultResumeMode It was only needed to customize resume mode from within of AbstractCoroutine.resumeWith method. This is now achieved by a separate open fun afterResume. Also, afterCompletionInternal is now renamed to afterCompletion and is only used from the job was completed from unknown context and thus should be resumed in a default way. This "default way" is now "cancellable resume" (to minimize the amount of code duplication) which does not introduce any functional difference, since that only happens on cancellation. Remove MODE_DIRECT resume mode --- .../kotlinx-coroutines-core.txt | 3 +- .../common/src/AbstractCoroutine.kt | 6 +-- .../common/src/Builders.common.kt | 30 +++++++---- .../common/src/CompletedExceptionally.kt | 10 +++- .../common/src/JobSupport.kt | 50 +++++++++---------- .../common/src/ResumeMode.kt | 13 ----- .../common/src/internal/DispatchedTask.kt | 10 +++- .../common/src/internal/Scopes.kt | 23 +++------ .../common/src/intrinsics/Undispatched.kt | 18 +++---- .../common/src/selects/Select.kt | 6 +-- kotlinx-coroutines-core/jvm/src/Builders.kt | 4 +- 11 files changed, 88 insertions(+), 85 deletions(-) delete mode 100644 kotlinx-coroutines-core/common/src/ResumeMode.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 26c152c141..fdc241be62 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -2,6 +2,7 @@ public abstract class kotlinx/coroutines/AbstractCoroutine : kotlinx/coroutines/ protected final field parentContext Lkotlin/coroutines/CoroutineContext; public fun (Lkotlin/coroutines/CoroutineContext;Z)V public synthetic fun (Lkotlin/coroutines/CoroutineContext;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected fun afterResume (Ljava/lang/Object;)V public final fun getContext ()Lkotlin/coroutines/CoroutineContext; public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; public fun isActive ()Z @@ -380,7 +381,7 @@ public final class kotlinx/coroutines/JobKt { public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlinx/coroutines/Job, kotlinx/coroutines/ParentJob, kotlinx/coroutines/selects/SelectClause0 { public fun (Z)V - protected fun afterCompletionInternal (Ljava/lang/Object;I)V + protected fun afterCompletion (Ljava/lang/Object;)V public final fun attachChild (Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; public synthetic fun cancel ()V public synthetic fun cancel (Ljava/lang/Throwable;)Z diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt index 5265d021aa..b1817f4ab4 100644 --- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt @@ -102,17 +102,17 @@ public abstract class AbstractCoroutine( onCompleted(state as T) } - internal open val defaultResumeMode: Int get() = MODE_ATOMIC_DEFAULT - /** * Completes execution of this with coroutine with the specified result. */ public final override fun resumeWith(result: Result) { val state = makeCompletingOnce(result.toState()) if (state === COMPLETING_WAITING_CHILDREN) return - afterCompletionInternal(state, defaultResumeMode) + afterResume(state) } + protected open fun afterResume(state: Any?) = afterCompletion(state) + internal final override fun handleOnCompletionException(exception: Throwable) { handleCoroutineException(context, exception) } diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index f79cfc7339..5973ed104a 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:JvmMultifileClass @@ -142,20 +142,20 @@ public suspend fun withContext( newContext.checkCompletion() // FAST PATH #1 -- new context is the same as the old one if (newContext === oldContext) { - val coroutine = ScopeCoroutine(newContext, uCont) // MODE_DIRECT + val coroutine = ScopeCoroutine(newContext, uCont) return@sc coroutine.startUndispatchedOrReturn(coroutine, block) } // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed) // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher) if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) { - val coroutine = UndispatchedCoroutine(newContext, uCont) // MODE_UNDISPATCHED + val coroutine = UndispatchedCoroutine(newContext, uCont) // There are changes in the context, so this thread needs to be updated withCoroutineContext(newContext, null) { return@sc coroutine.startUndispatchedOrReturn(coroutine, block) } } // SLOW PATH -- use new dispatcher - val coroutine = DispatchedCoroutine(newContext, uCont) // MODE_CANCELLABLE + val coroutine = DispatchedCoroutine(newContext, uCont) coroutine.initParentJob() block.startCoroutineCancellable(coroutine, coroutine) coroutine.getResult() @@ -200,7 +200,13 @@ private class UndispatchedCoroutine( context: CoroutineContext, uCont: Continuation ) : ScopeCoroutine(context, uCont) { - override val defaultResumeMode: Int get() = MODE_UNDISPATCHED + override fun afterResume(state: Any?) { + // resume undispatched -- update context by stay on the same dispatcher + val result = recoverResult(state, uCont) + withCoroutineContext(uCont.context, null) { + uCont.resumeWith(result) + } + } } private const val UNDECIDED = 0 @@ -212,8 +218,6 @@ private class DispatchedCoroutine( context: CoroutineContext, uCont: Continuation ) : ScopeCoroutine(context, uCont) { - override val defaultResumeMode: Int get() = MODE_CANCELLABLE - // this is copy-and-paste of a decision state machine inside AbstractionContinuation // todo: we may some-how abstract it via inline class private val _decision = atomic(UNDECIDED) @@ -238,10 +242,16 @@ private class DispatchedCoroutine( } } - override fun afterCompletionInternal(state: Any?, mode: Int) { + override fun afterCompletion(state: Any?) { + // Call afterResume from afterCompletion and not vice-versa, because stack-size is more + // important for afterResume implementation + afterResume(state) + } + + override fun afterResume(state: Any?) { if (tryResume()) return // completed before getResult invocation -- bail out - // otherwise, getResult has already commenced, i.e. completed later or in other thread - super.afterCompletionInternal(state, mode) + // Resume in a cancellable way because we have to switch back to the original dispatcher + uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont)) } fun getResult(): Any? { diff --git a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt b/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt index d15c857566..785041d524 100644 --- a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt +++ b/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt @@ -1,16 +1,24 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines import kotlinx.atomicfu.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* internal fun Result.toState(): Any? = if (isSuccess) getOrThrow() else CompletedExceptionally(exceptionOrNull()!!) // todo: need to do it better +@Suppress("RESULT_CLASS_IN_RETURN_TYPE", "UNCHECKED_CAST") +internal fun recoverResult(state: Any?, uCont: Continuation): Result = + if (state is CompletedExceptionally) + Result.failure(recoverStackTrace(state.cause, uCont)) + else + Result.success(state as T) + /** * Class for an internal state of a job that was cancelled (completed exceptionally). * diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 670076f635..a2bcbf55b4 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -194,16 +194,16 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // Finalizes Finishing -> Completed (terminal state) transition. // ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method. // Returns final state that was created and updated to - private fun tryFinalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? { + private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? { /* * Note: proposed state can be Incomplete, e.g. * async { * something.invokeOnCompletion {} // <- returns handle which implements Incomplete under the hood * } */ - require(this.state === state) // consistency check -- it cannot change - require(!state.isSealed) // consistency check -- cannot be sealed yet - require(state.isCompleting) // consistency check -- must be marked as completing + assert { this.state === state } // consistency check -- it cannot change + assert { !state.isSealed } // consistency check -- cannot be sealed yet + 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 @@ -233,7 +233,8 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (!wasCancelling) onCancelling(finalException) onCompletionInternal(finalState) // Then CAS to completed state -> it must succeed - require(_state.compareAndSet(state, finalState.boxIncomplete())) { "Unexpected state: ${_state.value}, expected: $state, update: $finalState" } + val casSuccess = _state.compareAndSet(state, finalState.boxIncomplete()) + assert { casSuccess } // And process all post-completion actions completeStateFinalization(state, finalState) return finalState @@ -269,7 +270,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } // fast-path method to finalize normally completed coroutines without children - // returns true if complete, and afterCompletionInternal(update, mode) shall be called + // returns true if complete, and afterCompletion(update) shall be called private fun tryFinalizeSimpleState(state: Incomplete, update: Any?): Boolean { assert { state is Empty || state is JobNode<*> } // only simple state without lists where children can concurrently add assert { update !is CompletedExceptionally } // only for normal completion @@ -495,10 +496,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren private fun makeNode(handler: CompletionHandler, onCancelling: Boolean): JobNode<*> { return if (onCancelling) - (handler as? JobCancellingNode<*>)?.also { require(it.job === this) } + (handler as? JobCancellingNode<*>)?.also { assert { it.job === this } } ?: InvokeOnCancelling(this, handler) else - (handler as? JobNode<*>)?.also { require(it.job === this && it !is JobCancellingNode) } + (handler as? JobNode<*>)?.also { assert { it.job === this && it !is JobCancellingNode } } ?: InvokeOnCompletion(this, handler) } @@ -653,7 +654,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren finalState === COMPLETING_WAITING_CHILDREN -> true finalState === TOO_LATE_TO_CANCEL -> false else -> { - afterCompletionInternal(finalState, MODE_ATOMIC_DEFAULT) + afterCompletion(finalState) true } } @@ -663,7 +664,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // It contains a loop and never returns COMPLETING_RETRY, can return // COMPLETING_ALREADY -- if already completed/completing // COMPLETING_WAITING_CHILDREN -- if started waiting for children - // final state -- when completed, for call to afterCompletionInternal + // final state -- when completed, for call to afterCompletion private fun cancelMakeCompleting(cause: Any?): Any? { loopOnState { state -> if (state !is Incomplete || state is Finishing && state.isCompleting) { @@ -703,7 +704,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // COMPLETING_ALREADY -- if already completing or successfully made cancelling, added exception // COMPLETING_WAITING_CHILDREN -- if started waiting for children, added exception // TOO_LATE_TO_CANCEL -- too late to cancel, did not add exception - // final state -- when completed, for call to afterCompletionInternal + // final state -- when completed, for call to afterCompletion private fun makeCancelling(cause: Any?): Any? { var causeExceptionCache: Throwable? = null // lazily init result of createCauseException(cause) loopOnState { state -> @@ -786,7 +787,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren finalState === COMPLETING_WAITING_CHILDREN -> return true finalState === COMPLETING_RETRY -> return@loopOnState else -> { - afterCompletionInternal(finalState, MODE_ATOMIC_DEFAULT) + afterCompletion(finalState) return true } } @@ -798,7 +799,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren * It throws [IllegalStateException] on repeated invocation (when this job is already completing). * Returns: * * [COMPLETING_WAITING_CHILDREN] if started waiting for children. - * * Final state otherwise (caller should do [afterCompletionInternal]) + * * Final state otherwise (caller should do [afterCompletion]) */ internal fun makeCompletingOnce(proposedUpdate: Any?): Any? { loopOnState { state -> @@ -819,7 +820,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // COMPLETING_ALREADY -- when already complete or completing // COMPLETING_RETRY -- when need to retry due to interference // COMPLETING_WAITING_CHILDREN -- when made completing and is waiting for children - // final state -- when completed, for call to afterCompletionInternal + // final state -- when completed, for call to afterCompletion private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?): Any? { if (state !is Incomplete) return COMPLETING_ALREADY @@ -844,7 +845,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // COMPLETING_ALREADY -- when already complete or completing // COMPLETING_RETRY -- when need to retry due to interference // COMPLETING_WAITING_CHILDREN -- when made completing and is waiting for children - // final state -- when completed, for call to afterCompletionInternal + // final state -- when completed, for call to afterCompletion private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? { // get state's list or else promote to list to correctly operate on child lists val list = getOrPromoteCancellingList(state) ?: return COMPLETING_RETRY @@ -866,7 +867,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (!_state.compareAndSet(state, finishing)) return COMPLETING_RETRY } // ## IMPORTANT INVARIANT: Only one thread (that had set isCompleting) can go past this point - require(!finishing.isSealed) // cannot be sealed + assert { !finishing.isSealed } // cannot be sealed // add new proposed exception to the finishing state val wasCancelling = finishing.isCancelling (proposedUpdate as? CompletedExceptionally)?.let { finishing.addExceptionLocked(it.cause) } @@ -880,7 +881,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (child != null && tryWaitForChild(finishing, child, proposedUpdate)) return COMPLETING_WAITING_CHILDREN // otherwise -- we have not children left (all were already cancelled?) - return tryFinalizeFinishingState(finishing, proposedUpdate) + return finalizeFinishingState(finishing, proposedUpdate) } private val Any?.exceptionOrNull: Throwable? @@ -903,14 +904,14 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // ## IMPORTANT INVARIANT: Only one thread can be concurrently invoking this method. private fun continueCompleting(state: Finishing, lastChild: ChildHandleNode, proposedUpdate: Any?) { - require(this.state === state) // consistency check -- it cannot change while we are waiting for children + assert { this.state === state } // consistency check -- it cannot change while we are waiting for children // figure out if we need to wait for next child val waitChild = lastChild.nextChild() // try wait for next child if (waitChild != null && tryWaitForChild(state, waitChild, proposedUpdate)) return // waiting for next child // no more children to wait -- try update state - val finalState = tryFinalizeFinishingState(state, proposedUpdate) - afterCompletionInternal(finalState, MODE_ATOMIC_DEFAULT) + val finalState = finalizeFinishingState(state, proposedUpdate) + afterCompletion(finalState) } private fun LockFreeLinkedListNode.nextChild(): ChildHandleNode? { @@ -1014,14 +1015,13 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren protected open fun onCompletionInternal(state: Any?) {} /** - * Override for the very last action on job's completion to resume the rest of the code in scoped coroutines. - * - * @param state the final state. - * @param mode completion mode. + * Override for the very last action on job's completion to resume the rest of the code in + * scoped coroutines. It is called when this job is externally completed in an unknown + * context and thus should resume with a default mode. * * @suppress **This is unstable API and it is subject to change.** */ - protected open fun afterCompletionInternal(state: Any?, mode: Int) {} + protected open fun afterCompletion(state: Any?) {} // for nicer debugging public override fun toString(): String = diff --git a/kotlinx-coroutines-core/common/src/ResumeMode.kt b/kotlinx-coroutines-core/common/src/ResumeMode.kt deleted file mode 100644 index 1675ad7e8d..0000000000 --- a/kotlinx-coroutines-core/common/src/ResumeMode.kt +++ /dev/null @@ -1,13 +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 - -@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_DIRECT = 2 // when the context is right just invoke the delegate continuation direct -@PublishedApi internal const val MODE_UNDISPATCHED = 3 // when the thread is right, but need to mark it with current coroutine - -internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE -internal val Int.isDispatchedMode get() = this == MODE_ATOMIC_DEFAULT || this == MODE_CANCELLABLE diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 0bc0dfa15f..6c33aba783 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -8,6 +8,13 @@ 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 + +internal val Int.isCancellableMode get() = this == MODE_CANCELLABLE +internal val Int.isDispatchedMode get() = this == MODE_ATOMIC_DEFAULT || this == MODE_CANCELLABLE + internal abstract class DispatchedTask( @JvmField public var resumeMode: Int ) : SchedulerTask() { @@ -89,7 +96,7 @@ internal abstract class DispatchedTask( } } -internal fun DispatchedTask.dispatch(mode: Int = MODE_CANCELLABLE) { +internal fun DispatchedTask.dispatch(mode: Int) { val delegate = this.delegate if (mode.isDispatchedMode && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { // dispatch directly using this instance's Runnable implementation @@ -125,7 +132,6 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In when (useMode) { MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) - MODE_DIRECT -> ((delegate as? DispatchedContinuation)?.continuation ?: delegate).resumeWith(result) MODE_UNDISPATCHED -> (delegate as DispatchedContinuation).resumeUndispatchedWith(result) else -> error("Invalid mode $useMode") } diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index aefffdb252..49770c2ed4 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -20,23 +20,16 @@ internal open class ScopeCoroutine( final override fun getStackTraceElement(): StackTraceElement? = null final override val isScopedCoroutine: Boolean get() = true - override val defaultResumeMode: Int get() = MODE_DIRECT - internal val parent: Job? get() = parentContext[Job] - @Suppress("UNCHECKED_CAST") - override fun afterCompletionInternal(state: Any?, mode: Int) { - val result = if (state is CompletedExceptionally) - Result.failure(recoverStackTrace(state.cause, uCont)) - else - Result.success(state as T) - when (mode) { - MODE_ATOMIC_DEFAULT -> uCont.intercepted().resumeWith(result) - MODE_CANCELLABLE -> uCont.intercepted().resumeCancellableWith(result) - MODE_DIRECT -> uCont.resumeWith(result) - MODE_UNDISPATCHED -> withCoroutineContext(uCont.context, null) { uCont.resumeWith(result) } - else -> error("Invalid mode $mode") - } + override fun afterCompletion(state: Any?) { + // Resume in a cancellable way by default when resuming from another context + uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont)) + } + + override fun afterResume(state: Any?) { + // Resume direct because scope is already in the correct context + uCont.resumeWith(recoverResult(state, uCont)) } } diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt index 4850bd619b..cfef935629 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt @@ -114,22 +114,20 @@ private inline fun AbstractCoroutine.undispatchedResult( } /* * We're trying to complete our undispatched block here and have three code-paths: - * 1) Suspended. - * - * Or we are completing our block (and its job). - * 2) If we can't complete it, we suspend, probably waiting for children (2) - * 3) If we have successfully completed the whole coroutine here in an undispatched manner, - * we should decide which result to return. We have two options: either return proposed update or actual final state. - * But if fact returning proposed value is not an option, otherwise we will ignore possible cancellation or child failure. + * (1) Coroutine is suspended. + * Otherwise, coroutine had returned result, so we are completing our block (and its job). + * (2) If we can't complete it or started waiting for children, we suspend. + * (3) If we have successfully completed the coroutine state machine here, + * then we take the actual final state of the coroutine from makeCompletingOnce and return it. * * shouldThrow parameter is a special code path for timeout coroutine: * If timeout is exceeded, but withTimeout() block was not suspended, we would like to return block value, * not a timeout exception. */ - if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED + if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED // (1) val state = makeCompletingOnce(result) - if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED - return if (state is CompletedExceptionally) { + if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED // (2) + return if (state is CompletedExceptionally) { // (3) when { shouldThrow(state.cause) -> throw tryRecover(state.cause) result is CompletedExceptionally -> throw tryRecover(result.cause) diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 29d31b9cb1..5af2911e0d 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -128,7 +128,7 @@ public interface SelectInstance { /** * Returns completion continuation of this select instance. * This select instance must be _selected_ first. - * All resumption through this instance happen _directly_ without going through dispatcher ([MODE_DIRECT]). + * All resumption through this instance happen _directly_ without going through dispatcher. */ public val completion: Continuation @@ -271,7 +271,7 @@ internal class SelectBuilderImpl( } } - // Resumes in MODE_DIRECT + // Resumes in direct mode, without going through dispatcher. Should be called in the same context. override fun resumeWith(result: Result) { doResume({ result.toState() }) { if (result.isFailure) { @@ -282,7 +282,7 @@ internal class SelectBuilderImpl( } } - // Resumes in MODE_CANCELLABLE + // Resumes in MODE_CANCELLABLE, can be called from an arbitrary context override fun resumeSelectCancellableWithException(exception: Throwable) { doResume({ CompletedExceptionally(exception) }) { uCont.intercepted().resumeCancellableWith(Result.failure(exception)) diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt index ac3cade065..c6e121f957 100644 --- a/kotlinx-coroutines-core/jvm/src/Builders.kt +++ b/kotlinx-coroutines-core/jvm/src/Builders.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ @file:JvmMultifileClass @@ -61,7 +61,7 @@ private class BlockingCoroutine( ) : AbstractCoroutine(parentContext, true) { override val isScopedCoroutine: Boolean get() = true - override fun afterCompletionInternal(state: Any?, mode: Int) { + override fun afterCompletion(state: Any?) { // wake up blocked thread if (Thread.currentThread() != blockedThread) LockSupport.unpark(blockedThread) From 3b2e4377ac14b9c9c4acfc01d9d50a5c9519e9de Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 3 Oct 2019 16:45:49 +0300 Subject: [PATCH 38/90] Change stacktrace recovery contract * Stacktraces are no longer recovered in DispatchedTask.run * Callers of uCont.resumeWithException are responsible for recovering the stacktrace * CancellableContinuation.resumeWithException recovers stacktrace if necessary * Properly recover stacktrace in Channel.receive fast-path * Properly recover stactraces on Channel.send fast-path * Restructure stacktrace recovery tests --- .../common/src/CancellableContinuationImpl.kt | 2 +- .../common/src/CompletedExceptionally.kt | 6 +- .../common/src/channels/AbstractChannel.kt | 12 +- .../common/src/internal/DispatchedTask.kt | 17 +- .../common/src/internal/Scopes.kt | 5 - .../common/src/intrinsics/Undispatched.kt | 10 +- .../channels/testCancelledOffer.txt | 15 ++ .../channels/testOfferFromScope.txt | 10 + .../channels/testOfferWithContextWrapped.txt | 9 + .../channels/testOfferWithCurrentContext.txt | 9 + .../channels/testReceiveFromChannel.txt | 8 + .../channels/testReceiveFromClosedChannel.txt | 8 + .../channels/testSendFromScope.txt | 10 + .../channels/testSendToChannel.txt | 12 ++ .../channels/testSendToClosedChannel.txt | 8 + .../resume-mode/testEventLoopDispatcher.txt | 12 ++ .../testEventLoopDispatcherSuspending.txt | 10 + .../testNestedEventLoopChangedContext.txt | 13 ++ ...estedEventLoopChangedContextSuspending.txt | 11 ++ .../testNestedEventLoopDispatcher.txt | 13 ++ ...estNestedEventLoopDispatcherSuspending.txt | 11 ++ .../resume-mode/testNestedUnconfined.txt | 13 ++ .../testNestedUnconfinedChangedContext.txt | 13 ++ ...stedUnconfinedChangedContextSuspending.txt | 11 ++ .../testNestedUnconfinedSuspending.txt | 11 ++ .../resume-mode/testUnconfined.txt | 12 ++ .../resume-mode/testUnconfinedSuspending.txt | 9 + .../StackTraceRecoveryChannelsTest.kt | 171 ++++++++++++++++++ .../StackTraceRecoveryNestedChannelsTest.kt | 165 ----------------- .../StackTraceRecoveryResumeModeTest.kt | 149 +++++++++++++++ .../test/exceptions/StackTraceRecoveryTest.kt | 79 +++----- .../jvm/test/exceptions/Stacktraces.kt | 7 + 32 files changed, 606 insertions(+), 245 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferFromScope.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithContextWrapped.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithCurrentContext.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromClosedChannel.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendFromScope.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToChannel.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToClosedChannel.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcher.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcherSuspending.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContext.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContextSuspending.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcher.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcherSuspending.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfined.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContext.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContextSuspending.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedSuspending.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfined.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfinedSuspending.txt create mode 100644 kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt delete mode 100644 kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryResumeModeTest.kt diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 559afb8289..31cf4154ae 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -244,7 +244,7 @@ internal open class CancellableContinuationImpl( } override fun resumeWith(result: Result) { - resumeImpl(result.toState(), resumeMode) + resumeImpl(result.toState(this), resumeMode) } override fun resume(value: T, onCancellation: (cause: Throwable) -> Unit) { diff --git a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt b/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt index 785041d524..b75d43070e 100644 --- a/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt +++ b/kotlinx-coroutines-core/common/src/CompletedExceptionally.kt @@ -9,8 +9,10 @@ import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.jvm.* -internal fun Result.toState(): Any? = - if (isSuccess) getOrThrow() else CompletedExceptionally(exceptionOrNull()!!) // todo: need to do it better +internal fun Result.toState(): Any? = fold({ it }, { CompletedExceptionally(it) }) + +internal fun Result.toState(caller: CancellableContinuation<*>): Any? = fold({ it }, + { CompletedExceptionally(recoverStackTrace(it, caller)) }) @Suppress("RESULT_CLASS_IN_RETURN_TYPE", "UNCHECKED_CAST") internal fun recoverResult(state: Any?, uCont: Continuation): Result = diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 8aadd26a73..4639bb835b 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -144,13 +144,13 @@ internal abstract class AbstractSendChannel : SendChannel { public final override suspend fun send(element: E) { // fast path -- try offer non-blocking - if (offer(element)) return - // slow-path does suspend + if (offerInternal(element) === OFFER_SUCCESS) return + // slow-path does suspend or throws exception return sendSuspend(element) } internal suspend fun sendFair(element: E) { - if (offer(element)) { + if (offerInternal(element) === OFFER_SUCCESS) { yield() // Works only on fast path to properly work in sequential use-cases return } @@ -547,7 +547,11 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) return receiveResult(result) // slow-path does suspend return receiveSuspend(RECEIVE_THROWS_ON_CLOSE) } diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 6c33aba783..98b45d56a3 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -52,7 +52,7 @@ internal abstract class DispatchedTask( cancelResult(state, cause) continuation.resumeWithStackTrace(cause) } else { - if (exception != null) continuation.resumeWithStackTrace(exception) + if (exception != null) continuation.resumeWithException(exception) else continuation.resume(getSuccessfulResult(state)) } } @@ -116,19 +116,8 @@ internal fun DispatchedTask.dispatch(mode: Int) { internal fun DispatchedTask.resume(delegate: Continuation, useMode: Int) { // slow-path - use delegate val state = takeState() - val exception = getExceptionalResult(state)?.let { - /* - * Recover stacktrace for non-dispatched tasks. - * We usually do not recover stacktrace in a `resume` as all resumes go through `DispatchedTask.run` - * and we recover stacktraces there, but this is not the case for a `suspend fun main()` that knows nothing about - * kotlinx.coroutines and DispatchedTask - */ - if (delegate is DispatchedTask<*>) it else recoverStackTrace(it, delegate) - } - val result = if (exception != null) - Result.failure(exception) - else - Result.success(state as T) + val exception = getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } + val result = if (exception != null) Result.failure(exception) else Result.success(state as T) when (useMode) { MODE_ATOMIC_DEFAULT -> delegate.resumeWith(result) MODE_CANCELLABLE -> delegate.resumeCancellableWith(result) diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index 49770c2ed4..c6cb18782c 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -33,11 +33,6 @@ internal open class ScopeCoroutine( } } -internal fun AbstractCoroutine<*>.tryRecover(exception: Throwable): Throwable { - val cont = (this as? ScopeCoroutine<*>)?.uCont ?: return exception - return recoverStackTrace(exception, cont) -} - internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context } diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt index cfef935629..0aa9cd7f84 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt @@ -85,7 +85,7 @@ private inline fun startDirect(completion: Continuation, block: (Continua * First, this function initializes the parent job from the `parentContext` of this coroutine that was passed to it * during construction. Second, it starts the coroutine using [startCoroutineUninterceptedOrReturn]. */ -internal fun AbstractCoroutine.startUndispatchedOrReturn(receiver: R, block: suspend R.() -> T): Any? { +internal fun ScopeCoroutine.startUndispatchedOrReturn(receiver: R, block: suspend R.() -> T): Any? { initParentJob() return undispatchedResult({ true }) { block.startCoroutineUninterceptedOrReturn(receiver, this) @@ -95,7 +95,7 @@ internal fun AbstractCoroutine.startUndispatchedOrReturn(receiver: R, /** * Same as [startUndispatchedOrReturn], but ignores [TimeoutCancellationException] on fast-path. */ -internal fun AbstractCoroutine.startUndispatchedOrReturnIgnoreTimeout( +internal fun ScopeCoroutine.startUndispatchedOrReturnIgnoreTimeout( receiver: R, block: suspend R.() -> T): Any? { initParentJob() return undispatchedResult({ e -> !(e is TimeoutCancellationException && e.coroutine === this) }) { @@ -103,7 +103,7 @@ internal fun AbstractCoroutine.startUndispatchedOrReturnIgnoreTimeout( } } -private inline fun AbstractCoroutine.undispatchedResult( +private inline fun ScopeCoroutine.undispatchedResult( shouldThrow: (Throwable) -> Boolean, startBlock: () -> Any? ): Any? { @@ -129,8 +129,8 @@ private inline fun AbstractCoroutine.undispatchedResult( if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED // (2) return if (state is CompletedExceptionally) { // (3) when { - shouldThrow(state.cause) -> throw tryRecover(state.cause) - result is CompletedExceptionally -> throw tryRecover(result.cause) + shouldThrow(state.cause) -> throw recoverStackTrace(state.cause, uCont) + result is CompletedExceptionally -> throw recoverStackTrace(result.cause, uCont) else -> result } } else { diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt new file mode 100644 index 0000000000..095be1e8d1 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt @@ -0,0 +1,15 @@ +kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@2a06d350 + (Coroutine boundary) + at kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:170) + at kotlinx.coroutines.channels.ChannelCoroutine.offer(ChannelCoroutine.kt) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testCancelledOffer$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:153) +Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@2a06d350 + at kotlinx.coroutines.JobSupport.createJobCancellationException(JobSupport.kt:680) + at kotlinx.coroutines.JobSupport.createCauseException(JobSupport.kt:696) + at kotlinx.coroutines.JobSupport.cancelMakeCompleting(JobSupport.kt:673) + at kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:645) + at kotlinx.coroutines.JobSupport.cancelInternal(JobSupport.kt:611) + at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:599) + at kotlinx.coroutines.Job$DefaultImpls.cancel$default(Job.kt:164) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testCancelledOffer$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:151) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferFromScope.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferFromScope.txt new file mode 100644 index 0000000000..bf3fd3a3ca --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferFromScope.txt @@ -0,0 +1,10 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferFromScope$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:109) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.sendInChannel(StackTraceRecoveryChannelsTest.kt:167) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$sendWithContext$2.invokeSuspend(StackTraceRecoveryChannelsTest.kt:162) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$sendFromScope$2.invokeSuspend(StackTraceRecoveryChannelsTest.kt:172) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferFromScope$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:112) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferFromScope$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:109) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithContextWrapped.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithContextWrapped.txt new file mode 100644 index 0000000000..612d00de06 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithContextWrapped.txt @@ -0,0 +1,9 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferWithContextWrapped$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:98) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.sendInChannel(StackTraceRecoveryChannelsTest.kt:199) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$sendWithContext$2.invokeSuspend(StackTraceRecoveryChannelsTest.kt:194) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferWithContextWrapped$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:100) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferWithContextWrapped$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:98) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithCurrentContext.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithCurrentContext.txt new file mode 100644 index 0000000000..833afbf8aa --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testOfferWithCurrentContext.txt @@ -0,0 +1,9 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferWithCurrentContext$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:86) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.sendInChannel(StackTraceRecoveryChannelsTest.kt:210) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$sendWithContext$2.invokeSuspend(StackTraceRecoveryChannelsTest.kt:205) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferWithCurrentContext$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:89) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testOfferWithCurrentContext$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:86) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt new file mode 100644 index 0000000000..66bb5e5e2e --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt @@ -0,0 +1,8 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromChannel$1$job$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:97) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.channelReceive(StackTraceRecoveryChannelsTest.kt:116) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:101) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromChannel$1$job$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:97) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromClosedChannel.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromClosedChannel.txt new file mode 100644 index 0000000000..76c0b1a8fa --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromClosedChannel.txt @@ -0,0 +1,8 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:110) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.channelReceive(StackTraceRecoveryChannelsTest.kt:116) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:111) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:110) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendFromScope.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendFromScope.txt new file mode 100644 index 0000000000..9f932032bd --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendFromScope.txt @@ -0,0 +1,10 @@ +kotlinx.coroutines.RecoverableTestCancellationException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendFromScope$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:136) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.sendInChannel(StackTraceRecoveryChannelsTest.kt:167) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$sendWithContext$2.invokeSuspend(StackTraceRecoveryChannelsTest.kt:162) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$sendFromScope$2.invokeSuspend(StackTraceRecoveryChannelsTest.kt:172) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendFromScope$1$deferred$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:126) +Caused by: kotlinx.coroutines.RecoverableTestCancellationException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendFromScope$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:136) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToChannel.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToChannel.txt new file mode 100644 index 0000000000..dab728fa79 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToChannel.txt @@ -0,0 +1,12 @@ +java.util.concurrent.CancellationException: RendezvousChannel was cancelled + at kotlinx.coroutines.channels.AbstractChannel.cancel(AbstractChannel.kt:630) + at kotlinx.coroutines.channels.ReceiveChannel$DefaultImpls.cancel$default(Channel.kt:311) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendToChannel$1$job$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:52) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.channelSend(StackTraceRecoveryChannelsTest.kt:73) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendToChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:56) +Caused by: java.util.concurrent.CancellationException: RendezvousChannel was cancelled + at kotlinx.coroutines.channels.AbstractChannel.cancel(AbstractChannel.kt:630) + at kotlinx.coroutines.channels.ReceiveChannel$DefaultImpls.cancel$default(Channel.kt:311) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendToChannel$1$job$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:52) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToClosedChannel.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToClosedChannel.txt new file mode 100644 index 0000000000..54fdbb3295 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToClosedChannel.txt @@ -0,0 +1,8 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendToClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:43) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.channelSend(StackTraceRecoveryChannelsTest.kt:74) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendToClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:44) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testSendToClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:43) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcher.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcher.txt new file mode 100644 index 0000000000..6b40ec8308 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcher.txt @@ -0,0 +1,12 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testEventLoopDispatcher$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:40) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:76) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doFastPath(StackTraceRecoveryResumeModeTest.kt:71) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:62) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testEventLoopDispatcher$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:40) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testEventLoopDispatcher$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:40) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcherSuspending.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcherSuspending.txt new file mode 100644 index 0000000000..5afc559fe0 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testEventLoopDispatcherSuspending.txt @@ -0,0 +1,10 @@ +otlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:99) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$4.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:116) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doSuspendingPath(StackTraceRecoveryResumeModeTest.kt:110) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeSuspending(StackTraceRecoveryResumeModeTest.kt:101) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testEventLoopDispatcherSuspending$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:89) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:99) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContext.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContext.txt new file mode 100644 index 0000000000..406b2d1c9c --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContext.txt @@ -0,0 +1,13 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopChangedContext$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:54) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:76) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doFastPath(StackTraceRecoveryResumeModeTest.kt:71) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:62) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopChangedContext$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:54) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopChangedContext$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:53) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopChangedContext$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:54) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContextSuspending.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContextSuspending.txt new file mode 100644 index 0000000000..86ec5e4bb2 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopChangedContextSuspending.txt @@ -0,0 +1,11 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:113) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$4.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:130) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doSuspendingPath(StackTraceRecoveryResumeModeTest.kt:124) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeSuspending(StackTraceRecoveryResumeModeTest.kt:115) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopChangedContextSuspending$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:103) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopChangedContextSuspending$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:102) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:113) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcher.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcher.txt new file mode 100644 index 0000000000..d9098bbaad --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcher.txt @@ -0,0 +1,13 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopDispatcher$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:47) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:76) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doFastPath(StackTraceRecoveryResumeModeTest.kt:71) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:62) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopDispatcher$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:47) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopDispatcher$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:46) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopDispatcher$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:47) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcherSuspending.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcherSuspending.txt new file mode 100644 index 0000000000..8caed7ac0c --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedEventLoopDispatcherSuspending.txt @@ -0,0 +1,11 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:113) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$4.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:130) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doSuspendingPath(StackTraceRecoveryResumeModeTest.kt:124) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeSuspending(StackTraceRecoveryResumeModeTest.kt:115) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopDispatcherSuspending$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:96) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedEventLoopDispatcherSuspending$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:95) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:113) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfined.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfined.txt new file mode 100644 index 0000000000..a2cd009dc8 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfined.txt @@ -0,0 +1,13 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfined$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:27) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:76) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doFastPath(StackTraceRecoveryResumeModeTest.kt:71) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:62) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfined$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:27) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfined$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:26) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfined$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:27) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContext.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContext.txt new file mode 100644 index 0000000000..a786682b7e --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContext.txt @@ -0,0 +1,13 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedChangedContext$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:34) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:76) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doFastPath(StackTraceRecoveryResumeModeTest.kt:71) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:62) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedChangedContext$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:34) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedChangedContext$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:33) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedChangedContext$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:34) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContextSuspending.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContextSuspending.txt new file mode 100644 index 0000000000..8c937a7c6b --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedChangedContextSuspending.txt @@ -0,0 +1,11 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:128) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$4.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:148) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doSuspendingPath(StackTraceRecoveryResumeModeTest.kt:140) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeSuspending(StackTraceRecoveryResumeModeTest.kt:130) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedChangedContextSuspending$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:95) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedChangedContextSuspending$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:94) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:128) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedSuspending.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedSuspending.txt new file mode 100644 index 0000000000..b6eef47911 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testNestedUnconfinedSuspending.txt @@ -0,0 +1,11 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:128) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$4.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:148) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doSuspendingPath(StackTraceRecoveryResumeModeTest.kt:140) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeSuspending(StackTraceRecoveryResumeModeTest.kt:130) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedSuspending$1$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:88) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testNestedUnconfinedSuspending$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:87) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:128) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfined.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfined.txt new file mode 100644 index 0000000000..9b9cba3eb4 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfined.txt @@ -0,0 +1,12 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testUnconfined$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:40) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:76) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doFastPath(StackTraceRecoveryResumeModeTest.kt:71) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:62) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testUnconfined$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:40) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt:61) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testUnconfined$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:40) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfinedSuspending.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfinedSuspending.txt new file mode 100644 index 0000000000..ca0bbe7fb8 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfinedSuspending.txt @@ -0,0 +1,9 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:128) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.doSuspendingPath(StackTraceRecoveryResumeModeTest.kt:140) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeSuspending(StackTraceRecoveryResumeModeTest.kt:130) + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testUnconfinedSuspending$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:82) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testResumeModeSuspending$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt:128) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt new file mode 100644 index 0000000000..478b9ad85a --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import org.junit.rules.* +import kotlin.coroutines.* + +class StackTraceRecoveryChannelsTest : TestBase() { + + @get:Rule + val name = TestName() + + @Test + fun testReceiveFromChannel() = runTest { + val channel = Channel() + val job = launch { + expect(2) + channel.close(RecoverableTestException()) + } + + expect(1) + channelReceive(channel) + expect(3) + job.join() + finish(4) + } + + @Test + fun testReceiveFromClosedChannel() = runTest { + val channel = Channel() + channel.close(RecoverableTestException()) + channelReceive(channel) + } + + + @Test + fun testSendToClosedChannel() = runTest { + val channel = Channel() + channel.close(RecoverableTestException()) + channelSend(channel) + } + + @Test + fun testSendToChannel() = runTest { + val channel = Channel() + val job = launch { + expect(2) + channel.cancel() + } + + expect(1) + channelSend(channel) + expect(3) + job.join() + finish(4) + } + + private suspend fun channelReceive(channel: Channel) { + try { + yield() + channel.receive() + expectUnreached() + } catch (e: RecoverableTestException) { + verifyStackTrace("channels/${name.methodName}", e) + } + } + + private suspend fun channelSend(channel: Channel) { + try { + yield() + channel.send(1) + expectUnreached() + } catch (e: Exception) { + verifyStackTrace("channels/${name.methodName}", e) + } + } + + @Test + fun testOfferWithCurrentContext() = runTest { + val channel = Channel() + channel.close(RecoverableTestException()) + + try { + channel.sendWithContext(coroutineContext) + } catch (e: RecoverableTestException) { + verifyStackTrace("channels/${name.methodName}", e) + } + } + + @Test + fun testOfferWithContextWrapped() = runTest { + val channel = Channel() + channel.close(RecoverableTestException()) + try { + channel.sendWithContext(wrapperDispatcher(coroutineContext)) + } catch (e: Exception) { + verifyStackTrace("channels/${name.methodName}", e) + } + } + + @Test + fun testOfferFromScope() = runTest { + val channel = Channel() + channel.close(RecoverableTestException()) + + try { + channel.sendFromScope() + } catch (e: Exception) { + verifyStackTrace("channels/${name.methodName}", e) + } + } + + // Slow path via suspending send + @Test + fun testSendFromScope() = runTest { + val channel = Channel() + val deferred = async { + try { + expect(1) + channel.sendFromScope() + } catch (e: Exception) { + verifyStackTrace("channels/${name.methodName}", e) + } + } + + yield() + expect(2) + // Cancel is an analogue of `produce` failure, just a shorthand + channel.cancel(RecoverableTestCancellationException()) + finish(3) + deferred.await() + } + + // See https://github.com/Kotlin/kotlinx.coroutines/issues/950 + @Test + fun testCancelledOffer() = runTest { + expect(1) + val job = Job() + val actor = actor(job, Channel.UNLIMITED) { + consumeEach { + expectUnreached() // is cancelled before offer + } + } + job.cancel() + try { + actor.offer(1) + } catch (e: Exception) { + verifyStackTrace("channels/${name.methodName}", e) + finish(2) + } + } + + private suspend fun Channel.sendWithContext(ctx: CoroutineContext) = withContext(ctx) { + sendInChannel() + yield() // TCE + } + + private suspend fun Channel.sendInChannel() { + send(42) + yield() // TCE + } + + private suspend fun Channel.sendFromScope() = coroutineScope { + sendWithContext(wrapperDispatcher(coroutineContext)) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt deleted file mode 100644 index 748f0c1697..0000000000 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("DeferredResultUnused", "DEPRECATION") - -package kotlinx.coroutines.exceptions - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import org.junit.* -import kotlin.coroutines.* - -class StackTraceRecoveryNestedChannelsTest : TestBase() { - - private val channel = Channel(0) - - private suspend fun sendWithContext(ctx: CoroutineContext) = withContext(ctx) { - sendInChannel() - yield() // TCE - } - - private suspend fun sendInChannel() { - channel.send(42) - yield() // TCE - } - - private suspend fun sendFromScope() = coroutineScope { - sendWithContext(wrapperDispatcher(coroutineContext)) - } - - @Test - fun testOfferWithCurrentContext() = runTest { - channel.close(RecoverableTestException()) - - try { - yield() // Will be fixed in 1.3.20 after KT-27190 - sendWithContext(coroutineContext) - } catch (e: Exception) { - verifyStackTrace(e, - "kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:34)\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invoke(StackTraceRecoveryNestedChannelsTest.kt)\n" + - "\tat kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:85)\n" + - "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:146)\n" + - "\tat kotlinx.coroutines.BuildersKt.withContext(Unknown Source)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendWithContext(StackTraceRecoveryNestedChannelsTest.kt:18)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:37)\n" + - "Caused by: kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:34)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") - } - } - - @Test - fun testOfferWithContextWrapped() = runTest { - channel.close(RecoverableTestException()) - - try { - sendWithContext(wrapperDispatcher(coroutineContext)) - } catch (e: Exception) { - verifyStackTrace(e, - "kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:59)\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:62)\n" + - "Caused by: kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:59)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") - } - } - - @Test - fun testOfferFromScope() = runTest { - channel.close(RecoverableTestException()) - - try { - sendFromScope() - } catch (e: Exception) { - verifyStackTrace(e, - "kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:81)\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendFromScope\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:28)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:84)\n" + - "Caused by: kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:81)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") - } - } - - // Slow path via suspending send - @Test - fun testSendFromScope() = runTest { - val deferred = async { - try { - expect(1) - sendFromScope() - } catch (e: Exception) { - verifyStackTrace(e, - "kotlinx.coroutines.RecoverableTestCancellationException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendFromScope\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:29)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:109)\n" + - "Caused by: kotlinx.coroutines.RecoverableTestCancellationException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") - } - } - - yield() - expect(2) - // Cancel is an analogue of `produce` failure, just a shorthand - channel.cancel(RecoverableTestCancellationException()) - finish(3) - deferred.await() - } - - // See https://github.com/Kotlin/kotlinx.coroutines/issues/950 - @Test - fun testCancelledOffer() = runTest { - expect(1) - val job = Job() - val actor = actor(job, Channel.UNLIMITED) { - consumeEach { - expectUnreached() // is cancelled before offer - } - } - job.cancel() - try { - actor.offer(1) - } catch (e: Exception) { - verifyStackTrace(e, - "kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@3af42ad0\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:186)\n" + - "\tat kotlinx.coroutines.channels.ChannelCoroutine.offer(ChannelCoroutine.kt)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testCancelledOffer\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:150)\n" + - "Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@3af42ad0\n", - // ... java.lang.* stuff and JobSupport.* snipped here ... - "\tat kotlinx.coroutines.Job\$DefaultImpls.cancel\$default(Job.kt:164)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testCancelledOffer\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:148)" - ) - finish(2) - } - } -} diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryResumeModeTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryResumeModeTest.kt new file mode 100644 index 0000000000..36f60523c5 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryResumeModeTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import kotlin.coroutines.* +import org.junit.rules.TestName +import org.junit.Rule + +class StackTraceRecoveryResumeModeTest : TestBase() { + + @get:Rule + val testName = TestName() + + @Test + fun testUnconfined() = runTest { + testResumeModeFastPath(Dispatchers.Unconfined) + } + + @Test + fun testNestedUnconfined() = runTest { + withContext(Dispatchers.Unconfined) { + testResumeModeFastPath(Dispatchers.Unconfined) + } + } + + @Test + fun testNestedUnconfinedChangedContext() = runTest { + withContext(Dispatchers.Unconfined) { + testResumeModeFastPath(CoroutineName("Test")) + } + } + + @Test + fun testEventLoopDispatcher() = runTest { + testResumeModeFastPath(wrapperDispatcher()) + } + + @Test + fun testNestedEventLoopDispatcher() = runTest { + val dispatcher = wrapperDispatcher() + withContext(dispatcher) { + testResumeModeFastPath(dispatcher) + } + } + + @Test + fun testNestedEventLoopChangedContext() = runTest { + withContext(wrapperDispatcher()) { + testResumeModeFastPath(CoroutineName("Test")) + } + } + + private suspend fun testResumeModeFastPath(context: CoroutineContext) { + try { + val channel = Channel() + channel.close(RecoverableTestException()) + doFastPath(context, channel) + } catch (e: Throwable) { + verifyStackTrace("resume-mode/${testName.methodName}", e) + } + } + + private suspend fun doFastPath(context: CoroutineContext, channel: Channel) { + yield() + withContext(context, channel) + } + + private suspend fun withContext(context: CoroutineContext, channel: Channel) { + withContext(context) { + channel.receive() + yield() + } + } + + @Test + fun testUnconfinedSuspending() = runTest { + testResumeModeSuspending(Dispatchers.Unconfined) + } + + @Test + fun testNestedUnconfinedSuspending() = runTest { + withContext(Dispatchers.Unconfined) { + testResumeModeSuspending(Dispatchers.Unconfined) + } + } + + @Test + fun testNestedUnconfinedChangedContextSuspending() = runTest { + withContext(Dispatchers.Unconfined) { + testResumeModeSuspending(CoroutineName("Test")) + } + } + + @Test + fun testEventLoopDispatcherSuspending() = runTest { + testResumeModeSuspending(wrapperDispatcher()) + } + + @Test + fun testNestedEventLoopDispatcherSuspending() = runTest { + val dispatcher = wrapperDispatcher() + withContext(dispatcher) { + testResumeModeSuspending(dispatcher) + } + } + + @Test + fun testNestedEventLoopChangedContextSuspending() = runTest { + withContext(wrapperDispatcher()) { + testResumeModeSuspending(CoroutineName("Test")) + } + } + + private suspend fun testResumeModeSuspending(context: CoroutineContext) { + try { + val channel = Channel() + val latch = Channel() + GlobalScope.launch(coroutineContext) { + latch.receive() + expect(3) + channel.close(RecoverableTestException()) + } + doSuspendingPath(context, channel, latch) + } catch (e: Throwable) { + finish(4) + verifyStackTrace("resume-mode/${testName.methodName}", e) + } + } + + private suspend fun doSuspendingPath(context: CoroutineContext, channel: Channel, latch: Channel) { + yield() + withContext(context, channel, latch) + } + + private suspend fun withContext(context: CoroutineContext, channel: Channel, latch: Channel) { + withContext(context) { + expect(1) + latch.send(1) + expect(2) + channel.receive() + yield() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt index 0f87960f24..3cddee56ca 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt @@ -8,7 +8,9 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* +import org.junit.* import org.junit.Test +import org.junit.rules.* import java.util.concurrent.* import kotlin.concurrent.* import kotlin.coroutines.* @@ -84,55 +86,6 @@ class StackTraceRecoveryTest : TestBase() { } } - @Test - fun testReceiveFromChannel() = runTest { - val channel = Channel() - val job = launch { - expect(2) - channel.close(IllegalArgumentException()) - } - - expect(1) - channelNestedMethod( - channel, - "java.lang.IllegalArgumentException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1\$job\$1.invokeSuspend(StackTraceRecoveryTest.kt:93)\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:110)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:89)", - "Caused by: java.lang.IllegalArgumentException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1\$job\$1.invokeSuspend(StackTraceRecoveryTest.kt:93)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") - expect(3) - job.join() - finish(4) - } - - @Test - fun testReceiveFromClosedChannel() = runTest { - val channel = Channel() - channel.close(IllegalArgumentException()) - channelNestedMethod( - channel, - "java.lang.IllegalArgumentException\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.channels.AbstractChannel.receiveResult(AbstractChannel.kt:574)\n" + - "\tat kotlinx.coroutines.channels.AbstractChannel.receive(AbstractChannel.kt:567)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:117)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:111)\n", - "Caused by: java.lang.IllegalArgumentException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:110)") - } - - private suspend fun channelNestedMethod(channel: Channel, vararg traces: String) { - try { - channel.receive() - expectUnreached() - } catch (e: IllegalArgumentException) { - verifyStackTrace(e, *traces) - } - } - @Test fun testWithContext() = runTest { val deferred = async(NonCancellable, start = CoroutineStart.LAZY) { @@ -312,4 +265,32 @@ class StackTraceRecoveryTest : TestBase() { "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testNonDispatchedRecovery\$await\$1.invokeSuspend(StackTraceRecoveryTest.kt:291)\n" + "Caused by: kotlinx.coroutines.RecoverableTestException") } + + private class Callback(val cont: CancellableContinuation<*>) + + @Test + fun testCancellableContinuation() = runTest { + val channel = Channel(1) + launch { + try { + awaitCallback(channel) + } catch (e: Throwable) { + verifyStackTrace(e, "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCancellableContinuation\$1.invokeSuspend(StackTraceRecoveryTest.kt:329)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.awaitCallback(StackTraceRecoveryTest.kt:348)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCancellableContinuation\$1\$1.invokeSuspend(StackTraceRecoveryTest.kt:322)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCancellableContinuation\$1.invokeSuspend(StackTraceRecoveryTest.kt:329)") + } + } + val callback = channel.receive() + callback.cont.resumeWithException(RecoverableTestException()) + } + + private suspend fun awaitCallback(channel: Channel) { + suspendCancellableCoroutine { cont -> + channel.offer(Callback(cont)) + } + } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt index 15884332b4..40c6930a43 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt @@ -1,5 +1,6 @@ package kotlinx.coroutines.exceptions +import kotlinx.coroutines.* import java.io.* import kotlin.test.* @@ -19,6 +20,12 @@ public fun verifyStackTrace(e: Throwable, vararg traces: String) { assertEquals(traces.map { it.count("Caused by") }.sum(), causes) } +public fun verifyStackTrace(path: String, e: Throwable) { + val resource = Job.javaClass.classLoader.getResourceAsStream("stacktraces/$path.txt") + val lines = resource.reader().readLines() + verifyStackTrace(e, *lines.toTypedArray()) +} + public fun toStackTrace(t: Throwable): String { val sw = StringWriter() as Writer t.printStackTrace(PrintWriter(sw)) From ae57774c1ea253e0272d92c4c064b2b721847989 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 4 Oct 2019 22:35:19 +0300 Subject: [PATCH 39/90] Eagerly create cancellation exception during Job.cancel call to make stacktrace even shorter Also, fix stacktrace recovery in select clause --- .../kotlinx-coroutines-core.txt | 12 +++-- .../common/src/AbstractCoroutine.kt | 2 + .../common/src/CancellableContinuationImpl.kt | 3 -- kotlinx-coroutines-core/common/src/Job.kt | 15 ++++-- .../common/src/JobSupport.kt | 36 +++++++------ .../common/src/channels/AbstractChannel.kt | 16 +++--- .../common/src/channels/Broadcast.kt | 13 ++--- .../common/src/channels/ChannelCoroutine.kt | 16 +++--- .../src/channels/ConflatedBroadcastChannel.kt | 2 +- .../src/internal/DispatchedContinuation.kt | 14 +++-- .../common/src/selects/Select.kt | 15 +++--- .../js/src/JSDispatcher.kt | 4 +- .../channels/testCancelledOffer.txt | 5 -- .../select/testSelectCompletedAwait.txt | 7 +++ .../stacktraces/select/testSelectJoin.txt | 7 +++ .../{JoinStressTest.kt => JoinStrTest.kt} | 14 ++--- .../jvm/test/channels/ActorTest.kt | 2 +- .../StackTraceRecoverySelectTest.kt | 53 +++++++++++++++++++ .../test/exceptions/StackTraceRecoveryTest.kt | 27 ---------- 19 files changed, 155 insertions(+), 108 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectCompletedAwait.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectJoin.txt rename kotlinx-coroutines-core/jvm/test/{JoinStressTest.kt => JoinStrTest.kt} (84%) create mode 100644 kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoverySelectTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index fdc241be62..166401ac5b 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -3,6 +3,7 @@ public abstract class kotlinx/coroutines/AbstractCoroutine : kotlinx/coroutines/ public fun (Lkotlin/coroutines/CoroutineContext;Z)V public synthetic fun (Lkotlin/coroutines/CoroutineContext;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V protected fun afterResume (Ljava/lang/Object;)V + protected fun cancellationExceptionMessage ()Ljava/lang/String; public final fun getContext ()Lkotlin/coroutines/CoroutineContext; public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; public fun isActive ()Z @@ -264,6 +265,10 @@ public final class kotlinx/coroutines/DelayKt { public static final fun delay (JLkotlin/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; @@ -387,7 +392,8 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public synthetic fun cancel (Ljava/lang/Throwable;)Z public fun cancel (Ljava/util/concurrent/CancellationException;)V public final fun cancelCoroutine (Ljava/lang/Throwable;)Z - public fun cancelInternal (Ljava/lang/Throwable;)Z + public fun cancelInternal (Ljava/lang/Throwable;)V + protected fun cancellationExceptionMessage ()Ljava/lang/String; public fun childCancelled (Ljava/lang/Throwable;)Z public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; @@ -1049,7 +1055,7 @@ public final class kotlinx/coroutines/selects/SelectBuilderImpl : kotlinx/corout public fun isSelected ()Z public fun onTimeout (JLkotlin/jvm/functions/Function1;)V public fun performAtomicTrySelect (Lkotlinx/coroutines/internal/AtomicDesc;)Ljava/lang/Object; - public fun resumeSelectCancellableWithException (Ljava/lang/Throwable;)V + public fun resumeSelectWithException (Ljava/lang/Throwable;)V public fun resumeWith (Ljava/lang/Object;)V public fun toString ()Ljava/lang/String; public fun trySelect ()Z @@ -1073,7 +1079,7 @@ public abstract interface class kotlinx/coroutines/selects/SelectInstance { public abstract fun getCompletion ()Lkotlin/coroutines/Continuation; public abstract fun isSelected ()Z public abstract fun performAtomicTrySelect (Lkotlinx/coroutines/internal/AtomicDesc;)Ljava/lang/Object; - public abstract fun resumeSelectCancellableWithException (Ljava/lang/Throwable;)V + public abstract fun resumeSelectWithException (Ljava/lang/Throwable;)V public abstract fun trySelect ()Z public abstract fun trySelectOther (Lkotlinx/coroutines/internal/LockFreeLinkedListNode$PrepareOp;)Ljava/lang/Object; } diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt index b1817f4ab4..18088777a5 100644 --- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt @@ -94,6 +94,8 @@ public abstract class AbstractCoroutine( */ protected open fun onCancelled(cause: Throwable, handled: Boolean) {} + override fun cancellationExceptionMessage(): String = "$classSimpleName was cancelled" + @Suppress("UNCHECKED_CAST") protected final override fun onCompletionInternal(state: Any?) { if (state is CompletedExceptionally) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index 31cf4154ae..f5b5900cb6 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -257,9 +257,6 @@ internal open class CancellableContinuationImpl( } } - internal fun resumeWithExceptionMode(exception: Throwable, mode: Int) = - resumeImpl(CompletedExceptionally(exception), mode) - public override fun invokeOnCancellation(handler: CompletionHandler) { var handleCache: CancelHandler? = null _state.loop { state -> diff --git a/kotlinx-coroutines-core/common/src/Job.kt b/kotlinx-coroutines-core/common/src/Job.kt index c6716bc903..133e24fa69 100644 --- a/kotlinx-coroutines-core/common/src/Job.kt +++ b/kotlinx-coroutines-core/common/src/Job.kt @@ -503,7 +503,7 @@ public fun Job.cancelChildren() = cancelChildren(null) */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun Job.cancelChildren(cause: Throwable? = null) { - children.forEach { (it as? JobSupport)?.cancelInternal(cause) } + children.forEach { (it as? JobSupport)?.cancelInternal(cause.orCancellation(this)) } } // -------------------- CoroutineContext extensions -------------------- @@ -586,9 +586,11 @@ public fun Job.cancel(message: String, cause: Throwable? = null): Unit = cancel( * @suppress This method has bad semantics when cause is not a [CancellationException]. Use [CoroutineContext.cancel]. */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") -public fun CoroutineContext.cancel(cause: Throwable? = null): Boolean = - @Suppress("DEPRECATION") - (this[Job] as? JobSupport)?.cancelInternal(cause) ?: false +public fun CoroutineContext.cancel(cause: Throwable? = null): Boolean { + val job = this[Job] as? JobSupport ?: return false + job.cancelInternal(cause.orCancellation(job)) + return true +} /** * Cancels all children of the [Job] in this context, without touching the state of this job itself @@ -610,9 +612,12 @@ public fun CoroutineContext.cancelChildren() = cancelChildren(null) */ @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") public fun CoroutineContext.cancelChildren(cause: Throwable? = null) { - this[Job]?.children?.forEach { (it as? JobSupport)?.cancelInternal(cause) } + val job = this[Job] ?: return + job.children.forEach { (it as? JobSupport)?.cancelInternal(cause.orCancellation(job)) } } +private fun Throwable?.orCancellation(job: Job): Throwable = this ?: JobCancellationException("Job was cancelled", null, job) + /** * No-op implementation of [DisposableHandle]. * @suppress **This an internal API and should not be used from general code.** diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index a2bcbf55b4..11c8094089 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -244,7 +244,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // A case of no exceptions if (exceptions.isEmpty()) { // materialize cancellation exception if it was not materialized yet - if (state.isCancelling) return createJobCancellationException() + if (state.isCancelling) return defaultCancellationException() return null } // Take either the first real exception (not a cancellation) or just the first exception @@ -406,8 +406,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } protected fun Throwable.toCancellationException(message: String? = null): CancellationException = - this as? CancellationException ?: - JobCancellationException(message ?: "$classSimpleName was cancelled", this, this@JobSupport) + this as? CancellationException ?: defaultCancellationException(message, this) /** * Returns the cause that signals the completion of this job -- it returns the original @@ -597,19 +596,23 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // external cancel with cause, never invoked implicitly from internal machinery public override fun cancel(cause: CancellationException?) { - cancelInternal(cause) // must delegate here, because some classes override cancelInternal(x) + cancelInternal(cause ?: defaultCancellationException()) } + protected open fun cancellationExceptionMessage(): String = "Job was cancelled" + // HIDDEN in Job interface. Invoked only by legacy compiled code. // external cancel with (optional) cause, never invoked implicitly from internal machinery @Deprecated(level = DeprecationLevel.HIDDEN, message = "Added since 1.2.0 for binary compatibility with versions <= 1.1.x") - public override fun cancel(cause: Throwable?): Boolean = - cancelInternal(cause) + public override fun cancel(cause: Throwable?): Boolean { + cancelInternal(cause?.toCancellationException() ?: defaultCancellationException()) + return true + } // It is overridden in channel-linked implementation - // Note: Boolean result is used only in HIDDEN DEPRECATED functions that were public in versions <= 1.1.x - public open fun cancelInternal(cause: Throwable?): Boolean = - cancelImpl(cause) && handlesException + public open fun cancelInternal(cause: Throwable) { + cancelImpl(cause) + } // Parent is cancelling child public final override fun parentCancelled(parentJob: ParentJob) { @@ -677,8 +680,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } } - private fun createJobCancellationException() = - JobCancellationException("Job was cancelled", null, this) + @Suppress("NOTHING_TO_INLINE") // Save a stack frame + internal inline fun defaultCancellationException(message: String? = null, cause: Throwable? = null) = + JobCancellationException(message ?: cancellationExceptionMessage(), cause, this) override fun getChildJobCancellationCause(): CancellationException { // determine root cancellation cause of this job (why is it cancelling its children?) @@ -694,7 +698,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // cause is Throwable or ParentJob when cancelChild was invoked private fun createCauseException(cause: Any?): Throwable = when (cause) { - is Throwable? -> cause ?: createJobCancellationException() + is Throwable? -> cause ?: defaultCancellationException() else -> (cause as ParentJob).getChildJobCancellationCause() } @@ -1225,7 +1229,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // already complete -- select result if (select.trySelect()) { if (state is CompletedExceptionally) { - select.resumeSelectCancellableWithException(state.cause) + select.resumeSelectWithException(state.cause) } else { block.startCoroutineUnintercepted(state.unboxState() as T, select.completion) @@ -1249,7 +1253,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren val state = this.state // Note: await is non-atomic (can be cancelled while dispatched) if (state is CompletedExceptionally) - select.resumeSelectCancellableWithException(state.cause) + select.resumeSelectWithException(state.cause) else block.startCoroutineCancellable(state.unboxState() as T, select.completion) } @@ -1384,8 +1388,8 @@ private class ResumeAwaitOnCompletion( val state = job.state assert { state !is Incomplete } if (state is CompletedExceptionally) { - // Resume with exception in atomic way to preserve exception - continuation.resumeWithExceptionMode(state.cause, MODE_ATOMIC_DEFAULT) + // Resume with with the corresponding exception to preserve it + continuation.resumeWithException(state.cause) } else { // Resuming with value in a cancellable way (AwaitContinuation is configured for this mode). @Suppress("UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 4639bb835b..6947b76e4e 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -463,7 +463,7 @@ internal abstract class AbstractSendChannel : SendChannel { override fun resumeSendClosed(closed: Closed<*>) { if (select.trySelect()) - select.resumeSelectCancellableWithException(closed.sendException) + select.resumeSelectWithException(closed.sendException) } override fun toString(): String = "SendSelect@$hexAddress($pollResult)[$channel, $select]" @@ -550,18 +550,14 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) return receiveResult(result) + @Suppress("UNCHECKED_CAST") + if (result !== POLL_FAILED && result !is Closed<*>) return result as E // slow-path does suspend return receiveSuspend(RECEIVE_THROWS_ON_CLOSE) } - @Suppress("UNCHECKED_CAST") - private fun receiveResult(result: Any?): E { - if (result is Closed<*>) throw recoverStackTrace(result.receiveException) - return result as E - } - @Suppress("UNCHECKED_CAST") private suspend fun receiveSuspend(receiveMode: Int): R = suspendAtomicCancellableCoroutineReusable sc@ { cont -> val receive = ReceiveElement(cont as CancellableContinuation, receiveMode) @@ -975,12 +971,12 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { if (!select.trySelect()) return when (receiveMode) { - RECEIVE_THROWS_ON_CLOSE -> select.resumeSelectCancellableWithException(closed.receiveException) + RECEIVE_THROWS_ON_CLOSE -> select.resumeSelectWithException(closed.receiveException) RECEIVE_RESULT -> block.startCoroutine(ValueOrClosed.closed(closed.closeCause), select.completion) RECEIVE_NULL_ON_CLOSE -> if (closed.closeCause == null) { block.startCoroutine(null, select.completion) } else { - select.resumeSelectCancellableWithException(closed.receiveException) + select.resumeSelectWithException(closed.receiveException) } } } diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index d230326a58..bbe80e0eed 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -97,17 +97,18 @@ private open class BroadcastCoroutine( get() = this @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") - final override fun cancel(cause: Throwable?): Boolean = - cancelInternal(cause) + final override fun cancel(cause: Throwable?): Boolean { + cancelInternal(cause ?: defaultCancellationException()) + return true + } final override fun cancel(cause: CancellationException?) { - cancelInternal(cause) + cancelInternal(cause ?: defaultCancellationException()) } - override fun cancelInternal(cause: Throwable?): Boolean { - _channel.cancel(cause?.toCancellationException()) // cancel the channel + override fun cancelInternal(cause: Throwable) { + _channel.cancel(cause.toCancellationException()) // cancel the channel cancelCoroutine(cause) // cancel the job - return true // does not matter - result is used in DEPRECATED functions only } override fun onCompleted(value: Unit) { diff --git a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt index dc2cc5b69f..f824e69f8d 100644 --- a/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/channels/ChannelCoroutine.kt @@ -16,23 +16,23 @@ internal open class ChannelCoroutine( val channel: Channel get() = this override fun cancel() { - cancelInternal(null) + cancelInternal(defaultCancellationException()) } @Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x") - final override fun cancel(cause: Throwable?): Boolean = - cancelInternal(cause) + final override fun cancel(cause: Throwable?): Boolean { + cancelInternal(defaultCancellationException()) + return true + } final override fun cancel(cause: CancellationException?) { - cancelInternal(cause) + cancelInternal(cause ?: defaultCancellationException()) } - override fun cancelInternal(cause: Throwable?): Boolean { - val exception = cause?.toCancellationException() - ?: JobCancellationException("$classSimpleName was cancelled", null, this) + override fun cancelInternal(cause: Throwable) { + val exception = cause.toCancellationException() _channel.cancel(exception) // cancel the channel cancelCoroutine(exception) // cancel the job - return true // does not matter - result is used in DEPRECATED functions only } @Suppress("UNCHECKED_CAST") diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt index 3f15550962..a3e72a9c1b 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt @@ -275,7 +275,7 @@ public class ConflatedBroadcastChannel() : BroadcastChannel { private fun registerSelectSend(select: SelectInstance, element: E, block: suspend (SendChannel) -> R) { if (!select.trySelect()) return offerInternal(element)?.let { - select.resumeSelectCancellableWithException(it.sendException) + select.resumeSelectWithException(it.sendException) return } block.startCoroutineUnintercepted(receiver = this, completion = select.completion) diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index 0a5553f053..cb2e4606b6 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -174,7 +174,9 @@ internal class DispatchedContinuation( } } - @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack + // 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() if (dispatcher.isDispatchNeeded(context)) { @@ -220,8 +222,14 @@ internal class DispatchedContinuation( "DispatchedContinuation[$dispatcher, ${continuation.toDebugString()}]" } -@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack -internal inline fun Continuation.resumeCancellableWith(result: Result) = when (this) { +/** + * It is not inline to save bytecode (it is pretty big and used in many places) + * and we leave it public so that its name is not mangled in use stack traces if it shows there. + * It may appear in stack traces when coroutines are started/resumed with unconfined dispatcher. + * @suppress **This an internal API and should not be used from general code.** + */ +@InternalCoroutinesApi +public fun Continuation.resumeCancellableWith(result: Result) = when (this) { is DispatchedContinuation -> resumeCancellableWith(result) else -> resumeWith(result) } diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 5af2911e0d..63e4dfa31b 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -133,9 +133,10 @@ public interface SelectInstance { public val completion: Continuation /** - * Resumes this instance in a cancellable way ([MODE_CANCELLABLE]). + * Resumes this instance in a dispatched way with exception. + * This method can be called from any context. */ - public fun resumeSelectCancellableWithException(exception: Throwable) + public fun resumeSelectWithException(exception: Throwable) /** * Disposes the specified handle when this instance is selected. @@ -282,10 +283,10 @@ internal class SelectBuilderImpl( } } - // Resumes in MODE_CANCELLABLE, can be called from an arbitrary context - override fun resumeSelectCancellableWithException(exception: Throwable) { - doResume({ CompletedExceptionally(exception) }) { - uCont.intercepted().resumeCancellableWith(Result.failure(exception)) + // Resumes in dispatched way so that it can be called from an arbitrary context + override fun resumeSelectWithException(exception: Throwable) { + doResume({ CompletedExceptionally(recoverStackTrace(exception, uCont)) }) { + uCont.intercepted().resumeWith(Result.failure(exception)) } } @@ -317,7 +318,7 @@ internal class SelectBuilderImpl( // Note: may be invoked multiple times, but only the first trySelect succeeds anyway override fun invoke(cause: Throwable?) { if (trySelect()) - resumeSelectCancellableWithException(job.getCancellationException()) + resumeSelectWithException(job.getCancellationException()) } override fun toString(): String = "SelectOnCancelling[${this@SelectBuilderImpl}]" } diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index 5a85244d4a..dc98332fe3 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -113,7 +113,7 @@ private class WindowMessageQueue(private val window: Window) : MessageQueue() { } /** - * An abstraction over JS scheduling mechanism that leverages micro-batching of [dispatch] blocks without + * An abstraction over JS scheduling mechanism that leverages micro-batching of dispatched blocks without * paying the cost of JS callbacks scheduling on every dispatch. * * Queue uses two scheduling mechanisms: diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt index 095be1e8d1..cfed5af47c 100644 --- a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testCancelledOffer.txt @@ -4,11 +4,6 @@ kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Canc at kotlinx.coroutines.channels.ChannelCoroutine.offer(ChannelCoroutine.kt) at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testCancelledOffer$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:153) Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@2a06d350 - at kotlinx.coroutines.JobSupport.createJobCancellationException(JobSupport.kt:680) - at kotlinx.coroutines.JobSupport.createCauseException(JobSupport.kt:696) - at kotlinx.coroutines.JobSupport.cancelMakeCompleting(JobSupport.kt:673) - at kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:645) - at kotlinx.coroutines.JobSupport.cancelInternal(JobSupport.kt:611) at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:599) at kotlinx.coroutines.Job$DefaultImpls.cancel$default(Job.kt:164) at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testCancelledOffer$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:151) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectCompletedAwait.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectCompletedAwait.txt new file mode 100644 index 0000000000..dbc39ccc55 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectCompletedAwait.txt @@ -0,0 +1,7 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoverySelectTest$testSelectCompletedAwait$1.invokeSuspend(StackTraceRecoverySelectTest.kt:40) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoverySelectTest$testSelectCompletedAwait$1.invokeSuspend(StackTraceRecoverySelectTest.kt:41) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoverySelectTest$testSelectCompletedAwait$1.invokeSuspend(StackTraceRecoverySelectTest.kt:40) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectJoin.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectJoin.txt new file mode 100644 index 0000000000..2d48086150 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/select/testSelectJoin.txt @@ -0,0 +1,7 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoverySelectTest$doSelect$$inlined$select$lambda$1.invokeSuspend(StackTraceRecoverySelectTest.kt:33) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoverySelectTest$testSelectJoin$1.invokeSuspend(StackTraceRecoverySelectTest.kt:20) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoverySelectTest$doSelect$$inlined$select$lambda$1.invokeSuspend(StackTraceRecoverySelectTest.kt:33) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/JoinStressTest.kt b/kotlinx-coroutines-core/jvm/test/JoinStrTest.kt similarity index 84% rename from kotlinx-coroutines-core/jvm/test/JoinStressTest.kt rename to kotlinx-coroutines-core/jvm/test/JoinStrTest.kt index 0d1a7c6c3f..5090e7c06c 100644 --- a/kotlinx-coroutines-core/jvm/test/JoinStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JoinStrTest.kt @@ -55,11 +55,10 @@ class JoinStressTest : TestBase() { @Test fun testExceptionalJoinWithMultipleCancellations() = runBlocking { val results = IntArray(2) - var successfulCancellations = 0 repeat(iterations) { val barrier = CyclicBarrier(4) - val exceptionalJob = async(pool + NonCancellable) { + val exceptionalJob = async(pool + NonCancellable) { barrier.await() throw TestException() } @@ -68,6 +67,7 @@ class JoinStressTest : TestBase() { barrier.await() try { exceptionalJob.await() + 2 } catch (e: TestException) { 0 } catch (e: TestException1) { @@ -83,19 +83,11 @@ class JoinStressTest : TestBase() { barrier.await() val awaiterResult = awaiterJob.await() - val cancellerResult = canceller.await() - if (awaiterResult == 1) { - assertTrue(cancellerResult) - } + canceller.await() ++results[awaiterResult] - - if (cancellerResult) { - ++successfulCancellations - } } assertTrue(results[0] > 0, results.toList().toString()) assertTrue(results[1] > 0, results.toList().toString()) - require(successfulCancellations > 0) { "Cancellation never succeeds, something wrong with stress test infra" } } } diff --git a/kotlinx-coroutines-core/jvm/test/channels/ActorTest.kt b/kotlinx-coroutines-core/jvm/test/channels/ActorTest.kt index 7be7983203..18349dddb6 100644 --- a/kotlinx-coroutines-core/jvm/test/channels/ActorTest.kt +++ b/kotlinx-coroutines-core/jvm/test/channels/ActorTest.kt @@ -128,7 +128,7 @@ class ActorTest(private val capacity: Int) : TestBase() { job.await() expectUnreached() } catch (e: CancellationException) { - assertTrue(e.message?.contains("Job was cancelled") ?: false) + assertTrue(e.message?.contains("DeferredCoroutine was cancelled") ?: false) } finish(3) diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoverySelectTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoverySelectTest.kt new file mode 100644 index 0000000000..290420e49a --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoverySelectTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlinx.coroutines.selects.* +import org.junit.* +import org.junit.rules.* + +class StackTraceRecoverySelectTest : TestBase() { + + @get:Rule + val name = TestName() + + @Test + fun testSelectJoin() = runTest { + expect(1) + val result = runCatching { doSelect() } + expect(3) + verifyStackTrace("select/${name.methodName}", result.exceptionOrNull()!!) + finish(4) + } + + private suspend fun doSelect(): Int { + val job = CompletableDeferred(Unit) + return select { + job.onJoin { + yield() // Hide the stackstrace + expect(2) + throw RecoverableTestException() + } + } + } + + @Test + fun testSelectCompletedAwait() = runTest { + val deferred = CompletableDeferred() + deferred.completeExceptionally(RecoverableTestException()) + val result = runCatching { doSelectAwait(deferred) } + verifyStackTrace("select/${name.methodName}", result.exceptionOrNull()!!) + } + + private suspend fun doSelectAwait(deferred: Deferred): Int { + return select { + deferred.onAwait { + yield() // Hide the stackstrace + 42 + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt index 3cddee56ca..c632424452 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt @@ -181,33 +181,6 @@ class StackTraceRecoveryTest : TestBase() { assertTrue(true) } - @Test - fun testSelect() = runTest { - expect(1) - val result = runCatching { doSelect() } - expect(3) - verifyStackTrace(result.exceptionOrNull()!!, - "kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$doSelect\$\$inlined\$select\$lambda\$1.invokeSuspend(StackTraceRecoveryTest.kt:211)\n" + - "\t(Coroutine boundary)\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testSelect\$1.invokeSuspend(StackTraceRecoveryTest.kt:199)\n" + - "Caused by: kotlinx.coroutines.RecoverableTestException\n" + - "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$doSelect\$\$inlined\$select\$lambda\$1.invokeSuspend(StackTraceRecoveryTest.kt:211)\n" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") - finish(4) - } - - private suspend fun doSelect(): Int { - val job = CompletableDeferred(Unit) - return select { - job.onJoin { - yield() - expect(2) - throw RecoverableTestException() - } - } - } - @Test fun testSelfSuppression() = runTest { try { From 7cc53d9f07f4406b446961dcb6f6b89e960e66bf Mon Sep 17 00:00:00 2001 From: Oleg Andreych Date: Sun, 6 Oct 2019 14:32:48 +0500 Subject: [PATCH 40/90] Typo in CoroutineScope's javadoc --- kotlinx-coroutines-core/common/src/CoroutineScope.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index b1f31e2f49..ca387338dc 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -181,7 +181,7 @@ public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R * Creates a [CoroutineScope] that wraps the given coroutine [context]. * * If the given [context] does not contain a [Job] element, then a default `Job()` is created. - * This way, cancellation or failure or any child coroutine in this scope cancels all the other children, + * This way, cancellation or failure of any child coroutine in this scope cancels all the other children, * just like inside [coroutineScope] block. */ @Suppress("FunctionName") From f86af23446dd313082c89c9442b4350ef2b2aa45 Mon Sep 17 00:00:00 2001 From: Marek Langiewicz Date: Sun, 6 Oct 2019 14:10:26 +0200 Subject: [PATCH 41/90] Update coroutines builders kdocs to link to correct param --- integration/kotlinx-coroutines-jdk8/src/future/Future.kt | 2 +- kotlinx-coroutines-core/common/src/Builders.common.kt | 4 ++-- kotlinx-coroutines-core/common/src/channels/Broadcast.kt | 2 +- kotlinx-coroutines-core/common/src/channels/Produce.kt | 2 +- kotlinx-coroutines-core/js/src/Promise.kt | 2 +- kotlinx-coroutines-core/jvm/src/channels/Actor.kt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/integration/kotlinx-coroutines-jdk8/src/future/Future.kt b/integration/kotlinx-coroutines-jdk8/src/future/Future.kt index 164ee2d2b6..16829a8018 100644 --- a/integration/kotlinx-coroutines-jdk8/src/future/Future.kt +++ b/integration/kotlinx-coroutines-jdk8/src/future/Future.kt @@ -17,7 +17,7 @@ import kotlin.coroutines.* * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [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 [coroutineContext] element. + * with corresponding [context] element. * * By default, the coroutine is immediately scheduled for execution. * Other options can be specified via `start` parameter. See [CoroutineStart] for details. diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 5973ed104a..ff5d406643 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -24,7 +24,7 @@ import kotlin.jvm.* * The coroutine context is inherited from a [CoroutineScope]. Additional context elements can be specified with [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 a corresponding [coroutineContext] element. + * with a corresponding [context] element. * * By default, the coroutine is immediately scheduled for execution. * Other start options can be specified via `start` parameter. See [CoroutineStart] for details. @@ -67,7 +67,7 @@ public fun CoroutineScope.launch( * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [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 [coroutineContext] element. + * with corresponding [context] element. * * By default, the coroutine is immediately scheduled for execution. * Other options can be specified via `start` parameter. See [CoroutineStart] for details. diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index bbe80e0eed..e487a53431 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -45,7 +45,7 @@ fun ReceiveChannel.broadcast( * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [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 [coroutineContext] element. + * with corresponding [context] element. * * Uncaught exceptions in this coroutine close the channel with this exception as a cause and * the resulting channel becomes _failed_, so that any attempt to receive from such a channel throws exception. diff --git a/kotlinx-coroutines-core/common/src/channels/Produce.kt b/kotlinx-coroutines-core/common/src/channels/Produce.kt index 59ebf52b46..68fb09a41c 100644 --- a/kotlinx-coroutines-core/common/src/channels/Produce.kt +++ b/kotlinx-coroutines-core/common/src/channels/Produce.kt @@ -69,7 +69,7 @@ public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) { * The coroutine context is inherited from this [CoroutineScope]. Additional context elements can be specified with the [context] argument. * If the context does not have any dispatcher or other [ContinuationInterceptor], then [Dispatchers.Default] is used. * The parent job is inherited from the [CoroutineScope] as well, but it can also be overridden - * with a corresponding [coroutineContext] element. + * with a corresponding [context] element. * * Any uncaught exception in this coroutine will close the channel with this exception as the cause and * the resulting channel will become _failed_, so that any attempt to receive from it thereafter will throw an exception. diff --git a/kotlinx-coroutines-core/js/src/Promise.kt b/kotlinx-coroutines-core/js/src/Promise.kt index b8177ad0d0..44615d2806 100644 --- a/kotlinx-coroutines-core/js/src/Promise.kt +++ b/kotlinx-coroutines-core/js/src/Promise.kt @@ -13,7 +13,7 @@ import kotlin.js.* * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [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 [coroutineContext] element. + * with corresponding [context] element. * * By default, the coroutine is immediately scheduled for execution. * Other options can be specified via `start` parameter. See [CoroutineStart] for details. diff --git a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt index c1c1fb337a..b951505839 100644 --- a/kotlinx-coroutines-core/jvm/src/channels/Actor.kt +++ b/kotlinx-coroutines-core/jvm/src/channels/Actor.kt @@ -41,7 +41,7 @@ public interface ActorScope : CoroutineScope, ReceiveChannel { * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [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 [coroutineContext] element. + * with corresponding [context] element. * * By default, the coroutine is immediately scheduled for execution. * Other options can be specified via `start` parameter. See [CoroutineStart] for details. From 7f1a927d7ac4d9198aea07ee0e86a60085279081 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 8 Oct 2019 18:46:08 +0300 Subject: [PATCH 42/90] Attempt to select SelectInstance in onReceiveOrClosed before starting a coroutine to avoid double resume Fixed #1584 --- .../common/src/channels/AbstractChannel.kt | 8 +++-- .../test/selects/SelectArrayChannelTest.kt | 31 +++++++++++++++++++ .../selects/SelectRendezvousChannelTest.kt | 15 +++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 6947b76e4e..18a64c85e6 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -747,7 +747,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel { - // selected successfully + // selected successfully, pollSelectInternal is responsible for the select block.startCoroutineUnintercepted(pollResult as E, select.completion) return } @@ -776,10 +776,12 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel {} // retry pollResult === RETRY_ATOMIC -> {} // retry pollResult is Closed<*> -> { - block.startCoroutineUnintercepted(ValueOrClosed.closed(pollResult.closeCause), select.completion) + if (select.trySelect()) + block.startCoroutineUnintercepted(ValueOrClosed.closed(pollResult.closeCause), select.completion) + return } else -> { - // selected successfully + // selected successfully, pollSelectInternal is responsible for the select block.startCoroutineUnintercepted(ValueOrClosed.value(pollResult as E), select.completion) return } diff --git a/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt index c9747c6fe8..a4f8c3ba2a 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectArrayChannelTest.kt @@ -388,4 +388,35 @@ class SelectArrayChannelTest : TestBase() { if (!trySelect()) return block.startCoroutineUnintercepted(this) } + + @Test + fun testSelectReceiveOrClosedForClosedChannel() = runTest { + val channel = Channel(1) + channel.close() + expect(1) + select { + expect(2) + channel.onReceiveOrClosed { + assertTrue(it.isClosed) + assertNull(it.closeCause) + finish(3) + } + } + } + + @Test + fun testSelectReceiveOrClosedForClosedChannelWithValue() = runTest { + val channel = Channel(1) + channel.send(42) + channel.close() + expect(1) + select { + expect(2) + channel.onReceiveOrClosed { + assertFalse(it.isClosed) + assertEquals(42, it.value) + finish(3) + } + } + } } diff --git a/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt b/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt index e84514ea5b..2027630f20 100644 --- a/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt +++ b/kotlinx-coroutines-core/common/test/selects/SelectRendezvousChannelTest.kt @@ -347,6 +347,21 @@ class SelectRendezvousChannelTest : TestBase() { finish(6) } + @Test + fun testSelectReceiveOrClosedForClosedChannel() = runTest { + val channel = Channel() + channel.close() + expect(1) + select { + expect(2) + channel.onReceiveOrClosed { + assertTrue(it.isClosed) + assertNull(it.closeCause) + finish(3) + } + } + } + @Test fun testSelectReceiveOrClosed() = runTest { val channel = Channel(Channel.RENDEZVOUS) From 38c3e9a445e41acb1445deee154ac7808313c6d1 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 8 Oct 2019 19:08:55 +0300 Subject: [PATCH 43/90] Recover stacktrace on the fast-path of receiveOrNull --- .../common/src/channels/AbstractChannel.kt | 3 ++- .../channels/testReceiveOrNullFromClosedChannel.txt | 8 ++++++++ .../exceptions/StackTraceRecoveryChannelsTest.kt | 13 +++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveOrNullFromClosedChannel.txt diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 18a64c85e6..79e10a7ef1 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -590,7 +590,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) return result as E // slow-path does suspend return receiveSuspend(RECEIVE_NULL_ON_CLOSE) } diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveOrNullFromClosedChannel.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveOrNullFromClosedChannel.txt new file mode 100644 index 0000000000..ac8f5f4ee6 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveOrNullFromClosedChannel.txt @@ -0,0 +1,8 @@ +kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveOrNullFromClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:43) + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.channelReceiveOrNull(StackTraceRecoveryChannelsTest.kt:70) + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveOrNullFromClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:44) +Caused by: kotlinx.coroutines.RecoverableTestException + at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveOrNullFromClosedChannel$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt:43) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt index 478b9ad85a..f52f8b5bcf 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryChannelsTest.kt @@ -37,6 +37,12 @@ class StackTraceRecoveryChannelsTest : TestBase() { channelReceive(channel) } + @Test + fun testReceiveOrNullFromClosedChannel() = runTest { + val channel = Channel() + channel.close(RecoverableTestException()) + channelReceiveOrNull(channel) + } @Test fun testSendToClosedChannel() = runTest { @@ -60,10 +66,13 @@ class StackTraceRecoveryChannelsTest : TestBase() { finish(4) } - private suspend fun channelReceive(channel: Channel) { + private suspend fun channelReceive(channel: Channel) = channelOp { channel.receive() } + private suspend fun channelReceiveOrNull(channel: Channel) = channelOp { channel.receiveOrNull() } + + private suspend inline fun channelOp(block: () -> Unit) { try { yield() - channel.receive() + block() expectUnreached() } catch (e: RecoverableTestException) { verifyStackTrace("channels/${name.methodName}", e) From e2a72a000c246f8d14b4c84438f77f84486c531d Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 9 Oct 2019 12:27:50 +0300 Subject: [PATCH 44/90] Generalize onReceive* family implementation in channels, reduce bytecode size --- .../common/src/channels/AbstractChannel.kt | 107 +++++++----------- 1 file changed, 39 insertions(+), 68 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 79e10a7ef1..f70164485a 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -692,100 +692,71 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel get() = object : SelectClause1 { + @Suppress("UNCHECKED_CAST") override fun registerSelectClause1(select: SelectInstance, block: suspend (E) -> R) { - registerSelectReceive(select, block) + registerSelectReceiveMode(select, RECEIVE_THROWS_ON_CLOSE, block as suspend (Any?) -> R) } } - @Suppress("UNCHECKED_CAST") - private fun registerSelectReceive(select: SelectInstance, block: suspend (E) -> R) { - while (true) { - if (select.isSelected) return - if (isEmpty) { - if (enqueueReceiveSelect(select, block as suspend (Any?) -> R, RECEIVE_THROWS_ON_CLOSE)) return - } else { - val pollResult = pollSelectInternal(select) - when { - pollResult === ALREADY_SELECTED -> return - pollResult === POLL_FAILED -> {} // retry - pollResult === RETRY_ATOMIC -> {} // retry - pollResult is Closed<*> -> throw recoverStackTrace(pollResult.receiveException) - else -> { - block.startCoroutineUnintercepted(pollResult as E, select.completion) - return - } - } - } - } - } - final override val onReceiveOrNull: SelectClause1 get() = object : SelectClause1 { + @Suppress("UNCHECKED_CAST") override fun registerSelectClause1(select: SelectInstance, block: suspend (E?) -> R) { - registerSelectReceiveOrNull(select, block) - } - } - - @Suppress("UNCHECKED_CAST") - private fun registerSelectReceiveOrNull(select: SelectInstance, block: suspend (E?) -> R) { - while (true) { - if (select.isSelected) return - if (isEmpty) { - if (enqueueReceiveSelect(select, block as suspend (Any?) -> R, RECEIVE_NULL_ON_CLOSE)) return - } else { - val pollResult = pollSelectInternal(select) - when { - pollResult === ALREADY_SELECTED -> return - pollResult === POLL_FAILED -> {} // retry - pollResult === RETRY_ATOMIC -> {} // retry - pollResult is Closed<*> -> { - if (pollResult.closeCause == null) { - if (select.trySelect()) - block.startCoroutineUnintercepted(null, select.completion) - return - } else { - throw recoverStackTrace(pollResult.closeCause) - } - } - else -> { - // selected successfully, pollSelectInternal is responsible for the select - block.startCoroutineUnintercepted(pollResult as E, select.completion) - return - } - } + registerSelectReceiveMode(select, RECEIVE_NULL_ON_CLOSE, block as suspend (Any?) -> R) } } - } - override val onReceiveOrClosed: SelectClause1> + final override val onReceiveOrClosed: SelectClause1> get() = object : SelectClause1> { + @Suppress("UNCHECKED_CAST") override fun registerSelectClause1(select: SelectInstance, block: suspend (ValueOrClosed) -> R) { - registerSelectReceiveOrClosed(select, block) + registerSelectReceiveMode(select, RECEIVE_RESULT, block as suspend (Any?) -> R) } } - @Suppress("UNCHECKED_CAST") - private fun registerSelectReceiveOrClosed(select: SelectInstance, block: suspend (ValueOrClosed) -> R) { + private fun registerSelectReceiveMode(select: SelectInstance, receiveMode: Int, block: suspend (Any?) -> R) { while (true) { if (select.isSelected) return if (isEmpty) { - if (enqueueReceiveSelect(select, block as suspend (Any?) -> R, RECEIVE_RESULT)) return + if (enqueueReceiveSelect(select, block, receiveMode)) return } else { val pollResult = pollSelectInternal(select) when { pollResult === ALREADY_SELECTED -> return pollResult === POLL_FAILED -> {} // retry pollResult === RETRY_ATOMIC -> {} // retry - pollResult is Closed<*> -> { - if (select.trySelect()) - block.startCoroutineUnintercepted(ValueOrClosed.closed(pollResult.closeCause), select.completion) - return + else -> block.tryStartBlockUnintercepted(select, receiveMode, pollResult) + } + } + } + } + + private fun (suspend (Any?) -> R).tryStartBlockUnintercepted(select: SelectInstance, receiveMode: Int, value: Any?) { + when (value) { + is Closed<*> -> { + when (receiveMode) { + RECEIVE_THROWS_ON_CLOSE -> { + throw recoverStackTrace(value.receiveException) } - else -> { - // selected successfully, pollSelectInternal is responsible for the select - block.startCoroutineUnintercepted(ValueOrClosed.value(pollResult as E), select.completion) - return + RECEIVE_RESULT -> { + if (!select.trySelect()) return + startCoroutineUnintercepted(ValueOrClosed.closed(value.closeCause), select.completion) } + RECEIVE_NULL_ON_CLOSE -> { + if (value.closeCause == null) { + if (!select.trySelect()) return + startCoroutineUnintercepted(null, select.completion) + } else { + throw recoverStackTrace(value.receiveException) + } + } + } + } + else -> { + if (receiveMode == RECEIVE_RESULT) { + startCoroutineUnintercepted(value.toResult(), select.completion) + } else { + startCoroutineUnintercepted(value, select.completion) } } } From a64e4dac55df757e29219b32118281bf486d17e9 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 10 Oct 2019 13:03:06 +0300 Subject: [PATCH 45/90] Do not filter out stress tests from IDEA --- kotlinx-coroutines-core/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 7ef0c8df4e..4d516962e9 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -76,7 +76,10 @@ jvmTest { maxHeapSize = '1g' enableAssertions = true systemProperty 'java.security.manager', 'kotlinx.coroutines.TestSecurityManager' - exclude '**/*StressTest.*' + // '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) { + exclude '**/*StressTest.*' + } systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test } From 3816837a5ebe8d381bed1293dbc8a61bd6c56a12 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 16 Oct 2019 10:38:31 +0300 Subject: [PATCH 46/90] Remove duplicate js/npm folder It was moved to the root kotlinx-coroutines-core/npm folder, because of the way NPM is supposed in project build scripts. It is not used here anymore. --- kotlinx-coroutines-core/js/npm/README.md | 19 --------------- kotlinx-coroutines-core/js/npm/package.json | 26 --------------------- 2 files changed, 45 deletions(-) delete mode 100644 kotlinx-coroutines-core/js/npm/README.md delete mode 100644 kotlinx-coroutines-core/js/npm/package.json diff --git a/kotlinx-coroutines-core/js/npm/README.md b/kotlinx-coroutines-core/js/npm/README.md deleted file mode 100644 index 7f88ea393f..0000000000 --- a/kotlinx-coroutines-core/js/npm/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# kotlinx.coroutines - -Library support for Kotlin coroutines in -[Kotlin/JS](https://kotlinlang.org/docs/reference/js-overview.html). - -```kotlin -suspend fun main() = coroutineScope { - launch { - delay(1000) - println("Kotlin Coroutines World!") - } - println("Hello") -} -``` - -## Documentation - -* [Guide to kotlinx.coroutines by example on JVM](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html) (**read it first**) -* [Full kotlinx.coroutines API reference](https://kotlin.github.io/kotlinx.coroutines) diff --git a/kotlinx-coroutines-core/js/npm/package.json b/kotlinx-coroutines-core/js/npm/package.json deleted file mode 100644 index 5dda39433d..0000000000 --- a/kotlinx-coroutines-core/js/npm/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "kotlinx-coroutines-core", - "version" : "$version", - "description" : "Library support for Kotlin coroutines", - "main" : "kotlinx-coroutines-core.js", - "author": "JetBrains", - "license": "Apache-2.0", - "homepage": "https://github.com/Kotlin/kotlinx.coroutines", - "bugs": { - "url": "https://github.com/Kotlin/kotlinx.coroutines/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Kotlin/kotlinx.coroutines.git" - }, - "keywords": [ - "Kotlin", - "async", - "coroutines", - "JavaScript", - "JetBrains" - ], - "peerDependencies": { - $kotlinDependency - } -} From b7b5adb16328cf1c759ecc34646f880d0af452d1 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 9 Oct 2019 13:40:11 +0300 Subject: [PATCH 47/90] Improve consumeAsFlow documentation Fixed #1576 --- 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 a554a4addf..1a572e8582 100644 --- a/kotlinx-coroutines-core/common/src/flow/Channels.kt +++ b/kotlinx-coroutines-core/common/src/flow/Channels.kt @@ -71,7 +71,7 @@ public suspend fun FlowCollector.emitAll(channel: ReceiveChannel) { * ### Cancellation semantics * * 1) Flow consumer is cancelled when the original channel is cancelled. - * 2) Flow consumer completes normally when the original channel completes (~is closed) normally. + * 2) Flow consumer completes normally when the original channel was closed normally and then fully consumed. * 3) If the flow consumer fails with an exception, channel is cancelled. * * ### Operator fusion From e303e6bf084c677dd4b4605116a31136b37f57d1 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 9 Oct 2019 13:55:57 +0300 Subject: [PATCH 48/90] Clarify difference between runBlocking and coroutineScope Fixes #1439 --- docs/basics.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 6a1248b56d..13760a618e 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -251,8 +251,14 @@ World! ### 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. The main difference between [runBlocking] and [coroutineScope] is that the latter does not block the current thread -while waiting for all children to complete. +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, +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. + +It can be demonstrated by the following example:
@@ -290,6 +296,9 @@ 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. + ### Extract function refactoring Let's extract the block of code inside `launch { ... }` into a separate function. When you From ef27ac3cf2f8c038b9c77eabdd049fdd98d14dda Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 9 Oct 2019 19:11:13 +0300 Subject: [PATCH 49/90] Introduce Stream.consumeAsFlow * Even though java.lang.Stream is collected rather than consumed, collectAsFlow will clash with Flow terminology where collect is a terminal operator * Close the stream in the end of collection despite the fact that regular terminal operations don't do that. We are already in suspending world (empty close() call won't make any difference in a common case) and "consume" implies closing the underlying resource (note that we already do it for channels) * Remove obsolete examples from the module Fixes #1601 --- .../kotlinx-coroutines-jdk8.txt | 4 ++ .../src/stream/Stream.kt | 33 ++++++++++++++++ .../test/examples/CancelFuture-example.kt | 31 --------------- .../test/examples/ExplicitJob-example.kt | 36 ------------------ .../test/examples/ToFuture-example.kt | 23 ----------- .../test/examples/Try.kt | 26 ------------- .../test/examples/simple-example-1.kt | 22 ----------- .../test/examples/simple-example-2.kt | 22 ----------- .../test/examples/simple-example-3.kt | 26 ------------- .../test/examples/withTimeout-example.kt | 38 ------------------- .../test/stream/ConsumeAsFlowTest.kt | 38 +++++++++++++++++++ 11 files changed, 75 insertions(+), 224 deletions(-) create mode 100644 integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/CancelFuture-example.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/ToFuture-example.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/Try.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/simple-example-1.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/simple-example-2.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/simple-example-3.kt delete mode 100644 integration/kotlinx-coroutines-jdk8/test/examples/withTimeout-example.kt create mode 100644 integration/kotlinx-coroutines-jdk8/test/stream/ConsumeAsFlowTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-jdk8.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-jdk8.txt index 7067e84797..417407dd14 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-jdk8.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-jdk8.txt @@ -7,6 +7,10 @@ public final class kotlinx/coroutines/future/FutureKt { public static synthetic fun future$default (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/util/concurrent/CompletableFuture; } +public final class kotlinx/coroutines/stream/StreamKt { + public static final fun consumeAsFlow (Ljava/util/stream/Stream;)Lkotlinx/coroutines/flow/Flow; +} + public final class kotlinx/coroutines/time/TimeKt { public static final fun delay (Ljava/time/Duration;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun onTimeout (Lkotlinx/coroutines/selects/SelectBuilder;Ljava/time/Duration;Lkotlin/jvm/functions/Function1;)V diff --git a/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt b/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt new file mode 100644 index 0000000000..1b5e479fd4 --- /dev/null +++ b/integration/kotlinx-coroutines-jdk8/src/stream/Stream.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.stream + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.util.stream.* + +/** + * Represents the given stream as a flow and [closes][Stream.close] the stream afterwards. + * The resulting flow can be [collected][Flow.collect] only once + * and throws [IllegalStateException] when trying to collect it more than once. + */ +public fun Stream.consumeAsFlow(): Flow = StreamFlow(this) + +private class StreamFlow(private val stream: Stream) : Flow { + private val consumed = atomic(false) + + @InternalCoroutinesApi + override suspend fun collect(collector: FlowCollector) { + if (!consumed.compareAndSet(false, true)) error("Stream.consumeAsFlow can be collected only once") + try { + for (value in stream.iterator()) { + collector.emit(value) + } + } finally { + stream.close() + } + } +} diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/CancelFuture-example.kt b/integration/kotlinx-coroutines-jdk8/test/examples/CancelFuture-example.kt deleted file mode 100644 index 4595f7624b..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/CancelFuture-example.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.* -import kotlinx.coroutines.future.* - - -fun main(args: Array) { - val f = GlobalScope.future { - try { - log("Started f") - delay(500) - log("Slept 500 ms #1") - delay(500) - log("Slept 500 ms #2") - delay(500) - log("Slept 500 ms #3") - delay(500) - log("Slept 500 ms #4") - delay(500) - log("Slept 500 ms #5") - } catch(e: Exception) { - log("Aborting because of $e") - } - } - Thread.sleep(1200) - f.cancel(false) -} \ No newline at end of file diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt b/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt deleted file mode 100644 index a331529a05..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/ExplicitJob-example.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.* -import kotlinx.coroutines.future.* -import java.util.concurrent.CancellationException - -fun main(args: Array) { - val job = Job() - log("Starting futures f && g") - val f = GlobalScope.future(job) { - log("Started f") - delay(500) - log("f should not execute this line") - } - val g = GlobalScope.future(job) { - log("Started g") - try { - delay(500) - } finally { - log("g is executing finally!") - } - log("g should not execute this line") - } - log("Started futures f && g... will not wait -- cancel them!!!") - job.cancel() - check(f.isCancelled) - check(g.isCancelled) - log("f result = ${Try { f.get() }}") - log("g result = ${Try { g.get() }}") - Thread.sleep(1000L) - log("Nothing executed!") -} \ No newline at end of file diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/ToFuture-example.kt b/integration/kotlinx-coroutines-jdk8/test/examples/ToFuture-example.kt deleted file mode 100644 index 35fdf4f13c..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/ToFuture-example.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.* -import kotlinx.coroutines.future.* -import java.util.concurrent.* - -fun main(args: Array) { - log("Started") - val deferred = GlobalScope.async { - log("Busy...") - delay(1000) - log("Done...") - 42 - } - val future = deferred.asCompletableFuture() - log("Got ${future.get()}") -} - - diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/Try.kt b/integration/kotlinx-coroutines-jdk8/test/examples/Try.kt deleted file mode 100644 index 7f0d9888f5..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/Try.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import java.text.* -import java.util.* - -public class Try private constructor(private val _value: Any?) { - private class Fail(val exception: Throwable) { - override fun toString(): String = "Failure[$exception]" - } - public companion object { - public operator fun invoke(block: () -> T): Try = - try { Success(block()) } catch(e: Throwable) { Failure(e) } - public fun Success(value: T) = Try(value) - public fun Failure(exception: Throwable) = Try(Fail(exception)) - } - @Suppress("UNCHECKED_CAST") - public val value: T get() = if (_value is Fail) throw _value.exception else _value as T - public val exception: Throwable? get() = (_value as? Fail)?.exception - override fun toString(): String = _value.toString() -} - -fun log(msg: String) = println("${SimpleDateFormat("yyyyMMdd-HHmmss.sss").format(Date())} [${Thread.currentThread().name}] $msg") diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-1.kt b/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-1.kt deleted file mode 100644 index ed9939102f..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-1.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.future.await -import kotlinx.coroutines.runBlocking -import java.util.concurrent.CompletableFuture - -fun main(args: Array) { - // Let's assume that we have a future coming from some 3rd party API - val future: CompletableFuture = CompletableFuture.supplyAsync { - Thread.sleep(1000L) // imitate some long-running computation, actually - 42 - } - // now let's launch a coroutine and await for this future inside it - runBlocking { - println("We can do something else, while we are waiting for future...") - println("We've got ${future.await()} from the future!") - } -} diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-2.kt b/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-2.kt deleted file mode 100644 index 0be80fc086..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-2.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.* -import kotlinx.coroutines.future.* -import java.util.concurrent.* - -// this function returns a CompletableFuture using Kotlin coroutines -fun supplyTheAnswerAsync(): CompletableFuture = GlobalScope.future { - println("We might be doing some asynchronous IO here or something else...") - delay(1000) // just do a non-blocking delay - 42 // The answer! -} - -fun main(args: Array) { - // We can use `supplyTheAnswerAsync` just like any other future-supplier function - val future = supplyTheAnswerAsync() - println("The answer is ${future.get()}") -} diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-3.kt b/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-3.kt deleted file mode 100644 index e284ac1638..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-3.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.* -import kotlinx.coroutines.future.* -import java.util.concurrent.* - -fun main(args: Array) { - // this example shows how easy it is to perform multiple async operations with coroutines - val future = GlobalScope.future { - (1..5).map { // loops are no problem at all - startLongAsyncOperation(it).await() // suspend while the long method is running - }.joinToString("\n") - } - println("We have a long-running computation in background, let's wait for its result...") - println(future.get()) -} - -fun startLongAsyncOperation(num: Int): CompletableFuture = - CompletableFuture.supplyAsync { - Thread.sleep(1000L) // imitate some long-running computation, actually - "$num" // and return a number converted to string -} diff --git a/integration/kotlinx-coroutines-jdk8/test/examples/withTimeout-example.kt b/integration/kotlinx-coroutines-jdk8/test/examples/withTimeout-example.kt deleted file mode 100644 index ef7c6b4752..0000000000 --- a/integration/kotlinx-coroutines-jdk8/test/examples/withTimeout-example.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.examples - -import kotlinx.coroutines.* -import kotlinx.coroutines.future.* - -fun main(args: Array) { - fun slow(s: String) = GlobalScope.future { - delay(500L) - s - } - val f = GlobalScope.future { - log("Started f") - val a = slow("A").await() - log("a = $a") - withTimeout(1000L) { - val b = slow("B").await() - log("b = $b") - } - try { - withTimeout(750L) { - val c = slow("C").await() - log("c = $c") - val d = slow("D").await() - log("d = $d") - } - } catch (ex: CancellationException) { - log("timed out with $ex") - } - val e = slow("E").await() - log("e = $e") - "done" - } - log("f.get() = ${f.get()}") -} diff --git a/integration/kotlinx-coroutines-jdk8/test/stream/ConsumeAsFlowTest.kt b/integration/kotlinx-coroutines-jdk8/test/stream/ConsumeAsFlowTest.kt new file mode 100644 index 0000000000..96ff963d61 --- /dev/null +++ b/integration/kotlinx-coroutines-jdk8/test/stream/ConsumeAsFlowTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.stream + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.junit.Test +import java.lang.IllegalStateException +import kotlin.test.* + +class ConsumeAsFlowTest : TestBase() { + + @Test + fun testCollect() = runTest { + val list = listOf(1, 2, 3) + assertEquals(list, list.stream().consumeAsFlow().toList()) + } + + @Test + fun testCollectInvokesClose() = runTest { + val list = listOf(3, 4, 5) + expect(1) + assertEquals(list, list.stream().onClose { expect(2) }.consumeAsFlow().toList()) + finish(3) + } + + @Test + fun testCollectTwice() = runTest { + val list = listOf(2, 3, 9) + val flow = list.stream().onClose { expect(2) } .consumeAsFlow() + expect(1) + assertEquals(list, flow.toList()) + assertFailsWith { flow.collect() } + finish(3) + } +} From bbf198ba4876ceeb762b324b846baf898235f304 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 18 Oct 2019 12:39:46 +0300 Subject: [PATCH 50/90] Fix StackTraceRecoveryTest.testCancellableContinuation for 1.3.60 Compiler now properly optimizes tail calls to suspendCancellableCoroutine so we need to make sure it is not a tail call for this test --- .../jvm/test/exceptions/StackTraceRecoveryTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt index c632424452..de93708453 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.exceptions @@ -7,10 +7,7 @@ package kotlinx.coroutines.exceptions import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.intrinsics.* -import kotlinx.coroutines.selects.* -import org.junit.* import org.junit.Test -import org.junit.rules.* import java.util.concurrent.* import kotlin.concurrent.* import kotlin.coroutines.* @@ -265,5 +262,6 @@ class StackTraceRecoveryTest : TestBase() { suspendCancellableCoroutine { cont -> channel.offer(Callback(cont)) } + yield() // nop to make sure it is not a tail call } } From adb616424b3553d239ded395f6ff9d9932431798 Mon Sep 17 00:00:00 2001 From: Marek Langiewicz Date: Fri, 18 Oct 2019 15:58:23 +0200 Subject: [PATCH 51/90] Fix JobSupport comment about new state. --- kotlinx-coroutines-core/common/src/JobSupport.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 11c8094089..62920cd6aa 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -90,7 +90,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren | while still performing all the notifications in this order. + Job object is created - ## NEW: state == EMPTY_ACTIVE | is InactiveNodeList + ## NEW: state == EMPTY_NEW | is InactiveNodeList + initParentJob / initParentJobInternal (invokes attachChild on its parent, initializes parentHandle) ~ waits for start >> start / join / await invoked From 340d501a3611dacf5cadededdd5ad811549f7ee8 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 21 Oct 2019 17:29:11 +0300 Subject: [PATCH 52/90] Optimize combine operator * Avoid linear complexity for emits * Reduce bytecode size --- .../common/src/flow/internal/Combine.kt | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt index f7edad08db..88240956cd 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt @@ -49,28 +49,26 @@ internal suspend fun FlowCollector.combineInternal( flows: Array>, arrayFactory: () -> Array, transform: suspend FlowCollector.(Array) -> Unit -) { - coroutineScope { - val size = flows.size - val channels = - Array(size) { asFairChannel(flows[it]) } - val latestValues = arrayOfNulls(size) - val isClosed = Array(size) { false } - - // See flow.combine(other) for explanation. - while (!isClosed.all { it }) { - select { - for (i in 0 until size) { - onReceive(isClosed[i], channels[i], { isClosed[i] = true }) { value -> - latestValues[i] = value - if (latestValues.all { it !== null }) { - val arguments = arrayFactory() - for (index in 0 until size) { - arguments[index] = NULL.unbox(latestValues[index]) - } - transform(arguments as Array) - } +): Unit = coroutineScope { + val size = flows.size + val channels = Array(size) { asFairChannel(flows[it]) } + 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) } } } @@ -84,6 +82,7 @@ private inline fun SelectBuilder.onReceive( noinline onReceive: suspend (value: Any) -> Unit ) { if (isClosed) return + @Suppress("DEPRECATION") channel.onReceiveOrNull { // TODO onReceiveOrClosed when boxing issues are fixed if (it === null) onClosed() From 83943ef8177dd97edf77a8b7a825a52e2b79c2a3 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 22 Oct 2019 19:26:50 +0300 Subject: [PATCH 53/90] Get rid of deprecated FlowCollector<*>.withContext This migration was motivated by the limited invariant preservation check, which is now more advanced; Additionally, such migration prohibits meaningful use-cases like "compute value in encapsulated context and then emit in flow context" Fixes #1616 --- .../kotlinx-coroutines-core.txt | 1 - docs/flow.md | 10 ++++------ .../common/src/flow/Migration.kt | 8 -------- .../common/test/flow/FlowInvariantsTest.kt | 19 +++++++------------ 4 files changed, 11 insertions(+), 27 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 166401ac5b..7e5c90bc67 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -969,7 +969,6 @@ public final class kotlinx/coroutines/flow/FlowKt { 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 unsafeTransform (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; - public static final fun withContext (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)V 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/docs/flow.md b/docs/flow.md index 7837a54058..ce4e80f1bb 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -708,17 +708,15 @@ fun main() = runBlocking { This code produces the following exception: - - -> Note that we had to use a fully qualified name of the [kotlinx.coroutines.withContext][withContext] function in this example to -demonstrate this exception. A short name of `withContext` would have resolved to a special stub function that -produces a compilation error to prevent us from running into this problem. +``` + + #### flowOn operator diff --git a/kotlinx-coroutines-core/common/src/flow/Migration.kt b/kotlinx-coroutines-core/common/src/flow/Migration.kt index 16769ad806..ade6078ff7 100644 --- a/kotlinx-coroutines-core/common/src/flow/Migration.kt +++ b/kotlinx-coroutines-core/common/src/flow/Migration.kt @@ -126,14 +126,6 @@ public fun Flow.onErrorResume(fallback: Flow): Flow = noImpl() ) public fun Flow.onErrorResumeNext(fallback: Flow): Flow = noImpl() -/** - * Self-explanatory, the reason of deprecation is "context preservation" property (you can read more in [Flow] documentation) - * @suppress - **/ -@Suppress("UNUSED_PARAMETER", "UNUSED", "DeprecatedCallableAddReplaceWith") -@Deprecated(message = "withContext in flow body is deprecated, use flowOn instead", level = DeprecationLevel.ERROR) -public fun FlowCollector.withContext(context: CoroutineContext, block: suspend () -> R): Unit = noImpl() - /** * `subscribe` is Rx-specific API that has no direct match in flows. * One can use [launchIn] instead, for example the following: diff --git a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt index e016b031b2..5dbc6e24ef 100644 --- a/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/FlowInvariantsTest.kt @@ -40,22 +40,22 @@ class FlowInvariantsTest : TestBase() { @Test fun testWithContextContract() = runParametrizedTest(IllegalStateException::class) { flow -> flow { - kotlinx.coroutines.withContext(NonCancellable) { + withContext(NonCancellable) { emit(1) } }.collect { - assertEquals(1, it) + expectUnreached() } } @Test fun testWithDispatcherContractViolated() = runParametrizedTest(IllegalStateException::class) { flow -> flow { - kotlinx.coroutines.withContext(NamedDispatchers("foo")) { + withContext(NamedDispatchers("foo")) { emit(1) } }.collect { - fail() + expectUnreached() } } @@ -63,16 +63,14 @@ class FlowInvariantsTest : TestBase() { fun testCachedInvariantCheckResult() = runParametrizedTest { flow -> flow { emit(1) - try { - kotlinx.coroutines.withContext(NamedDispatchers("foo")) { + withContext(NamedDispatchers("foo")) { emit(1) } fail() } catch (e: IllegalStateException) { expect(2) } - emit(3) }.collect { expect(it) @@ -83,11 +81,11 @@ class FlowInvariantsTest : TestBase() { @Test fun testWithNameContractViolated() = runParametrizedTest(IllegalStateException::class) { flow -> flow { - kotlinx.coroutines.withContext(CoroutineName("foo")) { + withContext(CoroutineName("foo")) { emit(1) } }.collect { - fail() + expectUnreached() } } @@ -107,7 +105,6 @@ class FlowInvariantsTest : TestBase() { } }.join() } - assertEquals("original", result) } @@ -116,7 +113,6 @@ class FlowInvariantsTest : TestBase() { flow { emit(1) }.buffer(EmptyCoroutineContext, flow).collect { expect(1) } - finish(2) } @@ -125,7 +121,6 @@ class FlowInvariantsTest : TestBase() { flow { emit(1) }.buffer(Dispatchers.Unconfined, flow).collect { expect(1) } - finish(2) } From 6aedb61807c98dea3d572bc008fa15ead36e8f91 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 22 Oct 2019 17:22:11 +0300 Subject: [PATCH 54/90] Make TimeoutCancellationException copyable * Make CopyableThrowable common, so multiplatform exceptions could be recovered on JVM --- .../common/src/Debug.common.kt | 30 +++++++++++++++++++ kotlinx-coroutines-core/common/src/Timeout.kt | 8 +++-- .../src/internal/StackTraceRecovery.common.kt | 5 ++++ .../js/src/internal/StackTraceRecovery.kt | 3 ++ kotlinx-coroutines-core/jvm/src/Debug.kt | 27 ----------------- .../jvm/src/internal/StackTraceRecovery.kt | 5 ++++ .../native/src/internal/StackTraceRecovery.kt | 3 ++ 7 files changed, 52 insertions(+), 29 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Debug.common.kt b/kotlinx-coroutines-core/common/src/Debug.common.kt index dd09a6a15e..3bd7aabe92 100644 --- a/kotlinx-coroutines-core/common/src/Debug.common.kt +++ b/kotlinx-coroutines-core/common/src/Debug.common.kt @@ -8,3 +8,33 @@ internal expect val DEBUG: Boolean internal expect val Any.hexAddress: String internal expect val Any.classSimpleName: String internal expect fun assert(value: () -> Boolean) + +/** + * Throwable which can be cloned during stacktrace recovery in a class-specific way. + * For additional information about stacktrace recovery see [STACKTRACE_RECOVERY_PROPERTY_NAME] + * + * Example of usage: + * ``` + * class BadResponseCodeException(val responseCode: Int) : Exception(), CopyableThrowable { + * + * override fun createCopy(): BadResponseCodeException { + * val result = BadResponseCodeException(responseCode) + * result.initCause(this) + * return result + * } + * ``` + * + * 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 +public interface CopyableThrowable where T : Throwable, T : CopyableThrowable { + + /** + * Creates a copy of the current instance. + * For better debuggability, it is recommended to use original exception as [cause][Throwable.cause] of the resulting one. + * Stacktrace of copied exception will be overwritten by stacktrace recovery machinery by [Throwable.setStackTrace] call. + * An exception can opt-out of copying by returning `null` from this function. + */ + public fun createCopy(): T? +} diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 512c2a55c3..3409463eb0 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -78,7 +78,7 @@ private fun setupTimeout( return coroutine.startUndispatchedOrReturnIgnoreTimeout(coroutine, block) } -private open class TimeoutCoroutine( +private class TimeoutCoroutine( @JvmField val time: Long, uCont: Continuation // unintercepted continuation ) : ScopeCoroutine(uCont.context, uCont), Runnable { @@ -96,13 +96,17 @@ private open class TimeoutCoroutine( public class TimeoutCancellationException internal constructor( message: String, @JvmField internal val coroutine: Job? -) : CancellationException(message) { +) : CancellationException(message), CopyableThrowable { /** * Creates a timeout exception with the given message. * This constructor is needed for exception stack-traces recovery. */ @Suppress("UNUSED") internal constructor(message: String) : this(message, null) + + // message is never null in fact + override fun createCopy(): TimeoutCancellationException? = + TimeoutCancellationException(message ?: "", coroutine).also { it.initCause(this) } } @Suppress("FunctionName") diff --git a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt index 8599143e95..06d6b694b0 100644 --- a/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/StackTraceRecovery.common.kt @@ -17,6 +17,11 @@ import kotlin.coroutines.* */ internal expect fun recoverStackTrace(exception: E, continuation: Continuation<*>): E +/** + * initCause on JVM, nop on other platforms + */ +internal expect fun Throwable.initCause(cause: Throwable) + /** * Tries to recover stacktrace for given [exception]. Used in non-suspendable points of awaiting. * Stacktrace recovery tries to instantiate exception of given type with original exception as a cause. diff --git a/kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt index 57c6247b5e..75b1d7915f 100644 --- a/kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt @@ -20,3 +20,6 @@ internal actual interface CoroutineStackFrame { @Suppress("ACTUAL_WITHOUT_EXPECT") internal actual typealias StackTraceElement = Any + +internal actual fun Throwable.initCause(cause: Throwable) { +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/Debug.kt b/kotlinx-coroutines-core/jvm/src/Debug.kt index 98a1c1ea7d..5aa37931ef 100644 --- a/kotlinx-coroutines-core/jvm/src/Debug.kt +++ b/kotlinx-coroutines-core/jvm/src/Debug.kt @@ -48,33 +48,6 @@ public const val DEBUG_PROPERTY_NAME = "kotlinx.coroutines.debug" */ internal const val STACKTRACE_RECOVERY_PROPERTY_NAME = "kotlinx.coroutines.stacktrace.recovery" -/** - * Throwable which can be cloned during stacktrace recovery in a class-specific way. - * For additional information about stacktrace recovery see [STACKTRACE_RECOVERY_PROPERTY_NAME] - * - * Example of usage: - * ``` - * class BadResponseCodeException(val responseCode: Int) : Exception(), CopyableThrowable { - * - * override fun createCopy(): BadResponseCodeException { - * val result = BadResponseCodeException(responseCode) - * result.initCause(this) - * return result - * } - * ``` - */ -@ExperimentalCoroutinesApi -public interface CopyableThrowable where T : Throwable, T : CopyableThrowable { - - /** - * Creates a copy of the current instance. - * For better debuggability, it is recommended to use original exception as [cause][Throwable.cause] of the resulting one. - * Stacktrace of copied exception will be overwritten by stacktrace recovery machinery by [Throwable.setStackTrace] call. - * An exception can opt-out of copying by returning `null` from this function. - */ - public fun createCopy(): T? -} - /** * Automatic debug configuration value for [DEBUG_PROPERTY_NAME]. */ diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index 2d7ed7a3d1..727d934136 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -204,3 +204,8 @@ internal actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.C @Suppress("ACTUAL_WITHOUT_EXPECT") internal actual typealias StackTraceElement = java.lang.StackTraceElement + +internal actual fun Throwable.initCause(cause: Throwable) { + // Resolved to member, verified by test + initCause(cause) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/native/src/internal/StackTraceRecovery.kt index 4faf16ac1d..c27280072c 100644 --- a/kotlinx-coroutines-core/native/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/native/src/internal/StackTraceRecovery.kt @@ -19,3 +19,6 @@ internal actual interface CoroutineStackFrame { @Suppress("ACTUAL_WITHOUT_EXPECT") internal actual typealias StackTraceElement = Any + +internal actual fun Throwable.initCause(cause: Throwable) { +} \ No newline at end of file From 3affb90f51029781fb3384ff2fda51586441d1d3 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 22 Oct 2019 19:18:22 +0300 Subject: [PATCH 55/90] Properly choose timeout exception with a recovered stacktrace to provide better debugging experience Fixes #1625 --- .../kotlinx-coroutines-core.txt | 4 +- .../common/src/JobSupport.kt | 20 ++++- kotlinx-coroutines-core/common/src/Timeout.kt | 2 - ...edFromLexicalBlockWhenTriggeredByChild.txt | 7 ++ ...acktraceIsRecoveredFromSuspensionPoint.txt | 10 +++ ...sRecoveredFromSuspensionPointWithChild.txt | 9 ++ .../jvm/test/AsyncJvmTest.kt | 50 +++++------ .../StackTraceRecoveryWithTimeoutTest.kt | 88 +++++++++++++++++++ .../jvm/test/exceptions/Stacktraces.kt | 2 +- 9 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromLexicalBlockWhenTriggeredByChild.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPoint.txt create mode 100644 kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPointWithChild.txt create mode 100644 kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryWithTimeoutTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 7e5c90bc67..5b7955c499 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -512,7 +512,9 @@ public final class kotlinx/coroutines/ThreadPoolDispatcherKt { public static final fun newSingleThreadContext (Ljava/lang/String;)Lkotlinx/coroutines/ExecutorCoroutineDispatcher; } -public final class kotlinx/coroutines/TimeoutCancellationException : java/util/concurrent/CancellationException { +public final class kotlinx/coroutines/TimeoutCancellationException : java/util/concurrent/CancellationException, kotlinx/coroutines/CopyableThrowable { + public synthetic fun createCopy ()Ljava/lang/Throwable; + public fun createCopy ()Lkotlinx/coroutines/TimeoutCancellationException; } public final class kotlinx/coroutines/TimeoutKt { diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 62920cd6aa..eb0d823f38 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -247,8 +247,22 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (state.isCancelling) return defaultCancellationException() return null } - // Take either the first real exception (not a cancellation) or just the first exception - return exceptions.firstOrNull { it !is CancellationException } ?: exceptions[0] + /* + * 1) If we have non-CE, use it as root cause + * 2) If our original cause was TCE, use *non-original* TCE because of the special nature of TCE + * * It is a CE, so it's not reported by children + * * The first instance (cancellation cause) is created by timeout coroutine and has no meaningful stacktrace + * * The potential second instance is thrown by withTimeout lexical block itself, then it has recovered stacktrace + * 3) Just return the very first CE + */ + val firstNonCancellation = exceptions.firstOrNull { it !is CancellationException } + if (firstNonCancellation != null) return firstNonCancellation + val first = exceptions[0] + if (first is TimeoutCancellationException) { + val detailedTimeoutException = exceptions.firstOrNull { it !== first && it is TimeoutCancellationException } + if (detailedTimeoutException != null) return detailedTimeoutException + } + return first } private fun addSuppressedExceptions(rootCause: Throwable, exceptions: List) { @@ -1073,7 +1087,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren get() = _exceptionsHolder.value set(value) { _exceptionsHolder.value = value } - // NotE: cannot be modified when sealed + // Note: cannot be modified when sealed val isSealed: Boolean get() = exceptionsHolder === SEALED val isCancelling: Boolean get() = rootCause != null override val isActive: Boolean get() = rootCause == null // !isCancelling diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 3409463eb0..7e6f0d0e2d 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -114,5 +114,3 @@ internal fun TimeoutCancellationException( time: Long, coroutine: Job ) : TimeoutCancellationException = TimeoutCancellationException("Timed out waiting for $time ms", coroutine) - - diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromLexicalBlockWhenTriggeredByChild.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromLexicalBlockWhenTriggeredByChild.txt new file mode 100644 index 0000000000..ab23c9a369 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromLexicalBlockWhenTriggeredByChild.txt @@ -0,0 +1,7 @@ +kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 200 ms + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest.outerChildWithTimeout(StackTraceRecoveryWithTimeoutTest.kt:48) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest$testStacktraceIsRecoveredFromLexicalBlockWhenTriggeredByChild$1.invokeSuspend(StackTraceRecoveryWithTimeoutTest.kt:40) +Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 200 ms + at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:116) + at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:86) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPoint.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPoint.txt new file mode 100644 index 0000000000..d3497face6 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPoint.txt @@ -0,0 +1,10 @@ +kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 200 ms + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest.suspendForever(StackTraceRecoveryWithTimeoutTest.kt:42) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest$outerWithTimeout$2.invokeSuspend(StackTraceRecoveryWithTimeoutTest.kt:32) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest.outerWithTimeout(StackTraceRecoveryWithTimeoutTest.kt:31) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest$testStacktraceIsRecoveredFromSuspensionPoint$1.invokeSuspend(StackTraceRecoveryWithTimeoutTest.kt:19) +Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 200 ms + at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:116) + at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:86) + at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:492) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPointWithChild.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPointWithChild.txt new file mode 100644 index 0000000000..8ec7691e50 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/timeout/testStacktraceIsRecoveredFromSuspensionPointWithChild.txt @@ -0,0 +1,9 @@ +kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 200 ms + (Coroutine boundary) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest.suspendForever(StackTraceRecoveryWithTimeoutTest.kt:92) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest$outerChild$2.invokeSuspend(StackTraceRecoveryWithTimeoutTest.kt:78) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest.outerChild(StackTraceRecoveryWithTimeoutTest.kt:74) + at kotlinx.coroutines.exceptions.StackTraceRecoveryWithTimeoutTest$testStacktraceIsRecoveredFromSuspensionPointWithChild$1.invokeSuspend(StackTraceRecoveryWithTimeoutTest.kt:66) +Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 200 ms + at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:116) + at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:86) \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt b/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt index dab7d5d033..ee3c66b2d8 100644 --- a/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt @@ -4,42 +4,32 @@ package kotlinx.coroutines +import kotlin.coroutines.* import kotlin.test.* class AsyncJvmTest : TestBase() { // This must be a common test but it fails on JS because of KT-21961 - @Test - fun testAsyncWithFinally() = runTest { - expect(1) + suspend fun test() { + println("test.begin") + delay(5000) + println("test.end") + } + + suspend fun proxy() { + println("?") + test() + println("?") + } - @Suppress("UNREACHABLE_CODE") - val d = async { - expect(3) - try { - yield() // to main, will cancel - } finally { - expect(6) // will go there on await - return@async "Fail" // result will not override cancellation +// @Test + fun main() { + println("AA") + runBlocking { + withTimeout(100) { + proxy() + println() } - expectUnreached() - "Fail2" - } - expect(2) - yield() // to async - expect(4) - check(d.isActive && !d.isCompleted && !d.isCancelled) - d.cancel() - check(!d.isActive && !d.isCompleted && d.isCancelled) - check(!d.isActive && !d.isCompleted && d.isCancelled) - expect(5) - try { - d.await() // awaits - expectUnreached() // does not complete normally - } catch (e: Throwable) { - expect(7) - check(e is CancellationException) + println() } - check(!d.isActive && d.isCompleted && d.isCancelled) - finish(8) } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryWithTimeoutTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryWithTimeoutTest.kt new file mode 100644 index 0000000000..8e628dbe90 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryWithTimeoutTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.rules.* + +class StackTraceRecoveryWithTimeoutTest : TestBase() { + + @get:Rule + val name = TestName() + + @Test + fun testStacktraceIsRecoveredFromSuspensionPoint() = runTest { + try { + outerWithTimeout() + } catch (e: TimeoutCancellationException) { + verifyStackTrace("timeout/${name.methodName}", e) + } + } + + private suspend fun outerWithTimeout() { + withTimeout(200) { + suspendForever() + } + expectUnreached() + } + + private suspend fun suspendForever() { + hang { } + expectUnreached() + } + + @Test + fun testStacktraceIsRecoveredFromLexicalBlockWhenTriggeredByChild() = runTest { + try { + outerChildWithTimeout() + } catch (e: TimeoutCancellationException) { + verifyStackTrace("timeout/${name.methodName}", e) + } + } + + private suspend fun outerChildWithTimeout() { + withTimeout(200) { + launch { + withTimeoutInChild() + } + yield() + } + expectUnreached() + } + + private suspend fun withTimeoutInChild() { + withTimeout(300) { + hang { } + } + expectUnreached() + } + + @Test + fun testStacktraceIsRecoveredFromSuspensionPointWithChild() = runTest { + try { + outerChild() + } catch (e: TimeoutCancellationException) { + verifyStackTrace("timeout/${name.methodName}", e) + } + } + + private suspend fun outerChild() { + withTimeout(200) { + launch { + smallWithTimeout() + } + suspendForever() + } + expectUnreached() + } + + private suspend fun smallWithTimeout() { + withTimeout(100) { + suspendForever() + } + expectUnreached() + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt index 40c6930a43..f79ad4ba74 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/Stacktraces.kt @@ -21,7 +21,7 @@ public fun verifyStackTrace(e: Throwable, vararg traces: String) { } public fun verifyStackTrace(path: String, e: Throwable) { - val resource = Job.javaClass.classLoader.getResourceAsStream("stacktraces/$path.txt") + val resource = Job::class.java.classLoader.getResourceAsStream("stacktraces/$path.txt") val lines = resource.reader().readLines() verifyStackTrace(e, *lines.toTypedArray()) } From ecbfa6da9bdb1823b1ec85697b4de6007e541aee Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 21 Oct 2019 20:25:45 +0300 Subject: [PATCH 56/90] Properly distinguish AbortFlowExceptions from different non-terminal operators Fixes #1610 --- .../common/src/flow/internal/Combine.kt | 6 +++--- .../src/flow/internal/FlowExceptions.common.kt | 11 ++++++++++- .../common/src/flow/operators/Limit.kt | 8 ++++---- .../common/src/flow/terminal/Collect.kt | 1 + .../common/src/flow/terminal/Reduce.kt | 4 ++-- .../common/test/flow/operators/TakeTest.kt | 14 ++++++++++++++ .../test/flow/operators/TransformLatestTest.kt | 14 +------------- .../js/src/flow/internal/FlowExceptions.kt | 5 ++++- .../jvm/src/flow/internal/FlowExceptions.kt | 6 +++++- .../native/src/flow/internal/FlowExceptions.kt | 5 ++++- 10 files changed, 48 insertions(+), 26 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt index 88240956cd..584178d8c4 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Combine.kt @@ -114,7 +114,7 @@ internal fun zipImpl(flow: Flow, flow2: Flow, transform: sus * 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()) + if (!first.isClosedForReceive) first.cancel(AbortFlowException(this@unsafeFlow)) } val otherIterator = second.iterator() @@ -126,9 +126,9 @@ internal fun zipImpl(flow: Flow, flow2: Flow, transform: sus emit(transform(NULL.unbox(value), NULL.unbox(otherIterator.next()))) } } catch (e: AbortFlowException) { - // complete + e.checkOwnership(owner = this@unsafeFlow) } finally { - if (!second.isClosedForReceive) second.cancel(AbortFlowException()) + if (!second.isClosedForReceive) second.cancel(AbortFlowException(this@unsafeFlow)) } } } diff --git a/kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt b/kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt index c3a85a3d70..7058dca5f5 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/FlowExceptions.common.kt @@ -5,12 +5,21 @@ package kotlinx.coroutines.flow.internal import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* /** * This exception is thrown when operator need no more elements from the flow. * This exception should never escape outside of operator's implementation. + * This exception can be safely ignored by non-terminal flow operator if and only if it was caught by its owner + * (see usages of [checkOwnership]). */ -internal expect class AbortFlowException() : CancellationException +internal expect class AbortFlowException(owner: FlowCollector<*>) : CancellationException { + public val owner: FlowCollector<*> +} + +internal fun AbortFlowException.checkOwnership(owner: FlowCollector<*>) { + if (this.owner !== owner) throw this +} /** * Exception used to cancel child of [scopedFlow] without cancelling the whole scope. diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt index 1343dad868..6f4e8e754c 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Limit.kt @@ -62,14 +62,14 @@ public fun Flow.take(count: Int): Flow { } } } catch (e: AbortFlowException) { - // Nothing, bail out + e.checkOwnership(owner = this) } } } private suspend fun FlowCollector.emitAbort(value: T) { emit(value) - throw AbortFlowException() + throw AbortFlowException(this) } /** @@ -80,9 +80,9 @@ public fun Flow.takeWhile(predicate: suspend (T) -> Boolean): Flow = f try { collect { value -> if (predicate(value)) emit(value) - else throw AbortFlowException() + else throw AbortFlowException(this) } } catch (e: AbortFlowException) { - // Nothing, bail out + e.checkOwnership(owner = this) } } diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt index c9480f99fa..de7d260d69 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Collect.kt @@ -130,5 +130,6 @@ public suspend fun Flow.collectLatest(action: suspend (value: T) -> Unit) * Collects all the values from the given [flow] and emits them to the collector. * It is a shorthand for `flow.collect { value -> emit(value) }`. */ +@BuilderInference @ExperimentalCoroutinesApi public suspend inline fun FlowCollector.emitAll(flow: Flow) = flow.collect(this) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index 875e6b6634..e8433de906 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -90,7 +90,7 @@ public suspend fun Flow.first(): T { try { collect { value -> result = value - throw AbortFlowException() + throw AbortFlowException(NopCollector) } } catch (e: AbortFlowException) { // Do nothing @@ -110,7 +110,7 @@ public suspend fun Flow.first(predicate: suspend (T) -> Boolean): T { collect { value -> if (predicate(value)) { result = value - throw AbortFlowException() + throw AbortFlowException(NopCollector) } } } catch (e: AbortFlowException) { diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt index 8ea137df08..62d2322c04 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/TakeTest.kt @@ -131,4 +131,18 @@ class TakeTest : TestBase() { } finish(2) } + + @Test + fun testNestedTake() = runTest { + val inner = flow { + emit(1) + expectUnreached() + }.take(1) + val outer = flow { + while(true) { + emitAll(inner) + } + } + assertEquals(listOf(1, 1, 1), outer.take(3).toList()) + } } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/TransformLatestTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/TransformLatestTest.kt index a37cca2124..8d3b40bb23 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/TransformLatestTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/TransformLatestTest.kt @@ -44,7 +44,7 @@ class TransformLatestTest : TestBase() { } @Test - fun testSwitchRendevouzBuffer() = runTest { + fun testSwitchRendezvousBuffer() = runTest { val flow = flowOf(1, 2, 3, 4, 5) flow.transformLatest { emit(it) @@ -157,16 +157,4 @@ class TransformLatestTest : TestBase() { val flow = flowOf(1, 2, 3, 4, 5).transformLatest { emit(it) } assertEquals(listOf(1), flow.take(1).toList()) } - - @Test - @Ignore // TODO separate branch and/or discuss - fun testTakeUpstreamCancellation() = runTest { - val flow = flow { - emit(1) - expectUnreached() - emit(2) - emit(3) - }.transformLatest { emit(it) } - assertEquals(listOf(1), flow.take(1).toList()) - } } diff --git a/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt index 8422f2bf33..2df8a5b73e 100644 --- a/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt +++ b/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt @@ -5,6 +5,9 @@ package kotlinx.coroutines.flow.internal import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* -internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") +internal actual class AbortFlowException actual constructor( + actual val owner: FlowCollector<*> +) : CancellationException("Flow was aborted, no more elements needed") internal actual class ChildCancelledException : CancellationException("Child of the scoped flow was cancelled") diff --git a/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt index d8d4d21e6f..3e714321f7 100644 --- a/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/flow/internal/FlowExceptions.kt @@ -5,8 +5,12 @@ package kotlinx.coroutines.flow.internal import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +internal actual class AbortFlowException actual constructor( + actual val owner: FlowCollector<*> +) : CancellationException("Flow was aborted, no more elements needed") { -internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") { override fun fillInStackTrace(): Throwable { if (DEBUG) super.fillInStackTrace() return this diff --git a/kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt index 4a291ea27e..ed74d58313 100644 --- a/kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt +++ b/kotlinx-coroutines-core/native/src/flow/internal/FlowExceptions.kt @@ -5,7 +5,10 @@ package kotlinx.coroutines.flow.internal import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* -internal actual class AbortFlowException : CancellationException("Flow was aborted, no more elements needed") +internal actual class AbortFlowException actual constructor( + actual val owner: FlowCollector<*> +) : CancellationException("Flow was aborted, no more elements needed") internal actual class ChildCancelledException : CancellationException("Child of the scoped flow was cancelled") From 2ba1d981ac584b21cf6fda66fd81a7e34b3268a8 Mon Sep 17 00:00:00 2001 From: Sebas LG Date: Mon, 21 Oct 2019 22:04:06 +0200 Subject: [PATCH 57/90] Fix mapLatest documentation example --- kotlinx-coroutines-core/common/src/flow/operators/Merge.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index dccc1cd8af..a7b7f709a5 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -169,7 +169,7 @@ public inline fun Flow.flatMapLatest(@BuilderInference crossinline tra * "Computed $value" * } * ``` - * will print "Started computing 1" and "Started computing 2", but the resulting flow will contain only "Computed 2" value. + * will print "Started computing a" and "Started computing b", but the resulting flow will contain only "Computed b" value. * * This operator is [buffered][buffer] by default and size of its output buffer can be changed by applying subsequent [buffer] operator. */ From 1a2707b6daac0d47687cd04da066ec24e6edac96 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 28 Oct 2019 12:50:13 +0300 Subject: [PATCH 58/90] Cleanup CombineParametersTestBase --- .../operators/CombineParametersTestBase.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt b/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt index a987c8343d..b51197e395 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/CombineParametersTestBase.kt @@ -99,13 +99,13 @@ class CombineParametersTest : TestBase() { } @Test - fun testEmptyVararg() = runTest { + fun testSingleVararg() = runTest { val list = combine(flowOf(1, 2, 3)) { args: Array -> args[0] }.toList() assertEquals(listOf(1, 2, 3), list) } @Test - fun testEmptyVarargTransform() = runTest { + fun testSingleVarargTransform() = runTest { val list = combineTransform(flowOf(1, 2, 3)) { args: Array -> emit(args[0]) }.toList() assertEquals(listOf(1, 2, 3), list) } @@ -131,33 +131,33 @@ class CombineParametersTest : TestBase() { } @Test - fun testEmpty() = runTest { - val value = combineTransform { args: Array -> + fun testTransformEmptyIterable() = runTest { + val value = combineTransform(emptyList()) { args: Array -> emit(args[0] + args[1]) }.singleOrNull() assertNull(value) } @Test - fun testEmptyIterable() = runTest { - val value = combineTransform(emptyList()) { args: Array -> + fun testTransformEmptyVararg() = runTest { + val value = combineTransform { args: Array -> emit(args[0] + args[1]) }.singleOrNull() assertNull(value) } @Test - fun testEmptyReified() = runTest { - val value = combineTransform { args: Array -> - emit(args[0] + args[1]) + fun testEmptyIterable() = runTest { + val value = combine(emptyList()) { args: Array -> + args[0] + args[1] }.singleOrNull() assertNull(value) } @Test - fun testEmptyIterableReified() = runTest { - val value = combineTransform(emptyList()) { args: Array -> - emit(args[0] + args[1]) + fun testEmptyVararg() = runTest { + val value = combine { args: Array -> + args[0] + args[1] }.singleOrNull() assertNull(value) } From 9a2eb3872e9c66de8183021b06f2269e137835ff Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 29 Oct 2019 14:51:41 +0300 Subject: [PATCH 59/90] Add missing TestBase inheritance to fail on asynchronous errors --- .../jvm/test/internal/FastServiceLoaderTest.kt | 5 ++--- .../jvm/test/internal/SegmentQueueTest.kt | 10 +++++----- .../test/ReactiveStreamTckTest.kt | 2 +- .../kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt | 4 ++-- .../kotlinx-coroutines-reactor/test/FluxSingleTest.kt | 2 +- .../test/ReactorContextTest.kt | 2 +- .../test/ObservableSingleTest.kt | 2 +- .../test/ordered/tests/CustomizedRobolectricTest.kt | 2 +- .../test/R8ServiceLoaderOptimizationTest.kt | 3 ++- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt b/kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt index a35081e5c1..a070f63519 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/FastServiceLoaderTest.kt @@ -1,10 +1,9 @@ package kotlinx.coroutines.internal -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Delay +import kotlinx.coroutines.* import kotlin.test.* -class FastServiceLoaderTest { +class FastServiceLoaderTest : TestBase() { @Test fun testCrossModuleService() { val providers = CoroutineScope::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) } diff --git a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt index 6fbe44717d..89cd83dac8 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/SegmentQueueTest.kt @@ -1,6 +1,6 @@ package kotlinx.coroutines.internal -import kotlinx.coroutines.stressTestMultiplier +import kotlinx.coroutines.* import org.junit.Test import java.util.* import java.util.concurrent.CyclicBarrier @@ -9,9 +9,9 @@ import kotlin.concurrent.thread import kotlin.random.Random import kotlin.test.assertEquals -class SegmentQueueTest { +class SegmentQueueTest : TestBase() { @Test - fun simpleTest() { + fun testSimpleTest() { val q = SegmentBasedQueue() assertEquals(1, q.numberOfSegments) assertEquals(null, q.dequeue()) @@ -69,10 +69,10 @@ class SegmentQueueTest { } @Test - fun stressTestRemoveSegmentsSerial() = stressTestRemoveSegments(false) + fun testRemoveSegmentsSerial() = stressTestRemoveSegments(false) @Test - fun stressTestRemoveSegmentsRandom() = stressTestRemoveSegments(true) + fun testRemoveSegmentsRandom() = stressTestRemoveSegments(true) private fun stressTestRemoveSegments(random: Boolean) { val N = 100_000 * stressTestMultiplier diff --git a/reactive/kotlinx-coroutines-reactive/test/ReactiveStreamTckTest.kt b/reactive/kotlinx-coroutines-reactive/test/ReactiveStreamTckTest.kt index 6816a986d3..a251d98371 100644 --- a/reactive/kotlinx-coroutines-reactive/test/ReactiveStreamTckTest.kt +++ b/reactive/kotlinx-coroutines-reactive/test/ReactiveStreamTckTest.kt @@ -11,7 +11,7 @@ import org.testng.* import org.testng.annotations.* -class ReactiveStreamTckTest { +class ReactiveStreamTckTest : TestBase() { @Factory(dataProvider = "dispatchers") fun createTests(dispatcher: Dispatcher): Array { diff --git a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt index 2f8ce9ac42..b0b2dd7ed2 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt @@ -1,14 +1,14 @@ package kotlinx.coroutines.reactor +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* -import kotlinx.coroutines.runBlocking import org.junit.Test import reactor.core.publisher.Mono import reactor.util.context.Context import kotlin.test.assertEquals -class FlowAsFluxTest { +class FlowAsFluxTest : TestBase() { @Test fun testFlowToFluxContextPropagation() = runBlocking { val flux = flow { diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt index 7d8d46984b..ea35fb56f5 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt @@ -11,7 +11,7 @@ import org.junit.Assert.* import reactor.core.publisher.* import java.time.Duration.* -class FluxSingleTest { +class FluxSingleTest : TestBase() { @Test fun testSingleNoWait() { val flux = flux { diff --git a/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt b/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt index e9ac200f49..4f337a5349 100644 --- a/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt @@ -9,7 +9,7 @@ import reactor.core.publisher.* import reactor.util.context.* import kotlin.test.* -class ReactorContextTest { +class ReactorContextTest : TestBase() { @Test fun testMonoHookedContext() = runBlocking { val mono = mono(Context.of(1, "1", 7, "7").asCoroutineContext()) { diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt index 2fa1d9b0df..c06130d9ad 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt @@ -10,7 +10,7 @@ import org.junit.* import org.junit.Assert.* import java.util.concurrent.* -class ObservableSingleTest { +class ObservableSingleTest : TestBase() { @Test fun testSingleNoWait() { val observable = rxObservable { diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt index 578cd7416a..bcc12d5441 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt +++ b/ui/kotlinx-coroutines-android/android-unit-tests/test/ordered/tests/CustomizedRobolectricTest.kt @@ -26,7 +26,7 @@ class InitMainDispatcherBeforeRobolectricTestRunner(testClass: Class<*>) : Robol @Config(manifest = Config.NONE, sdk = [28]) @RunWith(InitMainDispatcherBeforeRobolectricTestRunner::class) -class CustomizedRobolectricTest { +class CustomizedRobolectricTest : TestBase() { @Test fun testComponent() { // Note that main is not set at all diff --git a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt index 2d2281bdfe..76d67c76b3 100644 --- a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt +++ b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt @@ -4,13 +4,14 @@ package kotlinx.coroutines.android +import kotlinx.coroutines.* import org.jf.dexlib2.* import org.junit.Test import java.io.* import java.util.stream.* import kotlin.test.* -class R8ServiceLoaderOptimizationTest { +class R8ServiceLoaderOptimizationTest : TestBase() { private val r8Dex = File(System.getProperty("dexPath")!!).asDexFile() private val r8DexNoOptim = File(System.getProperty("noOptimDexPath")!!).asDexFile() From 1652bb9602a23a6c6a7c445a88f8da9a24f39ef5 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 29 Oct 2019 19:48:49 +0300 Subject: [PATCH 60/90] Fix ReactorFlow integration bug exposed by TestBase --- .../src/ReactorFlow.kt | 2 +- .../test/FlowAsFluxTest.kt | 14 ++-- .../test/FluxSingleTest.kt | 8 ++- .../test/ReactorContextTest.kt | 68 +++++++++---------- .../test/ObservableSingleTest.kt | 7 +- 5 files changed, 55 insertions(+), 44 deletions(-) diff --git a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt index 57260cfbb3..5d47e0218c 100644 --- a/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt +++ b/reactive/kotlinx-coroutines-reactor/src/ReactorFlow.kt @@ -21,7 +21,7 @@ public fun Flow.asFlux(): Flux = FlowAsFlux(this) private class FlowAsFlux(private val flow: Flow) : Flux() { override fun subscribe(subscriber: CoreSubscriber?) { if (subscriber == null) throw NullPointerException() - val hasContext = subscriber.currentContext().isEmpty + val hasContext = !subscriber.currentContext().isEmpty val source = if (hasContext) flow.flowOn(subscriber.currentContext().asCoroutineContext()) else flow subscriber.onSubscribe(FlowSubscription(source, subscriber)) } diff --git a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt index b0b2dd7ed2..ccef000b5b 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FlowAsFluxTest.kt @@ -10,18 +10,18 @@ import kotlin.test.assertEquals class FlowAsFluxTest : TestBase() { @Test - fun testFlowToFluxContextPropagation() = runBlocking { + fun testFlowToFluxContextPropagation() { val flux = flow { - (1..4).forEach { i -> emit(m(i).awaitFirst()) } + (1..4).forEach { i -> emit(createMono(i).awaitFirst()) } } .asFlux() .subscriberContext(Context.of(1, "1")) .subscriberContext(Context.of(2, "2", 3, "3", 4, "4")) - var i = 0 - flux.subscribe { str -> i++; println(str); assertEquals(str, i.toString()) } + val list = flux.collectList().block()!! + assertEquals(listOf("1", "2", "3", "4"), list) } - private fun m(i: Int): Mono = mono { - val ctx = coroutineContext[ReactorContext]?.context - ctx?.getOrDefault(i, "noValue") + private fun createMono(i: Int): Mono = mono { + val ctx = coroutineContext[ReactorContext]!!.context + ctx.getOrDefault(i, "noValue") } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt index ea35fb56f5..a3e9658930 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxSingleTest.kt @@ -12,6 +12,12 @@ import reactor.core.publisher.* import java.time.Duration.* class FluxSingleTest : TestBase() { + + @Before + fun setup() { + ignoreLostThreads("parallel-") + } + @Test fun testSingleNoWait() { val flux = flux { @@ -167,7 +173,7 @@ class FluxSingleTest : TestBase() { @Test fun testExceptionFromCoroutine() { val flux = flux { - error(Flux.just("O").awaitSingle() + "K") + throw IllegalStateException(Flux.just("O").awaitSingle() + "K") } checkErroneous(flux) { diff --git a/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt b/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt index 4f337a5349..3681261b0c 100644 --- a/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/ReactorContextTest.kt @@ -7,15 +7,16 @@ import kotlinx.coroutines.reactive.* import org.junit.Test import reactor.core.publisher.* import reactor.util.context.* +import kotlin.coroutines.* import kotlin.test.* class ReactorContextTest : TestBase() { @Test fun testMonoHookedContext() = runBlocking { val mono = mono(Context.of(1, "1", 7, "7").asCoroutineContext()) { - val ctx = coroutineContext[ReactorContext]?.context + val ctx = reactorContext() buildString { - (1..7).forEach { append(ctx?.getOrDefault(it, "noValue")) } + (1..7).forEach { append(ctx.getOrDefault(it, "noValue")) } } } .subscriberContext(Context.of(2, "2", 3, "3", 4, "4", 5, "5")) .subscriberContext { ctx -> ctx.put(6, "6") } @@ -23,22 +24,23 @@ class ReactorContextTest : TestBase() { } @Test - fun testFluxContext() = runBlocking { + fun testFluxContext() { val flux = flux(Context.of(1, "1", 7, "7").asCoroutineContext()) { - val ctx = coroutineContext[ReactorContext]!!.context + val ctx = reactorContext() (1..7).forEach { send(ctx.getOrDefault(it, "noValue")) } - } .subscriberContext(Context.of(2, "2", 3, "3", 4, "4", 5, "5")) + } + .subscriberContext(Context.of(2, "2", 3, "3", 4, "4", 5, "5")) .subscriberContext { ctx -> ctx.put(6, "6") } - var i = 0 - flux.subscribe { str -> i++; assertEquals(str, i.toString()) } + val list = flux.collectList().block()!! + assertEquals((1..7).map { it.toString() }, list) } @Test fun testAwait() = runBlocking(Context.of(3, "3").asCoroutineContext()) { val result = mono(Context.of(1, "1").asCoroutineContext()) { - val ctx = coroutineContext[ReactorContext]?.context + val ctx = reactorContext() buildString { - (1..3).forEach { append(ctx?.getOrDefault(it, "noValue")) } + (1..3).forEach { append(ctx.getOrDefault(it, "noValue")) } } } .subscriberContext(Context.of(2, "2")) .awaitFirst() @@ -47,36 +49,34 @@ class ReactorContextTest : TestBase() { @Test fun testMonoAwaitContextPropagation() = runBlocking(Context.of(7, "7").asCoroutineContext()) { - assertEquals(m().awaitFirst(), "7") - assertEquals(m().awaitFirstOrDefault("noValue"), "7") - assertEquals(m().awaitFirstOrNull(), "7") - assertEquals(m().awaitFirstOrElse { "noValue" }, "7") - assertEquals(m().awaitLast(), "7") - assertEquals(m().awaitSingle(), "7") + assertEquals(createMono().awaitFirst(), "7") + assertEquals(createMono().awaitFirstOrDefault("noValue"), "7") + assertEquals(createMono().awaitFirstOrNull(), "7") + assertEquals(createMono().awaitFirstOrElse { "noValue" }, "7") + assertEquals(createMono().awaitLast(), "7") + assertEquals(createMono().awaitSingle(), "7") } @Test fun testFluxAwaitContextPropagation() = runBlocking( Context.of(1, "1", 2, "2", 3, "3").asCoroutineContext() ) { - assertEquals(f().awaitFirst(), "1") - assertEquals(f().awaitFirstOrDefault("noValue"), "1") - assertEquals(f().awaitFirstOrNull(), "1") - assertEquals(f().awaitFirstOrElse { "noValue" }, "1") - assertEquals(f().awaitLast(), "3") - var i = 0 - f().subscribe { str -> i++; assertEquals(str, i.toString()) } + assertEquals(createFlux().awaitFirst(), "1") + assertEquals(createFlux().awaitFirstOrDefault("noValue"), "1") + assertEquals(createFlux().awaitFirstOrNull(), "1") + assertEquals(createFlux().awaitFirstOrElse { "noValue" }, "1") + assertEquals(createFlux().awaitLast(), "3") } - private fun m(): Mono = mono { - val ctx = coroutineContext[ReactorContext]?.context - ctx?.getOrDefault(7, "noValue") + private fun createMono(): Mono = mono { + val ctx = reactorContext() + ctx.getOrDefault(7, "noValue") } - private fun f(): Flux = flux { - val ctx = coroutineContext[ReactorContext]?.context - (1..3).forEach { send(ctx?.getOrDefault(it, "noValue")) } + private fun createFlux(): Flux = flux { + val ctx = reactorContext() + (1..3).forEach { send(ctx.getOrDefault(it, "noValue")) } } @Test @@ -95,17 +95,17 @@ class ReactorContextTest : TestBase() { fun testFlowToFluxDirectContextPropagation() = runBlocking( Context.of(1, "1", 2, "2", 3, "3").asCoroutineContext() ) { - var i = 0 // convert resulting flow to channel using "produceIn" val channel = bar().produceIn(this) - channel.consumeEach { str -> - i++; assertEquals(str, i.toString()) - } - assertEquals(i, 3) + val list = channel.toList() + assertEquals(listOf("1", "2", "3"), list) } private fun bar(): Flow = flux { - val ctx = coroutineContext[ReactorContext]!!.context + val ctx = reactorContext() (1..3).forEach { send(ctx.getOrDefault(it, "noValue")) } }.asFlow() + + private suspend fun reactorContext() = + coroutineContext[ReactorContext]!!.context } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt index c06130d9ad..7604b4ac26 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableSingleTest.kt @@ -11,6 +11,11 @@ import org.junit.Assert.* import java.util.concurrent.* class ObservableSingleTest : TestBase() { + @Before + fun setup() { + ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") + } + @Test fun testSingleNoWait() { val observable = rxObservable { @@ -166,7 +171,7 @@ class ObservableSingleTest : TestBase() { @Test fun testExceptionFromCoroutine() { val observable = rxObservable { - error(Observable.just("O").awaitSingle() + "K") + throw IllegalStateException(Observable.just("O").awaitSingle() + "K") } checkErroneous(observable) { From 1da7311d3948a653307999f00965ab5c2aa669cd Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 12 Nov 2019 15:26:38 +0300 Subject: [PATCH 61/90] Restore AsyncJvmTest --- .../jvm/test/AsyncJvmTest.kt | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt b/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt index ee3c66b2d8..dab7d5d033 100644 --- a/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AsyncJvmTest.kt @@ -4,32 +4,42 @@ package kotlinx.coroutines -import kotlin.coroutines.* import kotlin.test.* class AsyncJvmTest : TestBase() { // This must be a common test but it fails on JS because of KT-21961 - suspend fun test() { - println("test.begin") - delay(5000) - println("test.end") - } - - suspend fun proxy() { - println("?") - test() - println("?") - } + @Test + fun testAsyncWithFinally() = runTest { + expect(1) -// @Test - fun main() { - println("AA") - runBlocking { - withTimeout(100) { - proxy() - println() + @Suppress("UNREACHABLE_CODE") + val d = async { + expect(3) + try { + yield() // to main, will cancel + } finally { + expect(6) // will go there on await + return@async "Fail" // result will not override cancellation } - println() + expectUnreached() + "Fail2" + } + expect(2) + yield() // to async + expect(4) + check(d.isActive && !d.isCompleted && !d.isCancelled) + d.cancel() + check(!d.isActive && !d.isCompleted && d.isCancelled) + check(!d.isActive && !d.isCompleted && d.isCancelled) + expect(5) + try { + d.await() // awaits + expectUnreached() // does not complete normally + } catch (e: Throwable) { + expect(7) + check(e is CancellationException) } + check(!d.isActive && d.isCompleted && d.isCancelled) + finish(8) } } From 729dc5d1077646193c2f4c7adbd7a5f73c1a2db8 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 19 Nov 2019 11:03:13 +0300 Subject: [PATCH 62/90] Gradle: Don't resolve classpath during configuration phase --- gradle/dokka.gradle | 3 ++- ui/kotlinx-coroutines-android/build.gradle | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/gradle/dokka.gradle b/gradle/dokka.gradle index f1d8f21a6c..99201a9f28 100644 --- a/gradle/dokka.gradle +++ b/gradle/dokka.gradle @@ -38,7 +38,8 @@ dokka { if (project.name != "kotlinx-coroutines-core") { dependsOn(project.configurations.compileClasspath) dependsOn(project.sourceSets.main.output) - afterEvaluate { + doFirst { + // resolve classpath only during execution classpath = project.configurations.dokkaStubs.files + project.configurations.compileClasspath.files + project.sourceSets.main.output.files } } diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle index 5537577d7e..b3075f780e 100644 --- a/ui/kotlinx-coroutines-android/build.gradle +++ b/ui/kotlinx-coroutines-android/build.gradle @@ -48,7 +48,12 @@ class RunR8Task extends JavaExec { 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', @@ -59,11 +64,7 @@ class RunR8Task extends JavaExec { arguments.addAll(jarFile.absolutePath) args = arguments - return this - } - @Override - void exec() { if (outputDex.exists()) { outputDex.deleteDir() } From 1fd56f2b5ff2af6c9985390a56344be376d10b31 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 20 Nov 2019 17:52:23 +0300 Subject: [PATCH 63/90] Properly support exception cause on JS and Native --- .../common/src/Exceptions.common.kt | 6 ++- kotlinx-coroutines-core/js/src/Exceptions.kt | 40 ++++--------------- kotlinx-coroutines-core/jvm/src/Exceptions.kt | 11 ----- .../native/src/Exceptions.kt | 40 ++++--------------- 4 files changed, 20 insertions(+), 77 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/Exceptions.common.kt b/kotlinx-coroutines-core/common/src/Exceptions.common.kt index 62b6ba4d51..2c0b5ce2cd 100644 --- a/kotlinx-coroutines-core/common/src/Exceptions.common.kt +++ b/kotlinx-coroutines-core/common/src/Exceptions.common.kt @@ -5,14 +5,16 @@ package kotlinx.coroutines /** + * This exception gets thrown if an exception is caught while processing [CompletionHandler] invocation for [Job]. + * * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public expect class CompletionHandlerException(message: String, cause: Throwable) : RuntimeException +public class CompletionHandlerException(message: String, cause: Throwable) : RuntimeException(message, cause) public expect open class CancellationException(message: String?) : IllegalStateException -@Suppress("FunctionName") +@Suppress("FunctionName", "NO_ACTUAL_FOR_EXPECT") public expect fun CancellationException(message: String?, cause: Throwable?) : CancellationException internal expect class JobCancellationException( diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/js/src/Exceptions.kt index f42704107b..2a8968849a 100644 --- a/kotlinx-coroutines-core/js/src/Exceptions.kt +++ b/kotlinx-coroutines-core/js/src/Exceptions.kt @@ -4,31 +4,18 @@ package kotlinx.coroutines -/** - * This exception gets thrown if an exception is caught while processing [CompletionHandler] invocation for [Job]. - * - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual class CompletionHandlerException public actual constructor( - message: String, - public override val cause: Throwable -) : RuntimeException(message.withCause(cause)) - /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled while it is suspending. * It indicates _normal_ cancellation of a coroutine. * **It is not printed to console/log by default uncaught exception handler**. * (see [CoroutineExceptionHandler]). */ -public actual open class CancellationException actual constructor(message: String?) : IllegalStateException(message) - -/** - * Creates a cancellation exception with a specified message and [cause]. - */ -@Suppress("FunctionName") -public actual fun CancellationException(message: String?, cause: Throwable?) : CancellationException = - CancellationException(message.withCause(cause)) +public actual open class CancellationException( + message: String?, + cause: Throwable? +) : IllegalStateException(message, cause) { + actual constructor(message: String?) : this(message, null) +} /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed @@ -37,9 +24,9 @@ public actual fun CancellationException(message: String?, cause: Throwable?) : C */ internal actual class JobCancellationException public actual constructor( message: String, - public override val cause: Throwable?, + cause: Throwable?, internal actual val job: Job -) : CancellationException(message.withCause(cause)) { +) : CancellationException(message, cause) { override fun toString(): String = "${super.toString()}; job=$job" override fun equals(other: Any?): Boolean = other === this || @@ -48,17 +35,6 @@ internal actual class JobCancellationException public actual constructor( (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) } -@Suppress("FunctionName") -internal fun IllegalStateException(message: String, cause: Throwable?) = - IllegalStateException(message.withCause(cause)) - -private fun String?.withCause(cause: Throwable?) = - when { - cause == null -> this - this == null -> "caused by $cause" - else -> "$this; caused by $cause" - } - @Suppress("NOTHING_TO_INLINE") internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index 7a8f385e64..2aa399c21c 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -6,17 +6,6 @@ package kotlinx.coroutines -/** - * This exception gets thrown if an exception is caught while processing [CompletionHandler] invocation for [Job]. - * - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual class CompletionHandlerException actual constructor( - message: String, - cause: Throwable -) : RuntimeException(message, cause) - /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled while it is suspending. * It indicates _normal_ cancellation of a coroutine. diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt index 109b9100cb..b7657da74c 100644 --- a/kotlinx-coroutines-core/native/src/Exceptions.kt +++ b/kotlinx-coroutines-core/native/src/Exceptions.kt @@ -4,31 +4,18 @@ package kotlinx.coroutines -/** - * This exception gets thrown if an exception is caught while processing [CompletionHandler] invocation for [Job]. - * - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual class CompletionHandlerException public actual constructor( - message: String, - public override val cause: Throwable -) : RuntimeException(message.withCause(cause)) - /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled while it is suspending. * It indicates _normal_ cancellation of a coroutine. * **It is not printed to console/log by default uncaught exception handler**. * (see [CoroutineExceptionHandler]). */ -public actual open class CancellationException actual constructor(message: String?) : IllegalStateException(message) - -/** - * Creates a cancellation exception with a specified message and [cause]. - */ -@Suppress("FunctionName") -public actual fun CancellationException(message: String?, cause: Throwable?) : CancellationException = - CancellationException(message.withCause(cause)) +public actual open class CancellationException( + message: String?, + cause: Throwable? +) : IllegalStateException(message, cause) { + actual constructor(message: String?) : this(message, null) +} /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled or completed @@ -37,9 +24,9 @@ public actual fun CancellationException(message: String?, cause: Throwable?) : C */ internal actual class JobCancellationException public actual constructor( message: String, - public override val cause: Throwable?, + cause: Throwable?, internal actual val job: Job -) : CancellationException(message.withCause(cause)) { +) : CancellationException(message, cause) { override fun toString(): String = "${super.toString()}; job=$job" override fun equals(other: Any?): Boolean = other === this || @@ -48,17 +35,6 @@ internal actual class JobCancellationException public actual constructor( (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) } -@Suppress("FunctionName") -internal fun IllegalStateException(message: String, cause: Throwable?) = - IllegalStateException(message.withCause(cause)) - -private fun String?.withCause(cause: Throwable?) = - when { - cause == null -> this - this == null -> "caused by $cause" - else -> "$this; caused by $cause" - } - @Suppress("NOTHING_TO_INLINE") internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } From 7e895fcb85313f9183142acb1b1a64c2f7c22e2a Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 22 Nov 2019 12:00:23 +0300 Subject: [PATCH 64/90] Update Kotlin to 1.3.60 --- README.md | 8 ++--- gradle.properties | 4 +-- gradle/compile-native-multiplatform.gradle | 32 +++++++++++++------ .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ad5f5ddefd..f3ee3024c4 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.2) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.2) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.3.50` release. +This is a companion version for Kotlin `1.3.60` release. ```kotlin suspend fun main() = coroutineScope { @@ -90,7 +90,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.3.50 + 1.3.60 ``` @@ -108,7 +108,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.3.60' } ``` @@ -134,7 +134,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.3.50" + kotlin("jvm") version "1.3.60" } ``` diff --git a/gradle.properties b/gradle.properties index 34c11d5681..63a9c67783 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,11 +5,11 @@ # Kotlin version=1.3.2-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.3.50 +kotlin_version=1.3.60 # Dependencies junit_version=4.12 -atomicfu_version=0.13.2 +atomicfu_version=0.14.1 html_version=0.6.8 lincheck_version=2.0 dokka_version=0.9.16-rdev-2-mpp-hacks diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle index b5fad6935a..a51057ee3e 100644 --- a/gradle/compile-native-multiplatform.gradle +++ b/gradle/compile-native-multiplatform.gradle @@ -1,28 +1,42 @@ +project.ext.nativeMainSets = [] +project.ext.nativeTestSets = [] + kotlin { + targets.metaClass.addTarget = { preset -> + def target = delegate.fromPreset(preset, preset.name) + project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first()) + project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first()) + } + targets { if (project.ext.ideaActive) { fromPreset(project.ext.ideaPreset, 'native') } else { - fromPreset(presets.linuxX64, 'linuxX64') - fromPreset(presets.iosArm64, 'iosArm64') - fromPreset(presets.iosArm32, 'iosArm32') - fromPreset(presets.iosX64, 'iosX64') - fromPreset(presets.macosX64, 'macosX64') - fromPreset(presets.mingwX64, 'windowsX64') + 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 + // Empty source set is required in order to have native tests task nativeTest {} if (!project.ext.ideaActive) { - configure([linuxX64Main, macosX64Main, windowsX64Main, iosArm32Main, iosArm64Main, iosX64Main]) { + configure(nativeMainSets) { dependsOn nativeMain } - configure([linuxX64Test, macosX64Test, windowsX64Test, iosArm32Test, iosArm64Test, iosX64Test]) { + configure(nativeTestSets) { dependsOn nativeTest } } diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index ecc6578d1e..98d5e50e1c 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.50 +kotlin_version=1.3.60 coroutines_version=1.3.2 android.useAndroidX=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index ecc6578d1e..98d5e50e1c 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.50 +kotlin_version=1.3.60 coroutines_version=1.3.2 android.useAndroidX=true From 3ab34d808b5aa2c8673754bd43e4879b9d4af8c9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 20 Nov 2019 11:02:46 +0300 Subject: [PATCH 65/90] Support yield in immediate dispatchers So yield now checks for "Unconfined" dispatcher instead of "isDispatchNeeded" and works properly for immediate dispatchers. Fixes #1474 --- .../common/src/Unconfined.kt | 3 +- kotlinx-coroutines-core/common/src/Yield.kt | 19 ++++-- .../common/test/ImmediateYieldTest.kt | 46 +++++++++++++ .../test/HandlerDispatcherTest.kt | 14 ++++ .../src/JavaFxDispatcher.kt | 65 ++++++++++--------- .../test/JavaFxTest.kt | 22 +++++++ ui/kotlinx-coroutines-swing/test/SwingTest.kt | 15 +++++ 7 files changed, 147 insertions(+), 37 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt index 83e27a55b2..9fed0e89e6 100644 --- a/kotlinx-coroutines-core/common/src/Unconfined.kt +++ b/kotlinx-coroutines-core/common/src/Unconfined.kt @@ -11,6 +11,7 @@ import kotlin.coroutines.* */ internal object Unconfined : CoroutineDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = false - override fun dispatch(context: CoroutineContext, block: Runnable) { throw UnsupportedOperationException() } + // Just in case somebody wraps Unconfined dispatcher casing the "dispatch" to be called from "yield" + override fun dispatch(context: CoroutineContext, block: Runnable) = block.run() override fun toString(): String = "Unconfined" } diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index 2272352797..7342e288b8 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -8,20 +8,27 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* /** - * Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines to run. - * If the coroutine dispatcher does not have its own thread pool (like [Dispatchers.Unconfined]), this - * function does nothing but check if the coroutine's [Job] was completed. + * Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines to run if possible. + * * 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]. + * + * **Note**: This function always [checks for cancellation][ensureActive] even when it does not suspend. + * + * ### Implementation details + * + * If the coroutine dispatcher is [Unconfined][Dispatchers.Unconfined], this + * functions suspends only when there are other unconfined coroutines working and forming an event-loop. + * For other dispatchers, this function does not call [CoroutineDispatcher.isDispatchNeeded] and + * always suspends to be resumed later. If there is no [CoroutineDispatcher] in the context, it does not suspend. */ public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> val context = uCont.context context.checkCompletion() val cont = uCont.intercepted() as? DispatchedContinuation ?: return@sc Unit - if (!cont.dispatcher.isDispatchNeeded(context)) { - return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit - } + // Special case for the unconfined dispatcher that can yield only in existing unconfined loop + if (cont.dispatcher === Unconfined) return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit cont.dispatchYield(Unit) COROUTINE_SUSPENDED } diff --git a/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt b/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt new file mode 100644 index 0000000000..749ea4a9a2 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2019 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.* + +class ImmediateYieldTest : TestBase() { + + // See https://github.com/Kotlin/kotlinx.coroutines/issues/1474 + @Test + fun testImmediateYield() = runTest { + expect(1) + launch(ImmediateDispatcher(coroutineContext[ContinuationInterceptor])) { + expect(2) + yield() + expect(4) + } + expect(3) // after yield + yield() // yield back + finish(5) + } + + // imitate immediate dispatcher + private class ImmediateDispatcher(job: ContinuationInterceptor?) : CoroutineDispatcher() { + val delegate: CoroutineDispatcher = job as CoroutineDispatcher + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = false + + override fun dispatch(context: CoroutineContext, block: Runnable) = + delegate.dispatch(context, block) + } + + @Test + fun testWrappedUnconfinedDispatcherYield() = runTest { + expect(1) + launch(wrapperDispatcher(Dispatchers.Unconfined)) { + expect(2) + yield() // Would not work with wrapped unconfined dispatcher + expect(3) + } + finish(4) // after launch + } +} \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt index e006f0042b..1501639e5d 100644 --- a/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt +++ b/ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt @@ -141,4 +141,18 @@ class HandlerDispatcherTest : TestBase() { // TODO compile against API 22+ so this can be invoked without reflection. private val Message.isAsynchronous: Boolean get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean + + @Test + fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Main) { + expect(1) + // launch in the immediate dispatcher + launch(Dispatchers.Main.immediate) { + expect(2) + yield() + expect(4) + } + expect(3) // after yield + yield() // yield back + finish(5) + } } diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index eb45b3ebc3..4d7571c29f 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -115,36 +115,41 @@ private class PulseTimer : AnimationTimer() { } } -internal fun initPlatform(): Boolean { - /* - * Try to instantiate JavaFx platform in a way which works - * both on Java 8 and Java 11 and does not produce "illegal reflective access": - * - * 1) Try to invoke javafx.application.Platform.startup if this class is - * present in a classpath. - * 2) If it is not successful and does not because it is already started, - * fallback to PlatformImpl. - * - * Ignore exception anyway in case of unexpected changes in API, in that case - * user will have to instantiate it manually. - */ - val runnable = Runnable {} - return runCatching { - // Invoke public API if it is present - Class.forName("javafx.application.Platform") - .getMethod("startup", java.lang.Runnable::class.java) - .invoke(null, runnable) - }.recoverCatching { exception -> - // Recover -> check re-initialization - val cause = exception.cause - if (exception is InvocationTargetException && cause is IllegalStateException - && "Toolkit already initialized" == cause.message) { - // Toolkit is already initialized -> success, return - Unit - } else { // Fallback to Java 8 API - Class.forName("com.sun.javafx.application.PlatformImpl") +internal fun initPlatform(): Boolean = PlatformInitializer.success + +// Lazily try to initialize JavaFx platform just once +private object PlatformInitializer { + val success = run { + /* + * Try to instantiate JavaFx platform in a way which works + * both on Java 8 and Java 11 and does not produce "illegal reflective access": + * + * 1) Try to invoke javafx.application.Platform.startup if this class is + * present in a classpath. + * 2) If it is not successful and does not because it is already started, + * fallback to PlatformImpl. + * + * Ignore exception anyway in case of unexpected changes in API, in that case + * user will have to instantiate it manually. + */ + val runnable = Runnable {} + runCatching { + // Invoke public API if it is present + Class.forName("javafx.application.Platform") .getMethod("startup", java.lang.Runnable::class.java) .invoke(null, runnable) - } - }.isSuccess + }.recoverCatching { exception -> + // Recover -> check re-initialization + val cause = exception.cause + if (exception is InvocationTargetException && cause is IllegalStateException + && "Toolkit already initialized" == cause.message) { + // Toolkit is already initialized -> success, return + Unit + } else { // Fallback to Java 8 API + Class.forName("com.sun.javafx.application.PlatformImpl") + .getMethod("startup", java.lang.Runnable::class.java) + .invoke(null, runnable) + } + }.isSuccess + } } diff --git a/ui/kotlinx-coroutines-javafx/test/JavaFxTest.kt b/ui/kotlinx-coroutines-javafx/test/JavaFxTest.kt index 178f961ae5..e6a1ddb414 100644 --- a/ui/kotlinx-coroutines-javafx/test/JavaFxTest.kt +++ b/ui/kotlinx-coroutines-javafx/test/JavaFxTest.kt @@ -34,4 +34,26 @@ class JavaFxTest : TestBase() { finish(4) } } + + @Test + fun testImmediateDispatcherYield() { + if (!initPlatform()) { + println("Skipping JavaFxTest in headless environment") + return // ignore test in headless environments + } + + runBlocking(Dispatchers.JavaFx) { + expect(1) + check(Platform.isFxApplicationThread()) + // launch in the immediate dispatcher + launch(Dispatchers.JavaFx.immediate) { + expect(2) + yield() + expect(4) + } + expect(3) // after yield + yield() // yield back + finish(5) + } + } } \ No newline at end of file diff --git a/ui/kotlinx-coroutines-swing/test/SwingTest.kt b/ui/kotlinx-coroutines-swing/test/SwingTest.kt index f6cc43f54e..8b41b494cc 100644 --- a/ui/kotlinx-coroutines-swing/test/SwingTest.kt +++ b/ui/kotlinx-coroutines-swing/test/SwingTest.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.swing +import javafx.application.* import kotlinx.coroutines.* import org.junit.* import org.junit.Test @@ -83,4 +84,18 @@ class SwingTest : TestBase() { private suspend fun join(component: SwingTest.SwingComponent) { component.coroutineContext[Job]!!.join() } + + @Test + fun testImmediateDispatcherYield() = runBlocking(Dispatchers.Swing) { + expect(1) + // launch in the immediate dispatcher + launch(Dispatchers.Swing.immediate) { + expect(2) + yield() + expect(4) + } + expect(3) // after yield + yield() // yield back + finish(5) + } } \ No newline at end of file From 8f1f252a31a43534dc6dfac8af465c2bc78cb115 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Sat, 23 Nov 2019 18:21:22 +0300 Subject: [PATCH 66/90] Avoid potential StackOverflow on wrapped UnconfinedDispatcher+yield See ImmediateYieldTest.testWrappedUnconfinedDispatcherYieldStackOverflow This commit changes the way Unconfined dispatcher is detected. Instead of equality check we try to dispatch to the dispatcher with a specially updated coroutine context that has YieldContext element that is processed by the Unconfied dispatcher in a special way. --- .../common/src/CoroutineDispatcher.kt | 4 +++ .../common/src/Unconfined.kt | 26 +++++++++++++++++-- kotlinx-coroutines-core/common/src/Yield.kt | 11 ++++++-- .../src/internal/DispatchedContinuation.kt | 3 +-- .../common/test/ImmediateYieldTest.kt | 11 ++++++++ 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index f08f8f782f..33acee626a 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -76,6 +76,10 @@ public abstract class CoroutineDispatcher : * * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + * + * **Note**: This method must not immediately call [block]. Doing so would result in [StackOverflowError] + * when [yield] is repeatedly called from a loop. However, an implementation can delegate this function + * to `dispatch` method of [Dispatchers.Unconfined], which is integrated with [yield] to avoid this problem. */ public abstract fun dispatch(context: CoroutineContext, block: Runnable) diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt index 9fed0e89e6..a64cf23210 100644 --- a/kotlinx-coroutines-core/common/src/Unconfined.kt +++ b/kotlinx-coroutines-core/common/src/Unconfined.kt @@ -5,13 +5,35 @@ package kotlinx.coroutines import kotlin.coroutines.* +import kotlin.jvm.* /** * A coroutine dispatcher that is not confined to any specific thread. */ internal object Unconfined : CoroutineDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = false - // Just in case somebody wraps Unconfined dispatcher casing the "dispatch" to be called from "yield" - override fun dispatch(context: CoroutineContext, block: Runnable) = block.run() + + override fun dispatch(context: CoroutineContext, block: Runnable) { + // Just in case somebody wraps Unconfined dispatcher casing the "dispatch" to be called from "yield" + // See also code of "yield" function + val yieldContext = context[YieldContext] + if (yieldContext != null) { + // report to "yield" that it is an unconfined dispatcher and don't call "block.run()" + yieldContext.dispatcherWasUnconfined = true + return + } + block.run() + } + override fun toString(): String = "Unconfined" } + +/** + * Used to detect calls to [Unconfined.dispatch] from [yield] function. + */ +internal class YieldContext : AbstractCoroutineContextElement(Key) { + companion object Key : CoroutineContext.Key + + @JvmField + var dispatcherWasUnconfined = false +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index 7342e288b8..fd97aca202 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -27,9 +27,16 @@ public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { u val context = uCont.context context.checkCompletion() val cont = uCont.intercepted() as? DispatchedContinuation ?: return@sc Unit + // This code detects the Unconfined dispatcher even if it was wrapped into another dispatcher + val yieldContext = YieldContext() + cont.dispatchYield(context + yieldContext, Unit) // Special case for the unconfined dispatcher that can yield only in existing unconfined loop - if (cont.dispatcher === Unconfined) return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit - cont.dispatchYield(Unit) + if (yieldContext.dispatcherWasUnconfined) { + // Means that the Unconfined dispatcher got the call, but did not do anything. + // See also code of "Unconfined.dispatch" function. + return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit + } + // It was some other dispatcher that successfully dispatched the coroutine COROUTINE_SUSPENDED } diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt index cb2e4606b6..f04dde1cbc 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt @@ -211,8 +211,7 @@ internal class DispatchedContinuation( } // used by "yield" implementation - internal fun dispatchYield(value: T) { - val context = continuation.context + internal fun dispatchYield(context: CoroutineContext, value: T) { _state = value resumeMode = MODE_CANCELLABLE dispatcher.dispatchYield(context, this) diff --git a/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt b/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt index 749ea4a9a2..3801b8ab07 100644 --- a/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt +++ b/kotlinx-coroutines-core/common/test/ImmediateYieldTest.kt @@ -43,4 +43,15 @@ class ImmediateYieldTest : TestBase() { } finish(4) // after launch } + + @Test + fun testWrappedUnconfinedDispatcherYieldStackOverflow() = runTest { + expect(1) + withContext(wrapperDispatcher(Dispatchers.Unconfined)) { + repeat(100_000) { + yield() + } + } + finish(2) + } } \ No newline at end of file From 69d1d4160154105a7944feeb67f851d676ee7f94 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 26 Nov 2019 11:33:08 +0300 Subject: [PATCH 67/90] Optimize yield via dispatcher.isDispatchNeeded Also, as it was before, throw UnsupportedOperationException from the Dispatcher.Unconfined.dispatch method in case some code wraps the Unconfined dispatcher but fails to delegate isDispatchNeeded properly. --- .../common/src/CoroutineDispatcher.kt | 5 ++-- .../common/src/Unconfined.kt | 7 +++-- kotlinx-coroutines-core/common/src/Yield.kt | 29 ++++++++++++------- .../common/test/TestBase.common.kt | 5 ++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 33acee626a..59e2ca4549 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -78,8 +78,9 @@ public abstract class CoroutineDispatcher : * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. * * **Note**: This method must not immediately call [block]. Doing so would result in [StackOverflowError] - * when [yield] is repeatedly called from a loop. However, an implementation can delegate this function - * to `dispatch` method of [Dispatchers.Unconfined], which is integrated with [yield] to avoid this problem. + * when [yield] is repeatedly called from a loop. However, an implementation that returns `false` from + * [isDispatchNeeded] can delegate this function to `dispatch` method of [Dispatchers.Unconfined], which is + * integrated with [yield] to avoid this problem. */ public abstract fun dispatch(context: CoroutineContext, block: Runnable) diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt index a64cf23210..794ee1e181 100644 --- a/kotlinx-coroutines-core/common/src/Unconfined.kt +++ b/kotlinx-coroutines-core/common/src/Unconfined.kt @@ -14,15 +14,16 @@ internal object Unconfined : CoroutineDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = false override fun dispatch(context: CoroutineContext, block: Runnable) { - // Just in case somebody wraps Unconfined dispatcher casing the "dispatch" to be called from "yield" - // See also code of "yield" function + // It can only be called by the "yield" function. See also code of "yield" function. val yieldContext = context[YieldContext] if (yieldContext != null) { // report to "yield" that it is an unconfined dispatcher and don't call "block.run()" yieldContext.dispatcherWasUnconfined = true return } - block.run() + throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " + + "If you wrap Unconfined dispatcher in your code, make sure you properly delegate " + + "isDispatchNeeded and dispatch calls.") } override fun toString(): String = "Unconfined" diff --git a/kotlinx-coroutines-core/common/src/Yield.kt b/kotlinx-coroutines-core/common/src/Yield.kt index fd97aca202..5d931340af 100644 --- a/kotlinx-coroutines-core/common/src/Yield.kt +++ b/kotlinx-coroutines-core/common/src/Yield.kt @@ -20,23 +20,30 @@ import kotlin.coroutines.intrinsics.* * * If the coroutine dispatcher is [Unconfined][Dispatchers.Unconfined], this * functions suspends only when there are other unconfined coroutines working and forming an event-loop. - * For other dispatchers, this function does not call [CoroutineDispatcher.isDispatchNeeded] and - * always suspends to be resumed later. If there is no [CoroutineDispatcher] in the context, it does not suspend. + * For other dispatchers, this function calls [CoroutineDispatcher.dispatch] and + * always suspends to be resumed later regardless of the result of [CoroutineDispatcher.isDispatchNeeded]. + * If there is no [CoroutineDispatcher] in the context, it does not suspend. */ public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> val context = uCont.context context.checkCompletion() val cont = uCont.intercepted() as? DispatchedContinuation ?: return@sc Unit - // This code detects the Unconfined dispatcher even if it was wrapped into another dispatcher - val yieldContext = YieldContext() - cont.dispatchYield(context + yieldContext, Unit) - // Special case for the unconfined dispatcher that can yield only in existing unconfined loop - if (yieldContext.dispatcherWasUnconfined) { - // Means that the Unconfined dispatcher got the call, but did not do anything. - // See also code of "Unconfined.dispatch" function. - return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit + if (cont.dispatcher.isDispatchNeeded(context)) { + // this is a regular dispatcher -- do simple dispatchYield + cont.dispatchYield(context, Unit) + } else { + // This is either an "immediate" dispatcher or the Unconfined dispatcher + // This code detects the Unconfined dispatcher even if it was wrapped into another dispatcher + val yieldContext = YieldContext() + cont.dispatchYield(context + yieldContext, Unit) + // Special case for the unconfined dispatcher that can yield only in existing unconfined loop + if (yieldContext.dispatcherWasUnconfined) { + // Means that the Unconfined dispatcher got the call, but did not do anything. + // See also code of "Unconfined.dispatch" function. + return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit + } + // Otherwise, it was some other dispatcher that successfully dispatched the coroutine } - // It was some other dispatcher that successfully dispatched the coroutine COROUTINE_SUSPENDED } diff --git a/kotlinx-coroutines-core/common/test/TestBase.common.kt b/kotlinx-coroutines-core/common/test/TestBase.common.kt index 64f9f34d39..0fdce91fb4 100644 --- a/kotlinx-coroutines-core/common/test/TestBase.common.kt +++ b/kotlinx-coroutines-core/common/test/TestBase.common.kt @@ -71,9 +71,10 @@ public class RecoverableTestCancellationException(message: String? = null) : Can public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher return object : CoroutineDispatcher() { - override fun dispatch(context: CoroutineContext, block: Runnable) { + override fun isDispatchNeeded(context: CoroutineContext): Boolean = + dispatcher.isDispatchNeeded(context) + override fun dispatch(context: CoroutineContext, block: Runnable) = dispatcher.dispatch(context, block) - } } } From ef199d1aa7f4e4615a515ec68e722ac8621a224e Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 28 Nov 2019 15:27:39 +0300 Subject: [PATCH 68/90] Updated flow benchmark results to 1.3.60 --- .../kotlin/benchmarks/flow/scrabble/README.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md index 13e016fd8b..3ea3dd7b24 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/README.md @@ -27,16 +27,20 @@ The package (split into two sourcesets, `kotlin` and `java`), contains different ### Results -Benchmark results for throughput mode, Java `1.8.162`. -Full command: `taskset -c 0,1 java -jar benchmarks.jar -f 2 -jvmArgsPrepend "-XX:+UseParallelGC" .*Scrabble.*`. +Benchmark results for throughput mode, Java `1.8.0_172` +running on `Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz` +under `Darwin Kernel Version 18.7.0`. +Full command: `java -jar benchmarks.jar -f 2 -jvmArgsPrepend "-XX:+UseParallelGC" '.*Scrabble.*'`. ``` -FlowPlaysScrabbleBase.play avgt 14 94.845 ± 1.345 ms/op -FlowPlaysScrabbleOpt.play avgt 14 20.587 ± 0.173 ms/op +Benchmark Mode Cnt Score Error Units -RxJava2PlaysScrabble.play avgt 14 114.253 ± 3.450 ms/op -RxJava2PlaysScrabbleOpt.play avgt 14 30.795 ± 0.144 ms/op +FlowPlaysScrabbleBase.play avgt 14 62.480 ± 1.018 ms/op +FlowPlaysScrabbleOpt.play avgt 14 13.958 ± 0.278 ms/op -SaneFlowPlaysScrabble.play avgt 14 18.825 ± 0.231 ms/op -SequencePlaysScrabble.play avgt 14 13.787 ± 0.111 ms/op +RxJava2PlaysScrabble.play avgt 14 88.456 ± 0.950 ms/op +RxJava2PlaysScrabbleOpt.play avgt 14 23.653 ± 0.379 ms/op + +SaneFlowPlaysScrabble.play avgt 14 13.608 ± 0.332 ms/op +SequencePlaysScrabble.play avgt 14 9.824 ± 0.190 ms/op ``` From 445e026f95392c19e4eb8be17af9ef454d0647e8 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 25 Nov 2019 18:56:24 +0300 Subject: [PATCH 69/90] Improve documentation * Clarifications of CoroutineExceptionHandler execution * Clarifications of MainCoroutineDispatcher.immediate * Outdated documentation (pointed out by Google AndoidX team) for isDispatchNeeded is rewritten * Fixes #1650 Fixes #1651 Fixes #1634 --- docs/basics.md | 3 +- .../common/src/CoroutineDispatcher.kt | 46 ++++++------------- .../common/src/CoroutineExceptionHandler.kt | 7 ++- .../common/src/Dispatchers.common.kt | 2 +- .../common/src/MainCoroutineDispatcher.kt | 1 + 5 files changed, 25 insertions(+), 34 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 13760a618e..6c3c0caa78 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -75,7 +75,8 @@ Here we are launching a new coroutine in the [GlobalScope], meaning that the lif coroutine is limited only by the lifetime of the whole application. You can achieve the same result replacing -`GlobalScope.launch { ... }` with `thread { ... }` and `delay(...)` with `Thread.sleep(...)`. Try it. +`GlobalScope.launch { ... }` with `thread { ... }` and `delay(...)` with `Thread.sleep(...)`. +Try it (don't forget to import `kotlin.concurrent.thread`). If you start by replacing `GlobalScope.launch` by `thread`, the compiler produces the following error: diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 59e2ca4549..3f11da3761 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -27,46 +27,30 @@ import kotlin.coroutines.* * * This class ensures that debugging facilities in [newCoroutineContext] function work properly. */ -public abstract class CoroutineDispatcher : - AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { +public abstract class CoroutineDispatcher + : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { + /** - * Returns `true` if the execution shall be dispatched onto another thread. + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. * The default behavior for most dispatchers is to return `true`. * - * This method should never be used from general code, it is used only by `kotlinx.coroutines` - * internals and its contract with the rest of the API is an implementation detail. - * - * UI dispatchers _should not_ override `isDispatchNeeded`, but leave the default implementation that - * returns `true`. To understand the rationale beyond this recommendation, consider the following code: - * - * ```kotlin - * fun asyncUpdateUI() = async(Dispatchers.Main) { - * // do something here that updates something in UI - * } - * ``` - * - * When you invoke `asyncUpdateUI` in some background thread, it immediately continues to the next - * line, while the UI update happens asynchronously in the UI thread. However, if you invoke - * it in the UI thread itself, it will update the UI _synchronously_ if your `isDispatchNeeded` is - * overridden with a thread check. Checking if we are already in the UI thread seems more - * efficient (and it might indeed save a few CPU cycles), but this subtle and context-sensitive - * difference in behavior makes the resulting async code harder to debug. + * If this method returns `false`, the coroutine is resumed immediately in the current thread, + * potentially forming an event-loop to prevent stack overflows. + * The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation. * - * Basically, the choice here is between the "JS-style" asynchronous approach (async actions - * are always postponed to be executed later in the event dispatch thread) and "C#-style" approach - * (async actions are executed in the invoker thread until the first suspension point). - * While the C# approach seems to be more efficient, it ends up with recommendations like - * "use `yield` if you need to ....". This is error-prone. The JS-style approach is more consistent - * and does not require programmers to think about whether they need to yield or not. + * A dispatcher can override this method to provide a performance optimization and avoid paying a cost of an unnecessary dispatch. + * E.g. [MainCoroutineDispatcher.immediate] checks whether we are already in the required UI thread in this method and avoids + * an additional dispatch when it is not required. But this method should not return `false` by default, because + * it may expose unexpected behaviour (e.g. with interleaved event-loops) and unexpected order of events. * - * However, coroutine builders like [launch][CoroutineScope.launch] and [async][CoroutineScope.async] accept an optional [CoroutineStart] - * parameter that allows one to optionally choose the C#-style [CoroutineStart.UNDISPATCHED] behavior - * whenever it is needed for efficiency. + * Coroutine builders like [launch][CoroutineScope.launch] and [async][CoroutineScope.async] accept an optional [CoroutineStart] + * parameter that allows one to optionally choose the [undispatched][CoroutineStart.UNDISPATCHED] behavior to start coroutine immediately, + * but to be resumed only in the provided dispatcher. * * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. * - * **Note: This is an experimental api.** Execution semantics of coroutines may change in the future when this function returns `false`. + * **This is an experimental api.** Execution semantics of coroutines may change in the future when this function returns `false`. */ @ExperimentalCoroutinesApi public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true diff --git a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt index eca095f817..ee440b5310 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt @@ -65,7 +65,12 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte * * if there is a [Job] in the context, then [Job.cancel] is invoked; * * 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. + */ public interface CoroutineExceptionHandler : CoroutineContext.Element { /** * Key for [CoroutineExceptionHandler] instance in the coroutine context. diff --git a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt index ffb325a8d8..5a957e7555 100644 --- a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt +++ b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt @@ -66,7 +66,7 @@ public expect object Dispatchers { * Can print both "1 2 3" and "1 3 2", this is an implementation detail that can be changed. * But it is guaranteed that "Done" will be printed only when both `withContext` calls are completed. * - * Note that if you need your coroutine to be confined to a particular thread or a thread-pool after resumption, + * If you need your coroutine to be confined to a particular thread or a thread-pool after resumption, * but still want to execute it in the current call-frame until its first suspension, then you can use * an optional [CoroutineStart] parameter in coroutine builders like * [launch][CoroutineScope.launch] and [async][CoroutineScope.async] setting it to diff --git a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt index 2a20095aee..bead3c89a4 100644 --- a/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/MainCoroutineDispatcher.kt @@ -18,6 +18,7 @@ public abstract class MainCoroutineDispatcher : CoroutineDispatcher() { * * Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined]. * The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation. + * The formed event-loop is shared with [Unconfined] and other immediate dispatchers, potentially overlapping tasks between them. * * Example of usage: * ``` From 391042f20bbc12b77b33a550b566265346e2a423 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 26 Nov 2019 14:07:53 +0300 Subject: [PATCH 70/90] Promote isDispatchNeeded to stable API --- .../common/src/CoroutineDispatcher.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 3f11da3761..393f6cb526 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -27,8 +27,8 @@ import kotlin.coroutines.* * * This class ensures that debugging facilities in [newCoroutineContext] function work properly. */ -public abstract class CoroutineDispatcher - : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { +public abstract class CoroutineDispatcher : + AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { /** * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. @@ -40,8 +40,10 @@ public abstract class CoroutineDispatcher * * A dispatcher can override this method to provide a performance optimization and avoid paying a cost of an unnecessary dispatch. * E.g. [MainCoroutineDispatcher.immediate] checks whether we are already in the required UI thread in this method and avoids - * an additional dispatch when it is not required. But this method should not return `false` by default, because - * it may expose unexpected behaviour (e.g. with interleaved event-loops) and unexpected order of events. + * an additional dispatch when it is not required. + * + * While this approach can be more efficient, it is not chosen by default to provide a consistent dispatching behaviour + * so that users won't observe unexpected and non-consistent order of events by default. * * Coroutine builders like [launch][CoroutineScope.launch] and [async][CoroutineScope.async] accept an optional [CoroutineStart] * parameter that allows one to optionally choose the [undispatched][CoroutineStart.UNDISPATCHED] behavior to start coroutine immediately, @@ -49,10 +51,7 @@ public abstract class CoroutineDispatcher * * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. - * - * **This is an experimental api.** Execution semantics of coroutines may change in the future when this function returns `false`. */ - @ExperimentalCoroutinesApi public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true /** From bd1687f614348677544bdf2ab685500fad9d3d1d Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 12 Nov 2019 19:57:40 +0300 Subject: [PATCH 71/90] Throw NoSuchElementException instead of UnsupportedOperationException in Flow.reduce * We can do it safely because reduce is still experimental * We will be consistent with stdlib (as soon as KT-33874 is implemented) --- kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt | 4 ++-- .../common/test/flow/terminal/ReduceTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt index e8433de906..eb3ce288a2 100644 --- a/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt +++ b/kotlinx-coroutines-core/common/src/flow/terminal/Reduce.kt @@ -15,7 +15,7 @@ import kotlin.jvm.* /** * Accumulates value starting with the first element and applying [operation] to current accumulator value and each element. - * Throws [UnsupportedOperationException] if flow was empty. + * Throws [NoSuchElementException] if flow was empty. */ @ExperimentalCoroutinesApi public suspend fun Flow.reduce(operation: suspend (accumulator: S, value: T) -> S): S { @@ -30,7 +30,7 @@ public suspend fun Flow.reduce(operation: suspend (accumulator: S, } } - if (accumulator === NULL) throw UnsupportedOperationException("Empty flow can't be reduced") + if (accumulator === NULL) throw NoSuchElementException("Empty flow can't be reduced") @Suppress("UNCHECKED_CAST") return accumulator as S } diff --git a/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt b/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt index a84c02785a..99ee1d6641 100644 --- a/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/terminal/ReduceTest.kt @@ -23,7 +23,7 @@ class ReduceTest : TestBase() { @Test fun testEmptyReduce() = runTest { val flow = emptyFlow() - assertFailsWith { flow.reduce { acc, value -> value + acc } } + assertFailsWith { flow.reduce { acc, value -> value + acc } } } @Test @@ -42,7 +42,7 @@ class ReduceTest : TestBase() { fun testReduceNulls() = runTest { assertNull(flowOf(null).reduce { _, value -> value }) assertNull(flowOf(null, null).reduce { _, value -> value }) - assertFailsWith { flowOf().reduce { _, value -> value } } + assertFailsWith { flowOf().reduce { _, value -> value } } } @Test From 34dcfb19a5d236d207ad54fa66e7285fa75d2543 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 12 Nov 2019 19:16:50 +0300 Subject: [PATCH 72/90] Rethrow downstream exception during "onCompletion" emissions * We cannot allow emitting elements when downstream exception occurred, otherwise it may lead to a weird side-effects when "collect" block (or any other terminal operator) has thrown an exception, but keeps receiving new values * Another solution may be to silently ignore emitted values, but it may lead to a postponed cancellation and surprising behaviour for users Fixes #1654 --- .../common/src/flow/operators/Emitters.kt | 29 +++++-- .../test/flow/operators/OnCompletionTest.kt | 84 ++++++++++++++++++- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt index f3a112682a..6a910764b7 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Emitters.kt @@ -128,13 +128,26 @@ public fun Flow.onStart( 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 - var exception: Throwable? = null - try { - exception = catchImpl(this) - } finally { - // Separate method because of KT-32220 - SafeCollector(this, coroutineContext).invokeSafely(action, exception) - exception?.let { throw it } + val exception = try { + catchImpl(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) + throw e + } + // Exception from the upstream or normal completion + SafeCollector(this, coroutineContext).invokeSafely(action, exception) + exception?.let { throw it } +} + +private class ThrowingCollector(private val e: Throwable) : FlowCollector { + override suspend fun emit(value: Any?) { + throw e } } @@ -155,5 +168,3 @@ private suspend fun FlowCollector.invokeSafely( 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 af50608a2a..c079500ef7 100644 --- a/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt +++ b/kotlinx-coroutines-core/common/test/flow/operators/OnCompletionTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.flow import kotlinx.coroutines.* +import kotlinx.coroutines.flow.internal.* import kotlin.test.* class OnCompletionTest : TestBase() { @@ -171,14 +172,91 @@ class OnCompletionTest : TestBase() { .onCompletion { e -> expect(8) assertNull(e) - emit(TestData.Done(e)) + try { + emit(TestData.Done(e)) + expectUnreached() + } finally { + expect(9) + } }.collect { collected += it } } } - val expected = (1..5).map { TestData.Value(it) } + TestData.Done(null) + val expected = (1..5).map { TestData.Value(it) } assertEquals(expected, collected) - finish(9) + finish(10) + } + + @Test + fun testFailedEmit() = runTest { + val cause = TestException() + assertFailsWith { + flow { + expect(1) + emit(TestData.Value(2)) + expectUnreached() + }.onCompletion { + assertNull(it) + expect(3) + try { + emit(TestData.Done(it)) + expectUnreached() + } catch (e: TestException) { + assertSame(cause, e) + finish(4) + } + }.collect { + expect((it as TestData.Value).i) + throw cause + } + } + } + + @Test + fun testFirst() = runTest { + val value = flowOf(239).onCompletion { + assertNull(it) + expect(1) + try { + emit(42) + expectUnreached() + } catch (e: Throwable) { + assertTrue { e is AbortFlowException } + } + }.first() + assertEquals(239, value) + finish(2) + } + + @Test + fun testSingle() = runTest { + assertFailsWith { + flowOf(239).onCompletion { + assertNull(it) + expect(1) + try { + emit(42) + expectUnreached() + } catch (e: Throwable) { + // Second emit -- failure + assertTrue { e is IllegalStateException } + throw e + } + }.single() + expectUnreached() + } + finish(2) + } + + @Test + fun testEmptySingleInterference() = runTest { + val value = emptyFlow().onCompletion { + assertNull(it) + expect(1) + emit(42) + }.single() + assertEquals(42, value) + finish(2) } } From 6c98c192b96a42242a0c82d9142f70a69217d074 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 2 Dec 2019 21:02:50 +0300 Subject: [PATCH 73/90] Update Kotlin to 1.3.61 --- README.md | 8 ++++---- gradle.properties | 2 +- .../animation-app/gradle.properties | 2 +- .../example-app/gradle.properties | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f3ee3024c4..b8202bc41e 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.2) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.2) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. -This is a companion version for Kotlin `1.3.60` release. +This is a companion version for Kotlin `1.3.61` release. ```kotlin suspend fun main() = coroutineScope { @@ -90,7 +90,7 @@ And make sure that you use the latest Kotlin version: ```xml - 1.3.60 + 1.3.61 ``` @@ -108,7 +108,7 @@ And make sure that you use the latest Kotlin version: ```groovy buildscript { - ext.kotlin_version = '1.3.60' + ext.kotlin_version = '1.3.61' } ``` @@ -134,7 +134,7 @@ And make sure that you use the latest Kotlin version: ```groovy plugins { - kotlin("jvm") version "1.3.60" + kotlin("jvm") version "1.3.61" } ``` diff --git a/gradle.properties b/gradle.properties index 63a9c67783..1214a79e4f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ # Kotlin version=1.3.2-SNAPSHOT group=org.jetbrains.kotlinx -kotlin_version=1.3.60 +kotlin_version=1.3.61 # Dependencies junit_version=4.12 diff --git a/ui/kotlinx-coroutines-android/animation-app/gradle.properties b/ui/kotlinx-coroutines-android/animation-app/gradle.properties index 98d5e50e1c..e494b796d7 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.60 +kotlin_version=1.3.61 coroutines_version=1.3.2 android.useAndroidX=true diff --git a/ui/kotlinx-coroutines-android/example-app/gradle.properties b/ui/kotlinx-coroutines-android/example-app/gradle.properties index 98d5e50e1c..e494b796d7 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.60 +kotlin_version=1.3.61 coroutines_version=1.3.2 android.useAndroidX=true From a930b0cd1ad128c5782c697ba53cd514370f8042 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 5 Dec 2019 19:33:44 +0300 Subject: [PATCH 74/90] Consistently handle undeliverable exceptions in RxJava and Reactor integrations Use tryOnError in RxJava to make exception delivery check-and-act race free. Deliver undeliverable exceptions via RxJavaPlugins instead of handleCoroutineException. This is a deliberate choice for a multiple reasons: * When using Rx (whether with coroutines or not), undeliverable exceptions are inevitable and users should hook into RxJavaPlugins anyway. We don't want to force them using Rx-specific CoroutineExceptionHandler all over the place * Undeliverable exceptions provide additional helpful stacktrace and proper way to distinguish them from other unhandled exceptions * Be consistent with reactor where we don't have try*, thus cannot provide a completely consistent experience with CEH (at least, without wrapping all the subscribers)\ Do the similar in Reactor integration, but without try*, Reactor does not have notion of undeliverable exceoptions and handles them via Operators.* on its own. Also, get rid of ASCII tables that are not properly render in IDEA Fixes #252 Fixes #1614 --- .../kotlinx-coroutines-reactive.txt | 4 +- .../src/Publish.kt | 29 +++++---- .../kotlinx-coroutines-reactor/src/Flux.kt | 35 ++++++----- .../kotlinx-coroutines-reactor/src/Mono.kt | 31 ++++----- .../test/FluxTest.kt | 12 ++++ .../test/MonoTest.kt | 43 ++++++++++++- .../src/RxCancellable.kt | 11 ++++ .../src/RxCompletable.kt | 24 +++---- .../kotlinx-coroutines-rx2/src/RxConvert.kt | 10 ++- .../kotlinx-coroutines-rx2/src/RxFlowable.kt | 18 +++--- .../kotlinx-coroutines-rx2/src/RxMaybe.kt | 34 ++++------ .../src/RxObservable.kt | 22 +++---- .../kotlinx-coroutines-rx2/src/RxScheduler.kt | 3 +- .../kotlinx-coroutines-rx2/src/RxSingle.kt | 26 +++----- reactive/kotlinx-coroutines-rx2/test/Check.kt | 11 ++++ .../test/CompletableTest.kt | 53 ++++++++++------ .../test/FlowableExceptionHandlingTest.kt | 29 ++++----- .../test/LeakedExceptionTest.kt | 58 +++++++++++++++++ .../kotlinx-coroutines-rx2/test/MaybeTest.kt | 63 ++++++++++++------- .../test/ObservableExceptionHandlingTest.kt | 29 ++++----- .../test/ObservableTest.kt | 33 ++++++++++ .../kotlinx-coroutines-rx2/test/SingleTest.kt | 57 ++++++++++------- 22 files changed, 414 insertions(+), 221 deletions(-) create mode 100644 reactive/kotlinx-coroutines-rx2/test/LeakedExceptionTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt index 60baa74342..bed065d582 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-reactive.txt @@ -42,11 +42,11 @@ public final class kotlinx/coroutines/reactive/PublishKt { public static final fun publish (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lorg/reactivestreams/Publisher; public static synthetic fun publish$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; public static synthetic fun publish$default (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/reactivestreams/Publisher; - public static final fun publishInternal (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lorg/reactivestreams/Publisher; + public static final fun publishInternal (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lorg/reactivestreams/Publisher; } public final class kotlinx/coroutines/reactive/PublisherCoroutine : kotlinx/coroutines/AbstractCoroutine, kotlinx/coroutines/channels/ProducerScope, kotlinx/coroutines/selects/SelectClause2, org/reactivestreams/Subscription { - public fun (Lkotlin/coroutines/CoroutineContext;Lorg/reactivestreams/Subscriber;)V + public fun (Lkotlin/coroutines/CoroutineContext;Lorg/reactivestreams/Subscriber;Lkotlin/jvm/functions/Function2;)V public fun cancel ()V public fun close (Ljava/lang/Throwable;)Z public fun getChannel ()Lkotlinx/coroutines/channels/SendChannel; diff --git a/reactive/kotlinx-coroutines-reactive/src/Publish.kt b/reactive/kotlinx-coroutines-reactive/src/Publish.kt index 0e3a7b8c11..704b7142b3 100644 --- a/reactive/kotlinx-coroutines-reactive/src/Publish.kt +++ b/reactive/kotlinx-coroutines-reactive/src/Publish.kt @@ -17,18 +17,15 @@ import kotlin.internal.LowPriorityInOverloadResolution /** * Creates cold reactive [Publisher] that runs a given [block] in a coroutine. - * Every time the returned publisher is subscribed, it starts a new coroutine. - * Coroutine emits items with `send`. Unsubscribing cancels running coroutine. + * Every time the returned flux is subscribed, it starts a new coroutine in the specified [context]. + * Coroutine emits ([Subscriber.onNext]) values with `send`, completes ([Subscriber.onComplete]) + * when the coroutine completes or channel is explicitly closed and emits error ([Subscriber.onError]) + * if coroutine throws an exception or closes channel with a cause. + * Unsubscribing cancels running coroutine. * * Invocations of `send` are suspended appropriately when subscribers apply back-pressure and to ensure that * `onNext` is not invoked concurrently. * - * | **Coroutine action** | **Signal to subscriber** - * | -------------------------------------------- | ------------------------ - * | `send` | `onNext` - * | Normal completion or `close` without cause | `onComplete` - * | Failure with exception or `close` with cause | `onError` - * * 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. @@ -43,7 +40,7 @@ public fun publish( ): Publisher { require(context[Job] === null) { "Publisher context cannot contain job in it." + "Its lifecycle should be managed via subscription. Had $context" } - return publishInternal(GlobalScope, context, block) + return publishInternal(GlobalScope, context, DEFAULT_HANDLER, block) } @Deprecated( @@ -55,37 +52,39 @@ public fun publish( public fun CoroutineScope.publish( context: CoroutineContext = EmptyCoroutineContext, @BuilderInference block: suspend ProducerScope.() -> Unit -): Publisher = publishInternal(this, context, block) +): Publisher = publishInternal(this, context, DEFAULT_HANDLER ,block) /** @suppress For internal use from other reactive integration modules only */ @InternalCoroutinesApi public fun publishInternal( scope: CoroutineScope, // support for legacy publish in scope context: CoroutineContext, + exceptionOnCancelHandler: (Throwable, CoroutineContext) -> Unit, block: suspend ProducerScope.() -> Unit ): Publisher = Publisher { subscriber -> // specification requires NPE on null subscriber if (subscriber == null) throw NullPointerException("Subscriber cannot be null") val newContext = scope.newCoroutineContext(context) - val coroutine = PublisherCoroutine(newContext, subscriber) + val coroutine = PublisherCoroutine(newContext, subscriber, exceptionOnCancelHandler) subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions coroutine.start(CoroutineStart.DEFAULT, coroutine, block) } private const val CLOSED = -1L // closed, but have not signalled onCompleted/onError yet private const val SIGNALLED = -2L // already signalled subscriber onCompleted/onError +private val DEFAULT_HANDLER: (Throwable, CoroutineContext) -> Unit = { t, ctx -> if (t !is CancellationException) handleCoroutineException(ctx, t) } @Suppress("CONFLICTING_JVM_DECLARATIONS", "RETURN_TYPE_MISMATCH_ON_INHERITANCE") @InternalCoroutinesApi public class PublisherCoroutine( parentContext: CoroutineContext, - private val subscriber: Subscriber + private val subscriber: Subscriber, + private val exceptionOnCancelHandler: (Throwable, CoroutineContext) -> Unit ) : AbstractCoroutine(parentContext, true), ProducerScope, Subscription, SelectClause2> { override val channel: SendChannel get() = this // Mutex is locked when either nRequested == 0 or while subscriber.onXXX is being invoked private val mutex = Mutex(locked = true) - private val _nRequested = atomic(0L) // < 0 when closed (CLOSED or SIGNALLED) @Volatile @@ -198,7 +197,7 @@ public class PublisherCoroutine( // Specification requires that after cancellation requested we don't call onXXX if (cancelled) { // If the parent had failed to handle our exception, then we must not lose this exception - if (cause != null && !handled) handleCoroutineException(context, cause) + if (cause != null && !handled) exceptionOnCancelHandler(cause, context) return } @@ -217,7 +216,7 @@ public class PublisherCoroutine( */ subscriber.onError(cause) if (!handled && cause.isFatal()) { - handleCoroutineException(context, cause) + exceptionOnCancelHandler(cause, context) } } else { subscriber.onComplete() diff --git a/reactive/kotlinx-coroutines-reactor/src/Flux.kt b/reactive/kotlinx-coroutines-reactor/src/Flux.kt index 389428df11..b6cc1615b0 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Flux.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Flux.kt @@ -10,29 +10,24 @@ package kotlinx.coroutines.reactor import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.reactive.* -import org.reactivestreams.Publisher -import reactor.core.CoreSubscriber +import org.reactivestreams.* +import reactor.core.* import reactor.core.publisher.* +import reactor.util.context.* import kotlin.coroutines.* -import kotlin.internal.LowPriorityInOverloadResolution +import kotlin.internal.* /** * Creates cold reactive [Flux] that runs a given [block] in a coroutine. * Every time the returned flux is subscribed, it starts a new coroutine in the specified [context]. - * Coroutine emits items with `send`. 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. + * Coroutine emits ([Subscriber.onNext]) values with `send`, completes ([Subscriber.onComplete]) + * when the coroutine completes or channel is explicitly closed and emits error ([Subscriber.onError]) + * if coroutine throws an exception or closes channel with a cause. + * Unsubscribing cancels running coroutine. * * Invocations of `send` are suspended appropriately when subscribers apply back-pressure and to ensure that * `onNext` is not invoked concurrently. * - * | **Coroutine action** | **Signal to subscriber** - * | -------------------------------------------- | ------------------------ - * | `send` | `onNext` - * | Normal completion or `close` without cause | `onComplete` - * | Failure with exception or `close` with cause | `onError` - * * 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 @@ -71,7 +66,17 @@ private fun reactorPublish( val currentContext = subscriber.currentContext() val reactorContext = (context[ReactorContext]?.context?.putAll(currentContext) ?: currentContext).asCoroutineContext() val newContext = scope.newCoroutineContext(context + reactorContext) - val coroutine = PublisherCoroutine(newContext, subscriber) + val coroutine = PublisherCoroutine(newContext, subscriber, REACTOR_HANDLER) subscriber.onSubscribe(coroutine) // do it first (before starting coroutine), to avoid unnecessary suspensions coroutine.start(CoroutineStart.DEFAULT, coroutine, block) -} \ No newline at end of file +} + +private val REACTOR_HANDLER: (Throwable, CoroutineContext) -> Unit = { e, ctx -> + if (e !is CancellationException) { + try { + Operators.onOperatorError(e, ctx[ReactorContext]?.context ?: Context.empty()) + } catch (e: Throwable) { + handleCoroutineException(ctx, e) + } + } +} diff --git a/reactive/kotlinx-coroutines-reactor/src/Mono.kt b/reactive/kotlinx-coroutines-reactor/src/Mono.kt index 76f0418ea6..415932dd7d 100644 --- a/reactive/kotlinx-coroutines-reactor/src/Mono.kt +++ b/reactive/kotlinx-coroutines-reactor/src/Mono.kt @@ -13,15 +13,10 @@ import kotlin.coroutines.* import kotlin.internal.* /** - * Creates cold [mono][Mono] that will run a given [block] in a coroutine. + * Creates cold [mono][Mono] that will run a given [block] in a coroutine and emits its result. * Every time the returned mono is subscribed, it starts a new coroutine. - * Coroutine returns a single, possibly null value. Unsubscribing cancels running coroutine. - * - * | **Coroutine action** | **Signal to sink** - * | ------------------------------------- | ------------------------ - * | Returns a non-null value | `success(value)` - * | Returns a null | `success` - * | Failure with exception or unsubscribe | `error` + * If [block] result is `null`, [MonoSink.success] is invoked without a value. + * 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. @@ -64,18 +59,24 @@ private class MonoCoroutine( parentContext: CoroutineContext, private val sink: MonoSink ) : AbstractCoroutine(parentContext, true), Disposable { - var disposed = false + @Volatile + private var disposed = false override fun onCompleted(value: T) { - if (!disposed) { - if (value == null) sink.success() else sink.success(value) - } + if (value == null) sink.success() else sink.success(value) } override fun onCancelled(cause: Throwable, handled: Boolean) { - if (!disposed) { - sink.error(cause) - } else if (!handled) { + try { + /* + * sink.error handles exceptions on its own and, by default, handling of undeliverable exceptions is a no-op. + * Guard potentially non-empty handlers against meaningless cancellation exceptions + */ + if (getCancellationException() !== cause) { + sink.error(cause) + } + } catch (e: Throwable) { + // In case of improper error implementation or fatal exceptions handleCoroutineException(context, cause) } } diff --git a/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt b/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt index ee26455ec8..2562c9d3db 100644 --- a/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/FluxTest.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.reactor import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* import org.hamcrest.core.* import org.junit.* @@ -130,4 +131,15 @@ class FluxTest : TestBase() { fun testIllegalArgumentException() { assertFailsWith { flux(Job()) { } } } + + @Test + fun testLeakedException() = runBlocking { + // Test exception is not reported to global handler + val flow = flux { throw TestException() }.asFlow() + repeat(2000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect { } + } + } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt b/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt index 2283d45afc..223ba7be5d 100644 --- a/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt +++ b/reactive/kotlinx-coroutines-reactor/test/MonoTest.kt @@ -5,13 +5,17 @@ package kotlinx.coroutines.reactor import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.coroutines.reactive.* import org.hamcrest.core.* import org.junit.* import org.junit.Assert.* import org.reactivestreams.* import reactor.core.publisher.* +import reactor.util.context.* +import java.time.* import java.time.Duration.* +import java.util.function.* class MonoTest : TestBase() { @Before @@ -217,11 +221,13 @@ class MonoTest : TestBase() { fun testUnhandledException() = runTest { expect(1) var subscription: Subscription? = null - val mono = mono(currentDispatcher() + CoroutineExceptionHandler { _, t -> + val handler = BiFunction { t, _ -> assertTrue(t is TestException) expect(5) + t + } - }) { + val mono = mono(currentDispatcher()) { expect(4) subscription!!.cancel() // cancel our own subscription, so that delay will get cancelled try { @@ -229,7 +235,7 @@ class MonoTest : TestBase() { } finally { throw TestException() // would not be able to handle it since mono is disposed } - } + }.subscriberContext { Context.of("reactor.onOperatorError.local", handler) } mono.subscribe(object : Subscriber { override fun onSubscribe(s: Subscription) { expect(2) @@ -248,4 +254,35 @@ class MonoTest : TestBase() { fun testIllegalArgumentException() { assertFailsWith { mono(Job()) { } } } + + @Test + fun testExceptionAfterCancellation() = runTest { + // Test exception is not reported to global handler + Flux + .interval(ofMillis(1)) + .switchMap { + mono(coroutineContext) { + timeBomb().awaitFirst() + } + } + .onErrorReturn({ + expect(1) + true + }, 42) + .blockLast() + finish(2) + } + + private fun timeBomb() = Mono.delay(Duration.ofMillis(1)).doOnSuccess { throw Exception("something went wrong") } + + @Test + fun testLeakedException() = runBlocking { + // Test exception is not reported to global handler + val flow = mono { throw TestException() }.toFlux().asFlow() + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect { } + } + } } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxCancellable.kt b/reactive/kotlinx-coroutines-rx2/src/RxCancellable.kt index d76f12315b..a0c32f9f8a 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxCancellable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxCancellable.kt @@ -5,10 +5,21 @@ package kotlinx.coroutines.rx2 import io.reactivex.functions.* +import io.reactivex.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) + } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt index c59b4bd6b5..ab96844c60 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxCompletable.kt @@ -12,15 +12,9 @@ import kotlin.coroutines.* import kotlin.internal.* /** - * Creates cold [Completable] that runs a given [block] in a coroutine. + * 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 action** | **Signal to subscriber** - * | ------------------------------------- | ------------------------ - * | Completes successfully | `onCompleted` - * | Failure with exception or unsubscribe | `onError` - * * 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. @@ -62,21 +56,19 @@ private class RxCompletableCoroutine( ) : AbstractCoroutine(parentContext, true) { override fun onCompleted(value: Unit) { try { - if (!subscriber.isDisposed) subscriber.onComplete() + subscriber.onComplete() } catch (e: Throwable) { - handleCoroutineException(context, e) + handleUndeliverableException(e, context) } } override fun onCancelled(cause: Throwable, handled: Boolean) { - if (!subscriber.isDisposed) { - try { - subscriber.onError(cause) - } catch (e: Throwable) { - handleCoroutineException(context, e) + try { + if (!subscriber.tryOnError(cause)) { + handleUndeliverableException(cause, context) } - } else if (!handled) { - handleCoroutineException(context, cause) + } catch (e: Throwable) { + handleUndeliverableException(e, context) } } } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt index 8df6caeeed..bd369cad55 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxConvert.kt @@ -94,9 +94,13 @@ public fun Flow.asObservable() : Observable = Observable.create { 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) emitter.onError(e) - else emitter.onComplete() - + if (e !is CancellationException) { + if (!emitter.tryOnError(e)) { + handleUndeliverableException(e, coroutineContext) + } + } else { + emitter.onComplete() + } } } emitter.setCancellable(RxCancellable(job)) diff --git a/reactive/kotlinx-coroutines-rx2/src/RxFlowable.kt b/reactive/kotlinx-coroutines-rx2/src/RxFlowable.kt index 30a1ed7e92..7924a3f15c 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxFlowable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxFlowable.kt @@ -16,17 +16,15 @@ import kotlin.internal.* /** * 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 items with `send`. Unsubscribing cancels running 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 action** | **Signal to subscriber** - * | -------------------------------------------- | ------------------------ - * | `send` | `onNext` - * | Normal completion or `close` without cause | `onComplete` - * | Failure with exception or `close` with cause | `onError` - * * 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. @@ -40,7 +38,7 @@ public fun rxFlowable( ): 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, block)) + return Flowable.fromPublisher(publishInternal(GlobalScope, context, RX_HANDLER, block)) } @Deprecated( @@ -52,4 +50,6 @@ public fun rxFlowable( public fun CoroutineScope.rxFlowable( context: CoroutineContext = EmptyCoroutineContext, @BuilderInference block: suspend ProducerScope.() -> Unit -): Flowable = Flowable.fromPublisher(publishInternal(this, context, block)) +): Flowable = Flowable.fromPublisher(publishInternal(this, context, RX_HANDLER, block)) + +private val RX_HANDLER: (Throwable, CoroutineContext) -> Unit = ::handleUndeliverableException \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt b/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt index 9f176e938c..9fb5f650f4 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxMaybe.kt @@ -12,16 +12,10 @@ import kotlin.coroutines.* import kotlin.internal.* /** - * Creates cold [maybe][Maybe] that will run a given [block] in a coroutine. + * 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. - * Coroutine returns a single, possibly null value. Unsubscribing cancels running coroutine. - * - * | **Coroutine action** | **Signal to subscriber** - * | ------------------------------------- | ------------------------ - * | Returns a non-null value | `onSuccess` - * | Returns a null | `onComplete` - * | Failure with exception or unsubscribe | `onError` - * + * 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. @@ -62,24 +56,20 @@ private class RxMaybeCoroutine( private val subscriber: MaybeEmitter ) : AbstractCoroutine(parentContext, true) { override fun onCompleted(value: T) { - if (!subscriber.isDisposed) { - try { - if (value == null) subscriber.onComplete() else subscriber.onSuccess(value) - } catch(e: Throwable) { - handleCoroutineException(context, e) - } + try { + if (value == null) subscriber.onComplete() else subscriber.onSuccess(value) + } catch (e: Throwable) { + handleUndeliverableException(e, context) } } override fun onCancelled(cause: Throwable, handled: Boolean) { - if (!subscriber.isDisposed) { - try { - subscriber.onError(cause) - } catch (e: Throwable) { - handleCoroutineException(context, e) + try { + if (!subscriber.tryOnError(cause)) { + handleUndeliverableException(cause, context) } - } else if (!handled) { - handleCoroutineException(context, cause) + } catch (e: Throwable) { + handleUndeliverableException(e, context) } } } diff --git a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt index 6ccf0f0bae..b8de66df08 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxObservable.kt @@ -19,16 +19,14 @@ import kotlin.internal.* /** * 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 items with `send`. 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**. Use [rxFlowable]. + * 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. * - * | **Coroutine action** | **Signal to subscriber** - * | -------------------------------------------- | ------------------------ - * | `send` | `onNext` - * | Normal completion or `close` without cause | `onComplete` - * | Failure with exception or `close` with cause | `onError` + * 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. @@ -170,9 +168,9 @@ private class RxObservableCoroutine( * by coroutines machinery, anyway, they should not be present in regular program flow, * thus our goal here is just to expose it as soon as possible. */ - subscriber.onError(cause) + subscriber.tryOnError(cause) if (!handled && cause.isFatal()) { - handleCoroutineException(context, cause) + handleUndeliverableException(cause, context) } } else { @@ -180,7 +178,7 @@ private class RxObservableCoroutine( } } catch (e: Throwable) { // Unhandled exception (cannot handle in other way, since we are already complete) - handleCoroutineException(context, e) + handleUndeliverableException(e, context) } } } finally { @@ -208,4 +206,4 @@ internal fun Throwable.isFatal() = try { false } catch (e: Throwable) { true -} \ No newline at end of file +} diff --git a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt index 53fbaf6505..610a5bcd6d 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxScheduler.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxScheduler.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-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.rx2 @@ -17,7 +17,6 @@ public fun Scheduler.asCoroutineDispatcher(): SchedulerCoroutineDispatcher = Sch /** * Implements [CoroutineDispatcher] on top of an arbitrary [Scheduler]. - * @param scheduler a scheduler. */ public class SchedulerCoroutineDispatcher( /** diff --git a/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt b/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt index f3573ee6eb..07088909b5 100644 --- a/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt +++ b/reactive/kotlinx-coroutines-rx2/src/RxSingle.kt @@ -12,15 +12,9 @@ import kotlin.coroutines.* import kotlin.internal.* /** - * Creates cold [single][Single] that will run a given [block] in a coroutine. + * 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. - * Coroutine returns a single value. Unsubscribing cancels running coroutine. - * - * | **Coroutine action** | **Signal to subscriber** - * | ------------------------------------- | ------------------------ - * | Returns a value | `onSuccess` - * | Failure with exception or unsubscribe | `onError` - * + * 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. @@ -62,21 +56,19 @@ private class RxSingleCoroutine( ) : AbstractCoroutine(parentContext, true) { override fun onCompleted(value: T) { try { - if (!subscriber.isDisposed) subscriber.onSuccess(value) + subscriber.onSuccess(value) } catch (e: Throwable) { - handleCoroutineException(context, e) + handleUndeliverableException(e, context) } } override fun onCancelled(cause: Throwable, handled: Boolean) { - if (!subscriber.isDisposed) { - try { - subscriber.onError(cause) - } catch (e: Throwable) { - handleCoroutineException(context, e) + try { + if (!subscriber.tryOnError(cause)) { + handleUndeliverableException(cause, context) } - } else if (!handled) { - handleCoroutineException(context, cause) + } catch (e: Throwable) { + handleUndeliverableException(e, context) } } } diff --git a/reactive/kotlinx-coroutines-rx2/test/Check.kt b/reactive/kotlinx-coroutines-rx2/test/Check.kt index 29eda6fa00..beb2c43a3d 100644 --- a/reactive/kotlinx-coroutines-rx2/test/Check.kt +++ b/reactive/kotlinx-coroutines-rx2/test/Check.kt @@ -5,6 +5,8 @@ package kotlinx.coroutines.rx2 import io.reactivex.* +import io.reactivex.functions.Consumer +import io.reactivex.plugins.* fun checkSingleValue( observable: Observable, @@ -64,3 +66,12 @@ fun checkErroneous( } } +inline fun withExceptionHandler(noinline handler: (Throwable) -> Unit, block: () -> Unit) { + val original = RxJavaPlugins.getErrorHandler() + RxJavaPlugins.setErrorHandler { handler(it) } + try { + block() + } finally { + RxJavaPlugins.setErrorHandler(original) + } +} \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/CompletableTest.kt b/reactive/kotlinx-coroutines-rx2/test/CompletableTest.kt index 9a12bafb1d..04e869758c 100644 --- a/reactive/kotlinx-coroutines-rx2/test/CompletableTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/CompletableTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import io.reactivex.disposables.* +import io.reactivex.exceptions.* import kotlinx.coroutines.* import org.hamcrest.core.* import org.junit.* @@ -122,11 +123,11 @@ class CompletableTest : TestBase() { fun testUnhandledException() = runTest() { expect(1) var disposable: Disposable? = null - val eh = CoroutineExceptionHandler { _, t -> - assertTrue(t is TestException) + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is TestException) expect(5) } - val completable = rxCompletable(currentDispatcher() + eh) { + val completable = rxCompletable(currentDispatcher()) { expect(4) disposable!!.dispose() // cancel our own subscription, so that delay will get cancelled try { @@ -135,26 +136,40 @@ class CompletableTest : TestBase() { throw TestException() // would not be able to handle it since mono is disposed } } - 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) + 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 { - rxCompletable(Dispatchers.Unconfined + CoroutineExceptionHandler{ _, e -> assertTrue(e is LinkageError); expect(2)}) { - expect(1) - 42 - }.subscribe({ throw LinkageError() }) - finish(3) + 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 diff --git a/reactive/kotlinx-coroutines-rx2/test/FlowableExceptionHandlingTest.kt b/reactive/kotlinx-coroutines-rx2/test/FlowableExceptionHandlingTest.kt index 4f3e7241c6..05b7ee92b6 100644 --- a/reactive/kotlinx-coroutines-rx2/test/FlowableExceptionHandlingTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/FlowableExceptionHandlingTest.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.rx2 +import io.reactivex.exceptions.* import kotlinx.coroutines.* import org.junit.* import org.junit.Test @@ -16,15 +17,15 @@ class FlowableExceptionHandlingTest : TestBase() { ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") } - private inline fun ceh(expect: Int) = CoroutineExceptionHandler { _, t -> - assertTrue(t is T) + 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() = runTest { + fun testException() = withExceptionHandler({ expectUnreached() }) { rxFlowable(Dispatchers.Unconfined + cehUnreached()) { expect(1) throw TestException() @@ -37,8 +38,8 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testFatalException() = runTest { - rxFlowable(Dispatchers.Unconfined + ceh(3)) { + fun testFatalException() = withExceptionHandler(handler(3)) { + rxFlowable(Dispatchers.Unconfined) { expect(1) throw LinkageError() }.subscribe({ @@ -50,7 +51,7 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testExceptionAsynchronous() = runTest { + fun testExceptionAsynchronous() = withExceptionHandler({ expectUnreached() }) { rxFlowable(Dispatchers.Unconfined + cehUnreached()) { expect(1) throw TestException() @@ -65,8 +66,8 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testFatalExceptionAsynchronous() = runTest { - rxFlowable(Dispatchers.Unconfined + ceh(3)) { + fun testFatalExceptionAsynchronous() = withExceptionHandler(handler(3)) { + rxFlowable(Dispatchers.Unconfined) { expect(1) throw LinkageError() }.publish() @@ -80,8 +81,8 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testFatalExceptionFromSubscribe() = runTest { - rxFlowable(Dispatchers.Unconfined + ceh(4)) { + fun testFatalExceptionFromSubscribe() = withExceptionHandler(handler(4)) { + rxFlowable(Dispatchers.Unconfined) { expect(1) send(Unit) }.subscribe({ @@ -92,7 +93,7 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testExceptionFromSubscribe() = runTest { + fun testExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { rxFlowable(Dispatchers.Unconfined + cehUnreached()) { expect(1) send(Unit) @@ -104,7 +105,7 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testAsynchronousExceptionFromSubscribe() = runTest { + fun testAsynchronousExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { rxFlowable(Dispatchers.Unconfined + cehUnreached()) { expect(1) send(Unit) @@ -118,8 +119,8 @@ class FlowableExceptionHandlingTest : TestBase() { } @Test - fun testAsynchronousFatalExceptionFromSubscribe() = runTest { - rxFlowable(Dispatchers.Unconfined + ceh(3)) { + fun testAsynchronousFatalExceptionFromSubscribe() = withExceptionHandler(handler(3)) { + rxFlowable(Dispatchers.Unconfined) { expect(1) send(Unit) }.publish() diff --git a/reactive/kotlinx-coroutines-rx2/test/LeakedExceptionTest.kt b/reactive/kotlinx-coroutines-rx2/test/LeakedExceptionTest.kt new file mode 100644 index 0000000000..1430dbf381 --- /dev/null +++ b/reactive/kotlinx-coroutines-rx2/test/LeakedExceptionTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016-2019 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 io.reactivex.exceptions.* +import io.reactivex.plugins.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.reactive.* +import org.junit.Test +import java.io.* +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) { + val flow = rxSingle { throw TestException() }.toFlowable().asFlow() + runBlocking { + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect { } + } + } + } + + @Test + fun testObservable() = withExceptionHandler(handler) { + val flow = rxObservable { throw TestException() }.toFlowable(BackpressureStrategy.BUFFER).asFlow() + runBlocking { + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect { } + } + } + } + + @Test + fun testFlowable() = withExceptionHandler(handler) { + val flow = rxFlowable { throw TestException() }.asFlow() + runBlocking { + repeat(10000) { + combine(flow, flow) { _, _ -> Unit } + .catch {} + .collect { } + } + } + } +} diff --git a/reactive/kotlinx-coroutines-rx2/test/MaybeTest.kt b/reactive/kotlinx-coroutines-rx2/test/MaybeTest.kt index 326c83e45c..deca961e6d 100644 --- a/reactive/kotlinx-coroutines-rx2/test/MaybeTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/MaybeTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import io.reactivex.disposables.* +import io.reactivex.exceptions.* import io.reactivex.functions.* import io.reactivex.internal.functions.Functions.* import kotlinx.coroutines.* @@ -66,8 +67,8 @@ class MaybeTest : TestBase() { expectUnreached() }, { error -> expect(5) - Assert.assertThat(error, IsInstanceOf(RuntimeException::class.java)) - Assert.assertThat(error.message, IsEqual("OK")) + assertThat(error, IsInstanceOf(RuntimeException::class.java)) + assertThat(error.message, IsEqual("OK")) }) expect(3) yield() // to started coroutine @@ -251,11 +252,11 @@ class MaybeTest : TestBase() { fun testUnhandledException() = runTest { expect(1) var disposable: Disposable? = null - val eh = CoroutineExceptionHandler { _, t -> - assertTrue(t is TestException) + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is TestException) expect(5) } - val maybe = rxMaybe(currentDispatcher() + eh) { + val maybe = rxMaybe(currentDispatcher()) { expect(4) disposable!!.dispose() // cancel our own subscription, so that delay will get cancelled try { @@ -264,27 +265,45 @@ class MaybeTest : TestBase() { throw TestException() // would not be able to handle it since mono is disposed } } - 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) + 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 { - rxMaybe(Dispatchers.Unconfined + CoroutineExceptionHandler{ _, e -> assertTrue(e is LinkageError); expect(2)}) { - expect(1) - 42 - }.subscribe({ throw LinkageError() }) - finish(3) + 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 diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableExceptionHandlingTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableExceptionHandlingTest.kt index 6d247cfab7..d6cdd3ca24 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ObservableExceptionHandlingTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableExceptionHandlingTest.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.rx2 +import io.reactivex.exceptions.* import kotlinx.coroutines.* import org.junit.* import org.junit.Test @@ -16,15 +17,15 @@ class ObservableExceptionHandlingTest : TestBase() { ignoreLostThreads("RxComputationThreadPool-", "RxCachedWorkerPoolEvictor-", "RxSchedulerPurge-") } - private inline fun ceh(expect: Int) = CoroutineExceptionHandler { _, t -> - assertTrue(t is T) + 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() = runTest { + fun testException() = withExceptionHandler({ expectUnreached() }) { rxObservable(Dispatchers.Unconfined + cehUnreached()) { expect(1) throw TestException() @@ -37,8 +38,8 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testFatalException() = runTest { - rxObservable(Dispatchers.Unconfined + ceh(3)) { + fun testFatalException() = withExceptionHandler(handler(3)) { + rxObservable(Dispatchers.Unconfined) { expect(1) throw LinkageError() }.subscribe({ @@ -50,7 +51,7 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testExceptionAsynchronous() = runTest { + fun testExceptionAsynchronous() = withExceptionHandler({ expectUnreached() }) { rxObservable(Dispatchers.Unconfined) { expect(1) throw TestException() @@ -65,8 +66,8 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testFatalExceptionAsynchronous() = runTest { - rxObservable(Dispatchers.Unconfined + ceh(3)) { + fun testFatalExceptionAsynchronous() = withExceptionHandler(handler(3)) { + rxObservable(Dispatchers.Unconfined) { expect(1) throw LinkageError() }.publish() @@ -80,8 +81,8 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testFatalExceptionFromSubscribe() = runTest { - rxObservable(Dispatchers.Unconfined + ceh(4)) { + fun testFatalExceptionFromSubscribe() = withExceptionHandler(handler(4)) { + rxObservable(Dispatchers.Unconfined) { expect(1) send(Unit) }.subscribe({ @@ -92,7 +93,7 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testExceptionFromSubscribe() = runTest { + fun testExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { rxObservable(Dispatchers.Unconfined) { expect(1) send(Unit) @@ -104,7 +105,7 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testAsynchronousExceptionFromSubscribe() = runTest { + fun testAsynchronousExceptionFromSubscribe() = withExceptionHandler({ expectUnreached() }) { rxObservable(Dispatchers.Unconfined) { expect(1) send(Unit) @@ -118,8 +119,8 @@ class ObservableExceptionHandlingTest : TestBase() { } @Test - fun testAsynchronousFatalExceptionFromSubscribe() = runTest { - rxObservable(Dispatchers.Unconfined + ceh(4)) { + fun testAsynchronousFatalExceptionFromSubscribe() = withExceptionHandler(handler(4)) { + rxObservable(Dispatchers.Unconfined) { expect(1) send(Unit) }.publish() diff --git a/reactive/kotlinx-coroutines-rx2/test/ObservableTest.kt b/reactive/kotlinx-coroutines-rx2/test/ObservableTest.kt index c71ef566ba..b9f6fe35a6 100644 --- a/reactive/kotlinx-coroutines-rx2/test/ObservableTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/ObservableTest.kt @@ -4,13 +4,22 @@ package kotlinx.coroutines.rx2 +import io.reactivex.* +import io.reactivex.plugins.* import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException import org.hamcrest.core.* 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) @@ -129,4 +138,28 @@ class ObservableTest : TestBase() { 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() } } \ No newline at end of file diff --git a/reactive/kotlinx-coroutines-rx2/test/SingleTest.kt b/reactive/kotlinx-coroutines-rx2/test/SingleTest.kt index 9251149375..d9581f86f9 100644 --- a/reactive/kotlinx-coroutines-rx2/test/SingleTest.kt +++ b/reactive/kotlinx-coroutines-rx2/test/SingleTest.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.rx2 import io.reactivex.* import io.reactivex.disposables.* +import io.reactivex.exceptions.* import io.reactivex.functions.* import kotlinx.coroutines.* import org.hamcrest.core.* @@ -201,13 +202,19 @@ class SingleTest : TestBase() { @Test fun testFatalExceptionInSubscribe() = runTest { - rxSingle(Dispatchers.Unconfined + CoroutineExceptionHandler { _, e -> assertTrue(e is LinkageError); expect(2) }) { - expect(1) - 42 - }.subscribe(Consumer { - throw LinkageError() - }) - finish(3) + 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 @@ -223,11 +230,11 @@ class SingleTest : TestBase() { fun testUnhandledException() = runTest { expect(1) var disposable: Disposable? = null - val eh = CoroutineExceptionHandler { _, t -> - assertTrue(t is TestException) + val handler = { e: Throwable -> + assertTrue(e is UndeliverableException && e.cause is TestException) expect(5) } - val single = rxSingle(currentDispatcher() + eh) { + val single = rxSingle(currentDispatcher()) { expect(4) disposable!!.dispose() // cancel our own subscription, so that delay will get cancelled try { @@ -236,16 +243,24 @@ class SingleTest : TestBase() { throw TestException() // would not be able to handle it since mono is disposed } } - 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) + 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) + } } } From c02648baee0f490a150bdb645637e0ee940ef7bf Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 5 Dec 2019 18:52:43 +0300 Subject: [PATCH 75/90] ContextScope.toString for better debuggability --- kotlinx-coroutines-core/common/src/internal/Scopes.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index c6cb18782c..bbcb7238dc 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -35,4 +35,6 @@ internal open class ScopeCoroutine( internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context + // CoroutineScope is used intentionally for user-friendly representation + override fun toString(): String = "CoroutineScope(coroutineContext = $coroutineContext)" } From 6cb317b500d176aa0d13b3a2f2ca9ead336f6b0c Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 10 Dec 2019 17:14:46 +0300 Subject: [PATCH 76/90] Better diagnostic exception message in MissingMainCoroutineDispatcher --- kotlinx-coroutines-core/common/src/internal/Scopes.kt | 2 +- kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index bbcb7238dc..6adab128e3 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -36,5 +36,5 @@ internal open class ScopeCoroutine( internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context // CoroutineScope is used intentionally for user-friendly representation - override fun toString(): String = "CoroutineScope(coroutineContext = $coroutineContext)" + override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)" } diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 63e38cb084..0dce51c145 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -89,7 +89,8 @@ private class MissingMainCoroutineDispatcher( if (cause == null) { throw IllegalStateException( "Module with the Main dispatcher is missing. " + - "Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'" + "Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' " + + "and ensure it has the same version as 'kotlinx-coroutines-core'" ) } else { val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "") From 1b378baccaade2eef896837b6f057954e2ff988e Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 11 Dec 2019 14:54:31 +0300 Subject: [PATCH 77/90] Introduce merge operator Fixes #1491 --- .../kotlinx-coroutines-core.txt | 2 + .../common/src/flow/internal/Merge.kt | 20 ++++++ .../common/src/flow/operators/Merge.kt | 36 ++++++++++ .../common/test/flow/operators/MergeTest.kt | 68 +++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt index 5b7955c499..d8d4528eb4 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -926,7 +926,9 @@ public final class kotlinx/coroutines/flow/FlowKt { public static final fun map (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun mapLatest (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; public static final fun mapNotNull (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; + public static final fun merge (Ljava/lang/Iterable;)Lkotlinx/coroutines/flow/Flow; 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; diff --git a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt index 289a4ebcab..6fbbea31dd 100644 --- a/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/internal/Merge.kt @@ -75,3 +75,23 @@ internal class ChannelFlowMerge( 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) + + override fun produceImpl(scope: CoroutineScope): ReceiveChannel { + return scope.flowProduce(context, capacity, block = collectToFun) + } + + override suspend fun collectTo(scope: ProducerScope) { + val collector = SendingCollector(scope) + flows.forEach { flow -> + scope.launch { flow.collect(collector) } + } + } +} diff --git a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt index a7b7f709a5..d69afad2f3 100644 --- a/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt +++ b/kotlinx-coroutines-core/common/src/flow/operators/Merge.kt @@ -81,6 +81,42 @@ public fun Flow>.flattenConcat(): Flow = flow { collect { value -> emitAll(value) } } +/** + * Merges the given flows into a single flow without preserving an order of elements. + * All flows are merged concurrently, without limit on the number of simultaneously collected flows. + * + * ### Operator fusion + * + * Applications of [flowOn], [buffer], [produceIn], and [broadcastIn] _after_ this operator are fused with + * its concurrent merging so that only one properly configured channel is used for execution of merging logic. + */ +@ExperimentalCoroutinesApi +public fun Iterable>.merge(): Flow { + /* + * This is a fuseable implementation of the following operator: + * channelFlow { + * forEach { flow -> + * launch { + * flow.collect { send(it) } + * } + * } + * } + */ + return ChannelLimitedFlowMerge(this) +} + +/** + * Merges the given flows into a single flow without preserving an order of elements. + * All flows are merged concurrently, without limit on the number of simultaneously collected flows. + * + * ### Operator fusion + * + * Applications of [flowOn], [buffer], [produceIn], and [broadcastIn] _after_ this operator are fused with + * its concurrent merging so that only one properly configured channel is used for execution of merging logic. + */ +@ExperimentalCoroutinesApi +public fun merge(vararg flows: Flow): Flow = flows.asIterable().merge() + /** * Flattens the given flow of flows into a single flow with a [concurrency] limit on the number of * concurrently collected flows. diff --git a/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt new file mode 100644 index 0000000000..1248188554 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/MergeTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2019 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 kotlinx.coroutines.flow.merge as originalMerge + +abstract class MergeTest : TestBase() { + + abstract fun Iterable>.merge(): Flow + + @Test + fun testMerge() = runTest { + val n = 100 + val sum = (1..n).map { flowOf(it) } + .merge() + .sum() + + assertEquals(n * (n + 1) / 2, sum) + } + + @Test + fun testSingle() = runTest { + val flow = listOf(flowOf(), flowOf(42), flowOf()).merge() + val value = flow.single() + assertEquals(42, value) + } + + @Test + fun testNulls() = runTest { + val list = listOf(flowOf(1), flowOf(null), flowOf(2)).merge().toList() + assertEquals(listOf(1, null, 2), list) + } + + @Test + fun testContext() = runTest { + val flow = flow { + emit(NamedDispatchers.name()) + }.flowOn(NamedDispatchers("source")) + + val result = listOf(flow).merge().flowOn(NamedDispatchers("irrelevant")).toList() + assertEquals(listOf("source"), result) + } + + @Test + fun testIsolatedContext() = runTest { + val flow = flow { + emit(NamedDispatchers.name()) + } + + val result = listOf(flow.flowOn(NamedDispatchers("1")), flow.flowOn(NamedDispatchers("2"))) + .merge() + .flowOn(NamedDispatchers("irrelevant")) + .toList() + assertEquals(listOf("1", "2"), result) + } +} + +class IterableMergeTest : MergeTest() { + override fun Iterable>.merge(): Flow = originalMerge() +} + +class VarargMergeTest : MergeTest() { + override fun Iterable>.merge(): Flow = originalMerge(*toList().toTypedArray()) +} From af9a2010611248a16f6c271274028770d5a4f067 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 11 Dec 2019 19:04:47 +0300 Subject: [PATCH 78/90] Introduce DebugProbes.isInstalled method --- .../reference-public-api/kotlinx-coroutines-debug.txt | 1 + kotlinx-coroutines-debug/src/DebugProbes.kt | 6 ++++++ kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt index 79f5b75d15..3fba3e2f3a 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -15,6 +15,7 @@ public final class kotlinx/coroutines/debug/DebugProbes { public final fun dumpCoroutinesInfo ()Ljava/util/List; public final fun getSanitizeStackTraces ()Z public final fun install ()V + public final fun isInstalled ()Z public final fun jobToString (Lkotlinx/coroutines/Job;)Ljava/lang/String; public final fun printJob (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V public static synthetic fun printJob$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt index a81fd7a880..7508954298 100644 --- a/kotlinx-coroutines-debug/src/DebugProbes.kt +++ b/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -19,6 +19,7 @@ import kotlin.coroutines.* * Debug probes is a dynamic attach mechanism which installs multiple hooks into coroutines machinery. * It slows down all coroutine-related code, but in return provides a lot of diagnostic information, including * asynchronous stack-traces and coroutine dumps (similar to [ThreadMXBean.dumpAllThreads] and `jstack` via [DebugProbes.dumpCoroutines]. + * All introspecting methods throw [IllegalStateException] if debug probes were not installed. * * Installed hooks: * @@ -41,6 +42,11 @@ public object DebugProbes { */ public var sanitizeStackTraces: Boolean = true + /** + * Determines whether debug probes were [installed][DebugProbes.install]. + */ + public val isInstalled: Boolean get() = DebugProbesImpl.isInstalled + /** * Installs a [DebugProbes] instead of no-op stdlib probes by redefining * debug probes class using the same class loader as one loaded [DebugProbes] class. diff --git a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt index b07b715304..72416ab1f2 100644 --- a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt @@ -29,7 +29,7 @@ internal object DebugProbesImpl { private val capturedCoroutines = HashSet>() @Volatile private var installations = 0 - private val isInstalled: Boolean get() = installations > 0 + internal val isInstalled: Boolean get() = installations > 0 // To sort coroutines by creation order, used as unique id private var sequenceNumber: Long = 0 From 04e587cf1852a021f2799dd6f5ca022d3c017fd5 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 12 Dec 2019 11:04:51 +0300 Subject: [PATCH 79/90] Make sure that exception recovery does not break exception message Fixes #1631 --- .../jvm/src/internal/StackTraceRecovery.kt | 5 +++-- .../jvm/test/exceptions/StackTraceRecoveryTest.kt | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index 727d934136..b512815556 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -66,14 +66,15 @@ private fun recoverFromStackFrame(exception: E, continuation: Co // Try to create an exception of the same type and get stacktrace from continuation val newException = tryCopyException(cause) ?: return exception + // Verify that the new exception has the same message as the original one (bail out if not, see #1631) + if (newException.message != cause.message) return exception + // Update stacktrace val stacktrace = createStackTrace(continuation) if (stacktrace.isEmpty()) return exception - // Merge if necessary if (cause !== exception) { mergeRecoveredTraces(recoveredStacktrace, stacktrace) } - // Take recovered stacktrace, merge it with existing one if necessary and return return createFinalException(cause, newException, stacktrace) } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt index de93708453..dbbd77c4b7 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.intrinsics.* import org.junit.Test +import java.lang.RuntimeException import java.util.concurrent.* import kotlin.concurrent.* import kotlin.coroutines.* @@ -264,4 +265,18 @@ class StackTraceRecoveryTest : TestBase() { } yield() // nop to make sure it is not a tail call } + + @Test + fun testWrongMessageException() = runTest { + val result = runCatching { + coroutineScope { + throw WrongMessageException("OK") + } + } + val ex = result.exceptionOrNull() ?: error("Expected to fail") + assertTrue(ex is WrongMessageException) + assertEquals("Token OK", ex.message) + } + + public class WrongMessageException(token: String) : RuntimeException("Token $token") } From e60ec8e1fa2a55006f67b0e113b17388595bce97 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 12 Dec 2019 12:10:21 +0300 Subject: [PATCH 80/90] User Class.forName instead of ServiceLoader to instantiate Dispatchers.Main on Android (#1572) Fixes #1557 Fixes #878 Fixes #1606 --- .../jvm/src/CoroutineExceptionHandlerImpl.kt | 1 - .../jvm/src/internal/FastServiceLoader.kt | 60 ++++++++++++++++++- .../jvm/src/internal/MainDispatchers.kt | 10 ++-- .../r8-from-1.6.0/coroutines.pro | 4 ++ .../r8-upto-1.6.0/coroutines.pro | 4 ++ .../test/R8ServiceLoaderOptimizationTest.kt | 1 + 6 files changed, 72 insertions(+), 8 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt index 90c6a84980..7eb7576fdd 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt @@ -22,7 +22,6 @@ private val handlers: List = ServiceLoader.load( CoroutineExceptionHandler::class.java.classLoader ).iterator().asSequence().toList() - internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // use additional extension handlers for (handler in handlers) { diff --git a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt index f512bb31bc..6267581f3c 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt @@ -5,6 +5,12 @@ import java.net.* import java.util.* import java.util.jar.* import java.util.zip.* +import kotlin.collections.ArrayList + +/** + * Don't use JvmField here to enable R8 optimizations via "assumenosideeffects" + */ +internal val ANDROID_DETECTED = runCatching { Class.forName("android.os.Build") }.isSuccess /** * A simplified version of [ServiceLoader]. @@ -20,7 +26,59 @@ import java.util.zip.* internal object FastServiceLoader { private const val PREFIX: String = "META-INF/services/" - internal fun load(service: Class, loader: ClassLoader): List { + /** + * This method attempts to load [MainDispatcherFactory] in Android-friendly way. + * + * If we are not on Android, this method fallbacks to a regular service loading, + * else we attempt to do `Class.forName` lookup for + * `AndroidDispatcherFactory` and `TestMainDispatcherFactory`. + * If lookups are successful, we return resultinAg instances because we know that + * `MainDispatcherFactory` API is internal and this is the only possible classes of `MainDispatcherFactory` Service on Android. + * + * Such intricate dance is required to avoid calls to `ServiceLoader.load` for multiple reasons: + * 1) It eliminates disk lookup on potentially slow devices on the Main thread. + * 2) Various Android toolchain versions by various vendors don't tend to handle ServiceLoader calls properly. + * Sometimes META-INF is removed from the resulting APK, sometimes class names are mangled, etc. + * While it is not the problem of `kotlinx.coroutines`, it significantly worsens user experience, thus we are workarounding it. + * Examples of such issues are #932, #1072, #1557, #1567 + * + * We also use SL for [CoroutineExceptionHandler], but we do not experience the same problems and CEH is a public API + * that may already be injected vis SL, so we are not using the same technique for it. + */ + internal fun loadMainDispatcherFactory(): List { + val clz = MainDispatcherFactory::class.java + if (!ANDROID_DETECTED) { + return load(clz, clz.classLoader) + } + + return try { + val result = ArrayList(2) + createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) } + createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) } + result + } catch (e: Throwable) { + // Fallback to the regular SL in case of any unexpected exception + load(clz, clz.classLoader) + } + } + + /* + * This method is inline to have a direct Class.forName("string literal") in the byte code to avoid weird interactions with ProGuard/R8. + */ + @Suppress("NOTHING_TO_INLINE") + private inline fun createInstanceOf( + baseClass: Class, + serviceClass: String + ): MainDispatcherFactory? { + return try { + val clz = Class.forName(serviceClass, true, baseClass.classLoader) + baseClass.cast(clz.getDeclaredConstructor().newInstance()) + } catch (e: ClassNotFoundException) { // Do not fail if TestMainDispatcherFactory is not found + null + } + } + + private fun load(service: Class, loader: ClassLoader): List { return try { loadProviders(service, loader) } catch (e: Throwable) { diff --git a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt index 0dce51c145..6f11cdf795 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt @@ -20,13 +20,11 @@ internal object MainDispatcherLoader { private fun loadMainDispatcher(): MainCoroutineDispatcher { return try { val factories = if (FAST_SERVICE_LOADER_ENABLED) { - MainDispatcherFactory::class.java.let { clz -> - FastServiceLoader.load(clz, clz.classLoader) - } + FastServiceLoader.loadMainDispatcherFactory() } else { - //We are explicitly using the - //`ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()` - //form of the ServiceLoader call to enable R8 optimization when compiled on Android. + // We are explicitly using the + // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()` + // form of the ServiceLoader call to enable R8 optimization when compiled on Android. ServiceLoader.load( MainDispatcherFactory::class.java, MainDispatcherFactory::class.java.classLoader 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 3c0b7e6a3b..b57b07713d 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 @@ -3,4 +3,8 @@ # this results in direct instantiation when loading Dispatchers.Main -assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader { boolean FAST_SERVICE_LOADER_ENABLED return false; +} + +-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader { + boolean ANDROID_DETECTED return true; } \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro index de1b70fc87..549d0e85a1 100644 --- a/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro +++ b/ui/kotlinx-coroutines-android/resources/META-INF/com.android.tools/r8-upto-1.6.0/coroutines.pro @@ -3,3 +3,7 @@ # - META-INF/proguard/coroutines.pro -keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;} + +-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoader { + boolean ANDROID_DETECTED return true; +} \ No newline at end of file diff --git a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt index 76d67c76b3..7f03378d00 100644 --- a/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt +++ b/ui/kotlinx-coroutines-android/test/R8ServiceLoaderOptimizationTest.kt @@ -37,6 +37,7 @@ class R8ServiceLoaderOptimizationTest : TestBase() { } @Test + @Ignore fun noOptimRulesMatch() { val paths = listOf( "META-INF/com.android.tools/proguard/coroutines.pro", From 5202a8bf35f30c392d93af19e51321219b876ba0 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 19 Sep 2019 18:15:43 +0300 Subject: [PATCH 81/90] Regroup benchmarks and adapt them to structured concurrency, cleanup CoroutineScheduler --- .../ChannelProducerConsumerBenchmark.kt | 4 + .../benchmarks/ParametrizedDispatcherBase.kt | 8 +- .../{actors => akka}/PingPongAkkaBenchmark.kt | 21 +++-- .../StatefulActorAkkaBenchmark.kt | 28 +++---- .../DispatchersContextSwitchBenchmark.kt | 76 +++++++++++++++++++ .../{ => scheduler}/ForkJoinBenchmark.kt | 21 +++-- .../{ => scheduler}/LaunchBenchmark.kt | 7 +- .../StatefulAwaitsBenchmark.kt | 7 +- .../ConcurrentStatefulActorBenchmark.kt | 17 +++-- .../actors/CycledActorsBenchmark.kt | 7 +- .../actors/PingPongActorBenchmark.kt | 6 +- .../actors/PingPongWithBlockingContext.kt | 3 +- .../actors/StatefulActorBenchmark.kt | 7 +- .../jvm/src/scheduling/CoroutineScheduler.kt | 19 +++-- .../jvm/src/scheduling/WorkQueue.kt | 76 ++++++++++--------- .../jvm/test/scheduling/WorkQueueTest.kt | 49 ------------ 16 files changed, 204 insertions(+), 152 deletions(-) rename benchmarks/src/jmh/kotlin/benchmarks/{actors => akka}/PingPongAkkaBenchmark.kt (89%) rename benchmarks/src/jmh/kotlin/benchmarks/{actors => akka}/StatefulActorAkkaBenchmark.kt (92%) create mode 100644 benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/ForkJoinBenchmark.kt (90%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/LaunchBenchmark.kt (90%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/StatefulAwaitsBenchmark.kt (97%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/actors/ConcurrentStatefulActorBenchmark.kt (94%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/actors/CycledActorsBenchmark.kt (96%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/actors/PingPongActorBenchmark.kt (97%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/actors/PingPongWithBlockingContext.kt (97%) rename benchmarks/src/jmh/kotlin/benchmarks/{ => scheduler}/actors/StatefulActorBenchmark.kt (96%) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt index 77b907f6e9..941e3d84ba 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/ChannelProducerConsumerBenchmark.kt @@ -1,3 +1,7 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package benchmarks import kotlinx.coroutines.* diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt index f8e88bf63b..fab052370e 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/ParametrizedDispatcherBase.kt @@ -4,7 +4,7 @@ package benchmarks -import benchmarks.actors.CORES_COUNT +import benchmarks.akka.CORES_COUNT import kotlinx.coroutines.* import kotlinx.coroutines.scheduling.* import org.openjdk.jmh.annotations.Param @@ -22,14 +22,14 @@ abstract class ParametrizedDispatcherBase : CoroutineScope { abstract var dispatcher: String override lateinit var coroutineContext: CoroutineContext - var closeable: Closeable? = null + private var closeable: Closeable? = null - @UseExperimental(InternalCoroutinesApi::class) @Setup + @UseExperimental(InternalCoroutinesApi::class) open fun setup() { coroutineContext = when { dispatcher == "fjp" -> ForkJoinPool.commonPool().asCoroutineDispatcher() - dispatcher == "experimental" -> { + dispatcher == "scheduler" -> { ExperimentalCoroutineDispatcher(CORES_COUNT).also { closeable = it } } dispatcher.startsWith("ftp") -> { diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongAkkaBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/akka/PingPongAkkaBenchmark.kt similarity index 89% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongAkkaBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/akka/PingPongAkkaBenchmark.kt index 6b71e35f8a..1a6e9d4036 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongAkkaBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/akka/PingPongAkkaBenchmark.kt @@ -1,8 +1,8 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.akka import akka.actor.ActorRef import akka.actor.ActorSystem @@ -13,7 +13,6 @@ import org.openjdk.jmh.annotations.* import scala.concurrent.Await import scala.concurrent.duration.Duration import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit const val N_MESSAGES = 100_000 @@ -29,12 +28,12 @@ class Stop * PingPongAkkaBenchmark.singlePingPong default-dispatcher avgt 10 173.742 ± 41.984 ms/op * PingPongAkkaBenchmark.singlePingPong single-thread-dispatcher avgt 10 24.181 ± 0.730 ms/op */ -@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 2) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@State(Scope.Benchmark) +//@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +//@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +//@Fork(value = 2) +//@BenchmarkMode(Mode.AverageTime) +//@OutputTimeUnit(TimeUnit.MILLISECONDS) +//@State(Scope.Benchmark) open class PingPongAkkaBenchmark { lateinit var system: ActorSystem @@ -62,12 +61,12 @@ open class PingPongAkkaBenchmark { Await.ready(system.terminate(), Duration.Inf()) } - @Benchmark +// @Benchmark fun singlePingPong() { runPingPongs(1) } - @Benchmark +// @Benchmark fun coresCountPingPongs() { runPingPongs(Runtime.getRuntime().availableProcessors()) } diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/StatefulActorAkkaBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/akka/StatefulActorAkkaBenchmark.kt similarity index 92% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/StatefulActorAkkaBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/akka/StatefulActorAkkaBenchmark.kt index c19c91fa81..4e3ad6ce4d 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/StatefulActorAkkaBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/akka/StatefulActorAkkaBenchmark.kt @@ -1,8 +1,8 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.akka import akka.actor.ActorRef import akka.actor.ActorSystem @@ -14,7 +14,6 @@ import scala.concurrent.Await import scala.concurrent.duration.Duration import java.util.concurrent.CountDownLatch import java.util.concurrent.ThreadLocalRandom -import java.util.concurrent.TimeUnit const val ROUNDS = 10_000 const val STATE_SIZE = 1024 @@ -38,12 +37,12 @@ val CORES_COUNT = Runtime.getRuntime().availableProcessors() * StatefulActorAkkaBenchmark.singleComputationSingleRequestor default-dispatcher avgt 14 39.964 ± 2.343 ms/op * StatefulActorAkkaBenchmark.singleComputationSingleRequestor single-thread-dispatcher avgt 14 10.214 ± 2.152 ms/op */ -@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 2) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@State(Scope.Benchmark) +//@Warmup(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +//@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) +//@Fork(value = 2) +//@BenchmarkMode(Mode.AverageTime) +//@OutputTimeUnit(TimeUnit.MILLISECONDS) +//@State(Scope.Benchmark) open class StatefulActorAkkaBenchmark { lateinit var system: ActorSystem @@ -72,22 +71,22 @@ open class StatefulActorAkkaBenchmark { Await.ready(system.terminate(), Duration.Inf()) } - @Benchmark +// @Benchmark fun singleComputationSingleRequestor() { run(1, 1) } - @Benchmark +// @Benchmark fun singleComputationMultipleRequestors() { run(1, CORES_COUNT) } - @Benchmark +// @Benchmark fun multipleComputationsSingleRequestor() { run(CORES_COUNT, 1) } - @Benchmark +// @Benchmark fun multipleComputationsMultipleRequestors() { run(CORES_COUNT, CORES_COUNT) } @@ -120,7 +119,8 @@ open class StatefulActorAkkaBenchmark { private fun createComputationActors(initLatch: CountDownLatch, count: Int): List { return (0 until count).map { - system.actorOf(Props.create(ComputationActor::class.java, + system.actorOf(Props.create( + ComputationActor::class.java, LongArray(STATE_SIZE) { ThreadLocalRandom.current().nextLong(0, 100) }, initLatch) .withDispatcher("akka.actor.$dispatcher")) } diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt new file mode 100644 index 0000000000..6b61c99645 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package benchmarks.scheduler + +import benchmarks.akka.* +import kotlinx.coroutines.* +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.annotations.State +import java.lang.Thread.* +import java.util.concurrent.* +import kotlin.concurrent.* +import kotlin.coroutines.* + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Thread) +open class DispatchersContextSwitchBenchmark { + private val nCoroutines = 10000 + private val delayTimeMs = 1L + private val nRepeatDelay = 10 + + private val fjp = ForkJoinPool.commonPool().asCoroutineDispatcher() + private val ftp = Executors.newFixedThreadPool(CORES_COUNT - 1).asCoroutineDispatcher() + + @TearDown + fun teardown() { + ftp.close() + (ftp.executor as ExecutorService).awaitTermination(1, TimeUnit.SECONDS) + } + + @Benchmark + fun coroutinesIoDispatcher() = runBenchmark(Dispatchers.IO) + + @Benchmark + fun coroutinesDefaultDispatcher() = runBenchmark(Dispatchers.Default) + + @Benchmark + fun coroutinesFjpDispatcher() = runBenchmark(fjp) + + @Benchmark + fun coroutinesFtpDispatcher() = runBenchmark(ftp) + + @Benchmark + fun coroutinesBlockingDispatcher() = runBenchmark(EmptyCoroutineContext) + + @Benchmark + fun threads() { + val threads = List(nCoroutines) { + thread(start = true) { + repeat(nRepeatDelay) { + sleep(delayTimeMs) + } + } + } + threads.forEach { it.join() } + } + + private fun runBenchmark(dispatcher: CoroutineContext) = runBlocking { + repeat(nCoroutines) { + launch(dispatcher) { + repeat(nRepeatDelay) { + delayOrYield() + } + } + } + } + + private suspend fun delayOrYield() { + delay(delayTimeMs) + } +} \ No newline at end of file diff --git a/benchmarks/src/jmh/kotlin/benchmarks/ForkJoinBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/ForkJoinBenchmark.kt similarity index 90% rename from benchmarks/src/jmh/kotlin/benchmarks/ForkJoinBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/ForkJoinBenchmark.kt index 21d0f54bf0..0c731c3ba8 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/ForkJoinBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/ForkJoinBenchmark.kt @@ -1,9 +1,10 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks +package benchmarks.scheduler +import benchmarks.* import kotlinx.coroutines.* import org.openjdk.jmh.annotations.* import java.util.concurrent.* @@ -44,7 +45,7 @@ open class ForkJoinBenchmark : ParametrizedDispatcherBase() { } lateinit var coefficients: LongArray - override var dispatcher: String = "experimental" + override var dispatcher: String = "scheduler" @Setup override fun setup() { @@ -129,8 +130,18 @@ open class ForkJoinBenchmark : ParametrizedDispatcherBase() { } else { pendingCount = 2 // One may fork only once here and executing second task here with looping over firstComplete to be even more efficient - first = RecursiveAction(coefficients, start, start + (end - start) / 2, parent = this).fork() - second = RecursiveAction(coefficients, start + (end - start) / 2, end, parent = this).fork() + first = RecursiveAction( + coefficients, + start, + start + (end - start) / 2, + parent = this + ).fork() + second = RecursiveAction( + coefficients, + start + (end - start) / 2, + end, + parent = this + ).fork() } tryComplete() diff --git a/benchmarks/src/jmh/kotlin/benchmarks/LaunchBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/LaunchBenchmark.kt similarity index 90% rename from benchmarks/src/jmh/kotlin/benchmarks/LaunchBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/LaunchBenchmark.kt index 2639dbb2eb..8435ddc262 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/LaunchBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/LaunchBenchmark.kt @@ -1,9 +1,10 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks +package benchmarks.scheduler +import benchmarks.* import kotlinx.coroutines.* import org.openjdk.jmh.annotations.* import java.util.concurrent.* @@ -21,7 +22,7 @@ import java.util.concurrent.* @State(Scope.Benchmark) open class LaunchBenchmark : ParametrizedDispatcherBase() { - @Param("experimental", "fjp") + @Param("scheduler", "fjp") override var dispatcher: String = "fjp" private val jobsToLaunch = 100 diff --git a/benchmarks/src/jmh/kotlin/benchmarks/StatefulAwaitsBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/StatefulAwaitsBenchmark.kt similarity index 97% rename from benchmarks/src/jmh/kotlin/benchmarks/StatefulAwaitsBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/StatefulAwaitsBenchmark.kt index 8fdb146f77..93667c0c2f 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/StatefulAwaitsBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/StatefulAwaitsBenchmark.kt @@ -1,9 +1,10 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks +package benchmarks.scheduler +import benchmarks.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.openjdk.jmh.annotations.* @@ -52,7 +53,7 @@ open class StatefulAsyncBenchmark : ParametrizedDispatcherBase() { @Param("1", "8", "16") var jobsCount = 1 - @Param("fjp", "ftp_1", "ftp_8") + @Param("fjp", "ftp_1", "dispatcher") override var dispatcher: String = "fjp" @Volatile diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/ConcurrentStatefulActorBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/ConcurrentStatefulActorBenchmark.kt similarity index 94% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/ConcurrentStatefulActorBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/ConcurrentStatefulActorBenchmark.kt index db3195ff55..6998577310 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/ConcurrentStatefulActorBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/ConcurrentStatefulActorBenchmark.kt @@ -2,10 +2,11 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.scheduler.actors import benchmarks.* -import benchmarks.actors.StatefulActorBenchmark.* +import benchmarks.akka.* +import benchmarks.scheduler.actors.StatefulActorBenchmark.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.openjdk.jmh.annotations.* @@ -57,10 +58,10 @@ import java.util.concurrent.* @State(Scope.Benchmark) open class ConcurrentStatefulActorBenchmark : ParametrizedDispatcherBase() { - @Param("1024", "8192", "262144") + @Param("1024", "8192") var stateSize: Int = -1 - @Param("fjp", "ftp_1", "ftp_8", "experimental") + @Param("fjp", "scheduler") override var dispatcher: String = "fjp" @Benchmark @@ -68,7 +69,7 @@ open class ConcurrentStatefulActorBenchmark : ParametrizedDispatcherBase() { val resultChannel: Channel = Channel(1) val computations = (0 until CORES_COUNT).map { computationActor(stateSize) } val requestor = requestorActorUnfair(computations, resultChannel) - requestor.send(Letter(Start(), Channel(0))) + requestor.send(Letter(Start(), requestor)) resultChannel.receive() } @@ -77,7 +78,7 @@ open class ConcurrentStatefulActorBenchmark : ParametrizedDispatcherBase() { val resultChannel: Channel = Channel(1) val computations = (0 until CORES_COUNT).map { computationActor(stateSize) } val requestor = requestorActorFair(computations, resultChannel) - requestor.send(Letter(Start(), Channel(0))) + requestor.send(Letter(Start(), requestor)) resultChannel.receive() } @@ -95,6 +96,7 @@ open class ConcurrentStatefulActorBenchmark : ParametrizedDispatcherBase() { } is Long -> { if (++received >= ROUNDS * 8) { + computations.forEach { it.close() } stopChannel.send(Unit) return@actor } else { @@ -122,6 +124,7 @@ open class ConcurrentStatefulActorBenchmark : ParametrizedDispatcherBase() { } is Long -> { if (++receivedTotal >= ROUNDS * computations.size) { + computations.forEach { it.close() } stopChannel.send(Unit) return@actor } else { @@ -136,4 +139,4 @@ open class ConcurrentStatefulActorBenchmark : ParametrizedDispatcherBase() { } } } -} \ No newline at end of file +} diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/CycledActorsBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt similarity index 96% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/CycledActorsBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt index 385693e38d..d9ef8917cb 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/CycledActorsBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt @@ -2,10 +2,11 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.scheduler.actors import benchmarks.* -import benchmarks.actors.PingPongActorBenchmark.* +import benchmarks.akka.* +import benchmarks.scheduler.actors.PingPongActorBenchmark.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.openjdk.jmh.annotations.* @@ -39,7 +40,7 @@ open class CycledActorsBenchmark : ParametrizedDispatcherBase() { val NO_CHANNEL = Channel(0) } - @Param("fjp", "ftp_1", "experimental") + @Param("fjp", "ftp_1", "scheduler") override var dispatcher: String = "fjp" @Param("524288") diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongActorBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt similarity index 97% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongActorBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt index 82e9b15222..d8de8109c4 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongActorBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt @@ -2,14 +2,14 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.scheduler.actors import benchmarks.* +import benchmarks.akka.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.openjdk.jmh.annotations.* import java.util.concurrent.* -import kotlin.coroutines.* /* * Benchmark (dispatcher) Mode Cnt Score Error Units @@ -34,7 +34,7 @@ import kotlin.coroutines.* open class PingPongActorBenchmark : ParametrizedDispatcherBase() { data class Letter(val message: Any?, val sender: SendChannel) - @Param("experimental", "fjp", "ftp_1", "ftp_8") + @Param("scheduler", "fjp", "ftp_1") override var dispatcher: String = "fjp" @Benchmark diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongWithBlockingContext.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt similarity index 97% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongWithBlockingContext.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt index c6afdced25..c531de90f6 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/PingPongWithBlockingContext.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt @@ -2,8 +2,9 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.scheduler.actors +import benchmarks.akka.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.scheduling.* diff --git a/benchmarks/src/jmh/kotlin/benchmarks/actors/StatefulActorBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt similarity index 96% rename from benchmarks/src/jmh/kotlin/benchmarks/actors/StatefulActorBenchmark.kt rename to benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt index 6968c8952d..840ae0038a 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/actors/StatefulActorBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt @@ -2,9 +2,10 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -package benchmarks.actors +package benchmarks.scheduler.actors import benchmarks.* +import benchmarks.akka.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.openjdk.jmh.annotations.* @@ -38,9 +39,9 @@ import java.util.concurrent.* @State(Scope.Benchmark) open class StatefulActorBenchmark : ParametrizedDispatcherBase() { - data class Letter(val message: Any, val sender: Channel) + data class Letter(val message: Any, val sender: SendChannel) - @Param("fjp", "ftp_1", "ftp_8", "experimental") + @Param("fjp", "ftp_1", "ftp_8", "scheduler") override var dispatcher: String = "fjp" @Benchmark diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 4089710e75..63659aae65 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -8,9 +8,10 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import java.io.* -import java.util.* import java.util.concurrent.* +import java.util.concurrent.atomic.* import java.util.concurrent.locks.* +import kotlin.random.Random /** * Coroutine scheduler (pool of shared threads) which primary target is to distribute dispatched coroutines over worker threads, @@ -223,7 +224,7 @@ internal class CoroutineScheduler( * workers are 1-indexed, code path in [Worker.trySteal] is a bit faster and index swap during termination * works properly */ - private val workers: Array = arrayOfNulls(maxPoolSize + 1) + private val workers = AtomicReferenceArray(maxPoolSize + 1) /** * Long describing state of workers in this pool. @@ -244,11 +245,9 @@ internal class CoroutineScheduler( private inline fun incrementBlockingWorkers() { controlState.addAndGet(1L shl BLOCKING_SHIFT) } private inline fun decrementBlockingWorkers() { controlState.addAndGet(-(1L shl BLOCKING_SHIFT)) } - private val random = Random() - // This is used a "stop signal" for close and shutdown functions - private val _isTerminated = atomic(0) // todo: replace with atomic boolean on new versions of atomicFu - private val isTerminated: Boolean get() = _isTerminated.value != 0 + private val _isTerminated = atomic(false) + private val isTerminated: Boolean get() = _isTerminated.value companion object { private val MAX_SPINS = systemProp("kotlinx.coroutines.scheduler.spins", 1000, minValue = 1) @@ -297,7 +296,7 @@ internal class CoroutineScheduler( // Shuts down current scheduler and waits until all work is done and all threads are stopped. fun shutdown(timeout: Long) { // atomically set termination flag which is checked when workers are added or removed - if (!_isTerminated.compareAndSet(0, 1)) return + if (!_isTerminated.compareAndSet(false, true)) return // make sure we are not waiting for the current thread val currentWorker = currentWorker() // Capture # of created workers that cannot change anymore (mind the synchronized block!) @@ -550,8 +549,8 @@ internal class CoroutineScheduler( var retired = 0 var terminated = 0 val queueSizes = arrayListOf() - for (worker in workers) { - if (worker == null) continue + for (index in 0 until workers.length()) { + val worker = workers[index] ?: continue val queueSize = worker.localQueue.size() when (worker.state) { WorkerState.PARKING -> ++parkedWorkers @@ -714,7 +713,7 @@ internal class CoroutineScheduler( // Note: it is concurrently reset by idleResetBeforeUnpark private var parkTimeNs = MIN_PARK_TIME_NS - private var rngState = random.nextInt() + private var rngState = Random.nextInt() private var lastStealIndex = 0 // try in order repeated, reset when unparked override fun run() { diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt index a9aa86d4b3..8479936829 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt @@ -60,10 +60,9 @@ internal class WorkQueue { /** * Retrieves and removes task from the head of the queue - * Invariant: this method is called only by the owner of the queue ([pollExternal] is not) + * Invariant: this method is called only by the owner of the queue ([stealBatch] is not) */ - fun poll(): Task? = - lastScheduledTask.getAndSet(null) ?: pollExternal() + fun poll(): Task? = lastScheduledTask.getAndSet(null) ?: pollBuffer() /** * Invariant: this method is called only by the owner of the queue @@ -97,31 +96,18 @@ internal class WorkQueue { * @return whether any task was stolen */ fun trySteal(victim: WorkQueue, globalQueue: GlobalQueue): Boolean { - val time = schedulerTimeSource.nanoTime() - val bufferSize = victim.bufferSize - if (bufferSize == 0) return tryStealLastScheduled(time, victim, globalQueue) - /* - * Invariant: time is monotonically increasing (thanks to nanoTime), so we can stop as soon as we find the first task not satisfying a predicate. - * If queue size is larger than QUEUE_SIZE_OFFLOAD_THRESHOLD then unconditionally steal tasks over this limit to prevent possible queue overflow - */ - var wasStolen = false - repeat(((bufferSize / 2).coerceAtLeast(1))) { - val task = victim.pollExternal { task -> - time - task.submissionTime >= WORK_STEALING_TIME_RESOLUTION_NS || victim.bufferSize > QUEUE_SIZE_OFFLOAD_THRESHOLD - } - ?: return wasStolen // non-local return from trySteal as we're done - wasStolen = true - add(task, globalQueue) + if (victim.stealBatch { task -> add(task, globalQueue) }) { + return true } - return wasStolen + return tryStealLastScheduled(victim, globalQueue) } private fun tryStealLastScheduled( - time: Long, victim: WorkQueue, globalQueue: GlobalQueue ): Boolean { val lastScheduled = victim.lastScheduledTask.value ?: return false + val time = schedulerTimeSource.nanoTime() if (time - lastScheduled.submissionTime < WORK_STEALING_TIME_RESOLUTION_NS) { return false } @@ -139,42 +125,60 @@ internal class WorkQueue { * Offloads half of the current buffer to [globalQueue] */ private fun offloadWork(globalQueue: GlobalQueue) { - repeat((bufferSize / 2).coerceAtLeast(1)) { - val task = pollExternal() ?: return - addToGlobalQueue(globalQueue, task) - } + stealBatchTo(globalQueue) } - private fun addToGlobalQueue(globalQueue: GlobalQueue, task: Task) { + private fun GlobalQueue.add(task: Task) { /* * globalQueue is closed as the very last step in the shutdown sequence when all worker threads had * been already shutdown (with the only exception of the last worker thread that might be performing * shutdown procedure itself). As a consistency check we do a [cheap!] check that it is not closed here yet. */ - check(globalQueue.addLast(task)) { "GlobalQueue could not be closed yet" } + val added = addLast(task) + assert { added } } internal fun offloadAllWork(globalQueue: GlobalQueue) { - lastScheduledTask.getAndSet(null)?.let { addToGlobalQueue(globalQueue, it) } - while (true) { - addToGlobalQueue(globalQueue, pollExternal() ?: return) + lastScheduledTask.getAndSet(null)?.let { globalQueue.add(it) } + while (stealBatchTo(globalQueue)) { + // Steal everything } } /** - * [poll] for external (not owning this queue) workers + * Method that is invoked by external workers to steal work. + * Half of the buffer (at least 1) is stolen, returns `true` if at least one task was stolen. */ - private inline fun pollExternal(predicate: (Task) -> Boolean = { true }): Task? { - while (true) { + private inline fun stealBatch(consumer: (Task) -> Unit): Boolean { + val size = bufferSize + if (size == 0) return false + var toSteal = (size / 2).coerceAtLeast(1) + var wasStolen = false + while (toSteal-- > 0) { val tailLocal = consumerIndex.value - if (tailLocal - producerIndex.value == 0) return null + if (tailLocal - producerIndex.value == 0) return false val index = tailLocal and MASK val element = buffer[index] ?: continue - if (!predicate(element)) { - return null - } if (consumerIndex.compareAndSet(tailLocal, tailLocal + 1)) { // 1) Help GC 2) Signal producer that this slot is consumed and may be used + consumer(element) + buffer[index] = null + wasStolen = true + } + } + return wasStolen + } + + private fun stealBatchTo(queue: GlobalQueue): Boolean { + return stealBatch { queue.add(it) } + } + + private fun pollBuffer(): Task? { + while (true) { + val tailLocal = consumerIndex.value + if (tailLocal - producerIndex.value == 0) return null + val index = tailLocal and MASK + if (consumerIndex.compareAndSet(tailLocal, tailLocal + 1)) { return buffer.getAndSet(index, null) } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt index 0c55b184d1..2e67d02330 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt @@ -53,55 +53,6 @@ class WorkQueueTest : TestBase() { require(globalQueue.size == 63) } - @Test - fun testTimelyStealing() { - val victim = WorkQueue() - val globalQueue = GlobalQueue() - - (1L..96L).forEach { victim.add(task(it), globalQueue) } - - timeSource.step() - timeSource.step(2) - - val stealer = WorkQueue() - require(stealer.trySteal(victim, globalQueue)) - assertEquals(arrayListOf(2L, 1L), stealer.drain()) - - require(!stealer.trySteal(victim, globalQueue)) - assertEquals(emptyList(), stealer.drain()) - - timeSource.step(3) - require(stealer.trySteal(victim, globalQueue)) - assertEquals(arrayListOf(5L, 3L, 4L), stealer.drain()) - require(globalQueue.isEmpty) - assertEquals((6L..96L).toSet(), victim.drain().toSet()) - } - - @Test - fun testStealingBySize() { - val victim = WorkQueue() - val globalQueue = GlobalQueue() - - (1L..110L).forEach { victim.add(task(it), globalQueue) } - val stealer = WorkQueue() - require(stealer.trySteal(victim, globalQueue)) - assertEquals((1L..13L).toSet(), stealer.drain().toSet()) - - require(!stealer.trySteal(victim, globalQueue)) - require(stealer.drain().isEmpty()) - - - timeSource.step() - timeSource.step(13) - require(!stealer.trySteal(victim, globalQueue)) - require(stealer.drain().isEmpty()) - - timeSource.step(1) - require(stealer.trySteal(victim, globalQueue)) - assertEquals(arrayListOf(14L), stealer.drain()) - - } - @Test fun testStealingFromHead() { val victim = WorkQueue() From f27d176e7a6add3b92f268c413b0ea4d27de35aa Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 1 Oct 2019 20:22:56 +0300 Subject: [PATCH 82/90] CoroutineScheduler parking strategy rework * WorkQueue.trySteal reports not only whether the steal was successful, but also a waiting time unless task becomes stealable * CoroutineScheduler.trySteal attempts to steal from all the workers (starting from the random position) per iteration to have deterministic stealing * Parking mechanics rework. After unsuccessful findTask, worker immediately adds itself to parking stack, then rescans all the queues to avoid missing tryUnparks and only then parks itself (parking duration depends on WorkQueue.trySteal result), terminating later * Excessive spinning and parking is completely eliminated, significantly (x3) reducing CPU-consumption and making CoroutineScheduler on-par with FJP and FTP on Ktor-like workloads * Downside of aggressive parking is a cost of slow-path unpark payed by external submitters that can be shown in degraded DispatchersContextSwitchBenchmark. Follow-up commits will fix that problem * Retry on tryStealLastScheduled failures to avoid potential starvation * Merge available CPU permits with controlState to simplify reasoning about pool state and make all state transitions atomic * Get rid of synthetic accessors --- gradle.properties | 2 +- .../jvm/src/scheduling/CoroutineScheduler.kt | 301 +++++++++--------- .../jvm/src/scheduling/WorkQueue.kt | 45 ++- ...ockingCoroutineDispatcherRaceStressTest.kt | 11 +- .../BlockingCoroutineDispatcherTest.kt | 2 +- .../test/scheduling/CoroutineSchedulerTest.kt | 5 +- .../test/scheduling/WorkQueueStressTest.kt | 2 +- .../jvm/test/scheduling/WorkQueueTest.kt | 4 +- 8 files changed, 190 insertions(+), 182 deletions(-) diff --git a/gradle.properties b/gradle.properties index 63a9c67783..1d3093f227 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.3.2-SNAPSHOT +version=1.3.2-sched10 group=org.jetbrains.kotlinx kotlin_version=1.3.60 diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 63659aae65..c56fe236e8 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -11,7 +11,8 @@ import java.io.* import java.util.concurrent.* import java.util.concurrent.atomic.* import java.util.concurrent.locks.* -import kotlin.random.Random +import kotlin.math.* +import kotlin.random.* /** * Coroutine scheduler (pool of shared threads) which primary target is to distribute dispatched coroutines over worker threads, @@ -24,13 +25,13 @@ import kotlin.random.Random * ### Structural overview * * Scheduler consists of [corePoolSize] worker threads to execute CPU-bound tasks and up to [maxPoolSize] (lazily created) threads - * to execute blocking tasks. Every worker has local queue in addition to global scheduler queue and global queue - * has priority over local queue to avoid starvation of externally-submitted (e.g., from Android UI thread) tasks and work-stealing is implemented + * to execute blocking tasks. Every worker a has local queue in addition to a global scheduler queue and the global queue + * has priority over local queue to avoid starvation of externally-submitted (e.g. from Android UI thread) tasks. Work-stealing is implemented * on top of that queues to provide even load distribution and illusion of centralized run queue. * * ### Scheduling * - * When a coroutine is dispatched from within scheduler worker, it's placed into the head of worker run queue. + * When a coroutine is dispatched from within a scheduler worker, it's placed into the head of worker run queue. * If the head is not empty, the task from the head is moved to the tail. Though it is unfair scheduling policy, * it effectively couples communicating coroutines into one and eliminates scheduling latency that arises from placing task to the end of the queue. * Placing former head to the tail is necessary to provide semi-FIFO order, otherwise queue degenerates to stack. @@ -39,14 +40,16 @@ import kotlin.random.Random * ### Work stealing and affinity * * To provide even tasks distribution worker tries to steal tasks from other workers queues before parking when his local queue is empty. - * A non-standard solution is implemented to provide tasks affinity: task may be stolen only if it's 'stale' enough (based on the value of [WORK_STEALING_TIME_RESOLUTION_NS]). + * A non-standard solution is implemented to provide tasks affinity: task from FIFO buffer may be stolen only if it is stale enough + * (based on the value of [WORK_STEALING_TIME_RESOLUTION_NS]). * For this purpose monotonic global clock ([System.nanoTime]) is used and every task has associated with it submission time. * This approach shows outstanding results when coroutines are cooperative, but as downside scheduler now depends on high-resolution global clock * which may limit scalability on NUMA machines. + * Tasks from LIFO buffer can be stolen on a regular basis. * * ### Dynamic resizing and support of blocking tasks * - * To support possibly blocking tasks [TaskMode] and CPU quota (via [cpuPermits]) are used. + * To support possibly blocking tasks [TaskMode] and CPU quota (via cpu permits in control state) are used. * To execute [TaskMode.NON_BLOCKING] tasks from the global queue or to steal tasks from other workers * the worker should have CPU permit. When a worker starts executing [TaskMode.PROBABLY_BLOCKING] task, * it releases its CPU permit, giving a hint to a scheduler that additional thread should be created (or awaken) @@ -54,15 +57,13 @@ import kotlin.random.Random * all tasks from its local queue (including [TaskMode.NON_BLOCKING]) and then parks as retired without polling * global queue or trying to steal new tasks. Such approach may slightly limit scalability (allowing more than [corePoolSize] threads * to execute CPU-bound tasks at once), but in practice, it is not, significantly reducing context switches and tasks re-dispatching. - * - * @suppress **This is unstable API and it is subject to change.** */ @Suppress("NOTHING_TO_INLINE") internal class CoroutineScheduler( - private val corePoolSize: Int, - private val maxPoolSize: Int, - private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS, - private val schedulerName: String = DEFAULT_SCHEDULER_NAME + @JvmField val corePoolSize: Int, + @JvmField val maxPoolSize: Int, + @JvmField val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS, + @JvmField val schedulerName: String = DEFAULT_SCHEDULER_NAME ) : Executor, Closeable { init { require(corePoolSize >= MIN_SUPPORTED_POOL_SIZE) { @@ -79,23 +80,16 @@ internal class CoroutineScheduler( } } - private val globalQueue: GlobalQueue = GlobalQueue() - - /** - * Permits to execute non-blocking (~CPU-intensive) tasks. - * If worker owns a permit, it can schedule non-blocking tasks to its queue and steal work from other workers. - * If worker doesn't, it can execute only blocking tasks (and non-blocking leftovers from its local queue) - * and will try to park as soon as its queue is empty. - */ - private val cpuPermits = Semaphore(corePoolSize, false) + @JvmField + val globalQueue: GlobalQueue = GlobalQueue() /** * The stack of parker workers. * Every worker registers itself in a stack before parking (if it was not previously registered) * and callers of [requestCpuWorker] will try to unpark a thread from the top of a stack. - * This is a form of intrusive garbage-free Treiber stack where Worker also is a stack node. + * This is a form of intrusive garbage-free Treiber stack where [Worker] also is a stack node. * - * The stack is better than a queue (even with contention on top) because it unparks threads + * The stack is better than a queue (even with the contention on top) because it unparks threads * in most-recently used order, improving both performance and locality. * Moreover, it decreases threads thrashing, if the pool has n threads when only n / 2 is required, * the latter half will never be unparked and will terminate itself after [IDLE_WORKER_KEEP_ALIVE_NS]. @@ -112,7 +106,7 @@ internal class CoroutineScheduler( * * Note, [newIndex] can be zero for the worker that is being terminated (removed from [workers]). */ - private fun parkedWorkersStackTopUpdate(worker: Worker, oldIndex: Int, newIndex: Int) { + internal fun parkedWorkersStackTopUpdate(worker: Worker, oldIndex: Int, newIndex: Int) { parkedWorkersStack.loop { top -> val index = (top and PARKED_INDEX_MASK).toInt() val updVersion = (top + PARKED_VERSION_INC) and PARKED_VERSION_MASK @@ -136,9 +130,12 @@ internal class CoroutineScheduler( * This method is invoked only from the worker thread itself. * This invocation always precedes [LockSupport.parkNanos]. * See [Worker.doPark]. + * + * Returns `true` if worker was added to the stack by this invocation, `false` if it was already + * registered in the stack. */ - private fun parkedWorkersStackPush(worker: Worker) { - if (worker.nextParkedWorker !== NOT_IN_STACK) return // already in stack, bail out + internal fun parkedWorkersStackPush(worker: Worker): Boolean { + if (worker.nextParkedWorker !== NOT_IN_STACK) return false // already in stack, bail out /* * The below loop can be entered only if this worker was not in the stack and, since no other thread * can add it to the stack (only the worker itself), this invariant holds while this loop executes. @@ -154,7 +151,7 @@ internal class CoroutineScheduler( * also invokes parkedWorkersStackTopUpdate which updates version to make next CAS fail. * Successful CAS of the stack top completes successful push. */ - if (parkedWorkersStack.compareAndSet(top, updVersion or updIndex.toLong())) return + if (parkedWorkersStack.compareAndSet(top, updVersion or updIndex.toLong())) return true } } @@ -224,45 +221,56 @@ internal class CoroutineScheduler( * workers are 1-indexed, code path in [Worker.trySteal] is a bit faster and index swap during termination * works properly */ - private val workers = AtomicReferenceArray(maxPoolSize + 1) + @JvmField + val workers = AtomicReferenceArray(maxPoolSize + 1) /** * Long describing state of workers in this pool. - * Currently includes created and blocking workers each occupying [BLOCKING_SHIFT] bits. + * Currently includes created, CPU-acquired and blocking workers each occupying [BLOCKING_SHIFT] bits. */ - private val controlState = atomic(0L) - + private val controlState = atomic(corePoolSize.toLong() shl CPU_PERMITS_SHIFT) private val createdWorkers: Int inline get() = (controlState.value and CREATED_MASK).toInt() private val blockingWorkers: Int inline get() = (controlState.value and BLOCKING_MASK shr BLOCKING_SHIFT).toInt() + private val availableCpuPermits: Int inline get() = (controlState.value and CPU_PERMITS_MASK shr CPU_PERMITS_SHIFT).toInt() private inline fun createdWorkers(state: Long): Int = (state and CREATED_MASK).toInt() private inline fun blockingWorkers(state: Long): Int = (state and BLOCKING_MASK shr BLOCKING_SHIFT).toInt() + private inline fun availablePermits(state: Long): Int = (state and CPU_PERMITS_MASK shr CPU_PERMITS_SHIFT).toInt() // Guarded by synchronization private inline fun incrementCreatedWorkers(): Int = createdWorkers(controlState.incrementAndGet()) private inline fun decrementCreatedWorkers(): Int = createdWorkers(controlState.getAndDecrement()) - private inline fun incrementBlockingWorkers() { controlState.addAndGet(1L shl BLOCKING_SHIFT) } - private inline fun decrementBlockingWorkers() { controlState.addAndGet(-(1L shl BLOCKING_SHIFT)) } + private inline fun incrementBlockingWorkers() { + controlState.addAndGet(1L shl BLOCKING_SHIFT) + } - // This is used a "stop signal" for close and shutdown functions - private val _isTerminated = atomic(false) - private val isTerminated: Boolean get() = _isTerminated.value + private inline fun decrementBlockingWorkers() { + controlState.addAndGet(-(1L shl BLOCKING_SHIFT)) + } - companion object { - private val MAX_SPINS = systemProp("kotlinx.coroutines.scheduler.spins", 1000, minValue = 1) - private val MAX_YIELDS = MAX_SPINS + systemProp("kotlinx.coroutines.scheduler.yields", 0, minValue = 0) + private inline fun tryAcquireCpuPermit(): Boolean { + while (true) { + val state = controlState.value + val available = availablePermits(state) + if (available == 0) return false + val update = state - (1L shl CPU_PERMITS_SHIFT) + if (controlState.compareAndSet(state, update)) return true + } + } - @JvmStatic // Note that is fits into Int (it is equal to 10^9) - private val MAX_PARK_TIME_NS = TimeUnit.SECONDS.toNanos(1).toInt() + private inline fun releaseCpuPermit() { + controlState.addAndGet(1L shl CPU_PERMITS_SHIFT) + } - @JvmStatic - private val MIN_PARK_TIME_NS = (WORK_STEALING_TIME_RESOLUTION_NS / 4) - .coerceAtLeast(10) - .coerceAtMost(MAX_PARK_TIME_NS.toLong()).toInt() + // This is used a "stop signal" for close and shutdown functions + private val _isTerminated = atomic(false) + val isTerminated: Boolean get() = _isTerminated.value + companion object { // A symbol to mark workers that are not in parkedWorkersStack - private val NOT_IN_STACK = Symbol("NOT_IN_STACK") + @JvmField + val NOT_IN_STACK = Symbol("NOT_IN_STACK") // Local queue 'add' results private const val ADDED = -1 @@ -279,6 +287,8 @@ internal class CoroutineScheduler( private const val BLOCKING_SHIFT = 21 // 2M threads max private const val CREATED_MASK: Long = (1L shl BLOCKING_SHIFT) - 1 private const val BLOCKING_MASK: Long = CREATED_MASK shl BLOCKING_SHIFT + private const val CPU_PERMITS_SHIFT = BLOCKING_SHIFT * 2 + private const val CPU_PERMITS_MASK = CREATED_MASK shl CPU_PERMITS_SHIFT internal const val MIN_SUPPORTED_POOL_SIZE = 1 // we support 1 for test purposes, but it is not usually used internal const val MAX_SUPPORTED_POOL_SIZE = (1 shl BLOCKING_SHIFT) - 2 @@ -324,7 +334,7 @@ internal class CoroutineScheduler( // Shutdown current thread currentWorker?.tryReleaseCpu(WorkerState.TERMINATED) // check & cleanup state - assert { cpuPermits.availablePermits() == corePoolSize } + assert { availableCpuPermits == corePoolSize } parkedWorkersStack.value = 0L controlState.value = 0L } @@ -368,9 +378,9 @@ internal class CoroutineScheduler( /** * Unparks or creates a [Worker] for executing non-blocking tasks if there are idle cores */ - private fun requestCpuWorker() { + internal fun requestCpuWorker() { // No CPU available -- nothing to request - if (cpuPermits.availablePermits() == 0) { + if (availableCpuPermits == 0) { tryUnpark() return } @@ -412,20 +422,16 @@ internal class CoroutineScheduler( val worker = parkedWorkersStackPop() ?: return false /* * If we successfully took the worker out of the queue, it could be in the following states: - * 1) Worker is parked, then we'd like to reset its spin and park counters, so after - * unpark it will try to steal from every worker at least once - * 2) Worker is not parked, but it actually idle and - * tries to find work. Then idle reset is required as well. - * Worker state may be either PARKING or CPU_ACQUIRED (from `findTask`) - * 3) Worker is active (unparked itself from `idleCpuWorker`), found tasks to do and is currently busy. - * Then `idleResetBeforeUnpark` will do nothing, but we can't distinguish this state from previous - * one, so just retry. - * 4) Worker is terminated. No harm in resetting its counters either. - */ - worker.idleResetBeforeUnpark() - /* + * 1) Worker is parked. Just wake up it and reset its termination deadline to avoid + * "termination during tryUnpark" race. + * 2) Worker is not parked and is rescanning the queue before actual parking. + * Worker state may be CPU_ACQUIRED or BLOCKING (has no permit, wants to terminate). + * 3) Worker is executing some task. We can't really distinguish it from the previous case, so just proceed. + * 4) Worker is terminated, proceed and try to find another one. + * + * * Check that the thread we've found in the queue was indeed in parking state, before we - * actually try to unpark it. + * actually try to unpark it. */ val wasParking = worker.isParking /* @@ -434,8 +440,8 @@ internal class CoroutineScheduler( */ LockSupport.unpark(worker) /* - * If this thread was not in parking state then we definitely need to find another thread. - * We err on the side of unparking more threads than needed here. + * If worker was parking, then we can be sure that our signal is not lost. + * Otherwise it could be a thread in state "3", so let's try ti find another thread. */ if (!wasParking) continue /* @@ -465,13 +471,19 @@ internal class CoroutineScheduler( val cpuWorkers = created - blocking // Double check for overprovision if (cpuWorkers >= corePoolSize) return 0 - if (created >= maxPoolSize || cpuPermits.availablePermits() == 0) return 0 + if (created >= maxPoolSize || availableCpuPermits == 0) return 0 // start & register new worker, commit index only after successful creation val newIndex = createdWorkers + 1 require(newIndex > 0 && workers[newIndex] == null) - val worker = Worker(newIndex).apply { start() } - require(newIndex == incrementCreatedWorkers()) + /* + * 1) Claim the slot (under a lock) by the newly created worker + * 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) workers[newIndex] = worker + require(newIndex == incrementCreatedWorkers()) + worker.start() return cpuWorkers + 1 } } @@ -549,7 +561,7 @@ internal class CoroutineScheduler( var retired = 0 var terminated = 0 val queueSizes = arrayListOf() - for (index in 0 until workers.length()) { + for (index in 1 until workers.length()) { val worker = workers[index] ?: continue val queueSize = worker.localQueue.size() when (worker.state) { @@ -588,7 +600,7 @@ internal class CoroutineScheduler( "]" } - private fun runSafely(task: Task) { + internal fun runSafely(task: Task) { try { task.run() } catch (e: Throwable) { @@ -626,7 +638,6 @@ internal class CoroutineScheduler( */ @Volatile var state = WorkerState.RETIRING - val isParking: Boolean get() = state == WorkerState.PARKING val isBlocking: Boolean get() = state == WorkerState.BLOCKING @@ -645,7 +656,7 @@ internal class CoroutineScheduler( private val terminationState = atomic(ALLOWED) /** - * It is set to the termination deadline when started doing [blockingWorkerIdle] and it reset + * It is set to the termination deadline when started doing [park] and it reset * when there is a task. It servers as protection against spurious wakeups of parkNanos. */ private var terminationDeadline = 0L @@ -681,7 +692,7 @@ internal class CoroutineScheduler( fun tryAcquireCpuPermit(): Boolean { return when { state == WorkerState.CPU_ACQUIRED -> true - cpuPermits.tryAcquire() -> { + this@CoroutineScheduler.tryAcquireCpuPermit() -> { state = WorkerState.CPU_ACQUIRED true } @@ -696,7 +707,7 @@ internal class CoroutineScheduler( internal fun tryReleaseCpu(newState: WorkerState): Boolean { val previousState = state val hadCpu = previousState == WorkerState.CPU_ACQUIRED - if (hadCpu) cpuPermits.release() + if (hadCpu) releaseCpuPermit() if (previousState != newState) state = newState return hadCpu } @@ -707,42 +718,54 @@ internal class CoroutineScheduler( */ private var lastExhaustionTime = 0L - @Volatile // Required for concurrent idleResetBeforeUnpark - private var spins = 0 // spins until MAX_SPINS, then yields until MAX_YIELDS - - // Note: it is concurrently reset by idleResetBeforeUnpark - private var parkTimeNs = MIN_PARK_TIME_NS - private var rngState = Random.nextInt() - private var lastStealIndex = 0 // try in order repeated, reset when unparked + // The delay until at least one task in other worker queues will become stealable + private var minDelayUntilStealableTask = 0L - override fun run() { - var wasIdle = false // local variable to avoid extra idleReset invocations when tasks repeatedly arrive + override fun run() = runWorker() + + private fun runWorker() { while (!isTerminated && state != WorkerState.TERMINATED) { val task = findTask() - if (task == null) { - // Wait for a job with potential park - if (state == WorkerState.CPU_ACQUIRED) { - cpuWorkerIdle() - } else { - blockingWorkerIdle() - } - wasIdle = true + // Task found. Execute and repeat + if (task != null) { + executeTask(task) + continue + } + + /* + * No tasks were found: + * 1) Either at least one of the workers has stealable task in its FIFO-buffer with a stealing deadline. + * Then its deadline is stored in [minDelayUntilStealableTask] + * 2) No tasks available, time to park and, potentially, shut down the thread. + * + * In both cases, worker adds itself to the stack of parked workers, re-scans all the queues + * to avoid missing wake-up (requestCpuWorker) and either starts executing discovered tasks or parks itself awaiting for new tasks. + * + * Park duration depends on the possible state: either this is the idleWorkerKeepAliveNs or stealing deadline. + */ + if (parkedWorkersStackPush(this)) { + continue } else { - // Note: read task.mode before running the task, because Task object will be reused after run - val taskMode = task.mode - if (wasIdle) { - idleReset(taskMode) - wasIdle = false + tryReleaseCpu(WorkerState.PARKING) + if (minDelayUntilStealableTask > 0) { + LockSupport.parkNanos(minDelayUntilStealableTask) // No spurious wakeup check here + } else { + park() } - beforeTask(taskMode, task.submissionTime) - runSafely(task) - afterTask(taskMode) } } tryReleaseCpu(WorkerState.TERMINATED) } + private fun executeTask(task: Task) { + val taskMode = task.mode + idleReset(taskMode) + beforeTask(taskMode, task.submissionTime) + runSafely(task) + afterTask(taskMode) + } + private fun beforeTask(taskMode: TaskMode, taskSubmissionTime: Long) { if (taskMode != TaskMode.NON_BLOCKING) { /* @@ -759,7 +782,7 @@ internal class CoroutineScheduler( * If we have idle CPU and the current worker is exhausted, wake up one more worker. * Check last exhaustion time to avoid the race between steal and next task execution */ - if (cpuPermits.availablePermits() == 0) { + if (availableCpuPermits == 0) { return } val now = schedulerTimeSource.nanoTime() @@ -788,42 +811,20 @@ internal class CoroutineScheduler( * ThreadLocalRandom cannot be used to support Android and ThreadLocal is up to 15% slower on Ktor benchmarks */ internal fun nextInt(upperBound: Int): Int { - rngState = rngState xor (rngState shl 13) - rngState = rngState xor (rngState shr 17) - rngState = rngState xor (rngState shl 5) + var r = rngState + r = r xor (r shl 13) + r = r xor (r shr 17) + r = r xor (r shl 5) + rngState = r val mask = upperBound - 1 // Fast path for power of two bound if (mask and upperBound == 0) { - return rngState and mask - } - return (rngState and Int.MAX_VALUE) % upperBound - } - - private fun cpuWorkerIdle() { - /* - * Simple adaptive await of work: - * Spin on the volatile field with an empty loop in hope that new work will arrive, - * then start yielding to reduce CPU pressure, and finally start adaptive parking. - * - * The main idea is not to park while it's possible (otherwise throughput on asymmetric workloads suffers due to too frequent - * park/unpark calls and delays between job submission and thread queue checking) - */ - val spins = this.spins // volatile read - if (spins <= MAX_YIELDS) { - this.spins = spins + 1 // volatile write - if (spins >= MAX_SPINS) yield() - } else { - if (parkTimeNs < MAX_PARK_TIME_NS) { - parkTimeNs = (parkTimeNs * 3 ushr 1).coerceAtMost(MAX_PARK_TIME_NS) - } - tryReleaseCpu(WorkerState.PARKING) - doPark(parkTimeNs.toLong()) + return r and mask } + return (r and Int.MAX_VALUE) % upperBound } - private fun blockingWorkerIdle() { - tryReleaseCpu(WorkerState.PARKING) - if (!blockingQuiescence()) return + private fun park() { terminationState.value = ALLOWED // set termination deadline the first time we are here (it is reset in idleReset) if (terminationDeadline == 0L) terminationDeadline = System.nanoTime() + idleWorkerKeepAliveNs @@ -842,9 +843,8 @@ internal class CoroutineScheduler( * Here we are trying to park, then check whether there are new blocking tasks * (because submitting thread could have missed this thread in tryUnpark) */ - parkedWorkersStackPush(this) if (!blockingQuiescence()) return false - LockSupport.parkNanos(nanos) + LockSupport.parkNanos(nanos) // Spurious wakeup check in [park] return true } @@ -922,19 +922,10 @@ internal class CoroutineScheduler( // It is invoked by this worker when it finds a task private fun idleReset(mode: TaskMode) { terminationDeadline = 0L // reset deadline for termination - lastStealIndex = 0 // reset steal index (next time try random) if (state == WorkerState.PARKING) { assert { mode == TaskMode.PROBABLY_BLOCKING } state = WorkerState.BLOCKING - parkTimeNs = MIN_PARK_TIME_NS } - spins = 0 - } - - // It is invoked by other thread before this worker is unparked - fun idleResetBeforeUnpark() { - parkTimeNs = MIN_PARK_TIME_NS - spins = 0 // Volatile write, should be written last } internal fun findTask(): Task? { @@ -971,20 +962,26 @@ internal class CoroutineScheduler( private fun trySteal(): Task? { val created = createdWorkers // 0 to await an initialization and 1 to avoid excess stealing on single-core machines - if (created < 2) return null - - // TODO to guarantee quiescence it's probably worth to do a full scan - var stealIndex = lastStealIndex - if (stealIndex == 0) stealIndex = nextInt(created) // start with random steal index - stealIndex++ // then go sequentially - if (stealIndex > created) stealIndex = 1 - lastStealIndex = stealIndex - val worker = workers[stealIndex] - if (worker !== null && worker !== this) { - if (localQueue.trySteal(worker.localQueue, globalQueue)) { - return localQueue.poll() + if (created < 2) { + return null + } + + var currentIndex = nextInt(created) + var minDelay = Long.MAX_VALUE + repeat(created) { + ++currentIndex + if (currentIndex > created) currentIndex = 1 + val worker = workers[currentIndex] + if (worker !== null && worker !== this) { + val stealResult = localQueue.trySteal(worker.localQueue, globalQueue) + if (stealResult == TASK_STOLEN) { + return localQueue.poll() + } else if (stealResult > 0) { + minDelay = min(minDelay, stealResult) + } } } + minDelayUntilStealableTask = if (minDelay != Long.MAX_VALUE) minDelay else 0 return null } } diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt index 8479936829..32af79cbe6 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt @@ -12,6 +12,9 @@ internal const val BUFFER_CAPACITY_BASE = 7 internal const val BUFFER_CAPACITY = 1 shl BUFFER_CAPACITY_BASE internal const val MASK = BUFFER_CAPACITY - 1 // 128 by default +internal const val TASK_STOLEN = -1L +internal const val NOTHING_TO_STEAL = -2L + /** * Tightly coupled with [CoroutineScheduler] queue of pending tasks, but extracted to separate file for simplicity. * At any moment queue is used only by [CoroutineScheduler.Worker] threads, has only one producer (worker owning this queue) @@ -49,10 +52,7 @@ internal class WorkQueue { * This is in general harmless because steal will be blocked by timer */ internal val bufferSize: Int get() = producerIndex.value - consumerIndex.value - - // TODO replace with inlined array when atomicfu will support it private val buffer: AtomicReferenceArray = AtomicReferenceArray(BUFFER_CAPACITY) - private val lastScheduledTask = atomic(null) private val producerIndex = atomic(0) @@ -93,30 +93,43 @@ internal class WorkQueue { /** * Tries stealing from [victim] queue into this queue, using [globalQueue] to offload stolen tasks in case of current queue overflow. * - * @return whether any task was stolen + * Returns [NOTHING_TO_STEAL] if queue has nothing to steal, [TASK_STOLEN] if at least task was stolen + * or positive value of how many nanoseconds should pass until the head of this queue will be available to steal. */ - fun trySteal(victim: WorkQueue, globalQueue: GlobalQueue): Boolean { + fun trySteal(victim: WorkQueue, globalQueue: GlobalQueue): Long { if (victim.stealBatch { task -> add(task, globalQueue) }) { - return true + return TASK_STOLEN } return tryStealLastScheduled(victim, globalQueue) } + /** + * Contract on return value is the same as for [trySteal] + */ private fun tryStealLastScheduled( victim: WorkQueue, globalQueue: GlobalQueue - ): Boolean { - val lastScheduled = victim.lastScheduledTask.value ?: return false - val time = schedulerTimeSource.nanoTime() - if (time - lastScheduled.submissionTime < WORK_STEALING_TIME_RESOLUTION_NS) { - return false - } + ): Long { + while (true) { + val lastScheduled = victim.lastScheduledTask.value ?: return NOTHING_TO_STEAL + // TODO time wraparound ? + val time = schedulerTimeSource.nanoTime() + val staleness = time - lastScheduled.submissionTime + if (staleness < WORK_STEALING_TIME_RESOLUTION_NS) { + return WORK_STEALING_TIME_RESOLUTION_NS - staleness + } - if (victim.lastScheduledTask.compareAndSet(lastScheduled, null)) { - add(lastScheduled, globalQueue) - return true + /* + * If CAS has failed, either someone else had stolen this task or the owner executed this task + * and dispatched another one. In the latter case we should retry to avoid missing task. + */ + if (victim.lastScheduledTask.compareAndSet(lastScheduled, null)) { + add(lastScheduled, globalQueue) + return TASK_STOLEN + } + continue } - return false + } internal fun size(): Int = if (lastScheduledTask.value != null) bufferSize + 1 else bufferSize diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt index 2b5a8968ac..77fb71246e 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt @@ -6,7 +6,9 @@ package kotlinx.coroutines.scheduling import kotlinx.coroutines.* import org.junit.* +import org.junit.Test import java.util.concurrent.atomic.* +import kotlin.test.* class BlockingCoroutineDispatcherRaceStressTest : SchedulerTestBase() { private val concurrentWorkers = AtomicInteger(0) @@ -33,10 +35,8 @@ class BlockingCoroutineDispatcherRaceStressTest : SchedulerTestBase() { } } } - tasks.forEach { it.await() } } - checkPoolThreadsCreated(2..4) } @@ -44,19 +44,18 @@ class BlockingCoroutineDispatcherRaceStressTest : SchedulerTestBase() { fun testPingPongThreadsCount() = runBlocking { corePoolSize = CORES_COUNT val iterations = 100_000 * stressTestMultiplier - // Stress test for specific case (race #2 from LimitingDispatcher). Shouldn't hang. + val completed = AtomicInteger(0) for (i in 1..iterations) { val tasks = (1..2).map { async(dispatcher) { // Useless work concurrentWorkers.incrementAndGet() concurrentWorkers.decrementAndGet() + completed.incrementAndGet() } } - tasks.forEach { it.await() } } - - checkPoolThreadsCreated(CORES_COUNT) + assertEquals(2 * iterations, completed.get()) } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt index ce5ed99983..699d8d044d 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt @@ -221,4 +221,4 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { fun testZeroParallelism() { blockingDispatcher(0) } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt index 780ec1b95b..b6914764c4 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt @@ -4,11 +4,10 @@ package kotlinx.coroutines.scheduling -import kotlinx.coroutines.TestBase -import org.junit.Test +import kotlinx.coroutines.* +import org.junit.* import java.lang.Runnable import java.util.concurrent.* -import java.util.concurrent.CountDownLatch import kotlin.coroutines.* class CoroutineSchedulerTest : TestBase() { diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt index 41adddce7c..185de71dd9 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt @@ -91,7 +91,7 @@ class WorkQueueStressTest : TestBase() { val myQueue = WorkQueue() startLatch.await() while (stolen.size != offerIterations) { - if (!myQueue.trySteal(producerQueue, stolen)) { + if (myQueue.trySteal(producerQueue, stolen) != NOTHING_TO_STEAL) { stolen.addAll(myQueue.drain().map { task(it) }) } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt index 2e67d02330..524eff55a1 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt @@ -62,10 +62,10 @@ class WorkQueueTest : TestBase() { timeSource.step(3) val stealer = WorkQueue() - require(stealer.trySteal(victim, globalQueue)) + assertEquals(TASK_STOLEN, stealer.trySteal(victim, globalQueue)) assertEquals(arrayListOf(1L), stealer.drain()) - require(stealer.trySteal(victim, globalQueue)) + assertEquals(TASK_STOLEN, stealer.trySteal(victim, globalQueue)) assertEquals(arrayListOf(2L), stealer.drain()) } } From 4236c8c5b0b6296bd3bdde7a5dccd77ff48bdf24 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 11 Oct 2019 00:35:47 +0300 Subject: [PATCH 83/90] New work stealing and unparking strategies * Work stealing: get rid of global queue for offloading during stealing because it never happens in fact * Guard all critical invariants related to work-stealing with asserts * New work signalling strategy that guarantees complete liveness in the face of "accidentally-blocking" CPU tasks * Advanced double-phase unparking mechanism that mitigates the most expensive part of signalling an additional work * Current limitation: blocking tasks are not yet properly signalled --- .../jvm/src/scheduling/CoroutineScheduler.kt | 159 ++++++------------ .../jvm/src/scheduling/WorkQueue.kt | 89 ++++------ .../scheduling/CoroutineDispatcherTest.kt | 26 --- .../CoroutineSchedulerLivenessStressTest.kt | 52 ++++++ .../CoroutineSchedulerStressTest.kt | 64 +++---- .../test/scheduling/WorkQueueStressTest.kt | 18 +- .../jvm/test/scheduling/WorkQueueTest.kt | 44 +++-- 7 files changed, 199 insertions(+), 253 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index c56fe236e8..0b0061993b 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -8,6 +8,7 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import java.io.* +import java.lang.AssertionError import java.util.concurrent.* import java.util.concurrent.atomic.* import java.util.concurrent.locks.* @@ -272,12 +273,6 @@ internal class CoroutineScheduler( @JvmField val NOT_IN_STACK = Symbol("NOT_IN_STACK") - // Local queue 'add' results - private const val ADDED = -1 - // Added to the local queue, but pool requires additional worker to keep up - private const val ADDED_REQUIRES_HELP = 0 - private const val NOT_ADDED = 1 - // Worker termination states private const val FORBIDDEN = -1 private const val ALLOWED = 0 @@ -351,18 +346,13 @@ internal class CoroutineScheduler( trackTask() // this is needed for virtual time support val task = createTask(block, taskContext) // try to submit the task to the local queue and act depending on the result - when (submitToLocalQueue(task, fair)) { - ADDED -> return - NOT_ADDED -> { - // try to offload task to global queue - if (!globalQueue.addLast(task)) { - // Global queue is closed in the last step of close/shutdown -- no more tasks should be accepted - throw RejectedExecutionException("$schedulerName was terminated") - } - requestCpuWorker() + if (!submitToLocalQueue(task, fair)) { + if (!globalQueue.addLast(task)) { + // Global queue is closed in the last step of close/shutdown -- no more tasks should be accepted + throw RejectedExecutionException("$schedulerName was terminated") } - else -> requestCpuWorker() // ask for help } + requestCpuWorker() } internal fun createTask(block: Runnable, taskContext: TaskContext): Task { @@ -420,40 +410,12 @@ internal class CoroutineScheduler( private fun tryUnpark(): Boolean { while (true) { val worker = parkedWorkersStackPop() ?: return false - /* - * If we successfully took the worker out of the queue, it could be in the following states: - * 1) Worker is parked. Just wake up it and reset its termination deadline to avoid - * "termination during tryUnpark" race. - * 2) Worker is not parked and is rescanning the queue before actual parking. - * Worker state may be CPU_ACQUIRED or BLOCKING (has no permit, wants to terminate). - * 3) Worker is executing some task. We can't really distinguish it from the previous case, so just proceed. - * 4) Worker is terminated, proceed and try to find another one. - * - * - * Check that the thread we've found in the queue was indeed in parking state, before we - * actually try to unpark it. - */ - val wasParking = worker.isParking - /* - * Send unpark signal anyway, because the thread may have made decision to park but have not yet set its - * state to parking and this could be the last thread we have (unparking random thread would not harm). - */ - LockSupport.unpark(worker) - /* - * If worker was parking, then we can be sure that our signal is not lost. - * Otherwise it could be a thread in state "3", so let's try ti find another thread. - */ - if (!wasParking) continue - /* - * Terminating worker could be selected. - * If it's already TERMINATED or we cannot forbid it from terminating, then try find another worker. - */ - if (!worker.tryForbidTermination()) continue - /* - * Here we've successfully unparked a thread that was parked and had forbidden it from making - * decision to terminate, so we are now sure we've got some help. - */ - return true + val time = worker.minDelayUntilStealableTask // TODO explain + worker.parkingAllowed = false + if (worker.signallingAllowed && time == 0L) { + LockSupport.unpark(worker) + } + if (time == 0L && worker.tryForbidTermination()) return true } } @@ -489,57 +451,24 @@ internal class CoroutineScheduler( } /** - * Returns [ADDED], or [NOT_ADDED], or [ADDED_REQUIRES_HELP]. + * Returns `true` if added, `false` otherwise */ - private fun submitToLocalQueue(task: Task, fair: Boolean): Int { - val worker = currentWorker() ?: return NOT_ADDED + private fun submitToLocalQueue(task: Task, fair: Boolean): Boolean { + val worker = currentWorker() ?: return false /* * This worker could have been already terminated from this thread by close/shutdown and it should not * accept any more tasks into its local queue. */ - if (worker.state === WorkerState.TERMINATED) return NOT_ADDED - - var result = ADDED - if (task.mode == TaskMode.NON_BLOCKING) { - /* - * If the worker is currently executing blocking task and tries to dispatch non-blocking task, it's one the following reasons: - * 1) Blocking worker is finishing its block and resumes non-blocking continuation - * 2) Blocking worker starts to create non-blocking jobs - * - * First use-case is expected (as recommended way of using blocking contexts), - * so we add non-blocking task to local queue, but also request CPU worker to mitigate second case - */ - if (worker.isBlocking) { - result = ADDED_REQUIRES_HELP - } else { - /* - * If thread is not blocking, then it's just tries to finish its - * local work in order to park (or grab another blocking task), do not add non-blocking tasks - * to its local queue if it can't acquire CPU - */ - val hasPermit = worker.tryAcquireCpuPermit() - if (!hasPermit) { - return NOT_ADDED - } - } - } - - val noOffloadingHappened = if (fair) { - worker.localQueue.addLast(task, globalQueue) - } else { - worker.localQueue.add(task, globalQueue) - } - - if (noOffloadingHappened) { - // When we're close to queue capacity, wake up anyone to steal work - // Note: non-atomic bufferSize here is Ok (it is just a performance optimization) - if (worker.localQueue.bufferSize > QUEUE_SIZE_OFFLOAD_THRESHOLD) { - return ADDED_REQUIRES_HELP - } - return result - } - return ADDED_REQUIRES_HELP + if (worker.state === WorkerState.TERMINATED) return false + if (task.mode == TaskMode.NON_BLOCKING && worker.isBlocking) { + return false + } + val notAdded = with(worker.localQueue) { + if (fair) addLast(task) else add(task) + } ?: return true // Forgive me, Father, for this formatting + globalQueue.addLast(notAdded) + return true } private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this } @@ -563,7 +492,7 @@ internal class CoroutineScheduler( val queueSizes = arrayListOf() for (index in 1 until workers.length()) { val worker = workers[index] ?: continue - val queueSize = worker.localQueue.size() + val queueSize = worker.localQueue.size when (worker.state) { WorkerState.PARKING -> ++parkedWorkers WorkerState.BLOCKING -> { @@ -668,6 +597,10 @@ internal class CoroutineScheduler( */ @Volatile var nextParkedWorker: Any? = NOT_IN_STACK + @Volatile // TODO ughm don't ask + var parkingAllowed = false + @Volatile + var signallingAllowed = false /** * Tries to set [terminationState] to [FORBIDDEN], returns `false` if this attempt fails. @@ -719,8 +652,12 @@ internal class CoroutineScheduler( private var lastExhaustionTime = 0L private var rngState = Random.nextInt() - // The delay until at least one task in other worker queues will become stealable - private var minDelayUntilStealableTask = 0L + /* + * The delay until at least one task in other worker queues will become stealable. + * Volatile to avoid benign data-race + */ + @Volatile + public var minDelayUntilStealableTask = 0L override fun run() = runWorker() @@ -744,15 +681,21 @@ internal class CoroutineScheduler( * * Park duration depends on the possible state: either this is the idleWorkerKeepAliveNs or stealing deadline. */ + parkingAllowed = true if (parkedWorkersStackPush(this)) { continue } else { - tryReleaseCpu(WorkerState.PARKING) - if (minDelayUntilStealableTask > 0) { - LockSupport.parkNanos(minDelayUntilStealableTask) // No spurious wakeup check here - } else { - park() + signallingAllowed = true + if (parkingAllowed) { + tryReleaseCpu(WorkerState.PARKING) + if (minDelayUntilStealableTask > 0) { + LockSupport.parkNanos(minDelayUntilStealableTask) // No spurious wakeup check here + } else { + assert { localQueue.size == 0 } + park() + } } + signallingAllowed = false } } tryReleaseCpu(WorkerState.TERMINATED) @@ -800,7 +743,7 @@ internal class CoroutineScheduler( val currentState = state // Shutdown sequence of blocking dispatcher if (currentState !== WorkerState.TERMINATED) { - assert { currentState == WorkerState.BLOCKING } // "Expected BLOCKING state, but has $currentState" + assert { (currentState == WorkerState.BLOCKING).also { if (!it) throw AssertionError("AAA: $currentState") } } // "Expected BLOCKING state, but has $currentState" state = WorkerState.RETIRING } } @@ -910,10 +853,12 @@ internal class CoroutineScheduler( * Checks whether new blocking tasks arrived to the pool when worker decided * it can go to deep park/termination and puts recently arrived task to its local queue. * Returns `true` if there is no blocking tasks in the queue. + * Invariant: invoked only with empty local queue */ private fun blockingQuiescence(): Boolean { + assert { localQueue.size == 0 } globalQueue.removeFirstWithModeOrNull(TaskMode.PROBABLY_BLOCKING)?.let { - localQueue.add(it, globalQueue) + localQueue.add(it) return false } return true @@ -960,6 +905,7 @@ internal class CoroutineScheduler( } private fun trySteal(): Task? { + assert { localQueue.size == 0 } val created = createdWorkers // 0 to await an initialization and 1 to avoid excess stealing on single-core machines if (created < 2) { @@ -973,7 +919,8 @@ internal class CoroutineScheduler( if (currentIndex > created) currentIndex = 1 val worker = workers[currentIndex] if (worker !== null && worker !== this) { - val stealResult = localQueue.trySteal(worker.localQueue, globalQueue) + assert { localQueue.size == 0 } + val stealResult = localQueue.tryStealFrom(victim = worker.localQueue) if (stealResult == TASK_STOLEN) { return localQueue.poll() } else if (stealResult > 0) { diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt index 32af79cbe6..a2612b4a83 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt @@ -52,6 +52,7 @@ internal class WorkQueue { * This is in general harmless because steal will be blocked by timer */ internal val bufferSize: Int get() = producerIndex.value - consumerIndex.value + internal val size: Int get() = if (lastScheduledTask.value != null) bufferSize + 1 else bufferSize private val buffer: AtomicReferenceArray = AtomicReferenceArray(BUFFER_CAPACITY) private val lastScheduledTask = atomic(null) @@ -65,51 +66,53 @@ internal class WorkQueue { fun poll(): Task? = lastScheduledTask.getAndSet(null) ?: pollBuffer() /** - * Invariant: this method is called only by the owner of the queue - * - * @param task task to put into local queue - * @param globalQueue fallback queue which is used when the local queue is overflown - * @return true if no offloading happened, false otherwise + * Invariant: Called only by the owner of the queue, returns + * `null` if task was added, task that wasn't added otherwise. */ - fun add(task: Task, globalQueue: GlobalQueue): Boolean { - val previous = lastScheduledTask.getAndSet(task) ?: return true - return addLast(previous, globalQueue) + fun add(task: Task): Task? { + val previous = lastScheduledTask.getAndSet(task) ?: return null + return addLast(previous) } - // Called only by the owner, returns true if no offloading happened, false otherwise - fun addLast(task: Task, globalQueue: GlobalQueue): Boolean { - var noOffloadingHappened = true + /** + * Invariant: Called only by the owner of the queue, returns + * `null` if task was added, task that wasn't added otherwise. + */ + fun addLast(task: Task): Task? { + if (bufferSize == BUFFER_CAPACITY - 1) return task + val headLocal = producerIndex.value + val nextIndex = headLocal and MASK + /* - * We need the loop here because race possible not only on full queue, - * but also on queue with one element during stealing + * If current element is not null then we're racing with consumers for the tail. If we skip this check then + * the consumer can null out current element and it will be lost. If we're racing for tail then + * the queue is close to overflowing => return task */ - while (!tryAddLast(task)) { - offloadWork(globalQueue) - noOffloadingHappened = false + if (buffer[nextIndex] != null) { + return task } - return noOffloadingHappened + buffer.lazySet(nextIndex, task) + producerIndex.incrementAndGet() + return null } /** - * Tries stealing from [victim] queue into this queue, using [globalQueue] to offload stolen tasks in case of current queue overflow. + * Tries stealing from [victim] queue into this queue. * * Returns [NOTHING_TO_STEAL] if queue has nothing to steal, [TASK_STOLEN] if at least task was stolen * or positive value of how many nanoseconds should pass until the head of this queue will be available to steal. */ - fun trySteal(victim: WorkQueue, globalQueue: GlobalQueue): Long { - if (victim.stealBatch { task -> add(task, globalQueue) }) { + fun tryStealFrom(victim: WorkQueue): Long { + if (victim.stealBatch { task -> add(task) }) { return TASK_STOLEN } - return tryStealLastScheduled(victim, globalQueue) + return tryStealLastScheduled(victim) } /** - * Contract on return value is the same as for [trySteal] + * Contract on return value is the same as for [tryStealFrom] */ - private fun tryStealLastScheduled( - victim: WorkQueue, - globalQueue: GlobalQueue - ): Long { + private fun tryStealLastScheduled(victim: WorkQueue): Long { while (true) { val lastScheduled = victim.lastScheduledTask.value ?: return NOTHING_TO_STEAL // TODO time wraparound ? @@ -124,21 +127,11 @@ internal class WorkQueue { * and dispatched another one. In the latter case we should retry to avoid missing task. */ if (victim.lastScheduledTask.compareAndSet(lastScheduled, null)) { - add(lastScheduled, globalQueue) + add(lastScheduled) return TASK_STOLEN } continue } - - } - - internal fun size(): Int = if (lastScheduledTask.value != null) bufferSize + 1 else bufferSize - - /** - * Offloads half of the current buffer to [globalQueue] - */ - private fun offloadWork(globalQueue: GlobalQueue) { - stealBatchTo(globalQueue) } private fun GlobalQueue.add(task: Task) { @@ -169,7 +162,7 @@ internal class WorkQueue { var wasStolen = false while (toSteal-- > 0) { val tailLocal = consumerIndex.value - if (tailLocal - producerIndex.value == 0) return false + if (tailLocal - producerIndex.value == 0) return wasStolen val index = tailLocal and MASK val element = buffer[index] ?: continue if (consumerIndex.compareAndSet(tailLocal, tailLocal + 1)) { @@ -196,24 +189,4 @@ internal class WorkQueue { } } } - - // Called only by the owner - private fun tryAddLast(task: Task): Boolean { - if (bufferSize == BUFFER_CAPACITY - 1) return false - val headLocal = producerIndex.value - val nextIndex = headLocal and MASK - - /* - * If current element is not null then we're racing with consumers for the tail. If we skip this check then - * the consumer can null out current element and it will be lost. If we're racing for tail then - * the queue is close to overflowing => it's fine to offload work to global queue - */ - if (buffer[nextIndex] != null) { - return false - } - - buffer.lazySet(nextIndex, task) - producerIndex.incrementAndGet() - return true - } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt index da6ef2051f..9dfd6a9805 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt @@ -77,27 +77,6 @@ class CoroutineDispatcherTest : SchedulerTestBase() { checkPoolThreadsCreated(2) } - @Test - fun testNoStealing() = runBlocking { - corePoolSize = CORES_COUNT - schedulerTimeSource = TestTimeSource(0L) - withContext(dispatcher) { - val thread = Thread.currentThread() - val job = async(dispatcher) { - assertEquals(thread, Thread.currentThread()) - val innerJob = async(dispatcher) { - assertEquals(thread, Thread.currentThread()) - } - innerJob.await() - } - - job.await() - assertEquals(thread, Thread.currentThread()) - } - - checkPoolThreadsCreated(1..2) - } - @Test fun testDelay() = runBlocking { corePoolSize = 2 @@ -106,7 +85,6 @@ class CoroutineDispatcherTest : SchedulerTestBase() { delay(10) expect(2) } - finish(3) checkPoolThreadsCreated(2) } @@ -129,11 +107,9 @@ class CoroutineDispatcherTest : SchedulerTestBase() { yield() } } - assertNull(nullResult) finish(4) } - checkPoolThreadsCreated(1..CORES_COUNT) } @@ -164,7 +140,6 @@ class CoroutineDispatcherTest : SchedulerTestBase() { expect(4) innerJob.join() } - outerJob.join() finish(5) } @@ -183,6 +158,5 @@ class CoroutineDispatcherTest : SchedulerTestBase() { .count { it is CoroutineScheduler.Worker && it.name.contains("SomeTestName") } assertEquals(1, count) } - } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt new file mode 100644 index 0000000000..85ae849c8e --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.scheduling + +import kotlinx.coroutines.* +import kotlinx.coroutines.scheduling.CoroutineScheduler.Companion.MAX_SUPPORTED_POOL_SIZE +import org.junit.* +import java.util.concurrent.* + +class CoroutineSchedulerLivenessStressTest : TestBase() { + private val scheduler = lazy { CoroutineScheduler(CORE_POOL_SIZE, MAX_SUPPORTED_POOL_SIZE, Long.MAX_VALUE) } + private val iterations = 1000 * stressTestMultiplier + + @After + fun tearDown() { + if (scheduler.isInitialized()) { + scheduler.value.close() + } + } + + @Test + fun testInternalSubmissions() { + Assume.assumeTrue(CORE_POOL_SIZE >= 2) + repeat(iterations) { + val barrier = CyclicBarrier(CORE_POOL_SIZE + 1) + scheduler.value.execute { + repeat(CORE_POOL_SIZE) { + scheduler.value.execute { + barrier.await() + } + } + } + barrier.await() + } + } + + @Test + fun testExternalSubmissions() { + Assume.assumeTrue(CORE_POOL_SIZE >= 2) + repeat(iterations) { + val barrier = CyclicBarrier(CORE_POOL_SIZE + 1) + repeat(CORE_POOL_SIZE) { + scheduler.value.execute { + barrier.await() + } + } + barrier.await() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt index 683a889efa..6f35e7e1b8 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt @@ -4,11 +4,10 @@ package kotlinx.coroutines.scheduling +import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* -import kotlinx.coroutines.scheduling.SchedulerTestBase.Companion.checkPoolThreadsCreated import org.junit.* -import org.junit.Ignore import org.junit.Test import java.util.concurrent.* import java.util.concurrent.atomic.* @@ -37,19 +36,27 @@ class CoroutineSchedulerStressTest : TestBase() { } @Test - @Suppress("DEPRECATION") - @Ignore // this test often fails on windows, todo: figure out how to fix it. See issue #904 - fun testExternalTasksSubmission() { - stressTest(CommonPool) - } + fun testInternalTasksSubmissionProgress() { + /* + * Run 2 million tasks and validate that + * 1) All of them are completed successfully + * 2) Every thread executed task at least once + */ + dispatcher.dispatch(EmptyCoroutineContext, Runnable { + for (i in 1..tasksNum) { + dispatcher.dispatch(EmptyCoroutineContext, ValidatingRunnable()) + } + }) - @Test - fun testInternalTasksSubmission() { - stressTest(dispatcher) + finishLatch.await() + val observed = observedThreads.size + // on slow machines not all threads can be observed + assertTrue(observed in (AVAILABLE_PROCESSORS - 1)..(AVAILABLE_PROCESSORS + 1), "Observed $observed threads with $AVAILABLE_PROCESSORS available processors") + validateResults() } @Test - fun testStealingFromBlocking() { + fun testStealingFromNonProgressing() { /* * Work-stealing stress test, * one thread submits pack of tasks, waits until they are completed (to avoid work offloading) @@ -63,10 +70,7 @@ class CoroutineSchedulerStressTest : TestBase() { while (submittedTasks < tasksNum) { ++submittedTasks - dispatcher.dispatch(EmptyCoroutineContext, Runnable { - processTask() - }) - + dispatcher.dispatch(EmptyCoroutineContext, ValidatingRunnable()) while (submittedTasks - processed.get() > 100) { Thread.yield() } @@ -82,27 +86,6 @@ class CoroutineSchedulerStressTest : TestBase() { validateResults() } - private fun stressTest(submissionInitiator: CoroutineDispatcher) { - /* - * Run 2 million tasks and validate that - * 1) All of them are completed successfully - * 2) Every thread executed task at least once - */ - submissionInitiator.dispatch(EmptyCoroutineContext, Runnable { - for (i in 1..tasksNum) { - dispatcher.dispatch(EmptyCoroutineContext, Runnable { - processTask() - }) - } - }) - - finishLatch.await() - val observed = observedThreads.size - // on slow machines not all threads can be observed - assertTrue(observed in (AVAILABLE_PROCESSORS - 1)..(AVAILABLE_PROCESSORS + 1), "Observed $observed threads with $AVAILABLE_PROCESSORS available processors") - validateResults() - } - private fun processTask() { val counter = observedThreads[Thread.currentThread()] ?: 0L observedThreads[Thread.currentThread()] = counter + 1 @@ -115,6 +98,13 @@ class CoroutineSchedulerStressTest : TestBase() { private fun validateResults() { val result = observedThreads.values.sum() assertEquals(tasksNum.toLong(), result) - checkPoolThreadsCreated(AVAILABLE_PROCESSORS) + } + + private inner class ValidatingRunnable : Runnable { + private val invoked = atomic(false) + override fun run() { + if (!invoked.compareAndSet(false, true)) error("The same runnable was invoked twice") + processTask() + } } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt index 185de71dd9..4582b6810a 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.scheduling import kotlinx.coroutines.* import org.junit.* import org.junit.Test -import java.util.* import java.util.concurrent.* import kotlin.concurrent.* import kotlin.test.* @@ -45,7 +44,7 @@ class WorkQueueStressTest : TestBase() { Thread.yield() } - producerQueue.add(task(i.toLong()), globalQueue) + producerQueue.add(task(i.toLong()))?.let { globalQueue.addLast(it) } } producerFinished = true @@ -55,12 +54,16 @@ class WorkQueueStressTest : TestBase() { threads += thread(name = "stealer $i") { val myQueue = WorkQueue() startLatch.await() - while (!producerFinished || producerQueue.size() != 0) { - myQueue.trySteal(producerQueue, stolenTasks[i]) + while (!producerFinished || producerQueue.size != 0) { + if (myQueue.size > 100) { + stolenTasks[i].addAll(myQueue.drain().map { task(it) }) + } + myQueue.tryStealFrom(victim = producerQueue) } // Drain last element which is not counted in buffer - myQueue.trySteal(producerQueue, stolenTasks[i]) + stolenTasks[i].addAll(myQueue.drain().map { task(it) }) + myQueue.tryStealFrom(producerQueue) stolenTasks[i].addAll(myQueue.drain().map { task(it) }) } } @@ -73,7 +76,6 @@ class WorkQueueStressTest : TestBase() { @Test fun testSingleProducerSingleStealer() { val startLatch = CountDownLatch(1) - val fakeQueue = Queue() threads += thread(name = "producer") { startLatch.await() for (i in 1..offerIterations) { @@ -82,7 +84,7 @@ class WorkQueueStressTest : TestBase() { } // No offloading to global queue here - producerQueue.add(task(i.toLong()), fakeQueue) + producerQueue.add(task(i.toLong())) } } @@ -91,7 +93,7 @@ class WorkQueueStressTest : TestBase() { val myQueue = WorkQueue() startLatch.await() while (stolen.size != offerIterations) { - if (myQueue.trySteal(producerQueue, stolen) != NOTHING_TO_STEAL) { + if (myQueue.tryStealFrom(producerQueue) != NOTHING_TO_STEAL) { stolen.addAll(myQueue.drain().map { task(it) }) } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt index 524eff55a1..341aa0462d 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt @@ -26,46 +26,45 @@ class WorkQueueTest : TestBase() { @Test fun testLastScheduledComesFirst() { val queue = WorkQueue() - val globalQueue = GlobalQueue() - (1L..4L).forEach { queue.add(task(it), globalQueue) } + (1L..4L).forEach { queue.add(task(it)) } assertEquals(listOf(4L, 1L, 2L, 3L), queue.drain()) } @Test - fun testWorkOffload() { + fun testAddWithOffload() { val queue = WorkQueue() - val globalQueue = GlobalQueue() - (1L..130L).forEach { queue.add(task(it), globalQueue) } - - val expectedLocalResults = (64L..129L).toMutableList() - expectedLocalResults.add(0, 130L) - assertEquals(expectedLocalResults, queue.drain()) - assertEquals((1L..63L).toList(), globalQueue.asTimeList()) + val size = 130L + val offload = GlobalQueue() + (0 until size).forEach { queue.add(task(it))?.let { t -> offload.addLast(t) } } + + val expectedResult = listOf(129L) + (0L..126L).toList() + val actualResult = queue.drain() + assertEquals(expectedResult, actualResult) + assertEquals((0L until size).toSet().minus(expectedResult), offload.drain().toSet()) } @Test fun testWorkOffloadPrecision() { val queue = WorkQueue() val globalQueue = GlobalQueue() - repeat(128) { require(queue.add(task(0), globalQueue)) } - require(globalQueue.isEmpty) - require(!queue.add(task(0), globalQueue)) - require(globalQueue.size == 63) + repeat(128) { assertNull(queue.add(task(it.toLong()))) } + assertTrue(globalQueue.isEmpty) + assertEquals(127L, queue.add(task(0))?.submissionTime) } @Test fun testStealingFromHead() { val victim = WorkQueue() - val globalQueue = GlobalQueue() - (1L..2L).forEach { victim.add(task(it), globalQueue) } + victim.add(task(1L)) + victim.add(task(2L)) timeSource.step() timeSource.step(3) val stealer = WorkQueue() - assertEquals(TASK_STOLEN, stealer.trySteal(victim, globalQueue)) + assertEquals(TASK_STOLEN, stealer.tryStealFrom(victim)) assertEquals(arrayListOf(1L), stealer.drain()) - assertEquals(TASK_STOLEN, stealer.trySteal(victim, globalQueue)) + assertEquals(TASK_STOLEN, stealer.tryStealFrom(victim)) assertEquals(arrayListOf(2L), stealer.drain()) } } @@ -90,6 +89,15 @@ internal fun WorkQueue.drain(): List { result += task.submissionTime task = poll() } + return result +} +internal fun GlobalQueue.drain(): List { + var task: Task? = removeFirstOrNull() + val result = arrayListOf() + while (task != null) { + result += task.submissionTime + task = removeFirstOrNull() + } return result } From ab30d7241ac02a3521e143a16d49a4bc9a230bf5 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 17 Oct 2019 16:02:36 +0300 Subject: [PATCH 84/90] New deterministic algorithm for working with blocking tasks Invariants: * Steal only one task per attempt to avoid missing steals that potentially may block the progress (check-park-check may miss tasks that are being stolen) * New WorkQueue.add invariant: bufferSize < capacity => add is always successful * Re-visited tests that expose a lot of problems * Ability to steal from the middle of work queue in order to steal blocking tasks with ABA prevention Changes: * Instead of "blocking workers" use "blocking tasks" state that is incremented on each blocking submit and decrement only when task is completed * On each work signalling try to compensate blocking tasks by enforcinf invariant "created threads == blocking tasks + up to core size workers" * Now if worker was not spuriously woken up, it has a task dedicated for him that should be found. For that reason attempt to steal blocking tasks (that may be in the middle of the work queue). Additionally, instead of scanning the whole global queue, just split it in two (one for blocking, one for regular tasks) * Get rid of conditional remove from the global queue * Avoid excessive unparks for threads that are not yet able to steal the task due to workstealing resolution: do not add such workers to the stack --- gradle.properties | 2 +- .../common/src/internal/LockFreeTaskQueue.kt | 14 +- .../jvm/src/scheduling/CoroutineScheduler.kt | 426 ++++++++---------- .../jvm/src/scheduling/Tasks.kt | 16 +- .../jvm/src/scheduling/WorkQueue.kt | 152 ++++--- ...gCoroutineDispatcherLivenessStressTest.kt} | 9 +- ...outineDispatcherMixedStealingStressTest.kt | 80 ++++ .../BlockingCoroutineDispatcherTest.kt | 21 +- ...oroutineDispatcherThreadLimitStressTest.kt | 71 +++ ...tineDispatcherWorkSignallingStressTest.kt} | 74 +-- .../scheduling/CoroutineDispatcherTest.kt | 31 +- .../CoroutineSchedulerLivenessStressTest.kt | 2 +- .../CoroutineSchedulerShrinkTest.kt | 124 ----- .../CoroutineSchedulerStressTest.kt | 8 +- .../jvm/test/scheduling/SchedulerTestBase.kt | 32 +- .../test/scheduling/WorkQueueStressTest.kt | 14 +- .../jvm/test/scheduling/WorkQueueTest.kt | 11 - 17 files changed, 511 insertions(+), 576 deletions(-) rename kotlinx-coroutines-core/jvm/test/scheduling/{BlockingCoroutineDispatcherRaceStressTest.kt => BlockingCoroutineDispatcherLivenessStressTest.kt} (86%) create mode 100644 kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt create mode 100644 kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherThreadLimitStressTest.kt rename kotlinx-coroutines-core/jvm/test/scheduling/{BlockingCoroutineDispatcherStressTest.kt => BlockingCoroutineDispatcherWorkSignallingStressTest.kt} (60%) delete mode 100644 kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerShrinkTest.kt diff --git a/gradle.properties b/gradle.properties index 1d3093f227..63a9c67783 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.3.2-sched10 +version=1.3.2-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.3.60 diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt index 68723104e3..0cfb000f31 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt @@ -54,12 +54,9 @@ internal open class LockFreeTaskQueue( } @Suppress("UNCHECKED_CAST") - fun removeFirstOrNull(): E? = removeFirstOrNullIf { true } - - @Suppress("UNCHECKED_CAST") - inline fun removeFirstOrNullIf(predicate: (E) -> Boolean): E? { + fun removeFirstOrNull(): E? { _cur.loop { cur -> - val result = cur.removeFirstOrNullIf(predicate) + val result = cur.removeFirstOrNull() if (result !== Core.REMOVE_FROZEN) return result as E? _cur.compareAndSet(cur, cur.next()) } @@ -164,10 +161,7 @@ internal class LockFreeTaskQueueCore( } // REMOVE_FROZEN | null (EMPTY) | E (SUCCESS) - fun removeFirstOrNull(): Any? = removeFirstOrNullIf { true } - - // REMOVE_FROZEN | null (EMPTY) | E (SUCCESS) - inline fun removeFirstOrNullIf(predicate: (E) -> Boolean): Any? { + fun removeFirstOrNull(): Any? { _state.loop { state -> if (state and FROZEN_MASK != 0L) return REMOVE_FROZEN // frozen -- cannot modify state.withState { head, tail -> @@ -182,8 +176,6 @@ internal class LockFreeTaskQueueCore( // element == Placeholder can only be when add has not finished yet if (element is Placeholder) return null // consider it not added yet // now we tentative know element to remove -- check predicate - @Suppress("UNCHECKED_CAST") - if (!predicate(element as E)) return null // we cannot put null into array here, because copying thread could replace it with Placeholder and that is a disaster val newHead = (head + 1) and MAX_CAPACITY_MASK if (_state.compareAndSet(state, state.updateHead(newHead))) { diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 0b0061993b..0fc9259a97 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -8,7 +8,6 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import java.io.* -import java.lang.AssertionError import java.util.concurrent.* import java.util.concurrent.atomic.* import java.util.concurrent.locks.* @@ -16,8 +15,8 @@ import kotlin.math.* import kotlin.random.* /** - * Coroutine scheduler (pool of shared threads) which primary target is to distribute dispatched coroutines over worker threads, - * including both CPU-intensive and blocking tasks. + * Coroutine scheduler (pool of shared threads) which primary target is to distribute dispatched coroutines + * over worker threads, including both CPU-intensive and blocking tasks, is the most efficient manner. * * Current scheduler implementation has two optimization targets: * * Efficiency in the face of communication patterns (e.g., actors communicating via channel) @@ -25,16 +24,17 @@ import kotlin.random.* * * ### Structural overview * - * Scheduler consists of [corePoolSize] worker threads to execute CPU-bound tasks and up to [maxPoolSize] (lazily created) threads + * Scheduler consists of [corePoolSize] worker threads to execute CPU-bound tasks and up to [maxPoolSize] lazily created threads * to execute blocking tasks. Every worker a has local queue in addition to a global scheduler queue and the global queue - * has priority over local queue to avoid starvation of externally-submitted (e.g. from Android UI thread) tasks. Work-stealing is implemented - * on top of that queues to provide even load distribution and illusion of centralized run queue. + * has priority over local queue to avoid starvation of externally-submitted (e.g. from Android UI thread) tasks. + * Work-stealing is implemented on top of that queues to provide even load distribution and illusion of centralized run queue. * - * ### Scheduling + * ### Scheduling policy * * When a coroutine is dispatched from within a scheduler worker, it's placed into the head of worker run queue. * If the head is not empty, the task from the head is moved to the tail. Though it is unfair scheduling policy, - * it effectively couples communicating coroutines into one and eliminates scheduling latency that arises from placing task to the end of the queue. + * it effectively couples communicating coroutines into one and eliminates scheduling latency + * that arises from placing task to the end of the queue. * Placing former head to the tail is necessary to provide semi-FIFO order, otherwise queue degenerates to stack. * When a coroutine is dispatched from an external thread, it's put into the global queue. * @@ -45,19 +45,24 @@ import kotlin.random.* * (based on the value of [WORK_STEALING_TIME_RESOLUTION_NS]). * For this purpose monotonic global clock ([System.nanoTime]) is used and every task has associated with it submission time. * This approach shows outstanding results when coroutines are cooperative, but as downside scheduler now depends on high-resolution global clock - * which may limit scalability on NUMA machines. - * Tasks from LIFO buffer can be stolen on a regular basis. + * which may limit scalability on NUMA machines. Tasks from LIFO buffer can be stolen on a regular basis. + * + * ### Thread management + * One of the hardest parts of the scheduler is decentralized management of the threads with the progress guarantees similar + * to the regular centralized executors. The state of the threads consists of [controlState] and [parkedWorkersStack] fields. + * The former field incorporates the amount of created threads, CPU-tokens and blocking tasks that require a thread compensation, + * while the latter represents intrusive versioned Treiber stack of idle workers. + * When a worker cannot find any work, he first adds itself to the stack, then re-scans the queue (to avoid missing signal) + * and then attempts to park itself (there is additional layer of signalling against unnecessary park/unpark). + * If worker finds a task that it cannot yet steal due to timer constraints, it stores this fact in its state + * (to be uncounted when additional work is signalled) and parks for such duration. + * + * When a new task arrives to the scheduler (whether it's local or global queue), either an idle worker is being signalled, or + * a new worker is attempted to be created (only [corePoolSize] workers can be created for regular CPU tasks). * * ### Dynamic resizing and support of blocking tasks * - * To support possibly blocking tasks [TaskMode] and CPU quota (via cpu permits in control state) are used. - * To execute [TaskMode.NON_BLOCKING] tasks from the global queue or to steal tasks from other workers - * the worker should have CPU permit. When a worker starts executing [TaskMode.PROBABLY_BLOCKING] task, - * it releases its CPU permit, giving a hint to a scheduler that additional thread should be created (or awaken) - * if new [TaskMode.NON_BLOCKING] task will arrive. When a worker finishes executing blocking task, it executes - * all tasks from its local queue (including [TaskMode.NON_BLOCKING]) and then parks as retired without polling - * global queue or trying to steal new tasks. Such approach may slightly limit scalability (allowing more than [corePoolSize] threads - * to execute CPU-bound tasks at once), but in practice, it is not, significantly reducing context switches and tasks re-dispatching. + * TODO */ @Suppress("NOTHING_TO_INLINE") internal class CoroutineScheduler( @@ -82,12 +87,22 @@ internal class CoroutineScheduler( } @JvmField - val globalQueue: GlobalQueue = GlobalQueue() + val globalCpuQueue = GlobalQueue() + @JvmField + val globalBlockingQueue = GlobalQueue() + + private fun addToGlobalQueue(task: Task): Boolean { + return if (task.isBlocking) { + globalBlockingQueue.addLast(task) + } else { + globalCpuQueue.addLast(task) + } + } /** * The stack of parker workers. - * Every worker registers itself in a stack before parking (if it was not previously registered) - * and callers of [requestCpuWorker] will try to unpark a thread from the top of a stack. + * Every worker registers itself in a stack before parking (if it was not previously registered), + * so it can be signalled when new tasks arrive. * This is a form of intrusive garbage-free Treiber stack where [Worker] also is a stack node. * * The stack is better than a queue (even with the contention on top) because it unparks threads @@ -215,8 +230,7 @@ internal class CoroutineScheduler( * State of worker threads. * [workers] is array of lazily created workers up to [maxPoolSize] workers. * [createdWorkers] is count of already created workers (worker with index lesser than [createdWorkers] exists). - * [blockingWorkers] is count of running workers which are executing [TaskMode.PROBABLY_BLOCKING] task. - * All mutations of array's content are guarded by lock. + * [blockingTasks] is count of pending (either in the queue or being executed) tasks * * **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 @@ -231,38 +245,33 @@ internal class CoroutineScheduler( */ private val controlState = atomic(corePoolSize.toLong() shl CPU_PERMITS_SHIFT) private val createdWorkers: Int inline get() = (controlState.value and CREATED_MASK).toInt() - private val blockingWorkers: Int inline get() = (controlState.value and BLOCKING_MASK shr BLOCKING_SHIFT).toInt() private val availableCpuPermits: Int inline get() = (controlState.value and CPU_PERMITS_MASK shr CPU_PERMITS_SHIFT).toInt() private inline fun createdWorkers(state: Long): Int = (state and CREATED_MASK).toInt() - private inline fun blockingWorkers(state: Long): Int = (state and BLOCKING_MASK shr BLOCKING_SHIFT).toInt() - private inline fun availablePermits(state: Long): Int = (state and CPU_PERMITS_MASK shr CPU_PERMITS_SHIFT).toInt() + private inline fun blockingTasks(state: Long): Int = (state and BLOCKING_MASK shr BLOCKING_SHIFT).toInt() + public inline fun availableCpuPermits(state: Long): Int = (state and CPU_PERMITS_MASK shr CPU_PERMITS_SHIFT).toInt() // Guarded by synchronization private inline fun incrementCreatedWorkers(): Int = createdWorkers(controlState.incrementAndGet()) private inline fun decrementCreatedWorkers(): Int = createdWorkers(controlState.getAndDecrement()) - private inline fun incrementBlockingWorkers() { - controlState.addAndGet(1L shl BLOCKING_SHIFT) - } + private inline fun incrementBlockingTasks() = controlState.addAndGet(1L shl BLOCKING_SHIFT) - private inline fun decrementBlockingWorkers() { + private inline fun decrementBlockingTasks() { controlState.addAndGet(-(1L shl BLOCKING_SHIFT)) } private inline fun tryAcquireCpuPermit(): Boolean { while (true) { val state = controlState.value - val available = availablePermits(state) + val available = availableCpuPermits(state) if (available == 0) return false val update = state - (1L shl CPU_PERMITS_SHIFT) if (controlState.compareAndSet(state, update)) return true } } - private inline fun releaseCpuPermit() { - controlState.addAndGet(1L shl CPU_PERMITS_SHIFT) - } + private inline fun releaseCpuPermit() = controlState.addAndGet(1L shl CPU_PERMITS_SHIFT) // This is used a "stop signal" for close and shutdown functions private val _isTerminated = atomic(false) @@ -277,6 +286,7 @@ internal class CoroutineScheduler( private const val FORBIDDEN = -1 private const val ALLOWED = 0 private const val TERMINATED = 1 + private const val PARKED = 1 // Masks of control state private const val BLOCKING_SHIFT = 21 // 2M threads max @@ -316,14 +326,18 @@ internal class CoroutineScheduler( } val state = worker.state assert { state === WorkerState.TERMINATED } // Expected TERMINATED state - worker.localQueue.offloadAllWork(globalQueue) + worker.localQueue.offloadAllWorkTo(globalBlockingQueue) // Doesn't actually matter which queue to use } } // Make sure no more work is added to GlobalQueue from anywhere - globalQueue.close() + globalBlockingQueue.close() + globalCpuQueue.close() // Finish processing tasks from globalQueue and/or from this worker's local queue while (true) { - val task = currentWorker?.findTask() ?: globalQueue.removeFirstOrNull() ?: break + val task = currentWorker?.findTask() + ?: globalCpuQueue.removeFirstOrNull() + ?: globalBlockingQueue.removeFirstOrNull() + ?: break runSafely(task) } // Shutdown current thread @@ -338,7 +352,6 @@ internal class CoroutineScheduler( * Dispatches execution of a runnable [block] with a hint to a scheduler whether * this [block] may execute blocking operations (IO, system calls, locking primitives etc.) * - * @param block runnable to be dispatched * @param taskContext concurrency context of given [block] * @param fair whether the task should be dispatched fairly (strict FIFO) or not (semi-FIFO) */ @@ -346,13 +359,19 @@ internal class CoroutineScheduler( trackTask() // this is needed for virtual time support val task = createTask(block, taskContext) // try to submit the task to the local queue and act depending on the result - if (!submitToLocalQueue(task, fair)) { - if (!globalQueue.addLast(task)) { + val notAdded = submitToLocalQueue(task, fair) + if (notAdded != null) { + if (!addToGlobalQueue(notAdded)) { // Global queue is closed in the last step of close/shutdown -- no more tasks should be accepted throw RejectedExecutionException("$schedulerName was terminated") } } - requestCpuWorker() + // Checking 'task' instead of 'notAdded' is completely okay + if (task.mode == TaskMode.NON_BLOCKING) { + signalCpuWork() + } else { + signalBlockingWork() + } } internal fun createTask(block: Runnable, taskContext: TaskContext): Task { @@ -365,33 +384,24 @@ internal class CoroutineScheduler( return TaskImpl(block, nanoTime, taskContext) } - /** - * Unparks or creates a [Worker] for executing non-blocking tasks if there are idle cores - */ - internal fun requestCpuWorker() { - // No CPU available -- nothing to request - if (availableCpuPermits == 0) { - tryUnpark() - return - } - /* - * Fast path -- we have retired or parked worker, unpark it and we're done. - * The data race here: when only one permit is available, multiple retired workers - * can be unparked, but only one will continue execution, so we're overproviding with threads - * in case of race to avoid spurious starvation - */ + private fun signalBlockingWork() { + // Use state snapshot to avoid thread overprovision + val stateSnapshot = incrementBlockingTasks() if (tryUnpark()) return - /* - * Create a thread. - * It's not preferable to use 'cpuWorkersCounter' here (moreover, it's implicitly here as corePoolSize - cpuPermits.availableTokens), - * cpuWorkersCounter doesn't take into account threads which are created (and either running or parked), but haven't - * CPU token: retiring workers, recently unparked workers before `findTask` call, etc. - * So if we will use cpuWorkersCounter, we start to overprovide with threads too much. - */ - val state = controlState.value + if (tryCreateWorker(stateSnapshot)) return + tryUnpark() // Try unpark again in case there was race between permit release and parking + } + + internal fun signalCpuWork() { + if (tryUnpark()) return + if (tryCreateWorker()) return + tryUnpark() + } + + private fun tryCreateWorker(state: Long = controlState.value): Boolean { val created = createdWorkers(state) - val blocking = blockingWorkers(state) - val cpuWorkers = created - blocking + val blocking = blockingTasks(state) + val cpuWorkers = (created - blocking).coerceAtLeast(0) /* * We check how many threads are there to handle non-blocking work, * and create one more if we have not enough of them. @@ -401,21 +411,18 @@ internal class CoroutineScheduler( // If we've created the first cpu worker and corePoolSize > 1 then create // one more (second) cpu worker, so that stealing between them is operational if (newCpuWorkers == 1 && corePoolSize > 1) createNewWorker() - if (newCpuWorkers > 0) return + if (newCpuWorkers > 0) return true } - // Try unpark again in case there was race between permit release and parking - tryUnpark() + return false } private fun tryUnpark(): Boolean { while (true) { val worker = parkedWorkersStackPop() ?: return false - val time = worker.minDelayUntilStealableTask // TODO explain - worker.parkingAllowed = false - if (worker.signallingAllowed && time == 0L) { + if (!worker.parkingState.compareAndSet(ALLOWED, FORBIDDEN)) { LockSupport.unpark(worker) } - if (time == 0L && worker.tryForbidTermination()) return true + if (worker.tryForbidTermination()) return true } } @@ -429,11 +436,11 @@ internal class CoroutineScheduler( if (isTerminated) return -1 val state = controlState.value val created = createdWorkers(state) - val blocking = blockingWorkers(state) - val cpuWorkers = created - blocking + val blocking = blockingTasks(state) + val cpuWorkers = (created - blocking).coerceAtLeast(0) // Double check for overprovision if (cpuWorkers >= corePoolSize) return 0 - if (created >= maxPoolSize || availableCpuPermits == 0) return 0 + if (created >= maxPoolSize) return 0 // start & register new worker, commit index only after successful creation val newIndex = createdWorkers + 1 require(newIndex > 0 && workers[newIndex] == null) @@ -451,24 +458,22 @@ internal class CoroutineScheduler( } /** - * Returns `true` if added, `false` otherwise + * Returns `null` if task was successfully added or an instance of the + * task that was not added or replaced (thus should be added to global queue). */ - private fun submitToLocalQueue(task: Task, fair: Boolean): Boolean { - val worker = currentWorker() ?: return false - + private fun submitToLocalQueue(task: Task, fair: Boolean): Task? { + val worker = currentWorker() ?: return task /* * This worker could have been already terminated from this thread by close/shutdown and it should not * accept any more tasks into its local queue. */ - if (worker.state === WorkerState.TERMINATED) return false + if (worker.state === WorkerState.TERMINATED) return task + // Do not add CPU tasks in local queue if we are not able to execute it + // TODO discuss: maybe add it to the local queue and offload back in the global queue iff permit wasn't acquired? if (task.mode == TaskMode.NON_BLOCKING && worker.isBlocking) { - return false + return task } - val notAdded = with(worker.localQueue) { - if (fair) addLast(task) else add(task) - } ?: return true // Forgive me, Father, for this formatting - globalQueue.addLast(notAdded) - return true + return worker.localQueue.add(task, fair = fair) } private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this } @@ -479,15 +484,16 @@ internal class CoroutineScheduler( * * State of the queues: * b for blocking, c for CPU, r for retiring. - * E.g. for [1b, 1b, 2c, 1r] means that pool has + * E.g. for [1b, 1b, 2c, 1d] means that pool has * two blocking workers with queue size 1, one worker with CPU permit and queue size 1 - * and one retiring (executing his local queue before parking) worker with queue size 1. + * and one dormant (executing his local queue before parking) worker with queue size 1. + * TODO revisit */ override fun toString(): String { var parkedWorkers = 0 var blockingWorkers = 0 var cpuWorkers = 0 - var retired = 0 + var dormant = 0 var terminated = 0 val queueSizes = arrayListOf() for (index in 1 until workers.length()) { @@ -503,9 +509,9 @@ internal class CoroutineScheduler( ++cpuWorkers queueSizes += queueSize.toString() + "c" // CPU } - WorkerState.RETIRING -> { - ++retired - if (queueSize > 0) queueSizes += queueSize.toString() + "r" // Retiring + WorkerState.DORMANT -> { + ++dormant + if (queueSize > 0) queueSizes += queueSize.toString() + "d" // Retiring } WorkerState.TERMINATED -> ++terminated } @@ -519,17 +525,19 @@ internal class CoroutineScheduler( "CPU = $cpuWorkers, " + "blocking = $blockingWorkers, " + "parked = $parkedWorkers, " + - "retired = $retired, " + + "retired = $dormant, " + "terminated = $terminated}, " + "running workers queues = $queueSizes, "+ - "global queue size = ${globalQueue.size}, " + + "global CPU queue size = ${globalCpuQueue.size}, " + + "global blocking queue size = ${globalBlockingQueue.size}, " + "Control State Workers {" + "created = ${createdWorkers(state)}, " + - "blocking = ${blockingWorkers(state)}}" + - "]" + "blocking = ${blockingTasks(state)}, " + + "CPU acquired = ${corePoolSize - availableCpuPermits(state)}" + + "}]" } - internal fun runSafely(task: Task) { + fun runSafely(task: Task) { try { task.run() } catch (e: Throwable) { @@ -557,17 +565,17 @@ internal class CoroutineScheduler( indexInArray = index } - val scheduler get() = this@CoroutineScheduler + inline val scheduler get() = this@CoroutineScheduler + @JvmField val localQueue: WorkQueue = WorkQueue() /** * Worker state. **Updated only by this worker thread**. - * By default, worker is in RETIRING state in the case when it was created, but all CPU tokens or tasks were taken. + * By default, worker is in DORMANT state in the case when it was created, but all CPU tokens or tasks were taken. */ @Volatile - var state = WorkerState.RETIRING - val isParking: Boolean get() = state == WorkerState.PARKING + var state = WorkerState.DORMANT val isBlocking: Boolean get() = state == WorkerState.BLOCKING /** @@ -597,11 +605,15 @@ internal class CoroutineScheduler( */ @Volatile var nextParkedWorker: Any? = NOT_IN_STACK - @Volatile // TODO ughm don't ask - var parkingAllowed = false - @Volatile - var signallingAllowed = false + /* + * The delay until at least one task in other worker queues will become stealable. + */ + private var minDelayUntilStealableTaskNs = 0L + // ALLOWED | PARKED | FORBIDDEN + val parkingState = atomic(ALLOWED) + + private var rngState = Random.nextInt() /** * Tries to set [terminationState] to [FORBIDDEN], returns `false` if this attempt fails. * This attempt may fail either because worker terminated itself or because someone else @@ -620,9 +632,9 @@ internal class CoroutineScheduler( /** * Tries to acquire CPU token if worker doesn't have one - * @return whether worker has CPU token + * @return whether worker acquired (or already had) CPU token */ - fun tryAcquireCpuPermit(): Boolean { + private fun tryAcquireCpuPermit(): Boolean { return when { state == WorkerState.CPU_ACQUIRED -> true this@CoroutineScheduler.tryAcquireCpuPermit() -> { @@ -634,8 +646,8 @@ internal class CoroutineScheduler( } /** - * Releases CPU token if worker has any and changes state to [newState] - * @return whether worker had CPU token + * Releases CPU token if worker has any and changes state to [newState]. + * Returns `true` if CPU permit was returned to the pool */ internal fun tryReleaseCpu(newState: WorkerState): Boolean { val previousState = state @@ -645,107 +657,90 @@ internal class CoroutineScheduler( return hadCpu } - /** - * Time of the last call to [requestCpuWorker] due to missing tasks deadlines. - * Used as throttling mechanism to avoid unparking multiple threads when it's not necessary - */ - private var lastExhaustionTime = 0L - - private var rngState = Random.nextInt() - /* - * The delay until at least one task in other worker queues will become stealable. - * Volatile to avoid benign data-race - */ - @Volatile - public var minDelayUntilStealableTask = 0L - override fun run() = runWorker() private fun runWorker() { + var rescanned = false while (!isTerminated && state != WorkerState.TERMINATED) { val task = findTask() // Task found. Execute and repeat if (task != null) { + rescanned = false + minDelayUntilStealableTaskNs = 0L executeTask(task) continue } - /* * No tasks were found: * 1) Either at least one of the workers has stealable task in its FIFO-buffer with a stealing deadline. * Then its deadline is stored in [minDelayUntilStealableTask] + * + * Then just park for that duration (ditto re-scanning). + * While it could potentially lead to short (up to WORK_STEALING_TIME_RESOLUTION_NS ns) starvations, + * excess unparks and managing "one unpark per signalling" invariant become unfeasible, instead we are going to resolve + * it with "spinning via scans" mechanism. + */ + if (minDelayUntilStealableTaskNs != 0L) { + if (!rescanned) { + rescanned = true + continue + } else { + tryReleaseCpu(WorkerState.PARKING) + LockSupport.parkNanos(minDelayUntilStealableTaskNs) + minDelayUntilStealableTaskNs = 0L + } + } + /* * 2) No tasks available, time to park and, potentially, shut down the thread. * - * In both cases, worker adds itself to the stack of parked workers, re-scans all the queues + * Add itself to the stack of parked workers, re-scans all the queues * to avoid missing wake-up (requestCpuWorker) and either starts executing discovered tasks or parks itself awaiting for new tasks. - * - * Park duration depends on the possible state: either this is the idleWorkerKeepAliveNs or stealing deadline. */ - parkingAllowed = true + parkingState.value = ALLOWED if (parkedWorkersStackPush(this)) { continue } else { - signallingAllowed = true - if (parkingAllowed) { - tryReleaseCpu(WorkerState.PARKING) - if (minDelayUntilStealableTask > 0) { - LockSupport.parkNanos(minDelayUntilStealableTask) // No spurious wakeup check here - } else { - assert { localQueue.size == 0 } - park() + assert { localQueue.size == 0 } + tryReleaseCpu(WorkerState.PARKING) + interrupted() // Cleanup interruptions + while (inStack()) { // Prevent spurious wakeups + if (isTerminated) break + if (!parkingState.compareAndSet(ALLOWED, PARKED)) { + break } + park() } - signallingAllowed = false } } tryReleaseCpu(WorkerState.TERMINATED) } + private fun inStack(): Boolean = nextParkedWorker !== NOT_IN_STACK + private fun executeTask(task: Task) { val taskMode = task.mode idleReset(taskMode) - beforeTask(taskMode, task.submissionTime) + beforeTask(taskMode) runSafely(task) afterTask(taskMode) } - private fun beforeTask(taskMode: TaskMode, taskSubmissionTime: Long) { - if (taskMode != TaskMode.NON_BLOCKING) { - /* - * We should release CPU *before* checking for CPU starvation, - * otherwise requestCpuWorker() will not count current thread as blocking - */ - incrementBlockingWorkers() - if (tryReleaseCpu(WorkerState.BLOCKING)) { - requestCpuWorker() - } - return - } - /* - * If we have idle CPU and the current worker is exhausted, wake up one more worker. - * Check last exhaustion time to avoid the race between steal and next task execution - */ - if (availableCpuPermits == 0) { - return - } - val now = schedulerTimeSource.nanoTime() - if (now - taskSubmissionTime >= WORK_STEALING_TIME_RESOLUTION_NS && - now - lastExhaustionTime >= WORK_STEALING_TIME_RESOLUTION_NS * 5 - ) { - lastExhaustionTime = now - requestCpuWorker() + private fun beforeTask(taskMode: TaskMode) { + if (taskMode == TaskMode.NON_BLOCKING) return + // Always notify about new work when releasing CPU-permit to execute some blocking task + if (tryReleaseCpu(WorkerState.BLOCKING)) { + signalCpuWork() } } private fun afterTask(taskMode: TaskMode) { - if (taskMode != TaskMode.NON_BLOCKING) { - decrementBlockingWorkers() - val currentState = state - // Shutdown sequence of blocking dispatcher - if (currentState !== WorkerState.TERMINATED) { - assert { (currentState == WorkerState.BLOCKING).also { if (!it) throw AssertionError("AAA: $currentState") } } // "Expected BLOCKING state, but has $currentState" - state = WorkerState.RETIRING - } + if (taskMode == TaskMode.NON_BLOCKING) return + decrementBlockingTasks() + val currentState = state + // Shutdown sequence of blocking dispatcher + if (currentState !== WorkerState.TERMINATED) { + assert { currentState == WorkerState.BLOCKING } // "Expected BLOCKING state, but has $currentState" + state = WorkerState.DORMANT } } @@ -772,7 +767,7 @@ internal class CoroutineScheduler( // set termination deadline the first time we are here (it is reset in idleReset) if (terminationDeadline == 0L) terminationDeadline = System.nanoTime() + idleWorkerKeepAliveNs // actually park - if (!doPark(idleWorkerKeepAliveNs)) return + LockSupport.parkNanos(idleWorkerKeepAliveNs) // try terminate when we are idle past termination deadline // note that comparison is written like this to protect against potential nanoTime wraparound if (System.nanoTime() - terminationDeadline >= 0) { @@ -781,16 +776,6 @@ internal class CoroutineScheduler( } } - private fun doPark(nanos: Long): Boolean { - /* - * Here we are trying to park, then check whether there are new blocking tasks - * (because submitting thread could have missed this thread in tryUnpark) - */ - if (!blockingQuiescence()) return false - LockSupport.parkNanos(nanos) // Spurious wakeup check in [park] - return true - } - /** * Stops execution of current thread and removes it from [createdWorkers]. */ @@ -800,8 +785,6 @@ internal class CoroutineScheduler( if (isTerminated) return // Someone else terminated, bail out if (createdWorkers <= corePoolSize) return - // Try to find blocking task before termination - if (!blockingQuiescence()) return /* * See tryUnpark for state reasoning. * If this CAS fails, then we were successfully unparked by other worker and cannot terminate. @@ -849,21 +832,6 @@ internal class CoroutineScheduler( state = WorkerState.TERMINATED } - /** - * Checks whether new blocking tasks arrived to the pool when worker decided - * it can go to deep park/termination and puts recently arrived task to its local queue. - * Returns `true` if there is no blocking tasks in the queue. - * Invariant: invoked only with empty local queue - */ - private fun blockingQuiescence(): Boolean { - assert { localQueue.size == 0 } - globalQueue.removeFirstWithModeOrNull(TaskMode.PROBABLY_BLOCKING)?.let { - localQueue.add(it) - return false - } - return true - } - // It is invoked by this worker when it finds a task private fun idleReset(mode: TaskMode) { terminationDeadline = 0L // reset deadline for termination @@ -873,38 +841,36 @@ internal class CoroutineScheduler( } } - internal fun findTask(): Task? { - if (tryAcquireCpuPermit()) return findTaskWithCpuPermit() - /* - * If the local queue is empty, try to extract blocking task from global queue. - * It's helpful for two reasons: - * 1) We won't call excess park/unpark here and someone's else CPU token won't be transferred, - * which is a performance win - * 2) It helps with rare race when external submitter sends depending blocking tasks - * one by one and one of the requested workers may miss CPU token - */ - return localQueue.poll() ?: globalQueue.removeFirstWithModeOrNull(TaskMode.PROBABLY_BLOCKING) + fun findTask(): Task? { + if (tryAcquireCpuPermit()) return findAnyTask() + // If we can't acquire a CPU permit -- attempt to find blocking task + val task = localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull() + return task ?: trySteal(blockingOnly = true) } - private fun findTaskWithCpuPermit(): Task? { + private fun findAnyTask(): Task? { /* - * Anti-starvation mechanism: if pool is overwhelmed by external work - * or local work is frequently offloaded, global queue polling will - * starve tasks from local queue. But if we never poll global queue, - * then local tasks may starve global queue, so poll global queue - * once per two core pool size iterations. - * Poll global queue only for non-blocking tasks as for blocking task a separate thread was woken up. - * If current thread is woken up, then its local queue is empty and it will poll global queue anyway, - * otherwise current thread may already have blocking task in its local queue. + * Anti-starvation mechanism: probabilistically poll either local + * or global queue to ensure progress for both external and internal tasks. */ val globalFirst = nextInt(2 * corePoolSize) == 0 - if (globalFirst) globalQueue.removeFirstWithModeOrNull(TaskMode.NON_BLOCKING)?.let { return it } + if (globalFirst) pollGlobalQueues()?.let { return it } localQueue.poll()?.let { return it } - if (!globalFirst) globalQueue.removeFirstOrNull()?.let { return it } - return trySteal() + if (!globalFirst) pollGlobalQueues()?.let { return it } + return trySteal(blockingOnly = false) } - private fun trySteal(): Task? { + private fun pollGlobalQueues(): Task? { + if (nextInt(2) == 0) { + globalCpuQueue.removeFirstOrNull()?.let { return it } + return globalBlockingQueue.removeFirstOrNull() + } else { + globalBlockingQueue.removeFirstOrNull()?.let { return it } + return globalCpuQueue.removeFirstOrNull() + } + } + + private fun trySteal(blockingOnly: Boolean): Task? { assert { localQueue.size == 0 } val created = createdWorkers // 0 to await an initialization and 1 to avoid excess stealing on single-core machines @@ -920,7 +886,11 @@ internal class CoroutineScheduler( val worker = workers[currentIndex] if (worker !== null && worker !== this) { assert { localQueue.size == 0 } - val stealResult = localQueue.tryStealFrom(victim = worker.localQueue) + val stealResult = if (blockingOnly) { + localQueue.tryStealBlockingFrom(victim = worker.localQueue) + } else { + localQueue.tryStealFrom(victim = worker.localQueue) + } if (stealResult == TASK_STOLEN) { return localQueue.poll() } else if (stealResult > 0) { @@ -928,14 +898,14 @@ internal class CoroutineScheduler( } } } - minDelayUntilStealableTask = if (minDelay != Long.MAX_VALUE) minDelay else 0 + minDelayUntilStealableTaskNs = if (minDelay != Long.MAX_VALUE) minDelay else 0 return null } } enum class WorkerState { /** - * Has CPU token and either executes [TaskMode.NON_BLOCKING] task or tries to steal one + * Has CPU token and either executes [TaskMode.NON_BLOCKING] task or tries to find one. */ CPU_ACQUIRED, @@ -952,7 +922,7 @@ internal class CoroutineScheduler( /** * Tries to execute its local work and then goes to infinite sleep as no longer needed worker. */ - RETIRING, + DORMANT, /** * Terminal state, will no longer be used diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt index d7cb64ab2a..c6ce78face 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt @@ -19,11 +19,6 @@ internal val WORK_STEALING_TIME_RESOLUTION_NS = systemProp( "kotlinx.coroutines.scheduler.resolution.ns", 100000L ) -@JvmField -internal val QUEUE_SIZE_OFFLOAD_THRESHOLD = systemProp( - "kotlinx.coroutines.scheduler.offload.threshold", 96, maxValue = BUFFER_CAPACITY -) - @JvmField internal val BLOCKING_DEFAULT_PARALLELISM = systemProp( "kotlinx.coroutines.scheduler.blocking.parallelism", 16 @@ -50,7 +45,7 @@ internal val MAX_POOL_SIZE = systemProp( @JvmField internal val IDLE_WORKER_KEEP_ALIVE_NS = TimeUnit.SECONDS.toNanos( - systemProp("kotlinx.coroutines.scheduler.keep.alive.sec", 5L) + systemProp("kotlinx.coroutines.scheduler.keep.alive.sec", 100000L) ) @JvmField @@ -87,9 +82,11 @@ internal abstract class Task( @JvmField var taskContext: TaskContext ) : Runnable { constructor() : this(0, NonBlockingContext) - val mode: TaskMode get() = taskContext.taskMode + inline val mode: TaskMode get() = taskContext.taskMode } +internal inline val Task.isBlocking get() = taskContext.taskMode == TaskMode.PROBABLY_BLOCKING + // Non-reusable Task implementation to wrap Runnable instances that do not otherwise implement task internal class TaskImpl( @JvmField val block: Runnable, @@ -109,10 +106,7 @@ internal class TaskImpl( } // Open for tests -internal open class GlobalQueue : LockFreeTaskQueue(singleConsumer = false) { - public fun removeFirstWithModeOrNull(mode: TaskMode): Task? = - removeFirstOrNullIf { it.mode == mode } -} +internal class GlobalQueue : LockFreeTaskQueue(singleConsumer = false) internal abstract class TimeSource { abstract fun nanoTime(): Long diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt index a2612b4a83..3471a1aae4 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt @@ -26,15 +26,13 @@ internal const val NOTHING_TO_STEAL = -2L * that these two (current one and submitted) are communicating and sharing state thus making such communication extremely fast. * E.g. submitted jobs [1, 2, 3, 4] will be executed in [4, 1, 2, 3] order. * - * ### Work offloading - * - * When the queue is full, half of existing tasks are offloaded to global queue which is regularly polled by other pool workers. - * Offloading occurs in LIFO order for the sake of implementation simplicity: offloads should be extremely rare and occurs only in specific use-cases - * (e.g. when coroutine starts heavy fork-join-like computation), so fairness is not important. - * As an alternative, offloading directly to some [CoroutineScheduler.Worker] may be used, but then the strategy of selecting any idle worker - * should be implemented and implementation should be aware multiple producers. - * - * @suppress **This is unstable API and it is subject to change.** + * ### Algorithm and implementation details + * This is a regular SPMC bounded queue with the additional property that tasks can be removed from the middle of the queue + * (scheduler workers without a CPU permit steal blocking tasks via this mechanism). Such property enforces us to use CAS in + * order to properly claim value from the buffer. + * Moreover, [Task] objects are reusable, so it may seem that this queue is prone to ABA problem. + * Indeed it formally has ABA-problem, but the whole processing logic is written in the way that such ABA is harmless. + * "I have discovered a truly marvelous proof of this, which this margin is too narrow to contain" */ internal class WorkQueue { @@ -58,10 +56,12 @@ internal class WorkQueue { private val producerIndex = atomic(0) private val consumerIndex = atomic(0) + // Shortcut to avoid scanning queue without blocking tasks + private val blockingTasksInBuffer = atomic(0) /** * Retrieves and removes task from the head of the queue - * Invariant: this method is called only by the owner of the queue ([stealBatch] is not) + * Invariant: this method is called only by the owner of the queue. */ fun poll(): Task? = lastScheduledTask.getAndSet(null) ?: pollBuffer() @@ -69,7 +69,8 @@ internal class WorkQueue { * Invariant: Called only by the owner of the queue, returns * `null` if task was added, task that wasn't added otherwise. */ - fun add(task: Task): Task? { + fun add(task: Task, fair: Boolean = false): Task? { + if (fair) return addLast(task) val previous = lastScheduledTask.getAndSet(task) ?: return null return addLast(previous) } @@ -78,18 +79,20 @@ internal class WorkQueue { * Invariant: Called only by the owner of the queue, returns * `null` if task was added, task that wasn't added otherwise. */ - fun addLast(task: Task): Task? { + private fun addLast(task: Task): Task? { + if (task.isBlocking) blockingTasksInBuffer.incrementAndGet() if (bufferSize == BUFFER_CAPACITY - 1) return task - val headLocal = producerIndex.value - val nextIndex = headLocal and MASK - + val nextIndex = producerIndex.value and MASK /* - * If current element is not null then we're racing with consumers for the tail. If we skip this check then - * the consumer can null out current element and it will be lost. If we're racing for tail then - * the queue is close to overflowing => return task + * If current element is not null then we're racing with a really slow consumer that committed the consumer index, + * but hasn't yet nulled out the slot, effectively preventing us from using it. + * Such situations are very rare in practise (although possible) and we decided to give up a progress guarantee + * to have a stronger invariant "add to queue with bufferSize == 0 is always successful". + * This algorithm can still be wait-free for add, but if and only if tasks are not reusable, otherwise + * nulling out the buffer wouldn't be possible. */ - if (buffer[nextIndex] != null) { - return task + while (buffer[nextIndex] != null) { + Thread.yield() } buffer.lazySet(nextIndex, task) producerIndex.incrementAndGet() @@ -103,18 +106,52 @@ internal class WorkQueue { * or positive value of how many nanoseconds should pass until the head of this queue will be available to steal. */ fun tryStealFrom(victim: WorkQueue): Long { - if (victim.stealBatch { task -> add(task) }) { + assert { bufferSize == 0 } + val task = victim.pollBuffer() + if (task != null) { + val notAdded = add(task) + assert { notAdded == null } return TASK_STOLEN } - return tryStealLastScheduled(victim) + return tryStealLastScheduled(victim, blockingOnly = false) + } + + fun tryStealBlockingFrom(victim: WorkQueue): Long { + assert { bufferSize == 0 } + var start = victim.consumerIndex.value + val end = victim.producerIndex.value + val buffer = victim.buffer + + while (start != end) { + val index = start and MASK + if (victim.blockingTasksInBuffer.value == 0) break + val value = buffer[index] + if (value != null && value.isBlocking && buffer.compareAndSet(index, value, null)) { + victim.blockingTasksInBuffer.decrementAndGet() + add(value) + return TASK_STOLEN + } else { + ++start + } + } + return tryStealLastScheduled(victim, blockingOnly = true) + } + + fun offloadAllWorkTo(globalQueue: GlobalQueue) { + lastScheduledTask.getAndSet(null)?.let { globalQueue.add(it) } + while (pollTo(globalQueue)) { + // Steal everything + } } /** * Contract on return value is the same as for [tryStealFrom] */ - private fun tryStealLastScheduled(victim: WorkQueue): Long { + private fun tryStealLastScheduled(victim: WorkQueue, blockingOnly: Boolean): Long { while (true) { val lastScheduled = victim.lastScheduledTask.value ?: return NOTHING_TO_STEAL + if (blockingOnly && !lastScheduled.isBlocking) return NOTHING_TO_STEAL + // TODO time wraparound ? val time = schedulerTimeSource.nanoTime() val staleness = time - lastScheduled.submissionTime @@ -134,49 +171,10 @@ internal class WorkQueue { } } - private fun GlobalQueue.add(task: Task) { - /* - * globalQueue is closed as the very last step in the shutdown sequence when all worker threads had - * been already shutdown (with the only exception of the last worker thread that might be performing - * shutdown procedure itself). As a consistency check we do a [cheap!] check that it is not closed here yet. - */ - val added = addLast(task) - assert { added } - } - - internal fun offloadAllWork(globalQueue: GlobalQueue) { - lastScheduledTask.getAndSet(null)?.let { globalQueue.add(it) } - while (stealBatchTo(globalQueue)) { - // Steal everything - } - } - - /** - * Method that is invoked by external workers to steal work. - * Half of the buffer (at least 1) is stolen, returns `true` if at least one task was stolen. - */ - private inline fun stealBatch(consumer: (Task) -> Unit): Boolean { - val size = bufferSize - if (size == 0) return false - var toSteal = (size / 2).coerceAtLeast(1) - var wasStolen = false - while (toSteal-- > 0) { - val tailLocal = consumerIndex.value - if (tailLocal - producerIndex.value == 0) return wasStolen - val index = tailLocal and MASK - val element = buffer[index] ?: continue - if (consumerIndex.compareAndSet(tailLocal, tailLocal + 1)) { - // 1) Help GC 2) Signal producer that this slot is consumed and may be used - consumer(element) - buffer[index] = null - wasStolen = true - } - } - return wasStolen - } - - private fun stealBatchTo(queue: GlobalQueue): Boolean { - return stealBatch { queue.add(it) } + private fun pollTo(queue: GlobalQueue): Boolean { + val task = pollBuffer() ?: return false + queue.add(task) + return true } private fun pollBuffer(): Task? { @@ -185,8 +183,28 @@ internal class WorkQueue { if (tailLocal - producerIndex.value == 0) return null val index = tailLocal and MASK if (consumerIndex.compareAndSet(tailLocal, tailLocal + 1)) { - return buffer.getAndSet(index, null) + // Nulls are allowed when blocking tasks are stolen from the middle of the queue. + val value = buffer.getAndSet(index, null) ?: continue + value.decrementIfBlocking() + return value } } } + + private fun Task?.decrementIfBlocking() { + if (this != null && isBlocking) { + val value = blockingTasksInBuffer.decrementAndGet() + assert { value >= 0 } + } + } } + +private fun GlobalQueue.add(task: Task) { + /* + * globalQueue is closed as the very last step in the shutdown sequence when all worker threads had + * been already shutdown (with the only exception of the last worker thread that might be performing + * shutdown procedure itself). As a consistency check we do a [cheap!] check that it is not closed here yet. + */ + val added = addLast(task) + assert { added } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherLivenessStressTest.kt similarity index 86% rename from kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt rename to kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherLivenessStressTest.kt index 77fb71246e..7fc212f59f 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherRaceStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherLivenessStressTest.kt @@ -10,7 +10,11 @@ import org.junit.Test import java.util.concurrent.atomic.* import kotlin.test.* -class BlockingCoroutineDispatcherRaceStressTest : SchedulerTestBase() { +/** + * Test that ensures implementation correctness of [LimitingDispatcher] and + * designed to stress its particular implementation details. + */ +class BlockingCoroutineDispatcherLivenessStressTest : SchedulerTestBase() { private val concurrentWorkers = AtomicInteger(0) @Before @@ -29,7 +33,7 @@ class BlockingCoroutineDispatcherRaceStressTest : SchedulerTestBase() { async(limitingDispatcher) { try { val currentlyExecuting = concurrentWorkers.incrementAndGet() - require(currentlyExecuting == 1) + assertEquals(1, currentlyExecuting) } finally { concurrentWorkers.decrementAndGet() } @@ -37,7 +41,6 @@ class BlockingCoroutineDispatcherRaceStressTest : SchedulerTestBase() { } tasks.forEach { it.await() } } - checkPoolThreadsCreated(2..4) } @Test diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt new file mode 100644 index 0000000000..1fe0d8386d --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherMixedStealingStressTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.scheduling + +import kotlinx.coroutines.* +import org.junit.* +import java.util.concurrent.* + +/** + * Specific test that was designed to expose inference between stealing/polling of blocking and non-blocking tasks.RunningThreadStackMergeTest + */ +class BlockingCoroutineDispatcherMixedStealingStressTest : SchedulerTestBase() { + + private val iterations = 10_000 + + @Before + fun setUp() { + idleWorkerKeepAliveNs = Long.MAX_VALUE + } + + @Test + fun testBlockingProgressPreventedInternal() { + val blocking = blockingDispatcher(corePoolSize).asExecutor() + val regular = dispatcher.asExecutor() + repeat(iterations * stressTestMultiplier) { + val cpuBlocker = CyclicBarrier(corePoolSize + 1) + val blockingBlocker = CyclicBarrier(2) + regular.execute(Runnable { + // Block all CPU cores except current one + repeat(corePoolSize - 1) { + regular.execute(Runnable { + cpuBlocker.await() + }) + } + + blocking.execute(Runnable { + blockingBlocker.await() + }) + + regular.execute(Runnable { + blockingBlocker.await() + cpuBlocker.await() + }) + }) + cpuBlocker.await() + } + } + + @Test + fun testBlockingProgressPreventedExternal() { + val blocking = blockingDispatcher(corePoolSize).asExecutor() + val regular = dispatcher.asExecutor() + repeat(iterations / 2 * stressTestMultiplier) { + val cpuBlocker = CyclicBarrier(corePoolSize + 1) + val blockingBlocker = CyclicBarrier(2) + repeat(corePoolSize) { + regular.execute(Runnable { + cpuBlocker.await() + }) + } + // Wait for all threads to park + while (true) { + val waiters = Thread.getAllStackTraces().keys.count { (it.state == Thread.State.TIMED_WAITING || it.state == Thread.State.WAITING) + && it is CoroutineScheduler.Worker } + if (waiters >= corePoolSize) break + Thread.yield() + } + blocking.execute(Runnable { + blockingBlocker.await() + }) + regular.execute(Runnable { + }) + + blockingBlocker.await() + cpuBlocker.await() + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt index 699d8d044d..66b93be9cf 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt @@ -6,11 +6,15 @@ package kotlinx.coroutines.scheduling import kotlinx.coroutines.* import org.junit.* +import org.junit.rules.* import java.util.concurrent.* class BlockingCoroutineDispatcherTest : SchedulerTestBase() { - @Test(timeout = 1_000) + @get:Rule + val timeout = Timeout.seconds(10L)!! + + @Test fun testNonBlockingWithBlockingExternal() = runBlocking { val barrier = CyclicBarrier(2) @@ -24,10 +28,10 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { nonBlockingJob.join() blockingJob.join() - checkPoolThreadsCreated(2) + checkPoolThreadsCreated(2..3) } - @Test(timeout = 10_000) + @Test fun testNonBlockingFromBlocking() = runBlocking { val barrier = CyclicBarrier(2) @@ -41,10 +45,10 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { } blocking.join() - checkPoolThreadsCreated(2) + checkPoolThreadsCreated(2..3) } - @Test(timeout = 1_000) + @Test fun testScheduleBlockingThreadCount() = runTest { // After first iteration pool is idle, repeat, no new threads should be created repeat(2) { @@ -59,7 +63,7 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { } } - @Test(timeout = 1_000) + @Test fun testNoCpuStarvation() = runBlocking { val tasksNum = 100 val barrier = CyclicBarrier(tasksNum + 1) @@ -73,10 +77,9 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { tasks.forEach { require(it.isActive) } barrier.await() tasks.joinAll() - checkPoolThreadsCreated(101) } - @Test(timeout = 1_000) + @Test fun testNoCpuStarvationWithMultipleBlockingContexts() = runBlocking { val firstBarrier = CyclicBarrier(11) val secondBarrier = CyclicBarrier(11) @@ -101,7 +104,7 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() { checkPoolThreadsCreated(21..22) } - @Test(timeout = 1_000) + @Test fun testNoExcessThreadsCreated() = runBlocking { corePoolSize = 4 diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherThreadLimitStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherThreadLimitStressTest.kt new file mode 100644 index 0000000000..123fe3c9c4 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherThreadLimitStressTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.scheduling + +import kotlinx.coroutines.* +import org.junit.Ignore +import org.junit.Test +import java.util.concurrent.* +import java.util.concurrent.atomic.* +import kotlin.test.* + +class BlockingCoroutineDispatcherThreadLimitStressTest : SchedulerTestBase() { + + init { + corePoolSize = CORES_COUNT + } + + private val observedConcurrency = ConcurrentHashMap() + private val concurrentWorkers = AtomicInteger(0) + + @Test + @Ignore + fun testLimitParallelismToOne() = runTest { + val limitingDispatcher = blockingDispatcher(1) + // Do in bursts to avoid OOM + repeat(100 * stressTestMultiplierSqrt) { + val iterations = 1_000 * stressTestMultiplierSqrt + val tasks = (1..iterations).map { + async(limitingDispatcher) { + try { + val currentlyExecuting = concurrentWorkers.incrementAndGet() + observedConcurrency[currentlyExecuting] = true + assertTrue(currentlyExecuting <= CORES_COUNT) + } finally { + concurrentWorkers.decrementAndGet() + } + } + } + tasks.forEach { it.await() } + for (i in CORES_COUNT + 1..CORES_COUNT * 2) { + require(i !in observedConcurrency.keys) { "Unexpected state: $observedConcurrency" } + } + checkPoolThreadsCreated(0..CORES_COUNT + 1) + } + } + + @Test + @Ignore + fun testLimitParallelism() = runBlocking { + val limitingDispatcher = blockingDispatcher(CORES_COUNT) + val iterations = 50_000 * stressTestMultiplier + val tasks = (1..iterations).map { + async(limitingDispatcher) { + try { + val currentlyExecuting = concurrentWorkers.incrementAndGet() + observedConcurrency[currentlyExecuting] = true + assertTrue(currentlyExecuting <= CORES_COUNT) + } finally { + concurrentWorkers.decrementAndGet() + } + } + } + tasks.forEach { it.await() } + for (i in CORES_COUNT + 1..CORES_COUNT * 2) { + require(i !in observedConcurrency.keys) { "Unexpected state: $observedConcurrency" } + } + checkPoolThreadsCreated(CORES_COUNT..CORES_COUNT * 3) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt similarity index 60% rename from kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherStressTest.kt rename to kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt index 08b4914c4c..3280527f2a 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherWorkSignallingStressTest.kt @@ -7,48 +7,15 @@ package kotlinx.coroutines.scheduling import kotlinx.coroutines.* -import org.junit.* +import org.junit.Test import java.util.concurrent.* -import java.util.concurrent.atomic.* +import kotlin.test.* -class BlockingCoroutineDispatcherStressTest : SchedulerTestBase() { - - init { - corePoolSize = CORES_COUNT - } - - private val observedConcurrency = ConcurrentHashMap() - private val concurrentWorkers = AtomicInteger(0) - - @Test - fun testLimitParallelism() = runBlocking { - val limitingDispatcher = blockingDispatcher(CORES_COUNT) - val iterations = 50_000 * stressTestMultiplier - val tasks = (1..iterations).map { - async(limitingDispatcher) { - try { - val currentlyExecuting = concurrentWorkers.incrementAndGet() - observedConcurrency[currentlyExecuting] = true - require(currentlyExecuting <= CORES_COUNT) - } finally { - concurrentWorkers.decrementAndGet() - } - } - } - - tasks.forEach { it.await() } - require(tasks.isNotEmpty()) - for (i in CORES_COUNT + 1..CORES_COUNT * 2) { - require(i !in observedConcurrency.keys) { "Unexpected state: $observedConcurrency" } - } - - checkPoolThreadsCreated(CORES_COUNT..CORES_COUNT + CORES_COUNT * 2) - } +class BlockingCoroutineDispatcherWorkSignallingStressTest : SchedulerTestBase() { @Test fun testCpuTasksStarvation() = runBlocking { val iterations = 1000 * stressTestMultiplier - repeat(iterations) { // Create a dispatcher every iteration to increase probability of race val dispatcher = ExperimentalCoroutineDispatcher(CORES_COUNT) @@ -63,28 +30,36 @@ class BlockingCoroutineDispatcherStressTest : SchedulerTestBase() { repeat(CORES_COUNT) { async(dispatcher) { // These two will be stolen first - blockingTasks += async(blockingDispatcher) { blockingBarrier.await() } - blockingTasks += async(blockingDispatcher) { blockingBarrier.await() } - - - // Empty on CPU job which should be executed while blocked tasks are hang - cpuTasks += async(dispatcher) { cpuBarrier.await() } - + blockingTasks += blockingAwait(blockingDispatcher, blockingBarrier) + blockingTasks += blockingAwait(blockingDispatcher, blockingBarrier) + // Empty on CPU job which should be executed while blocked tasks are waiting + cpuTasks += cpuAwait(dispatcher, cpuBarrier) // Block with next task. Block cores * 3 threads in total - blockingTasks += async(blockingDispatcher) { blockingBarrier.await() } + blockingTasks += blockingAwait(blockingDispatcher, blockingBarrier) } } cpuTasks.forEach { require(it.isActive) } cpuBarrier.await() - cpuTasks.forEach { it.await() } + cpuTasks.awaitAll() blockingTasks.forEach { require(it.isActive) } blockingBarrier.await() - blockingTasks.forEach { it.await() } + blockingTasks.awaitAll() dispatcher.close() } } + private fun CoroutineScope.blockingAwait( + blockingDispatcher: CoroutineDispatcher, + blockingBarrier: CyclicBarrier + ) = async(blockingDispatcher) { blockingBarrier.await() } + + + private fun CoroutineScope.cpuAwait( + blockingDispatcher: CoroutineDispatcher, + blockingBarrier: CyclicBarrier + ) = async(blockingDispatcher) { blockingBarrier.await() } + @Test fun testBlockingTasksStarvation() = runBlocking { corePoolSize = 2 // Easier to reproduce race with unparks @@ -96,8 +71,7 @@ class BlockingCoroutineDispatcherStressTest : SchedulerTestBase() { val barrier = CyclicBarrier(blockingLimit + 1) // Should eat all limit * 3 cpu without any starvation val tasks = (1..blockingLimit).map { async(blocking) { barrier.await() } } - - tasks.forEach { require(it.isActive) } + tasks.forEach { assertTrue(it.isActive) } barrier.await() tasks.joinAll() } @@ -112,12 +86,10 @@ class BlockingCoroutineDispatcherStressTest : SchedulerTestBase() { repeat(iterations) { // Overwhelm global queue with external CPU tasks val cpuTasks = (1..CORES_COUNT).map { async(dispatcher) { while (true) delay(1) } } - val barrier = CyclicBarrier(blockingLimit + 1) // Should eat all limit * 3 cpu without any starvation val tasks = (1..blockingLimit).map { async(blocking) { barrier.await() } } - - tasks.forEach { require(it.isActive) } + tasks.forEach { assertTrue(it.isActive) } barrier.await() tasks.joinAll() cpuTasks.forEach { it.cancelAndJoin() } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt index 9dfd6a9805..062b849c0a 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineDispatcherTest.kt @@ -19,6 +19,7 @@ class CoroutineDispatcherTest : SchedulerTestBase() { @Test fun testSingleThread() = runBlocking { + corePoolSize = 1 expect(1) withContext(dispatcher) { require(Thread.currentThread() is CoroutineScheduler.Worker) @@ -41,14 +42,12 @@ class CoroutineDispatcherTest : SchedulerTestBase() { fun testFairScheduling() = runBlocking { corePoolSize = 1 expect(1) - val outerJob = launch(dispatcher) { val d1 = launch(dispatcher) { expect(3) } val d2 = launch(dispatcher) { expect(4) } val d3 = launch(dispatcher) { expect(2) } listOf(d1, d2, d3).joinAll() } - outerJob.join() finish(5) } @@ -57,13 +56,12 @@ class CoroutineDispatcherTest : SchedulerTestBase() { fun testStealing() = runBlocking { corePoolSize = 2 val flag = AtomicBoolean(false) - val job = async(context = dispatcher) { + val job = async(dispatcher) { expect(1) val innerJob = async { expect(2) flag.set(true) } - while (!flag.get()) { Thread.yield() // Block current thread, submitted inner job will be stolen } @@ -71,7 +69,6 @@ class CoroutineDispatcherTest : SchedulerTestBase() { innerJob.await() expect(3) } - job.await() finish(4) checkPoolThreadsCreated(2) @@ -89,30 +86,6 @@ class CoroutineDispatcherTest : SchedulerTestBase() { checkPoolThreadsCreated(2) } - @Test - fun testWithTimeout() = runBlocking { - corePoolSize = CORES_COUNT - withContext(dispatcher) { - expect(1) - val result = withTimeoutOrNull(1000) { - expect(2) - yield() // yield only now - "OK" - } - assertEquals("OK", result) - - val nullResult = withTimeoutOrNull(1000) { - expect(3) - while (true) { - yield() - } - } - assertNull(nullResult) - finish(4) - } - checkPoolThreadsCreated(1..CORES_COUNT) - } - @Test fun testMaxSize() = runBlocking { corePoolSize = 1 diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt index 85ae849c8e..b7677bef29 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerLivenessStressTest.kt @@ -49,4 +49,4 @@ class CoroutineSchedulerLivenessStressTest : TestBase() { barrier.await() } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerShrinkTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerShrinkTest.kt deleted file mode 100644 index 50090b533e..0000000000 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerShrinkTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.scheduling - -import kotlinx.coroutines.* -import org.junit.* -import java.util.concurrent.* -import kotlin.coroutines.* - -@Ignore // these tests are too unstable on Windows, should be virtualized -class CoroutineSchedulerShrinkTest : SchedulerTestBase() { - - private val blockingTasksCount = CORES_COUNT * 3 - private val blockingTasksBarrier = CyclicBarrier(blockingTasksCount + 1) - lateinit var blocking: CoroutineContext - - @Before - fun setUp() { - corePoolSize = CORES_COUNT - // shutdown after 100ms - idleWorkerKeepAliveNs = TimeUnit.MILLISECONDS.toNanos(100) - blocking = blockingDispatcher(100) - } - - @Test(timeout = 10_000) - fun testShrinkOnlyBlockingTasks() = runBlocking { - // Init dispatcher - async(dispatcher) { }.await() - // Pool is initialized with core size in the beginning - checkPoolThreadsExist(1..2) - - // Run blocking tasks and check increased threads count - val blockingTasks = launchBlocking() - checkBlockingTasks(blockingTasks) - - delay(2000) - // Pool should shrink to core size +- eps - checkPoolThreadsExist(CORES_COUNT..CORES_COUNT + 3) - } - - @Test(timeout = 10_000) - fun testShrinkMixedWithWorkload() = runBlocking { - // Block blockingTasksCount cores in blocking dispatcher - val blockingTasks = launchBlocking() - - // Block cores count CPU threads - val nonBlockingBarrier = CyclicBarrier(CORES_COUNT + 1) - val nonBlockingTasks = (1..CORES_COUNT).map { - async(dispatcher) { - nonBlockingBarrier.await() - } - } - - // Check CPU tasks succeeded properly even though blocking tasks acquired everything - nonBlockingTasks.forEach { require(it.isActive) } - nonBlockingBarrier.await() - nonBlockingTasks.joinAll() - - // Check blocking tasks succeeded properly - checkBlockingTasks(blockingTasks) - - delay(2000) - // Pool should shrink to core size - checkPoolThreadsExist(CORES_COUNT..CORES_COUNT + 3) - } - - private suspend fun checkBlockingTasks(blockingTasks: List>) { - checkPoolThreadsExist(blockingTasksCount..corePoolSize + blockingTasksCount) - blockingTasksBarrier.await() - blockingTasks.joinAll() - } - - @Test(timeout = 10_000) - fun testShrinkWithExternalTasks() = runBlocking { - val nonBlockingBarrier = CyclicBarrier(CORES_COUNT + 1) - val blockingTasks = launchBlocking() - - val nonBlockingTasks = (1..CORES_COUNT).map { - async(dispatcher) { - nonBlockingBarrier.await() - } - } - - // Tasks that burn CPU. Delay is important so tasks will be scheduled from external thread - val busySpinTasks = (1..2).map { - async(dispatcher) { - while (true) { - yield() - } - } - } - - nonBlockingTasks.forEach { require(it.isActive) } - nonBlockingBarrier.await() - nonBlockingTasks.joinAll() - - checkBlockingTasks(blockingTasks) - - delay(2000) - // Pool should shrink almost to core size (+/- eps) - checkPoolThreadsExist(CORES_COUNT..CORES_COUNT + 3) - - busySpinTasks.forEach { - require(it.isActive) - it.cancelAndJoin() - } - } - - private suspend fun launchBlocking(): List> { - val result = (1..blockingTasksCount).map { - GlobalScope.async(blocking) { - blockingTasksBarrier.await() - } - } - - while (blockingTasksBarrier.numberWaiting != blockingTasksCount) { - delay(1) - } - - return result - } -} diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt index 6f35e7e1b8..cb49f054ce 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerStressTest.kt @@ -37,8 +37,8 @@ class CoroutineSchedulerStressTest : TestBase() { @Test fun testInternalTasksSubmissionProgress() { - /* - * Run 2 million tasks and validate that + /* + * Run a lot of tasks and validate that * 1) All of them are completed successfully * 2) Every thread executed task at least once */ @@ -75,21 +75,19 @@ class CoroutineSchedulerStressTest : TestBase() { Thread.yield() } } - // Block current thread finishLatch.await() }) finishLatch.await() - require(!observedThreads.containsKey(blockingThread!!)) + assertFalse(observedThreads.containsKey(blockingThread!!)) validateResults() } private fun processTask() { val counter = observedThreads[Thread.currentThread()] ?: 0L observedThreads[Thread.currentThread()] = counter + 1 - if (processed.incrementAndGet() == tasksNum) { finishLatch.countDown() } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt b/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt index e01f027f55..bfabf5b2f3 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/SchedulerTestBase.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import org.junit.* import kotlin.coroutines.* +import kotlin.test.* abstract class SchedulerTestBase : TestBase() { companion object { @@ -22,17 +23,20 @@ abstract class SchedulerTestBase : TestBase() { */ fun checkPoolThreadsCreated(expectedThreadsCount: Int = CORES_COUNT) { val threadsCount = maxSequenceNumber()!! - require(threadsCount == expectedThreadsCount) - { "Expected $expectedThreadsCount pool threads, but has $threadsCount" } + assertEquals(expectedThreadsCount, threadsCount, "Expected $expectedThreadsCount pool threads, but has $threadsCount") } /** * Asserts that any number of pool worker threads in [range] were created. * Note that 'created' doesn't mean 'exists' because pool supports dynamic shrinking */ - fun checkPoolThreadsCreated(range: IntRange) { + fun checkPoolThreadsCreated(range: IntRange, base: Int = CORES_COUNT) { val maxSequenceNumber = maxSequenceNumber()!! - require(maxSequenceNumber in range) { "Expected pool threads to be in interval $range, but has $maxSequenceNumber" } + val r = (range.first)..(range.last + base) + assertTrue( + maxSequenceNumber in r, + "Expected pool threads to be in interval $r, but has $maxSequenceNumber" + ) } /** @@ -40,7 +44,7 @@ abstract class SchedulerTestBase : TestBase() { */ fun checkPoolThreadsExist(range: IntRange) { val threads = Thread.getAllStackTraces().keys.asSequence().filter { it is CoroutineScheduler.Worker }.count() - require(threads in range) { "Expected threads in $range interval, but has $threads" } + assertTrue(threads in range, "Expected threads in $range interval, but has $threads") } private fun maxSequenceNumber(): Int? { @@ -61,15 +65,12 @@ abstract class SchedulerTestBase : TestBase() { suspend fun Iterable.joinAll() = forEach { it.join() } } - private val exception = atomic(null) - private val handler = CoroutineExceptionHandler { _, e -> exception.value = e } - - protected var corePoolSize = 1 + protected var corePoolSize = CORES_COUNT protected var maxPoolSize = 1024 protected var idleWorkerKeepAliveNs = IDLE_WORKER_KEEP_ALIVE_NS private var _dispatcher: ExperimentalCoroutineDispatcher? = null - protected val dispatcher: CoroutineContext + protected val dispatcher: CoroutineDispatcher get() { if (_dispatcher == null) { _dispatcher = ExperimentalCoroutineDispatcher( @@ -79,21 +80,21 @@ abstract class SchedulerTestBase : TestBase() { ) } - return _dispatcher!! + handler + return _dispatcher!! } protected var blockingDispatcher = lazy { blockingDispatcher(1000) } - protected fun blockingDispatcher(parallelism: Int): CoroutineContext { + protected fun blockingDispatcher(parallelism: Int): CoroutineDispatcher { val intitialize = dispatcher - return _dispatcher!!.blocking(parallelism) + handler + return _dispatcher!!.blocking(parallelism) } - protected fun view(parallelism: Int): CoroutineContext { + protected fun view(parallelism: Int): CoroutineDispatcher { val intitialize = dispatcher - return _dispatcher!!.limited(parallelism) + handler + return _dispatcher!!.limited(parallelism) } @After @@ -103,6 +104,5 @@ abstract class SchedulerTestBase : TestBase() { _dispatcher?.close() } } - exception.value?.let { throw it } } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt index 4582b6810a..5e170c9f6b 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueStressTest.kt @@ -16,8 +16,8 @@ class WorkQueueStressTest : TestBase() { private val threads = mutableListOf() private val offerIterations = 100_000 * stressTestMultiplierSqrt // memory pressure, not CPU time private val stealersCount = 6 - private val stolenTasks = Array(stealersCount) { Queue() } - private val globalQueue = Queue() // only producer will use it + private val stolenTasks = Array(stealersCount) { GlobalQueue() } + private val globalQueue = GlobalQueue() // only producer will use it private val producerQueue = WorkQueue() @Volatile @@ -55,9 +55,7 @@ class WorkQueueStressTest : TestBase() { val myQueue = WorkQueue() startLatch.await() while (!producerFinished || producerQueue.size != 0) { - if (myQueue.size > 100) { - stolenTasks[i].addAll(myQueue.drain().map { task(it) }) - } + stolenTasks[i].addAll(myQueue.drain().map { task(it) }) myQueue.tryStealFrom(victim = producerQueue) } @@ -88,7 +86,7 @@ class WorkQueueStressTest : TestBase() { } } - val stolen = Queue() + val stolen = GlobalQueue() threads += thread(name = "stealer") { val myQueue = WorkQueue() startLatch.await() @@ -116,10 +114,8 @@ class WorkQueueStressTest : TestBase() { val expected = (1L..offerIterations).toSet() assertEquals(expected, result, "Following elements are missing: ${(expected - result)}") } -} -internal class Queue : GlobalQueue() { - fun addAll(tasks: Collection) { + private fun GlobalQueue.addAll(tasks: Collection) { tasks.forEach { addLast(it) } } } diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt index 341aa0462d..7acd1620f4 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/WorkQueueTest.kt @@ -69,17 +69,6 @@ class WorkQueueTest : TestBase() { } } -internal fun GlobalQueue.asTimeList(): List { - val result = mutableListOf() - var next = removeFirstOrNull() - while (next != null) { - result += next.submissionTime - next = removeFirstOrNull() - } - - return result -} - internal fun task(n: Long) = TaskImpl(Runnable {}, n, NonBlockingContext) internal fun WorkQueue.drain(): List { From d77c17c230deae1eff46cda24a1b0a4f7a6238f7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 11 Nov 2019 18:47:27 +0300 Subject: [PATCH 85/90] Improve performance of task acquisition * Do not push worker to the stack during second pass of "min duration scanning" * Locally cache whether local queue has any work to execute to save atomic getAndSet and a bunch of atomic loads * More precise rendezvous for parking * Long-scanning stealing to emulate spinning * Properly handle interference of termination sequence and protection against spurious wakeups * Documentation improvements, naming, proper spurious wakeup check --- .../DispatchersContextSwitchBenchmark.kt | 7 +- .../scheduler/actors/CycledActorsBenchmark.kt | 6 +- .../actors/PingPongActorBenchmark.kt | 2 +- .../actors/PingPongWithBlockingContext.kt | 4 +- .../actors/StatefulActorBenchmark.kt | 2 +- .../common/src/internal/LockFreeTaskQueue.kt | 1 - .../jvm/src/scheduling/CoroutineScheduler.kt | 136 ++++++++++-------- .../jvm/src/scheduling/Tasks.kt | 2 +- ...routineDispatcherTerminationStressTest.kt} | 2 +- 9 files changed, 91 insertions(+), 71 deletions(-) rename kotlinx-coroutines-core/jvm/test/scheduling/{BlockingIOTerminationStressTest.kt => BlockingCoroutineDispatcherTerminationStressTest.kt} (93%) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt index 6b61c99645..e7f806760e 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/DispatchersContextSwitchBenchmark.kt @@ -64,13 +64,10 @@ open class DispatchersContextSwitchBenchmark { repeat(nCoroutines) { launch(dispatcher) { repeat(nRepeatDelay) { - delayOrYield() + delay(delayTimeMs) } } } } +} - private suspend fun delayOrYield() { - delay(delayTimeMs) - } -} \ No newline at end of file diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt index d9ef8917cb..67548f624e 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/CycledActorsBenchmark.kt @@ -29,8 +29,8 @@ import java.util.concurrent.* * CycledActorsBenchmark.cycledActors 262144 experimental avgt 14 1804.146 ± 57.275 ms/op */ @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 3) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) @@ -43,7 +43,7 @@ open class CycledActorsBenchmark : ParametrizedDispatcherBase() { @Param("fjp", "ftp_1", "scheduler") override var dispatcher: String = "fjp" - @Param("524288") + @Param("1", "1024") var actorStateSize = 1 @Benchmark diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt index d8de8109c4..2d547e2660 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongActorBenchmark.kt @@ -27,7 +27,7 @@ import java.util.concurrent.* */ @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 2) +@Fork(value = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt index c531de90f6..86a9440a58 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/PingPongWithBlockingContext.kt @@ -20,8 +20,8 @@ import kotlin.coroutines.* * PingPongWithBlockingContext.withContextPingPong avgt 20 761.669 ± 41.371 ms/op */ @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 2) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) diff --git a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt index 840ae0038a..fb342295a6 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/scheduler/actors/StatefulActorBenchmark.kt @@ -33,7 +33,7 @@ import java.util.concurrent.* */ @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(value = 2) +@Fork(value = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt index 0cfb000f31..c764f51792 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeTaskQueue.kt @@ -175,7 +175,6 @@ internal class LockFreeTaskQueueCore( } // element == Placeholder can only be when add has not finished yet if (element is Placeholder) return null // consider it not added yet - // now we tentative know element to remove -- check predicate // we cannot put null into array here, because copying thread could replace it with Placeholder and that is a disaster val newHead = (head + 1) and MAX_CAPACITY_MASK if (_state.compareAndSet(state, state.updateHead(newHead))) { diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 0fc9259a97..6e9c11cb5a 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -25,7 +25,7 @@ import kotlin.random.* * ### Structural overview * * Scheduler consists of [corePoolSize] worker threads to execute CPU-bound tasks and up to [maxPoolSize] lazily created threads - * to execute blocking tasks. Every worker a has local queue in addition to a global scheduler queue and the global queue + * to execute blocking tasks. Every worker has a local queue in addition to a global scheduler queue and the global queue * has priority over local queue to avoid starvation of externally-submitted (e.g. from Android UI thread) tasks. * Work-stealing is implemented on top of that queues to provide even load distribution and illusion of centralized run queue. * @@ -245,7 +245,7 @@ internal class CoroutineScheduler( */ 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() = (controlState.value and CPU_PERMITS_MASK shr CPU_PERMITS_SHIFT).toInt() + private val availableCpuPermits: Int inline get() = availableCpuPermits(controlState.value) private inline fun createdWorkers(state: Long): Int = (state and CREATED_MASK).toInt() private inline fun blockingTasks(state: Long): Int = (state and BLOCKING_MASK shr BLOCKING_SHIFT).toInt() @@ -261,14 +261,11 @@ internal class CoroutineScheduler( controlState.addAndGet(-(1L shl BLOCKING_SHIFT)) } - private inline fun tryAcquireCpuPermit(): Boolean { - while (true) { - val state = controlState.value - val available = availableCpuPermits(state) - if (available == 0) return false - val update = state - (1L shl CPU_PERMITS_SHIFT) - if (controlState.compareAndSet(state, update)) return true - } + private inline fun tryAcquireCpuPermit(): Boolean = controlState.loop { state -> + val available = availableCpuPermits(state) + if (available == 0) return false + val update = state - (1L shl CPU_PERMITS_SHIFT) + if (controlState.compareAndSet(state, update)) return true } private inline fun releaseCpuPermit() = controlState.addAndGet(1L shl CPU_PERMITS_SHIFT) @@ -283,9 +280,12 @@ internal class CoroutineScheduler( val NOT_IN_STACK = Symbol("NOT_IN_STACK") // Worker termination states - private const val FORBIDDEN = -1 - private const val ALLOWED = 0 + private const val TERMINATION_FORBIDDEN = -1 + private const val TERMINATION_ALLOWED = 0 private const val TERMINATED = 1 + // Worker parking states + private const val PARKING_FORBIDDEN = -1 + private const val PARKING_ALLOWED = 0 private const val PARKED = 1 // Masks of control state @@ -334,7 +334,7 @@ internal class CoroutineScheduler( globalCpuQueue.close() // Finish processing tasks from globalQueue and/or from this worker's local queue while (true) { - val task = currentWorker?.findTask() + val task = currentWorker?.findTask(true) ?: globalCpuQueue.removeFirstOrNull() ?: globalBlockingQueue.removeFirstOrNull() ?: break @@ -419,7 +419,7 @@ internal class CoroutineScheduler( private fun tryUnpark(): Boolean { while (true) { val worker = parkedWorkersStackPop() ?: return false - if (!worker.parkingState.compareAndSet(ALLOWED, FORBIDDEN)) { + if (!worker.parkingState.compareAndSet(PARKING_ALLOWED, PARKING_FORBIDDEN)) { LockSupport.unpark(worker) } if (worker.tryForbidTermination()) return true @@ -469,10 +469,10 @@ internal class CoroutineScheduler( */ if (worker.state === WorkerState.TERMINATED) return task // Do not add CPU tasks in local queue if we are not able to execute it - // TODO discuss: maybe add it to the local queue and offload back in the global queue iff permit wasn't acquired? if (task.mode == TaskMode.NON_BLOCKING && worker.isBlocking) { return task } + worker.mayHaveLocalTasks = true return worker.localQueue.add(task, fair = fair) } @@ -525,7 +525,7 @@ internal class CoroutineScheduler( "CPU = $cpuWorkers, " + "blocking = $blockingWorkers, " + "parked = $parkedWorkers, " + - "retired = $dormant, " + + "dormant = $dormant, " + "terminated = $terminated}, " + "running workers queues = $queueSizes, "+ "global CPU queue size = ${globalCpuQueue.size}, " + @@ -581,16 +581,16 @@ internal class CoroutineScheduler( /** * Small state machine for termination. * Followed states are allowed: - * [ALLOWED] -- worker can wake up and terminate itself - * [FORBIDDEN] -- worker is not allowed to terminate (because it was chosen by another thread to help) + * [TERMINATION_ALLOWED] -- worker can wake up and terminate itself + * [TERMINATION_FORBIDDEN] -- worker is not allowed to terminate (because it was chosen by another thread to help) * [TERMINATED] -- final state, thread is terminating and cannot be resurrected * * Allowed transitions: - * [ALLOWED] -> [FORBIDDEN] - * [ALLOWED] -> [TERMINATED] - * [FORBIDDEN] -> [ALLOWED] + * [TERMINATION_ALLOWED] -> [TERMINATION_FORBIDDEN] + * [TERMINATION_ALLOWED] -> [TERMINATED] + * [TERMINATION_FORBIDDEN] -> [TERMINATION_ALLOWED] */ - private val terminationState = atomic(ALLOWED) + private val terminationState = atomic(TERMINATION_ALLOWED) /** * It is set to the termination deadline when started doing [park] and it reset @@ -610,22 +610,22 @@ internal class CoroutineScheduler( * The delay until at least one task in other worker queues will become stealable. */ private var minDelayUntilStealableTaskNs = 0L - // ALLOWED | PARKED | FORBIDDEN - val parkingState = atomic(ALLOWED) + // PARKING_ALLOWED | PARKING_FORBIDDEN | PARKED + val parkingState = atomic(PARKING_ALLOWED) private var rngState = Random.nextInt() /** - * Tries to set [terminationState] to [FORBIDDEN], returns `false` if this attempt fails. + * Tries to set [terminationState] to [TERMINATION_FORBIDDEN], returns `false` if this attempt fails. * This attempt may fail either because worker terminated itself or because someone else * claimed this worker (though this case is rare, because require very bad timings) */ fun tryForbidTermination(): Boolean = when (val state = terminationState.value) { TERMINATED -> false // already terminated - FORBIDDEN -> false // already forbidden, someone else claimed this worker - ALLOWED -> terminationState.compareAndSet( - ALLOWED, - FORBIDDEN + TERMINATION_FORBIDDEN -> false // already forbidden, someone else claimed this worker + TERMINATION_ALLOWED -> terminationState.compareAndSet( + TERMINATION_ALLOWED, + TERMINATION_FORBIDDEN ) else -> error("Invalid terminationState = $state") } @@ -658,17 +658,21 @@ internal class CoroutineScheduler( } override fun run() = runWorker() + @JvmField + var mayHaveLocalTasks = false private fun runWorker() { var rescanned = false while (!isTerminated && state != WorkerState.TERMINATED) { - val task = findTask() + val task = findTask(mayHaveLocalTasks) // Task found. Execute and repeat if (task != null) { rescanned = false minDelayUntilStealableTaskNs = 0L executeTask(task) continue + } else { + mayHaveLocalTasks = false } /* * No tasks were found: @@ -676,43 +680,55 @@ internal class CoroutineScheduler( * Then its deadline is stored in [minDelayUntilStealableTask] * * Then just park for that duration (ditto re-scanning). - * While it could potentially lead to short (up to WORK_STEALING_TIME_RESOLUTION_NS ns) starvations, + * While it could potentially lead to short (up to WORK_STEALING_TIME_RESOLUTION_NS ns) starvations, * excess unparks and managing "one unpark per signalling" invariant become unfeasible, instead we are going to resolve * it with "spinning via scans" mechanism. + * NB: this short potential parking does not interfere with `tryUnpark` */ if (minDelayUntilStealableTaskNs != 0L) { if (!rescanned) { rescanned = true - continue } else { + rescanned = false tryReleaseCpu(WorkerState.PARKING) + interrupted() LockSupport.parkNanos(minDelayUntilStealableTaskNs) minDelayUntilStealableTaskNs = 0L } + continue } /* - * 2) No tasks available, time to park and, potentially, shut down the thread. - * + * 2) Or no tasks available, time to park and, potentially, shut down the thread. * Add itself to the stack of parked workers, re-scans all the queues * to avoid missing wake-up (requestCpuWorker) and either starts executing discovered tasks or parks itself awaiting for new tasks. */ - parkingState.value = ALLOWED - if (parkedWorkersStackPush(this)) { - continue - } else { - assert { localQueue.size == 0 } + tryPark() + } + tryReleaseCpu(WorkerState.TERMINATED) + } + + // Counterpart to "tryUnpark" + private fun tryPark() { + if (!inStack()) { + parkingState.value = PARKING_ALLOWED + } + if (parkedWorkersStackPush(this)) { + return + } else { + assert { localQueue.size == 0 } + // Failed to get a parking permit => we are not in the stack + while (inStack()) { + if (isTerminated || state == WorkerState.TERMINATED) break + if (parkingState.value != PARKED && !parkingState.compareAndSet(PARKING_ALLOWED, PARKED)) { + return + } tryReleaseCpu(WorkerState.PARKING) interrupted() // Cleanup interruptions - while (inStack()) { // Prevent spurious wakeups - if (isTerminated) break - if (!parkingState.compareAndSet(ALLOWED, PARKED)) { - break - } + if (inStack()) { park() } } } - tryReleaseCpu(WorkerState.TERMINATED) } private fun inStack(): Boolean = nextParkedWorker !== NOT_IN_STACK @@ -763,7 +779,7 @@ internal class CoroutineScheduler( } private fun park() { - terminationState.value = ALLOWED + terminationState.value = TERMINATION_ALLOWED // set termination deadline the first time we are here (it is reset in idleReset) if (terminationDeadline == 0L) terminationDeadline = System.nanoTime() + idleWorkerKeepAliveNs // actually park @@ -789,7 +805,7 @@ internal class CoroutineScheduler( * See tryUnpark for state reasoning. * If this CAS fails, then we were successfully unparked by other worker and cannot terminate. */ - if (!terminationState.compareAndSet(ALLOWED, TERMINATED)) return + if (!terminationState.compareAndSet(TERMINATION_ALLOWED, TERMINATED)) return /* * At this point this thread is no longer considered as usable for scheduling. * We need multi-step choreography to reindex workers. @@ -841,22 +857,30 @@ internal class CoroutineScheduler( } } - fun findTask(): Task? { - if (tryAcquireCpuPermit()) return findAnyTask() + fun findTask(scanLocalQueue: Boolean): Task? { + if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue) // If we can't acquire a CPU permit -- attempt to find blocking task - val task = localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull() + val task = if (scanLocalQueue) { + localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull() + } else { + globalBlockingQueue.removeFirstOrNull() + } return task ?: trySteal(blockingOnly = true) } - private fun findAnyTask(): Task? { + private fun findAnyTask(scanLocalQueue: Boolean): Task? { /* * Anti-starvation mechanism: probabilistically poll either local * or global queue to ensure progress for both external and internal tasks. */ - val globalFirst = nextInt(2 * corePoolSize) == 0 - if (globalFirst) pollGlobalQueues()?.let { return it } - localQueue.poll()?.let { return it } - if (!globalFirst) pollGlobalQueues()?.let { return it } + if (scanLocalQueue) { + val globalFirst = nextInt(2 * corePoolSize) == 0 + if (globalFirst) pollGlobalQueues()?.let { return it } + localQueue.poll()?.let { return it } + if (!globalFirst) pollGlobalQueues()?.let { return it } + } else { + pollGlobalQueues()?.let { return it } + } return trySteal(blockingOnly = false) } @@ -880,7 +904,7 @@ internal class CoroutineScheduler( var currentIndex = nextInt(created) var minDelay = Long.MAX_VALUE - repeat(created) { + repeat(workers.length()) { ++currentIndex if (currentIndex > created) currentIndex = 1 val worker = workers[currentIndex] diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt index c6ce78face..c0a3e6435d 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt @@ -45,7 +45,7 @@ internal val MAX_POOL_SIZE = systemProp( @JvmField internal val IDLE_WORKER_KEEP_ALIVE_NS = TimeUnit.SECONDS.toNanos( - systemProp("kotlinx.coroutines.scheduler.keep.alive.sec", 100000L) + systemProp("kotlinx.coroutines.scheduler.keep.alive.sec", 60L) ) @JvmField diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingIOTerminationStressTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt similarity index 93% rename from kotlinx-coroutines-core/jvm/test/scheduling/BlockingIOTerminationStressTest.kt rename to kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt index de59a84a99..9c17e6988d 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/BlockingIOTerminationStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTerminationStressTest.kt @@ -9,7 +9,7 @@ import org.junit.* import java.util.* import java.util.concurrent.* -class BlockingIOTerminationStressTest : TestBase() { +class BlockingCoroutineDispatcherTerminationStressTest : TestBase() { private val baseDispatcher = ExperimentalCoroutineDispatcher( 2, 20, TimeUnit.MILLISECONDS.toNanos(10) From 966020ee5f6fe6e26fc150e937fb479d88207962 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 27 Nov 2019 14:47:07 +0300 Subject: [PATCH 86/90] Document CoroutineScheduler machinery --- .../jvm/src/scheduling/CoroutineScheduler.kt | 127 ++++++++++-------- .../jvm/src/scheduling/WorkQueue.kt | 14 +- 2 files changed, 75 insertions(+), 66 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 6e9c11cb5a..40daf1cd41 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -20,49 +20,73 @@ import kotlin.random.* * * Current scheduler implementation has two optimization targets: * * Efficiency in the face of communication patterns (e.g., actors communicating via channel) - * * Dynamic resizing to support blocking calls without re-dispatching coroutine to separate "blocking" thread pool + * * Dynamic resizing to support blocking calls without re-dispatching coroutine to separate "blocking" thread pool. * * ### Structural overview * - * Scheduler consists of [corePoolSize] worker threads to execute CPU-bound tasks and up to [maxPoolSize] lazily created threads - * to execute blocking tasks. Every worker has a local queue in addition to a global scheduler queue and the global queue - * has priority over local queue to avoid starvation of externally-submitted (e.g. from Android UI thread) tasks. - * Work-stealing is implemented on top of that queues to provide even load distribution and illusion of centralized run queue. + * Scheduler consists of [corePoolSize] worker threads to execute CPU-bound tasks and up to + * [maxPoolSize] lazily created threads to execute blocking tasks. + * Every worker has a local queue in addition to a global scheduler queue + * and the global queue has priority over local queue to avoid starvation of externally-submitted + * (e.g. from Android UI thread) tasks. + * Work-stealing is implemented on top of that queues to provide + * even load distribution and illusion of centralized run queue. * * ### Scheduling policy * * When a coroutine is dispatched from within a scheduler worker, it's placed into the head of worker run queue. - * If the head is not empty, the task from the head is moved to the tail. Though it is unfair scheduling policy, + * If the head is not empty, the task from the head is moved to the tail. Though it is an unfair scheduling policy, * it effectively couples communicating coroutines into one and eliminates scheduling latency - * that arises from placing task to the end of the queue. - * Placing former head to the tail is necessary to provide semi-FIFO order, otherwise queue degenerates to stack. + * that arises from placing tasks to the end of the queue. + * Placing former head to the tail is necessary to provide semi-FIFO order, otherwise, queue degenerates to stack. * When a coroutine is dispatched from an external thread, it's put into the global queue. + * The original idea with a single-slot LIFO buffer comes from Golang runtime scheduler by D. Vyukov. + * It was proven to be "fair enough", performant and generally well accepted and initially was a significant inspiration + * source for the coroutine scheduler. * * ### Work stealing and affinity * - * To provide even tasks distribution worker tries to steal tasks from other workers queues before parking when his local queue is empty. - * A non-standard solution is implemented to provide tasks affinity: task from FIFO buffer may be stolen only if it is stale enough - * (based on the value of [WORK_STEALING_TIME_RESOLUTION_NS]). - * For this purpose monotonic global clock ([System.nanoTime]) is used and every task has associated with it submission time. - * This approach shows outstanding results when coroutines are cooperative, but as downside scheduler now depends on high-resolution global clock + * To provide even tasks distribution worker tries to steal tasks from other workers queues + * before parking when his local queue is empty. + * A non-standard solution is implemented to provide tasks affinity: a task from FIFO buffer may be stolen + * only if it is stale enough based on the value of [WORK_STEALING_TIME_RESOLUTION_NS]. + * For this purpose, monotonic global clock is used, and every task has associated with its submission time. + * This approach shows outstanding results when coroutines are cooperative, + * but as downside scheduler now depends on a high-resolution global clock, * which may limit scalability on NUMA machines. Tasks from LIFO buffer can be stolen on a regular basis. * * ### Thread management - * One of the hardest parts of the scheduler is decentralized management of the threads with the progress guarantees similar - * to the regular centralized executors. The state of the threads consists of [controlState] and [parkedWorkersStack] fields. - * The former field incorporates the amount of created threads, CPU-tokens and blocking tasks that require a thread compensation, + * One of the hardest parts of the scheduler is decentralized management of the threads with the progress guarantees + * similar to the regular centralized executors. + * The state of the threads consists of [controlState] and [parkedWorkersStack] fields. + * The former field incorporates the amount of created threads, CPU-tokens and blocking tasks + * that require a thread compensation, * while the latter represents intrusive versioned Treiber stack of idle workers. - * When a worker cannot find any work, he first adds itself to the stack, then re-scans the queue (to avoid missing signal) - * and then attempts to park itself (there is additional layer of signalling against unnecessary park/unpark). - * If worker finds a task that it cannot yet steal due to timer constraints, it stores this fact in its state + * When a worker cannot find any work, they first add themselves to the stack, + * then re-scans the queue to avoid missing signals and then attempts to park + * with additional rendezvous against unnecessary parking. + * If a worker finds a task that it cannot yet steal due to time constraints, it stores this fact in its state * (to be uncounted when additional work is signalled) and parks for such duration. * - * When a new task arrives to the scheduler (whether it's local or global queue), either an idle worker is being signalled, or - * a new worker is attempted to be created (only [corePoolSize] workers can be created for regular CPU tasks). + * When a new task arrives in the scheduler (whether it is local or global queue), + * either an idle worker is being signalled, or a new worker is attempted to be created. + * Only [corePoolSize] workers can be created for regular CPU tasks) * - * ### Dynamic resizing and support of blocking tasks + * ### Support for blocking tasks + * The scheduler also supports the notion of [blocking][TaskMode.PROBABLY_BLOCKING] tasks. + * When executing or enqueuing blocking tasks, the scheduler notifies or creates one more worker in + * addition to core pool size, so at any given moment, it has [corePoolSize] threads (potentially not yet created) + * to serve CPU-bound tasks. To properly guarantee liveness, the scheduler maintains + * "CPU permits" -- [corePoolSize] special tokens that permit an arbitrary worker to execute and steal CPU-bound tasks. + * When worker encounters blocking tasks, it basically hands off its permit to another thread (not directly though) to + * keep invariant "scheduler always has at least min(pending CPU tasks, core pool size) + * and at most core pool size threads to execute CPU tasks". + * To avoid overprovision, workers without CPU permit are allowed to scan [globalBlockingQueue] + * and steal **only** blocking tasks from other workers. * - * TODO + * The scheduler does not limit the count of pending blocking tasks, potentially creating up to [maxPoolSize] threads. + * End users do not have access to the scheduler directly and can dispatch blocking tasks only with + * [LimitingDispatcher] that does control concurrency level by its own mechanism. */ @Suppress("NOTHING_TO_INLINE") internal class CoroutineScheduler( @@ -469,7 +493,7 @@ internal class CoroutineScheduler( */ if (worker.state === WorkerState.TERMINATED) return task // Do not add CPU tasks in local queue if we are not able to execute it - if (task.mode == TaskMode.NON_BLOCKING && worker.isBlocking) { + if (task.mode === TaskMode.NON_BLOCKING && worker.state === WorkerState.BLOCKING) { return task } worker.mayHaveLocalTasks = true @@ -487,7 +511,6 @@ internal class CoroutineScheduler( * E.g. for [1b, 1b, 2c, 1d] means that pool has * two blocking workers with queue size 1, one worker with CPU permit and queue size 1 * and one dormant (executing his local queue before parking) worker with queue size 1. - * TODO revisit */ override fun toString(): String { var parkedWorkers = 0 @@ -530,10 +553,10 @@ internal class CoroutineScheduler( "running workers queues = $queueSizes, "+ "global CPU queue size = ${globalCpuQueue.size}, " + "global blocking queue size = ${globalBlockingQueue.size}, " + - "Control State Workers {" + - "created = ${createdWorkers(state)}, " + - "blocking = ${blockingTasks(state)}, " + - "CPU acquired = ${corePoolSize - availableCpuPermits(state)}" + + "Control State {" + + "created workers= ${createdWorkers(state)}, " + + "blocking tasks = ${blockingTasks(state)}, " + + "CPUs acquired = ${corePoolSize - availableCpuPermits(state)}" + "}]" } @@ -574,9 +597,8 @@ internal class CoroutineScheduler( * Worker state. **Updated only by this worker thread**. * By default, worker is in DORMANT state in the case when it was created, but all CPU tokens or tasks were taken. */ - @Volatile + @JvmField var state = WorkerState.DORMANT - val isBlocking: Boolean get() = state == WorkerState.BLOCKING /** * Small state machine for termination. @@ -634,15 +656,13 @@ internal class CoroutineScheduler( * Tries to acquire CPU token if worker doesn't have one * @return whether worker acquired (or already had) CPU token */ - private fun tryAcquireCpuPermit(): Boolean { - return when { - state == WorkerState.CPU_ACQUIRED -> true - this@CoroutineScheduler.tryAcquireCpuPermit() -> { - state = WorkerState.CPU_ACQUIRED - true - } - else -> false + private fun tryAcquireCpuPermit(): Boolean = when { + state == WorkerState.CPU_ACQUIRED -> true + this@CoroutineScheduler.tryAcquireCpuPermit() -> { + state = WorkerState.CPU_ACQUIRED + true } + else -> false } /** @@ -711,22 +731,21 @@ internal class CoroutineScheduler( private fun tryPark() { if (!inStack()) { parkingState.value = PARKING_ALLOWED - } - if (parkedWorkersStackPush(this)) { + parkedWorkersStackPush(this) return - } else { - assert { localQueue.size == 0 } - // Failed to get a parking permit => we are not in the stack - while (inStack()) { - if (isTerminated || state == WorkerState.TERMINATED) break - if (parkingState.value != PARKED && !parkingState.compareAndSet(PARKING_ALLOWED, PARKED)) { - return - } - tryReleaseCpu(WorkerState.PARKING) - interrupted() // Cleanup interruptions - if (inStack()) { - park() - } + + } + assert { localQueue.size == 0 } + // Failed to get a parking permit => we are not in the stack + while (inStack()) { + if (isTerminated || state == WorkerState.TERMINATED) break + if (parkingState.value != PARKED && !parkingState.compareAndSet(PARKING_ALLOWED, PARKED)) { + return + } + tryReleaseCpu(WorkerState.PARKING) + interrupted() // Cleanup interruptions + if (inStack()) { + park() } } } diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt index 3471a1aae4..f5d94fb779 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt @@ -138,7 +138,7 @@ internal class WorkQueue { } fun offloadAllWorkTo(globalQueue: GlobalQueue) { - lastScheduledTask.getAndSet(null)?.let { globalQueue.add(it) } + lastScheduledTask.getAndSet(null)?.let { globalQueue.addLast(it) } while (pollTo(globalQueue)) { // Steal everything } @@ -173,7 +173,7 @@ internal class WorkQueue { private fun pollTo(queue: GlobalQueue): Boolean { val task = pollBuffer() ?: return false - queue.add(task) + queue.addLast(task) return true } @@ -198,13 +198,3 @@ internal class WorkQueue { } } } - -private fun GlobalQueue.add(task: Task) { - /* - * globalQueue is closed as the very last step in the shutdown sequence when all worker threads had - * been already shutdown (with the only exception of the last worker thread that might be performing - * shutdown procedure itself). As a consistency check we do a [cheap!] check that it is not closed here yet. - */ - val added = addLast(task) - assert { added } -} \ No newline at end of file From 4dcfced70ea5efd69a12d258ef5edc33a1129b44 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 9 Dec 2019 12:32:20 +0300 Subject: [PATCH 87/90] Ensure that interruption flag is cleaned up properly Fixes #1691 --- .../test/scheduling/CoroutineSchedulerTest.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt index b6914764c4..ff831950b5 100644 --- a/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt +++ b/kotlinx-coroutines-core/jvm/test/scheduling/CoroutineSchedulerTest.kt @@ -5,10 +5,11 @@ package kotlinx.coroutines.scheduling import kotlinx.coroutines.* -import org.junit.* +import org.junit.Test import java.lang.Runnable import java.util.concurrent.* import kotlin.coroutines.* +import kotlin.test.* class CoroutineSchedulerTest : TestBase() { @@ -127,6 +128,29 @@ class CoroutineSchedulerTest : TestBase() { latch.await() } + @Test + fun testInterruptionCleanup() { + ExperimentalCoroutineDispatcher(1, 1).use { + val executor = it.executor + var latch = CountDownLatch(1) + executor.execute { + Thread.currentThread().interrupt() + latch.countDown() + } + latch.await() + Thread.sleep(100) // I am really sorry + latch = CountDownLatch(1) + executor.execute { + try { + assertFalse(Thread.currentThread().isInterrupted) + } finally { + latch.countDown() + } + } + latch.await() + } + } + private fun testUniformDistribution(worker: CoroutineScheduler.Worker, bound: Int) { val result = IntArray(bound) val iterations = 10_000_000 From 4224e01552ace0cc0f719762ddda8c13a84a62e7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 10 Dec 2019 17:09:43 +0300 Subject: [PATCH 88/90] Merge termination state machine and parking state machine into one, get rid of long-scanning sequence --- .../jvm/src/scheduling/CoroutineScheduler.kt | 67 +++++-------------- .../jvm/src/scheduling/WorkQueue.kt | 2 +- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt index 40daf1cd41..09e9deb838 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt @@ -303,14 +303,10 @@ internal class CoroutineScheduler( @JvmField val NOT_IN_STACK = Symbol("NOT_IN_STACK") - // Worker termination states - private const val TERMINATION_FORBIDDEN = -1 - private const val TERMINATION_ALLOWED = 0 + // Worker ctl states + private const val PARKED = -1 + private const val CLAIMED = 0 private const val TERMINATED = 1 - // Worker parking states - private const val PARKING_FORBIDDEN = -1 - private const val PARKING_ALLOWED = 0 - private const val PARKED = 1 // Masks of control state private const val BLOCKING_SHIFT = 21 // 2M threads max @@ -443,10 +439,10 @@ internal class CoroutineScheduler( private fun tryUnpark(): Boolean { while (true) { val worker = parkedWorkersStackPop() ?: return false - if (!worker.parkingState.compareAndSet(PARKING_ALLOWED, PARKING_FORBIDDEN)) { + if (worker.workerCtl.compareAndSet(PARKED, CLAIMED)) { LockSupport.unpark(worker) + return true } - if (worker.tryForbidTermination()) return true } } @@ -596,23 +592,19 @@ internal class CoroutineScheduler( /** * Worker state. **Updated only by this worker thread**. * By default, worker is in DORMANT state in the case when it was created, but all CPU tokens or tasks were taken. + * Is used locally by the worker to maintain its own invariants. */ @JvmField var state = WorkerState.DORMANT /** - * Small state machine for termination. - * Followed states are allowed: - * [TERMINATION_ALLOWED] -- worker can wake up and terminate itself - * [TERMINATION_FORBIDDEN] -- worker is not allowed to terminate (because it was chosen by another thread to help) - * [TERMINATED] -- final state, thread is terminating and cannot be resurrected - * - * Allowed transitions: - * [TERMINATION_ALLOWED] -> [TERMINATION_FORBIDDEN] - * [TERMINATION_ALLOWED] -> [TERMINATED] - * [TERMINATION_FORBIDDEN] -> [TERMINATION_ALLOWED] + * Worker control state responsible for worker claiming, parking and termination. + * List of states: + * [PARKED] -- worker is parked and can self-terminate after a termination deadline. + * [CLAIMED] -- worker is claimed by an external submitter. + * [TERMINATED] -- worker is terminated and no longer usable. */ - private val terminationState = atomic(TERMINATION_ALLOWED) + val workerCtl = atomic(CLAIMED) /** * It is set to the termination deadline when started doing [park] and it reset @@ -632,25 +624,8 @@ internal class CoroutineScheduler( * The delay until at least one task in other worker queues will become stealable. */ private var minDelayUntilStealableTaskNs = 0L - // PARKING_ALLOWED | PARKING_FORBIDDEN | PARKED - val parkingState = atomic(PARKING_ALLOWED) private var rngState = Random.nextInt() - /** - * Tries to set [terminationState] to [TERMINATION_FORBIDDEN], returns `false` if this attempt fails. - * This attempt may fail either because worker terminated itself or because someone else - * claimed this worker (though this case is rare, because require very bad timings) - */ - fun tryForbidTermination(): Boolean = - when (val state = terminationState.value) { - TERMINATED -> false // already terminated - TERMINATION_FORBIDDEN -> false // already forbidden, someone else claimed this worker - TERMINATION_ALLOWED -> terminationState.compareAndSet( - TERMINATION_ALLOWED, - TERMINATION_FORBIDDEN - ) - else -> error("Invalid terminationState = $state") - } /** * Tries to acquire CPU token if worker doesn't have one @@ -730,23 +705,16 @@ internal class CoroutineScheduler( // Counterpart to "tryUnpark" private fun tryPark() { if (!inStack()) { - parkingState.value = PARKING_ALLOWED parkedWorkersStackPush(this) return - } assert { localQueue.size == 0 } - // Failed to get a parking permit => we are not in the stack - while (inStack()) { + workerCtl.value = PARKED // Update value once + while (inStack()) { // Prevent spurious wakeups if (isTerminated || state == WorkerState.TERMINATED) break - if (parkingState.value != PARKED && !parkingState.compareAndSet(PARKING_ALLOWED, PARKED)) { - return - } tryReleaseCpu(WorkerState.PARKING) interrupted() // Cleanup interruptions - if (inStack()) { - park() - } + park() } } @@ -798,7 +766,6 @@ internal class CoroutineScheduler( } private fun park() { - terminationState.value = TERMINATION_ALLOWED // set termination deadline the first time we are here (it is reset in idleReset) if (terminationDeadline == 0L) terminationDeadline = System.nanoTime() + idleWorkerKeepAliveNs // actually park @@ -824,7 +791,7 @@ internal class CoroutineScheduler( * See tryUnpark for state reasoning. * If this CAS fails, then we were successfully unparked by other worker and cannot terminate. */ - if (!terminationState.compareAndSet(TERMINATION_ALLOWED, TERMINATED)) return + if (!workerCtl.compareAndSet(PARKED, TERMINATED)) return /* * At this point this thread is no longer considered as usable for scheduling. * We need multi-step choreography to reindex workers. @@ -923,7 +890,7 @@ internal class CoroutineScheduler( var currentIndex = nextInt(created) var minDelay = Long.MAX_VALUE - repeat(workers.length()) { + repeat(created) { ++currentIndex if (currentIndex > created) currentIndex = 1 val worker = workers[currentIndex] diff --git a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt index f5d94fb779..1a0603e413 100644 --- a/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt +++ b/kotlinx-coroutines-core/jvm/src/scheduling/WorkQueue.kt @@ -32,7 +32,7 @@ internal const val NOTHING_TO_STEAL = -2L * order to properly claim value from the buffer. * Moreover, [Task] objects are reusable, so it may seem that this queue is prone to ABA problem. * Indeed it formally has ABA-problem, but the whole processing logic is written in the way that such ABA is harmless. - * "I have discovered a truly marvelous proof of this, which this margin is too narrow to contain" + * I have discovered a truly marvelous proof of this, which this KDoc is too narrow to contain. */ internal class WorkQueue { From 41dca58f7d6f313bac77b6d6d6aa0ff404a75d31 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Thu, 12 Dec 2019 11:45:55 +0300 Subject: [PATCH 89/90] Proguard rule to keep volatile name of SafeContinuation Fixes #1694 --- .../jvm/resources/META-INF/proguard/coroutines.pro | 5 +++++ 1 file changed, 5 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 31c464f68d..2f9a2daee5 100644 --- a/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro +++ b/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro @@ -8,3 +8,8 @@ -keepclassmembernames class kotlinx.** { volatile ; } + +# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater +-keepclassmembernames class kotlin.coroutines.SafeContinuation { + volatile ; +} \ No newline at end of file From efc234f934765aae6e4e1c80d54116c3864f12c9 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 12 Dec 2019 19:40:37 +0300 Subject: [PATCH 90/90] Version 1.3.3 --- CHANGES.md | 43 +++++++++++++++++++ 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, 58 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8f08a9d58b..bf1c2e90d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,48 @@ # Change log for kotlinx.coroutines +## Version 1.3.3 + +### Flow +* `Flow.take` performance is significantly improved (#1538). +* `Flow.merge` operator (#1491). +* Reactive Flow adapters are promoted to stable API (#1549). +* Reusable cancellable continuations were introduced that improved the performance of various flow operators and iteration over channels (#1534). +* Fixed interaction of multiple flows with `take` operator (#1610). +* Throw `NoSuchElementException` instead of `UnsupportedOperationException` for empty `Flow` in `reduce` operator (#1659). +* `onCompletion` now rethrows downstream exceptions on emit attempt (#1654). +* Allow non-emitting `withContext` from `flow` builder (#1616). + +### Debugging + +* `DebugProbes.dumpCoroutines` is optimized to be able to print the 6-digit number of coroutines (#1535). +* Properly capture unstarted lazy coroutines in debugger (#1544). +* Capture coroutines launched from within a test constructor with `CoroutinesTimeout` test rule (#1542). +* Stacktraces of `Job`-related coroutine machinery are shortened and prettified (#1574). +* Stacktrace recovery unification that should provide a consistent experience recover of stacktrace (#1597). +* Stacktrace recovery for `withTimeout` is supported (#1625). +* Do not recover exception with a single `String` parameter constructor that is not a `message` (#1631). + +### Other features + +* `Dispatchers.Default` and `Dispatchers.IO` rework: CPU consumption is significantly lower, predictable idle threads termination (#840, #1046, #1286). +* Avoid `ServiceLoader` for loading `Dispatchers.Main` (#1572, #1557, #878, #1606). +* Consistently handle undeliverable exceptions in RxJava and Reactor integrations (#252, #1614). +* `yield` support in immediate dispatchers (#1474). +* `CompletableDeferred.completeWith(result: Result)` is introduced. +* Added support for tvOS and watchOS-based Native targets (#1596). + +### Bug fixes and improvements + +* Kotlin version is updated to 1.3.61. +* `CoroutineDispatcher.isDispatchNeeded` is promoted to stable API (#1014). +* Livelock and stackoverflows in mutual `select` expressions are fixed (#1411, #504). +* Properly handle `null` values in `ListenableFuture` integration (#1510). +* Making ReceiveChannel.cancel linearizability-friendly. +* Linearizability of Channel.close in a complex contended cases (#1419). +* ArrayChannel.isBufferEmpty atomicity is fixed (#1526). +* Various documentation improvements. +* Reduced bytecode size of `kotlinx-coroutines-core`, reduced size of minified `dex` when using basic functionality of `kotlinx-coroutines`. + ## Version 1.3.2 This is a maintenance release that does not include any new features or bug fixes. diff --git a/README.md b/README.md index 321c7e5d61..bbd8726b42 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.2) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.2) +[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=1.3.3) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/1.3.3) Library support for Kotlin coroutines with [multiplatform](#multiplatform) support. This is a companion version for Kotlin `1.3.61` release. @@ -82,7 +82,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.3.2 + 1.3.3 ``` @@ -100,7 +100,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' } ``` @@ -126,7 +126,7 @@ Add dependencies (you can also add other modules that you need): ```groovy dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3") } ``` @@ -145,7 +145,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.2/jar) +[`kotlinx-coroutines-core-common`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common/1.3.3/jar) (follow the link to get the dependency declaration snippet). ### Android @@ -154,7 +154,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.2' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' ``` This gives you access to Android [Dispatchers.Main] @@ -173,7 +173,7 @@ R8 is a replacement for ProGuard in Android ecosystem, it is enabled by default ### 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.2/jar) +[`kotlinx-coroutines-core-js`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.3.3/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. @@ -181,7 +181,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.2/jar) +[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.3/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 1214a79e4f..b87d2b009f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.3.2-SNAPSHOT +version=1.3.3-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.3.61 diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index bfed91b7e3..21cc76e74f 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -18,7 +18,7 @@ of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances usin Add `kotlinx-coroutines-debug` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.3' } ``` @@ -56,7 +56,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent It is possible to use this module 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.2.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.3.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. diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index a634fecc68..aefd5a3f07 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.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3' } ``` diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index ffaf1ae9f4..fc020cbd86 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -165,7 +165,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.2" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3" ``` 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 e494b796d7..c07db5d8f0 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.61 -coroutines_version=1.3.2 +coroutines_version=1.3.3 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 e494b796d7..c07db5d8f0 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.61 -coroutines_version=1.3.2 +coroutines_version=1.3.3 android.useAndroidX=true android.enableJetifier=true