Skip to content

Commit 29eeb1a

Browse files
committed
Merge branch 'master' into develop
2 parents ec8cbd2 + f6e5742 commit 29eeb1a

File tree

12 files changed

+96
-98
lines changed

12 files changed

+96
-98
lines changed

CHANGES.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change log for kotlinx.coroutines
22

3+
## Version 0.30.1
4+
Maintenance release:
5+
* Added `Dispatchers.Main` to common dispatchers, which can be used from Android, Swing and JavaFx projects if a corresponding integration library is added to dependencies.
6+
* With `Dispatchers.Main` improvement tooling bug in Android Studio #626 is mitigated, so Android users now can safely start the migration to the latest `kotlinx.coroutines` version.
7+
* Fixed bug with thread unsafety of shutdown sequence in `EventLoop`.
8+
* Experimental coroutine dispatcher now has `close` contract similar to Java `Executor`, so it can be safely instantiated and closed multiple times (affects only unit tests).
9+
* Atomicfu version is updated with fixes in JS transformer (see #609)
10+
311
## Version 0.30.0
412

513
* **[Major]** Further improvements in exception handling — no failure exception is lost.

COMPATIBILITY.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ stable public API, and thus `kotlinx.coroutines` is leaving its "experimental" s
1818
Version `1.0.0` (starting with its release candidate build) will have all its deprecated declarations removed and `kotlinx.coroutines.experimental` package will be renamed to `kotlinx.coroutines` without functional changes.
1919
In order to migrate `kotlinx.coroutines` to `1.0.0`, follow these steps:
2020

21-
1. Update `kotlinx.coroutines` to `0.30.0` version.
21+
1. Update `kotlinx.coroutines` to `0.30.1` version.
2222
2. Inspect compiler warnings about deprecated API and migrate it to a proposed alternative. Most of deprecated API has a corresponding replacement which can be applied from IDEA with quickfix.
23-
3. Update Kotlin version to `1.3.0` or to the latest `1.3.0-rc` and `kotlinx.coroutines` to version `0.30.0-eap13`. Then just get rid of `experimental` suffix in all imports.
23+
3. Update Kotlin version to `1.3.0` or to the latest `1.3.0-rc` and `kotlinx.coroutines` to version `0.30.1-eap13`. Then just get rid of `experimental` suffix in all imports.
2424
4. Update `kotlinx.coroutines` to version `1.0.0` or to the corresponding release candidate of it).
2525

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![official JetBrains project](http://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
44
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
5-
[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=0.30.0) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/0.30.0)
5+
[![Download](https://api.bintray.com/packages/kotlin/kotlinx/kotlinx.coroutines/images/download.svg?version=0.30.1) ](https://bintray.com/kotlin/kotlinx/kotlinx.coroutines/0.30.1)
66

77
Library support for Kotlin coroutines with [multiplatform](#multiplatform) support.
88
This is a companion version for Kotlin 1.2.70 release.
@@ -68,7 +68,7 @@ Add dependencies (you can also add other modules that you need):
6868
<dependency>
6969
<groupId>org.jetbrains.kotlinx</groupId>
7070
<artifactId>kotlinx-coroutines-core</artifactId>
71-
<version>0.30.0</version>
71+
<version>0.30.1</version>
7272
</dependency>
7373
```
7474

@@ -85,7 +85,7 @@ And make sure that you use the latest Kotlin version:
8585
Add dependencies (you can also add other modules that you need):
8686

8787
```groovy
88-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.0'
88+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.1'
8989
```
9090

9191
And make sure that you use the latest Kotlin version:
@@ -118,7 +118,7 @@ Add [`kotlinx-coroutines-android`](ui/kotlinx-coroutines-android)
118118
module as dependency when using `kotlinx.coroutines` on Android:
119119

120120
```groovy
121-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.0'
121+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.1'
122122
```
123123
This gives you access to Android [Dispatchers.Main](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-android/kotlinx.coroutines.experimental.android/kotlinx.coroutines.experimental.-dispatchers/index.html)
124124
coroutine dispatcher and also makes sure that in case of crashed coroutine with unhandled exception this

common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ internal abstract class AbstractContinuation<in T>(
105105
/**
106106
* It is used when parent is cancelled to get the cancellation cause for this continuation.
107107
*/
108-
open fun getParentCancellationCause(parent: Job): Throwable =
108+
open fun getContinuationCancellationCause(parent: Job): Throwable =
109109
parent.getCancellationException()
110110

111111
private fun trySuspend(): Boolean {

common/kotlinx-coroutines-core-common/src/Job.kt

+19-2
Original file line numberDiff line numberDiff line change
@@ -425,13 +425,30 @@ public inline fun DisposableHandle(crossinline block: () -> Unit) =
425425
internal interface ChildJob : Job {
426426
/**
427427
* Parent is cancelling its child by invoking this method.
428-
* Child finds the cancellation cause using [getCancellationException] of the [parentJob].
428+
* Child finds the cancellation cause using [ParentJob.getChildJobCancellationCause].
429429
* This method does nothing is the child is already being cancelled.
430430
*
431431
* @suppress **This is unstable API and it is subject to change.**
432432
*/
433433
@InternalCoroutinesApi
434-
public fun parentCancelled(parentJob: Job)
434+
public fun parentCancelled(parentJob: ParentJob)
435+
}
436+
437+
/**
438+
* A reference that child receives from its parent when it is being cancelled by the parent.
439+
*
440+
* @suppress **This is unstable API and it is subject to change.**
441+
*/
442+
@InternalCoroutinesApi
443+
internal interface ParentJob : Job {
444+
/**
445+
* Child job is using this method to learn its cancellation cause when the parent cancels it with [ChildJob.parentCancelled].
446+
* This method is invoked only if the child was not already being cancelled.
447+
*
448+
* @suppress **This is unstable API and it is subject to change.**
449+
*/
450+
@InternalCoroutinesApi
451+
public fun getChildJobCancellationCause(): Throwable
435452
}
436453

437454
/**

common/kotlinx-coroutines-core-common/src/JobSupport.kt

+45-72
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import kotlin.coroutines.experimental.intrinsics.*
2020
* @param active when `true` the job is created in _active_ state, when `false` in _new_ state. See [Job] for details.
2121
* @suppress **This is unstable API and it is subject to change.**
2222
*/
23-
internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, SelectClause0 {
23+
internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob, SelectClause0 {
2424
final override val key: CoroutineContext.Key<*> get() = Job
2525

2626
/*
@@ -230,63 +230,23 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
230230
if (state.isCancelling) return createJobCancellationException()
231231
return null
232232
}
233-
/*
234-
* This is a place where we step on our API limitation:
235-
* We can't distinguish internal JobCancellationException from our parent
236-
* from external cancellation, thus we ought to collect all exceptions.
237-
* If parent is cancelling, it cancels its children with JCE(rootCause).
238-
* When child is building final exception, it can skip JCE(anything) if it knows
239-
* that parent handles exceptions, because parent should already have this exception.
240-
* If parent does not, then we should unwrap exception, otherwise in the following code
241-
* ```
242-
* val parent = Job()
243-
* launch(parent) {
244-
* try { delay() } finally { throw E2() }
245-
* }
246-
* parent.cancel(E1)
247-
* ```
248-
* E1 will be lost.
249-
*/
250-
var rootCause = exceptions[0]
251-
if (rootCause is CancellationException) {
252-
val cause = unwrap(rootCause)
253-
rootCause = if (cause !== null) {
254-
cause
255-
} else {
256-
exceptions.firstOrNull { unwrap(it) != null } ?: return rootCause
257-
}
258-
}
259-
return rootCause
233+
// Take either the first real exception (not a cancellation) or just the first exception
234+
return exceptions.firstOrNull { it !is CancellationException } ?: exceptions[0]
260235
}
261236

262237
private fun suppressExceptions(rootCause: Throwable, exceptions: List<Throwable>): Boolean {
263238
if (exceptions.size <= 1) return false // nothing more to do here
264239
val seenExceptions = identitySet<Throwable>(exceptions.size)
265240
var suppressed = false
266-
for (i in 1 until exceptions.size) {
267-
val unwrapped = unwrap(exceptions[i])
268-
if (unwrapped !== null && unwrapped !== rootCause) {
269-
if (seenExceptions.add(unwrapped)) {
270-
rootCause.addSuppressedThrowable(unwrapped)
271-
suppressed = true
272-
}
241+
for (exception in exceptions) {
242+
if (exception !== rootCause && exception !is CancellationException && seenExceptions.add(exception)) {
243+
rootCause.addSuppressedThrowable(exception)
244+
suppressed = true
273245
}
274246
}
275247
return suppressed
276248
}
277249

278-
private tailrec fun unwrap(exception: Throwable): Throwable? {
279-
if (exception is CancellationException && parentHandlesExceptions) {
280-
return null
281-
}
282-
return if (exception is CancellationException) {
283-
val cause = exception.cause
284-
if (cause !== null) unwrap(cause) else null
285-
} else {
286-
exception
287-
}
288-
}
289-
290250
// fast-path method to finalize normally completed coroutines without children
291251
private fun tryFinalizeSimpleState(state: Incomplete, update: Any?, mode: Int): Boolean {
292252
check(state is Empty || state is JobNode<*>) // only simple state without lists where children can concurrently add
@@ -616,15 +576,15 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
616576
cancelImpl(cause) && handlesException
617577

618578
// Parent is cancelling child
619-
public final override fun parentCancelled(parentJob: Job) {
579+
public final override fun parentCancelled(parentJob: ParentJob) {
620580
cancelImpl(parentJob)
621581
}
622582

623583
// Child was cancelled with cause
624584
public open fun childCancelled(cause: Throwable): Boolean =
625585
cancelImpl(cause) && handlesException
626586

627-
// cause is Throwable or Job when cancelChild was invoked
587+
// cause is Throwable or ParentJob when cancelChild was invoked
628588
// returns true is exception was handled, false otherwise
629589
private fun cancelImpl(cause: Any?): Boolean {
630590
if (onCancelComplete) {
@@ -636,6 +596,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
636596
return makeCancelling(cause)
637597
}
638598

599+
// cause is Throwable or ParentJob when cancelChild was invoked
639600
private fun cancelMakeCompleting(cause: Any?): Boolean {
640601
loopOnState { state ->
641602
if (state !is Incomplete || state is Finishing && state.isCompleting) {
@@ -654,14 +615,35 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
654615
private fun createJobCancellationException() =
655616
JobCancellationException("Job was cancelled", null, this)
656617

657-
// cause is Throwable or Job when cancelChild was invoked, cause can be null only on cancel
618+
override fun getChildJobCancellationCause(): Throwable {
619+
// determine root cancellation cause of this job (why is it cancelling its children?)
620+
val state = this.state
621+
val rootCause = when (state) {
622+
is Finishing -> state.rootCause
623+
is Incomplete -> error("Cannot be cancelling child in this state: $state")
624+
is CompletedExceptionally -> state.cause
625+
else -> null // create exception with the below code on normal completion
626+
}
627+
/*
628+
* If this parent job handles exceptions, then wrap cause into JobCancellationException, because we
629+
* don't want the child to handle this exception on more time. Otherwise, pass our original rootCause
630+
* to the child for cancellation.
631+
*/
632+
return if (rootCause == null || handlesException && rootCause !is CancellationException) {
633+
JobCancellationException("Parent job is ${stateString(state)}", rootCause, this)
634+
} else {
635+
rootCause
636+
}
637+
}
638+
639+
// cause is Throwable or ParentJob when cancelChild was invoked
658640
private fun createCauseException(cause: Any?): Throwable = when(cause) {
659641
is Throwable? -> cause ?: createJobCancellationException()
660-
else -> (cause as Job).getCancellationException()
642+
else -> (cause as ParentJob).getChildJobCancellationCause()
661643
}
662644

663645
// transitions to Cancelling state
664-
// cause is Throwable or Job when cancelChild was invoked, cause can be null only on cancel
646+
// cause is Throwable or ParentJob when cancelChild was invoked
665647
private fun makeCancelling(cause: Any?): Boolean {
666648
var causeExceptionCache: Throwable? = null // lazily init result of createCauseException(cause)
667649
loopOnState { state ->
@@ -927,10 +909,6 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
927909
*/
928910
protected open val handlesException: Boolean get() = true
929911

930-
// returns true when we know that parent handles exceptions
931-
private val parentHandlesExceptions: Boolean get() =
932-
(parentHandle as? ChildHandleNode)?.job?.handlesException ?: false
933-
934912
/**
935913
* This method is invoked **exactly once** when the final exception of the job is determined
936914
* and before it becomes complete. At the moment of invocation the job and all its children are complete.
@@ -959,27 +937,22 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
959937

960938
// for nicer debugging
961939
public override fun toString(): String =
962-
"${nameString()}{${stateString()}}@$hexAddress"
940+
"${nameString()}{${stateString(state)}}@$hexAddress"
963941

964942
/**
965943
* @suppress **This is unstable API and it is subject to change.**
966944
*/
967945
internal open fun nameString(): String = classSimpleName
968946

969-
private fun stateString(): String {
970-
val state = this.state
971-
return when (state) {
972-
is Finishing -> buildString {
973-
when {
974-
state.isCancelling -> append("Cancelling")
975-
else -> append("Active")
976-
}
977-
if (state.isCompleting) append("Completing")
978-
}
979-
is Incomplete -> if (state.isActive) "Active" else "New"
980-
is CompletedExceptionally -> "Cancelled"
981-
else -> "Completed"
947+
private fun stateString(state: Any?): String = when (state) {
948+
is Finishing -> when {
949+
state.isCancelling -> "Cancelling"
950+
state.isCompleting -> "Completing"
951+
else -> "Active"
982952
}
953+
is Incomplete -> if (state.isActive) "Active" else "New"
954+
is CompletedExceptionally -> "Cancelled"
955+
else -> "Completed"
983956
}
984957

985958
// Completing & Cancelling states,
@@ -1068,7 +1041,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, ChildJob, Sel
10681041
delegate: Continuation<T>,
10691042
private val job: JobSupport
10701043
) : CancellableContinuationImpl<T>(delegate, MODE_CANCELLABLE) {
1071-
override fun getParentCancellationCause(parent: Job): Throwable {
1044+
override fun getContinuationCancellationCause(parent: Job): Throwable {
10721045
val state = job.state
10731046
/*
10741047
* When the job we are waiting for had already completely completed exceptionally or
@@ -1351,7 +1324,7 @@ internal class ChildContinuation(
13511324
@JvmField val child: AbstractContinuation<*>
13521325
) : JobCancellingNode<Job>(parent) {
13531326
override fun invoke(cause: Throwable?) {
1354-
child.cancelImpl(child.getParentCancellationCause(job))
1327+
child.cancelImpl(child.getContinuationCancellationCause(job))
13551328
}
13561329
override fun toString(): String =
13571330
"ChildContinuation[$child]"

core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt

+12-12
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import kotlin.coroutines.experimental.*
1313
import kotlin.test.*
1414

1515
@RunWith(Parameterized::class)
16-
class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestBase() {
16+
class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() {
17+
enum class Mode { WITH_CONTEXT, ASYNC_AWAIT }
18+
1719
companion object {
18-
@Parameterized.Parameters(name = "withContext={0}")
20+
@Parameterized.Parameters(name = "mode={0}")
1921
@JvmStatic
20-
fun params(): Collection<Array<Any>> = listOf<Array<Any>>(arrayOf(true), arrayOf(false))
22+
fun params(): Collection<Array<Any>> = Mode.values().map { arrayOf<Any>(it) }
2123
}
2224

2325
@Test
@@ -106,16 +108,14 @@ class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestB
106108
/*
107109
* context cancelled with ISE
108110
* block itself throws CE(IOE)
109-
* Result: ISE suppressed IOE
111+
* Result: ISE (because cancellation exception is always ignored and not handled)
110112
*/
111113
val cancellationCause = IllegalStateException()
112114
val thrown = CancellationException()
113115
thrown.initCause(IOException())
114116
runCancellation(cancellationCause, thrown) { e ->
115117
assertSame(cancellationCause, e)
116-
val suppressed = e.suppressed
117-
assertEquals(1, suppressed.size)
118-
assertTrue(suppressed[0] is IOException)
118+
assertTrue(e.suppressed.isEmpty())
119119
}
120120
}
121121

@@ -161,10 +161,11 @@ class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestB
161161

162162
@Test
163163
fun testThrowingCancellationWithCause() = runTest {
164+
// Exception are never unwrapped, so if CE(IOE) is thrown then it is the cancellation cause
164165
val thrown = CancellationException()
165166
thrown.initCause(IOException())
166167
runThrowing(thrown) { e ->
167-
checkException<IOException>(e)
168+
assertSame(thrown, e)
168169
}
169170
}
170171

@@ -247,12 +248,11 @@ class WithContextExceptionHandlingTest(private val withContext: Boolean) : TestB
247248
}
248249

249250
private suspend fun withCtx(context: CoroutineContext, job: Job = Job(), block: suspend CoroutineScope.(Job) -> Nothing) {
250-
if (withContext) {
251-
withContext(context + job) {
251+
when (mode) {
252+
Mode.WITH_CONTEXT -> withContext(context + job) {
252253
block(job)
253254
}
254-
} else {
255-
CoroutineScope(coroutineContext).async(context + job) {
255+
Mode.ASYNC_AWAIT -> CoroutineScope(coroutineContext).async(context + job) {
256256
block(job)
257257
}.await()
258258
}

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Kotlin
2-
version=0.30.0-SNAPSHOT
2+
version=0.30.1-SNAPSHOT
33
group=org.jetbrains.kotlinx
44
kotlin_version=1.2.70
55
kotlin_native_version=0.8.2

native/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ repositories {
4242
}
4343
4444
dependencies {
45-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:0.30.0'
45+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:0.30.1'
4646
}
4747
4848
sourceSets {

0 commit comments

Comments
 (0)