diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 62fd5c7dfd..461a31ed76 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -12,4 +12,4 @@ - \ No newline at end of file + diff --git a/CHANGES.md b/CHANGES.md index acf2d2c28b..c59e3b306e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,78 @@ # Change log for kotlinx.coroutines +## Version 1.7.0 + +### Core API significant improvements + +* New `Channel` implementation with significant performance improvements across the API (#3621). +* New `select` operator implementation: faster, more lightweight, and more robust (#3020). +* `Mutex` and `Semaphore` now share the same underlying data structure (#3020). +* `Dispatchers.IO` is added to K/N (#3205) + * `newFixedThreadPool` and `Dispatchers.Default` implementations on K/N were wholly rewritten to support graceful growth under load (#3595). +* `kotlinx-coroutines-test` rework: + - Add the `timeout` parameter to `runTest` for the whole-test timeout, 10 seconds by default (#3270). This replaces the configuration of quiescence timeouts, which is now deprecated (#3603). + - The `withTimeout` exception messages indicate if the timeout used the virtual time (#3588). + - `TestCoroutineScheduler`, `runTest`, and `TestScope` API are promoted to stable (#3622). + - `runTest` now also fails if there were uncaught exceptions in coroutines not inherited from the test coroutine (#1205). + +### Breaking changes + +* Old K/N memory model is no longer supported (#3375). +* New generic upper bounds were added to reactive integration API where the language since 1.8.0 dictates (#3393). +* `kotlinx-coroutines-core` and `kotlinx-coroutines-jdk8` artifacts were merged into a single artifact (#3268). +* Artificial stackframes in stacktrace recovery no longer contain the `\b` symbol and are now navigable in IDE and supplied with proper documentation (#2291). +* `CoroutineContext.isActive` returns `true` for contexts without any job in them (#3300). + +### Bug fixes and improvements + +* Kotlin version is updated to 1.8.20 +* Atomicfu version is updated to 0.20.2. +* `JavaFx` version is updated to 17.0.2 in `kotlinx-coroutines-javafx` (#3671).. +* JPMS is supported (#2237). Thanks @lion7! +* `BroadcastChannel` and all the corresponding API are deprecated (#2680). +* Added all supported K/N targets (#3601, #812, #855). +* K/N `Dispatchers.Default` is backed by the number of threads equal to the number of available cores (#3366). +* Fixed an issue where some coroutines' internal exceptions were not properly serializable (#3328). +* Introduced `Job.parent` API (#3201). +* Fixed a bug when `TestScheduler` leaked cancelled jobs (#3398). +* `TestScope.timeSource` now provides comparable time marks (#3617). Thanks @hfhbd! +* Fixed an issue when cancelled `withTimeout` handles were preserved in JS runtime (#3440). +* Ensure `awaitFrame` only awaits a single frame when used from the main looper (#3432). Thanks @pablobaxter! +* Obsolete `Class-Path` attribute was removed from `kotlinx-coroutines-debug.jar` manifest (#3361). +* Fixed a bug when `updateThreadContext` operated on the parent context (#3411). +* Added new `Flow.filterIsInstance` extension (#3240). +* `Dispatchers.Default` thread name prefixes are now configurable with system property (#3231). +* Added `Flow.timeout` operator as `@FlowPreview` (#2624). Thanks @pablobaxter! +* Improved the performance of the `future` builder in case of exceptions (#3475). Thanks @He-Pin! +* `Mono.awaitSingleOrNull` now waits for the `onComplete` signal (#3487). +* `Channel.isClosedForSend` and `Channel.isClosedForReceive` are promoted from experimental to delicate (#3448). +* Fixed a data race in native `EventLoop` (#3547). +* `Dispatchers.IO.limitedParallelism(valueLargerThanIOSize)` no longer creates an additional wrapper (#3442). Thanks @dovchinnikov! +* Various `@FlowPreview` and `@ExperimentalCoroutinesApi` are promoted to experimental and stable respectively (#3542, #3097, #3548). +* Performance improvements in `Dispatchers.Default` and `Dispatchers.IO` (#3416, #3418). +* Fixed a bug when internal `suspendCancellableCoroutineReusable` might have hanged (#3613). +* Introduced internal API to process events in the current system dispatcher (#3439). +* Global `CoroutineExceptionHandler` is no longer invoked in case of unprocessed `future` failure (#3452). +* Performance improvements and reduced thread-local pressure for the `withContext` operator (#3592). +* Improved performance of `DebugProbes` (#3527). +* Fixed a bug when the coroutine debugger might have detected the state of a coroutine incorrectly (#3193). +* `CoroutineDispatcher.asExecutor()` runs tasks without dispatching if the dispatcher is unconfined (#3683). Thanks @odedniv! +* `SharedFlow.toMutableList` and `SharedFlow.toSet` lints are introduced (#3706). +* `Channel.invokeOnClose` is promoted to stable API (#3358). +* Improved lock contention in `Dispatchers.Default` and `Dispatchers.IO` during the startup phase (#3652). +* Fixed a bug that led to threads oversubscription in `Dispatchers.Default` (#3642). +* Fixed a bug that allowed `limitedParallelism` to perform dispatches even after the underlying dispatcher was closed (#3672). +* Fixed a bug that prevented stacktrace recovery when the exception's constructor from `cause` was selected (#3714). +* Improved sanitizing of stracktrace-recovered traces (#3714). +* Introduced an internal flag to disable uncaught exceptions reporting in tests as a temporary migration mechanism (#3736). +* Various documentation improvements and fixes. + +### Changelog relative to version 1.7.0-RC + +* Fixed a bug that prevented stacktrace recovery when the exception's constructor from `cause` was selected (#3714). +* Improved sanitizing of stracktrace-recovered traces (#3714). +* Introduced an internal flag to disable uncaught exceptions reporting in tests as a temporary migration mechanism (#3736). + ## Version 1.7.0-RC * Kotlin version is updated to 1.8.20. diff --git a/README.md b/README.md index 283afb85ba..dad7e02f52 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Kotlin Stable](https://kotl.in/badges/stable.svg)](https://kotlinlang.org/docs/components-stability.html) [![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0-RC)](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0-RC) +[![Download](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0)](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.7.0) [![Kotlin](https://img.shields.io/badge/kotlin-1.8.20-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Slack channel](https://img.shields.io/badge/chat-slack-green.svg?logo=slack)](https://kotlinlang.slack.com/messages/coroutines/) @@ -85,7 +85,7 @@ Add dependencies (you can also add other modules that you need): org.jetbrains.kotlinx kotlinx-coroutines-core - 1.7.0-RC + 1.7.0 ``` @@ -103,7 +103,7 @@ Add dependencies (you can also add other modules that you need): ```kotlin dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") } ``` @@ -133,7 +133,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android) module as a dependency when using `kotlinx.coroutines` on Android: ```kotlin -implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0-RC") +implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0") ``` This gives you access to the Android [Dispatchers.Main] @@ -168,7 +168,7 @@ In common code that should get compiled for different platforms, you can add a d ```kotlin commonMain { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") } } ``` @@ -180,7 +180,7 @@ Platform-specific dependencies are recommended to be used only for non-multiplat #### JS Kotlin/JS version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-js`](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.7.0-RC) +[`kotlinx-coroutines-core-js`](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js/1.7.0) (follow the link to get the dependency declaration snippet) and as [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotlinx-coroutines-core) NPM package. #### Native diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index b4629809db..e64f18905f 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -24,7 +24,7 @@ java { tasks.named("compileJmhKotlin") { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs += "-Xjvm-default=enable" + freeCompilerArgs += "-Xjvm-default=all" } } diff --git a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt index 006d36c04b..10433fcb45 100644 --- a/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt +++ b/benchmarks/src/jmh/kotlin/benchmarks/flow/scrabble/ShakespearePlaysScrabble.kt @@ -34,14 +34,12 @@ abstract class ShakespearePlaysScrabble { public interface LongWrapper { fun get(): Long - @JvmDefault fun incAndSet(): LongWrapper { return object : LongWrapper { override fun get(): Long = this@LongWrapper.get() + 1L } } - @JvmDefault fun add(other: LongWrapper): LongWrapper { return object : LongWrapper { override fun get(): Long = this@LongWrapper.get() + other.get() diff --git a/gradle.properties b/gradle.properties index 966a90daf6..2daed3cf4c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.7.0-RC-SNAPSHOT +version=1.7.0-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.8.20 @@ -13,7 +13,7 @@ junit5_version=5.7.0 atomicfu_version=0.20.2 knit_version=0.4.0 html_version=0.7.2 -lincheck_version=2.16 +lincheck_version=2.17 dokka_version=1.8.10 byte_buddy_version=1.10.9 reactor_version=3.4.1 diff --git a/integration-testing/gradle.properties b/integration-testing/gradle.properties index 4644d2346a..ac155f1dac 100644 --- a/integration-testing/gradle.properties +++ b/integration-testing/gradle.properties @@ -1,5 +1,5 @@ kotlin_version=1.8.20 -coroutines_version=1.7.0-RC-SNAPSHOT +coroutines_version=1.7.0-SNAPSHOT asm_version=9.3 kotlin.code.style=official diff --git a/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt b/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt index edb1c3c9f7..b749ee63f8 100644 --- a/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/BufferedChannel.kt @@ -1220,9 +1220,9 @@ internal open class BufferedChannel( incCompletedExpandBufferAttempts() return } - // Is `bufferEndSegment` outdated? + // Is `bufferEndSegment` outdated or is the segment with the required id already removed? // Find the required segment, creating new ones if needed. - if (segment.id < id) { + if (segment.id != id) { segment = findSegmentBufferEnd(id, segment, b) // Restart if the required segment is removed, or // the linked list of segments is already closed, diff --git a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt index de15225266..03308f6137 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/ExceptionsConstructor.kt @@ -32,42 +32,48 @@ internal fun tryCopyException(exception: E): E? { private fun createConstructor(clz: Class): Ctor { val nullResult: Ctor = { null } // Pre-cache class - // Skip reflective copy if an exception has additional fields (that are usually populated in user-defined constructors) + // Skip reflective copy if an exception has additional fields (that are typically populated in user-defined constructors) if (throwableFields != clz.fieldsCountOrDefault(0)) return nullResult /* - * Try to reflectively find constructor(), constructor(message, cause), constructor(cause) or constructor(message). - * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace. - */ - val constructors = clz.constructors.sortedByDescending { it.parameterTypes.size } - for (constructor in constructors) { - val result = createSafeConstructor(constructor) - if (result != null) return result - } - return nullResult -} - -private fun createSafeConstructor(constructor: Constructor<*>): Ctor? { - val p = constructor.parameterTypes - return when (p.size) { - 2 -> when { - p[0] == String::class.java && p[1] == Throwable::class.java -> - safeCtor { e -> constructor.newInstance(e.message, e) as Throwable } - else -> null + * Try to reflectively find constructor(message, cause), constructor(message), constructor(cause), or constructor(), + * in that order of priority. + * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace. + * + * By default, Java's reflection iterates over ctors in the source-code order and the sorting is stable, so we can + * not rely on the order of iteration. Instead, we assign a unique priority to each ctor type. + */ + return clz.constructors.map { constructor -> + val p = constructor.parameterTypes + when (p.size) { + 2 -> when { + p[0] == String::class.java && p[1] == Throwable::class.java -> + safeCtor { e -> constructor.newInstance(e.message, e) as Throwable } to 3 + else -> null to -1 + } + 1 -> when (p[0]) { + String::class.java -> + safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } } to 2 + Throwable::class.java -> + safeCtor { e -> constructor.newInstance(e) as Throwable } to 1 + else -> null to -1 + } + 0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } } to 0 + else -> null to -1 } - 1 -> when (p[0]) { - Throwable::class.java -> - safeCtor { e -> constructor.newInstance(e) as Throwable } - String::class.java -> - safeCtor { e -> (constructor.newInstance(e.message) as Throwable).also { it.initCause(e) } } - else -> null - } - 0 -> safeCtor { e -> (constructor.newInstance() as Throwable).also { it.initCause(e) } } - else -> null - } + }.maxByOrNull(Pair<*, Int>::second)?.first ?: nullResult } -private inline fun safeCtor(crossinline block: (Throwable) -> Throwable): Ctor = - { e -> runCatching { block(e) }.getOrNull() } +private fun safeCtor(block: (Throwable) -> Throwable): Ctor = { e -> + runCatching { + val result = block(e) + /* + * Verify that the new exception has the same message as the original one (bail out if not, see #1631) + * or if the new message complies the contract from `Throwable(cause).message` contract. + */ + if (e.message != result.message && result.message != e.toString()) null + else result + }.getOrNull() +} private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) = kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue) diff --git a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt index 5193b0dc26..afc9989646 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/StackTraceRecovery.kt @@ -33,16 +33,16 @@ private val stackTraceRecoveryClassName = runCatching { internal actual fun recoverStackTrace(exception: E): E { if (!RECOVER_STACK_TRACES) return exception // No unwrapping on continuation-less path: exception is not reported multiple times via slow paths - val copy = tryCopyAndVerify(exception) ?: return exception + val copy = tryCopyException(exception) ?: return exception return copy.sanitizeStackTrace() } private fun E.sanitizeStackTrace(): E { val stackTrace = stackTrace val size = stackTrace.size - val lastIntrinsic = stackTrace.frameIndex(stackTraceRecoveryClassName) + val lastIntrinsic = stackTrace.indexOfLast { stackTraceRecoveryClassName == it.className } val startIndex = lastIntrinsic + 1 - val endIndex = stackTrace.frameIndex(baseContinuationImplClassName) + val endIndex = stackTrace.firstFrameIndex(baseContinuationImplClassName) val adjustment = if (endIndex == -1) 0 else size - endIndex val trace = Array(size - lastIntrinsic - adjustment) { if (it == 0) { @@ -70,7 +70,7 @@ private fun recoverFromStackFrame(exception: E, continuation: Co val (cause, recoveredStacktrace) = exception.causeAndStacktrace() // Try to create an exception of the same type and get stacktrace from continuation - val newException = tryCopyAndVerify(cause) ?: return exception + val newException = tryCopyException(cause) ?: return exception // Update stacktrace val stacktrace = createStackTrace(continuation) if (stacktrace.isEmpty()) return exception @@ -82,14 +82,6 @@ private fun recoverFromStackFrame(exception: E, continuation: Co return createFinalException(cause, newException, stacktrace) } -private fun tryCopyAndVerify(exception: E): E? { - val newException = tryCopyException(exception) ?: return null - // Verify that the new exception has the same message as the original one (bail out if not, see #1631) - // CopyableThrowable has control over its message and thus can modify it the way it wants - if (exception !is CopyableThrowable<*> && newException.message != exception.message) return null - return newException -} - /* * Here we partially copy original exception stackTrace to make current one much prettier. * E.g. for @@ -109,7 +101,7 @@ private fun tryCopyAndVerify(exception: E): E? { private fun createFinalException(cause: E, result: E, resultStackTrace: ArrayDeque): E { resultStackTrace.addFirst(ARTIFICIAL_FRAME) val causeTrace = cause.stackTrace - val size = causeTrace.frameIndex(baseContinuationImplClassName) + val size = causeTrace.firstFrameIndex(baseContinuationImplClassName) if (size == -1) { result.stackTrace = resultStackTrace.toTypedArray() return result @@ -157,7 +149,6 @@ private fun mergeRecoveredTraces(recoveredStacktrace: Array, } } -@Suppress("NOTHING_TO_INLINE") internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing { if (!RECOVER_STACK_TRACES) throw exception suspendCoroutineUninterceptedOrReturn { @@ -198,7 +189,7 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque.frameIndex(methodName: String) = indexOfFirst { methodName == it.className } +private fun Array.firstFrameIndex(methodName: String) = indexOfFirst { methodName == it.className } private fun StackTraceElement.elementWiseEquals(e: StackTraceElement): Boolean { /* diff --git a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt index 64085ad329..da3558ba30 100644 --- a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testReceiveFromChannel.txt @@ -1,5 +1,4 @@ kotlinx.coroutines.RecoverableTestException - at kotlinx.coroutines.internal.StackTraceRecoveryKt.recoverStackTrace(StackTraceRecovery.kt) at kotlinx.coroutines.channels.BufferedChannel.receive$suspendImpl(BufferedChannel.kt) at kotlinx.coroutines.channels.BufferedChannel.receive(BufferedChannel.kt) at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest.channelReceive(StackTraceRecoveryChannelsTest.kt) @@ -7,4 +6,4 @@ kotlinx.coroutines.RecoverableTestException at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$channelReceive$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt) Caused by: kotlinx.coroutines.RecoverableTestException at kotlinx.coroutines.exceptions.StackTraceRecoveryChannelsTest$testReceiveFromChannel$1$job$1.invokeSuspend(StackTraceRecoveryChannelsTest.kt) - at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt) \ No newline at end of file + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt) 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 index a8461556d1..9fc7167833 100644 --- a/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfined.txt +++ b/kotlinx-coroutines-core/jvm/test-resources/stacktraces/resume-mode/testUnconfined.txt @@ -1,5 +1,4 @@ kotlinx.coroutines.RecoverableTestException - at kotlinx.coroutines.internal.StackTraceRecoveryKt.recoverStackTrace(StackTraceRecovery.kt) at kotlinx.coroutines.channels.BufferedChannel.receive$suspendImpl(BufferedChannel.kt) at kotlinx.coroutines.channels.BufferedChannel.receive(BufferedChannel.kt) at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$withContext$2.invokeSuspend(StackTraceRecoveryResumeModeTest.kt) @@ -7,4 +6,4 @@ Caused by: kotlinx.coroutines.RecoverableTestException at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt) at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest.access$testResumeModeFastPath(StackTraceRecoveryResumeModeTest.kt) at kotlinx.coroutines.exceptions.StackTraceRecoveryResumeModeTest$testUnconfined$1.invokeSuspend(StackTraceRecoveryResumeModeTest.kt) - at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt) \ No newline at end of file + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt) diff --git a/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt index bdb615d83c..da73ca6220 100644 --- a/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/AbstractLincheckTest.kt @@ -9,7 +9,7 @@ import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.junit.* -abstract class AbstractLincheckTest : VerifierState() { +abstract class AbstractLincheckTest { open fun > O.customize(isStressTest: Boolean): O = this open fun ModelCheckingOptions.customize(isStressTest: Boolean): ModelCheckingOptions = this open fun StressOptions.customize(isStressTest: Boolean): StressOptions = this @@ -41,6 +41,4 @@ abstract class AbstractLincheckTest : VerifierState() { .actorsPerThread(if (isStressTest) 3 else 2) .actorsAfter(if (isStressTest) 3 else 0) .customize(isStressTest) - - override fun extractState(): Any = error("Not implemented") } diff --git a/kotlinx-coroutines-core/jvm/test/MutexCancellationStressTest.kt b/kotlinx-coroutines-core/jvm/test/MutexCancellationStressTest.kt index eb6360dac0..20798b837d 100644 --- a/kotlinx-coroutines-core/jvm/test/MutexCancellationStressTest.kt +++ b/kotlinx-coroutines-core/jvm/test/MutexCancellationStressTest.kt @@ -8,7 +8,11 @@ import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* import kotlinx.coroutines.sync.* import org.junit.* +import org.junit.Test import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.* class MutexCancellationStressTest : TestBase() { @Test @@ -18,13 +22,16 @@ class MutexCancellationStressTest : TestBase() { val mutexOwners = Array(mutexJobNumber) { "$it" } val dispatcher = Executors.newFixedThreadPool(mutexJobNumber + 2).asCoroutineDispatcher() var counter = 0 - val counterLocal = Array(mutexJobNumber) { LocalAtomicInt(0) } - val completed = LocalAtomicInt(0) + val counterLocal = Array(mutexJobNumber) { AtomicInteger(0) } + val completed = AtomicBoolean(false) val mutexJobLauncher: (jobNumber: Int) -> Job = { jobId -> val coroutineName = "MutexJob-$jobId" - launch(dispatcher + CoroutineName(coroutineName)) { - while (completed.value == 0) { + // ATOMIC to always have a chance to proceed + launch(dispatcher + CoroutineName(coroutineName), CoroutineStart.ATOMIC) { + while (!completed.get()) { + // Stress out holdsLock mutex.holdsLock(mutexOwners[(jobId + 1) % mutexJobNumber]) + // Stress out lock-like primitives if (mutex.tryLock(mutexOwners[jobId])) { counterLocal[jobId].incrementAndGet() counter++ @@ -47,18 +54,20 @@ class MutexCancellationStressTest : TestBase() { val mutexJobs = (0 until mutexJobNumber).map { mutexJobLauncher(it) }.toMutableList() val checkProgressJob = launch(dispatcher + CoroutineName("checkProgressJob")) { var lastCounterLocalSnapshot = (0 until mutexJobNumber).map { 0 } - while (completed.value == 0) { - delay(1000) + while (!completed.get()) { + delay(500) + // If we've caught the completion after delay, then there is a chance no progress were made whatsoever, bail out + if (completed.get()) return@launch val c = counterLocal.map { it.value } for (i in 0 until mutexJobNumber) { - assert(c[i] > lastCounterLocalSnapshot[i]) { "No progress in MutexJob-$i" } + assert(c[i] > lastCounterLocalSnapshot[i]) { "No progress in MutexJob-$i, last observed state: ${c[i]}" } } lastCounterLocalSnapshot = c } } val cancellationJob = launch(dispatcher + CoroutineName("cancellationJob")) { var cancellingJobId = 0 - while (completed.value == 0) { + while (!completed.get()) { val jobToCancel = mutexJobs.removeFirst() jobToCancel.cancelAndJoin() mutexJobs += mutexJobLauncher(cancellingJobId) @@ -66,11 +75,11 @@ class MutexCancellationStressTest : TestBase() { } } delay(2000L * stressTestMultiplier) - completed.value = 1 + completed.set(true) cancellationJob.join() mutexJobs.forEach { it.join() } checkProgressJob.join() - check(counter == counterLocal.sumOf { it.value }) + assertEquals(counter, counterLocal.sumOf { it.value }) dispatcher.close() } } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt index d4e19040a5..0f987e56e0 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryCustomExceptionsTest.kt @@ -87,6 +87,26 @@ class StackTraceRecoveryCustomExceptionsTest : TestBase() { assertEquals("Token OK", ex.message) } + @Test + fun testNestedExceptionWithCause() = runTest { + val result = runCatching { + coroutineScope { + throw NestedException(IllegalStateException("ERROR")) + } + } + val ex = result.exceptionOrNull() ?: error("Expected to fail") + assertIs(ex) + assertIs(ex.cause) + val originalCause = ex.cause?.cause + assertIs(originalCause) + assertEquals("ERROR", originalCause.message) + } + + class NestedException : RuntimeException { + constructor(cause: Throwable) : super(cause) + constructor() : super() + } + @Test fun testWrongMessageExceptionInChannel() = runTest { val result = produce(SupervisorJob() + Dispatchers.Unconfined) { diff --git a/kotlinx-coroutines-core/jvm/test/internal/OnDemandAllocatingPoolLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/internal/OnDemandAllocatingPoolLincheckTest.kt index de9ab8d5cd..9655890c4f 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/OnDemandAllocatingPoolLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/internal/OnDemandAllocatingPoolLincheckTest.kt @@ -27,8 +27,6 @@ abstract class OnDemandAllocatingPoolLincheckTest(maxCapacity: Int) : AbstractLi @Operation fun close(): String = pool.close().sorted().toString() - - override fun extractState(): Any = pool.stateRepresentation() } abstract class OnDemandAllocatingSequentialPool(private val maxCapacity: Int) { diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt index 87ed74b715..092ef6fc52 100644 --- a/kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/ChannelsLincheckTest.kt @@ -16,7 +16,6 @@ import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.annotations.Operation import org.jetbrains.kotlinx.lincheck.paramgen.* import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* -import org.jetbrains.kotlinx.lincheck.verifier.* class RendezvousChannelLincheckTest : ChannelLincheckTestBaseWithOnSend( c = Channel(RENDEZVOUS), @@ -176,7 +175,7 @@ private class NumberedCancellationException(number: Int) : CancellationException } -abstract class SequentialIntChannelBase(private val capacity: Int) : VerifierState() { +abstract class SequentialIntChannelBase(private val capacity: Int) { private val senders = ArrayList, Int>>() private val receivers = ArrayList>() private val buffer = ArrayList() @@ -266,8 +265,6 @@ abstract class SequentialIntChannelBase(private val capacity: Int) : VerifierSta if (closedMessage !== null) return false return buffer.isEmpty() && senders.isEmpty() } - - override fun extractState() = buffer to closedMessage } private fun CancellableContinuation.resume(res: T): Boolean { diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt index 2a9164e1d7..11f5535b5a 100644 --- a/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/LockFreeTaskQueueLincheckTest.kt @@ -30,8 +30,6 @@ internal abstract class AbstractLockFreeTaskQueueWithoutRemoveLincheckTest( override fun > O.customize(isStressTest: Boolean): O = verifier(QuiescentConsistencyVerifier::class.java) - override fun extractState() = q.map { it } to q.isClosed() - override fun ModelCheckingOptions.customize(isStressTest: Boolean) = checkObstructionFreedom() } @@ -42,9 +40,8 @@ internal class MCLockFreeTaskQueueWithRemoveLincheckTest : AbstractLockFreeTaskQ fun removeFirstOrNull() = q.removeFirstOrNull() } -@OpGroupConfig(name = "consumer", nonParallel = true) internal class SCLockFreeTaskQueueWithRemoveLincheckTest : AbstractLockFreeTaskQueueWithoutRemoveLincheckTest(singleConsumer = true) { @QuiescentConsistent - @Operation(group = "consumer") + @Operation(nonParallelGroup = "consumer") fun removeFirstOrNull() = q.removeFirstOrNull() } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt index 6fd28e424e..983a64acda 100644 --- a/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt @@ -40,8 +40,5 @@ class MutexLincheckTest : AbstractLincheckTest() { override fun > O.customize(isStressTest: Boolean): O = actorsBefore(0) - // state[i] == true <=> mutex.holdsLock(i) with the only exception for 0 that specifies `null`. - override fun extractState() = (1..2).map { mutex.holdsLock(it) } + mutex.isLocked - private val Int.asOwnerOrNull get() = if (this == 0) null else this } diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt index 1948a78ecc..e937b37e08 100644 --- a/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/ResizableAtomicArrayLincheckTest.kt @@ -11,17 +11,14 @@ import org.jetbrains.kotlinx.lincheck.paramgen.* @Param(name = "index", gen = IntGen::class, conf = "0:4") @Param(name = "value", gen = IntGen::class, conf = "1:5") -@OpGroupConfig(name = "sync", nonParallel = true) class ResizableAtomicArrayLincheckTest : AbstractLincheckTest() { private val a = ResizableAtomicArray(2) @Operation fun get(@Param(name = "index") index: Int): Int? = a[index] - @Operation(group = "sync") + @Operation(nonParallelGroup = "writer") fun set(@Param(name = "index") index: Int, @Param(name = "value") value: Int) { a.setSynchronized(index, value) } - - override fun extractState() = (0..4).map { a[it] } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt b/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt index 09dee56c51..d093e8066a 100644 --- a/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt +++ b/kotlinx-coroutines-core/jvm/test/lincheck/SemaphoreLincheckTest.kt @@ -25,8 +25,6 @@ abstract class SemaphoreLincheckTestBase(permits: Int) : AbstractLincheckTest() override fun > O.customize(isStressTest: Boolean): O = actorsBefore(0) - override fun extractState() = semaphore.availablePermits - override fun ModelCheckingOptions.customize(isStressTest: Boolean) = checkObstructionFreedom() } diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index ff9ba9fffa..117c663afc 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -61,7 +61,7 @@ stacktraces will be dumped to the console. ### Using as JVM agent Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup. -You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.7.0-RC.jar`. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.7.0.jar`. Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control [DebugProbes.enableCreationStackTraces] along with agent startup. diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index 965f17883f..89dd914879 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -1,7 +1,6 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package kotlinx.coroutines.debug import kotlinx.coroutines.* @@ -170,7 +169,8 @@ class RunningThreadStackMergeTest : DebugTestBase() { assertTrue(true) } - @Test + @Test // IDEA-specific debugger API test + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") fun testActiveThread() = runBlocking { launchCoroutine() awaitCoroutineStarted() diff --git a/kotlinx-coroutines-debug/test/StacktraceUtils.kt b/kotlinx-coroutines-debug/test/StacktraceUtils.kt index 55bdd7e0b0..869e29751c 100644 --- a/kotlinx-coroutines-debug/test/StacktraceUtils.kt +++ b/kotlinx-coroutines-debug/test/StacktraceUtils.kt @@ -74,6 +74,18 @@ private fun cleanBlockHoundTraces(frames: List): List { return result } +/** + * Removes all frames that contain "java.util.concurrent" in it. + * + * We do leverage Java's locks for proper rendezvous and to fix the coroutine stack's state, + * but this API doesn't have (nor expected to) stable stacktrace, so we are filtering all such + * frames out. + * + * See https://github.com/Kotlin/kotlinx.coroutines/issues/3700 for the example of failure + */ +private fun removeJavaUtilConcurrentTraces(frames: List): List = + frames.filter { !it.contains("java.util.concurrent") } + private data class CoroutineDump( val header: CoroutineDumpHeader, val coroutineStackTrace: List, @@ -185,7 +197,9 @@ public fun verifyDump(vararg expectedTraces: String, ignoredCoroutine: String? = .drop(1) // Parse dumps and filter out ignored coroutines .mapNotNull { trace -> - val dump = CoroutineDump.parse(trace, traceCleaner = ::cleanBlockHoundTraces) + val dump = CoroutineDump.parse(trace, { + removeJavaUtilConcurrentTraces(cleanBlockHoundTraces(it)) + }) if (dump.header.className == ignoredCoroutine) { null } else { @@ -194,9 +208,10 @@ public fun verifyDump(vararg expectedTraces: String, ignoredCoroutine: String? = } assertEquals(expectedTraces.size, dumps.size) - dumps.zip(expectedTraces.map(CoroutineDump::parse)).forEach { (dump, expectedDump) -> - dump.verify(expectedDump) - } + dumps.zip(expectedTraces.map { CoroutineDump.parse(it, ::removeJavaUtilConcurrentTraces) }) + .forEach { (dump, expectedDump) -> + dump.verify(expectedDump) + } } public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "") diff --git a/kotlinx-coroutines-test/README.md b/kotlinx-coroutines-test/README.md index f2805086bb..fae47fa777 100644 --- a/kotlinx-coroutines-test/README.md +++ b/kotlinx-coroutines-test/README.md @@ -26,7 +26,7 @@ Provided [TestDispatcher] implementations: Add `kotlinx-coroutines-test` to your project test dependencies: ``` dependencies { - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0-RC' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0' } ``` diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index 00d9fb659e..a41502b2e1 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -121,9 +121,11 @@ public final class kotlinx/coroutines/test/TestScopeKt { public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestScope;J)V public static final fun advanceTimeBy-HG0u8IE (Lkotlinx/coroutines/test/TestScope;J)V public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestScope;)V + public static final fun getCatchNonTestRelatedExceptions ()Z public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestScope;)J public static final fun getTestTimeSource (Lkotlinx/coroutines/test/TestScope;)Lkotlin/time/TimeSource$WithComparableMarks; public static final fun runCurrent (Lkotlinx/coroutines/test/TestScope;)V + public static final fun setCatchNonTestRelatedExceptions (Z)V } public abstract interface class kotlinx/coroutines/test/UncaughtExceptionCaptor { diff --git a/kotlinx-coroutines-test/common/src/TestScope.kt b/kotlinx-coroutines-test/common/src/TestScope.kt index a5a36a8524..fa6e3930d8 100644 --- a/kotlinx-coroutines-test/common/src/TestScope.kt +++ b/kotlinx-coroutines-test/common/src/TestScope.kt @@ -233,7 +233,9 @@ internal class TestScopeImpl(context: CoroutineContext) : * after the previous one, and learning about such exceptions as soon is possible is nice. */ @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") run { ensurePlatformExceptionHandlerLoaded(ExceptionCollector) } - ExceptionCollector.addOnExceptionCallback(lock, this::reportException) + if (catchNonTestRelatedExceptions) { + ExceptionCollector.addOnExceptionCallback(lock, this::reportException) + } uncaughtExceptions } if (exceptions.isNotEmpty()) { @@ -312,7 +314,7 @@ internal fun TestScope.asSpecificImplementation(): TestScopeImpl = when (this) { } internal class UncaughtExceptionsBeforeTest : IllegalStateException( - "There were uncaught exceptions in coroutines launched from TestScope before the test started. Please avoid this," + + "There were uncaught exceptions before the test started. Please avoid this," + " as such exceptions are also reported in a platform-dependent manner so that they are not lost." ) @@ -321,3 +323,12 @@ internal class UncaughtExceptionsBeforeTest : IllegalStateException( */ @ExperimentalCoroutinesApi internal class UncompletedCoroutinesError(message: String) : AssertionError(message) + +/** + * A flag that controls whether [TestScope] should attempt to catch arbitrary exceptions flying through the system. + * If it is enabled, then any exception that is not caught by the user code will be reported as a test failure. + * By default, it is enabled, but some tests may want to disable it to test the behavior of the system when they have + * their own exception handling procedures. + */ +@PublishedApi +internal var catchNonTestRelatedExceptions: Boolean = true diff --git a/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt b/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt index 90fa763523..70fcb9487f 100644 --- a/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt +++ b/kotlinx-coroutines-test/common/src/internal/ExceptionCollector.kt @@ -43,8 +43,10 @@ internal object ExceptionCollector : AbstractCoroutineContextElement(CoroutineEx * Unregisters the callback associated with [owner]. */ fun removeOnExceptionCallback(owner: Any) = synchronized(lock) { - val existingValue = callbacks.remove(owner) - check(existingValue !== null) + if (enabled) { + val existingValue = callbacks.remove(owner) + check(existingValue !== null) + } } /** diff --git a/ui/coroutines-guide-ui.md b/ui/coroutines-guide-ui.md index e00d74edee..f45c382a2f 100644 --- a/ui/coroutines-guide-ui.md +++ b/ui/coroutines-guide-ui.md @@ -110,7 +110,7 @@ Add dependencies on `kotlinx-coroutines-android` module to the `dependencies { . `app/build.gradle` file: ```groovy -implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0-RC" +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0" ``` You can clone [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) project from GitHub onto your