Skip to content

Commit c9afb67

Browse files
qwwdfsadelizarov
authored andcommitted
Optimize exceptions list in JobSupport, invoke onJobException in active -> cancelled path, update documentation
1 parent 06f57aa commit c9afb67

File tree

5 files changed

+83
-43
lines changed

5 files changed

+83
-43
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public interface Deferred<out T> : Job {
7777

7878
/**
7979
* Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
80-
* returning the resulting value or throwing the corresponding exception if the deferred had completed exceptionally.
80+
* returning the resulting value or throwing the corresponding exception if the deferred had completed exceptionally or was cancelled.
8181
*
8282
* This suspending function is cancellable.
8383
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function

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

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.coroutines.experimental
66

77
import kotlinx.atomicfu.*
8+
import kotlinx.coroutines.experimental.NotInitialized.*
89
import kotlinx.coroutines.experimental.internal.*
910
import kotlinx.coroutines.experimental.internalAnnotations.*
1011
import kotlinx.coroutines.experimental.intrinsics.*
@@ -155,6 +156,18 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
155156
* If this method succeeds, state of this job will never be changed again
156157
*/
157158
private fun tryFinalizeState(expect: Incomplete, proposedUpdate: Any?, mode: Int): Boolean {
159+
if (expect is Finishing && expect.cancelled != null) {
160+
return tryFinalizeCancellingState(expect, proposedUpdate, mode)
161+
}
162+
163+
val update = coerceProposedUpdate(expect, proposedUpdate)
164+
if (!tryFinalizeState(expect, update)) return false
165+
if (update is CompletedExceptionally) handleJobException(update.cause)
166+
completeStateFinalization(expect, update, mode)
167+
return true
168+
}
169+
170+
private fun tryFinalizeCancellingState(expect: Finishing, proposedUpdate: Any?, mode: Int): Boolean {
158171
/*
159172
* If job is in 'cancelling' state and we're finalizing job state, we start intricate dance:
160173
* 1) Synchronize on state to avoid races with concurrent
@@ -164,38 +177,31 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
164177
* collection
165178
* 4) Pass it upstream
166179
*/
167-
if (expect is Finishing && expect.cancelled != null) {
168-
val finalException = synchronized(expect) {
169-
if (_state.value !== expect) {
170-
return false
171-
}
172-
173-
if (proposedUpdate is CompletedExceptionally) {
174-
expect.addLocked(proposedUpdate.cause)
175-
}
176-
177-
/*
178-
* Note that new exceptions cannot be added concurrently: state is guarded by lock
179-
* and storage is sealed in the end, so all new exceptions will be reported separately
180-
*/
181-
buildException(expect).also { expect.seal() }
180+
val finalException = synchronized(expect) {
181+
if (_state.value !== expect) {
182+
return false
182183
}
183184

184-
val update = Cancelled(this, finalException ?: expect.cancelled.cause)
185-
handleJobException(update.cause)
186-
// This CAS never fails: we're in the state when no jobs can be attached, because state is already sealed
187-
if (!tryFinalizeState(expect, update)) {
188-
val error = AssertionError("Unexpected state: ${_state.value}, expected: $expect, update: $update")
189-
handleOnCompletionException(error)
190-
throw error
185+
if (proposedUpdate is CompletedExceptionally) {
186+
expect.addLocked(proposedUpdate.cause)
191187
}
192188

193-
completeStateFinalization(expect, update, mode)
194-
return true
189+
/*
190+
* Note that new exceptions cannot be added concurrently: state is guarded by lock
191+
* and storage is sealed in the end, so all new exceptions will be reported separately
192+
*/
193+
buildException(expect).also { expect.seal() }
194+
}
195+
196+
val update = Cancelled(this, finalException ?: expect.cancelled!!.cause)
197+
handleJobException(update.cause)
198+
// This CAS never fails: we're in the state when no jobs can be attached, because state is already sealed
199+
if (!tryFinalizeState(expect, update)) {
200+
val error = AssertionError("Unexpected state: ${_state.value}, expected: $expect, update: $update")
201+
handleOnCompletionException(error)
202+
throw error
195203
}
196204

197-
val update = coerceProposedUpdate(expect, proposedUpdate)
198-
if (!tryFinalizeState(expect, update)) return false
199205
completeStateFinalization(expect, update, mode)
200206
return true
201207
}
@@ -832,31 +838,58 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
832838
}
833839

834840
// Cancelling or Completing
841+
@Suppress("UNCHECKED_CAST")
835842
private class Finishing(
836843
override val list: NodeList,
837844
@JvmField val cancelled: Cancelled?, /* != null when cancelling */
838845
@JvmField val completing: Boolean /* true when completing */
839846
) : Incomplete {
840847
override val isActive: Boolean get() = cancelled == null
841-
val exceptions: List<Throwable> get() = _exceptionsHolder as List<Throwable>
842848

843-
// TODO optimize
844-
private var _exceptionsHolder: Any? = if (cancelled == null) null else ArrayList<Throwable>(2)
849+
val exceptions: List<Throwable> get() = when(_exceptionsHolder) {
850+
NOT_INITIALIZED -> emptyList()
851+
is Throwable -> listOf(_exceptionsHolder as Throwable) // EA should handle this
852+
else -> (_exceptionsHolder as List<Throwable>)
853+
}
854+
855+
private var _exceptionsHolder: Any? = if (cancelled == null) null else NOT_INITIALIZED
845856

846857
fun addException(exception: Throwable): Boolean {
847858
synchronized(this) {
848-
return if (_exceptionsHolder == null) {
849-
false
850-
} else {
851-
@Suppress("UNCHECKED_CAST")
852-
(_exceptionsHolder as MutableList<Throwable>).add(exception)
853-
true
859+
return when (_exceptionsHolder) {
860+
null -> false
861+
NOT_INITIALIZED -> {
862+
_exceptionsHolder = exception
863+
return true
864+
}
865+
is Throwable -> {
866+
val previous = _exceptionsHolder
867+
val list = ArrayList<Any?>(4)
868+
list.add(previous)
869+
list.add(exception)
870+
_exceptionsHolder = list
871+
return true
872+
}
873+
else -> (_exceptionsHolder as MutableList<Throwable>).add(exception)
854874
}
855875
}
856876
}
857877

858878
fun addLocked(exception: Throwable) {
859-
(_exceptionsHolder as MutableList<Throwable>).add(exception)
879+
// Cannot be null at this point here
880+
when (_exceptionsHolder) {
881+
NOT_INITIALIZED -> {
882+
_exceptionsHolder = exception
883+
}
884+
is Throwable -> {
885+
val previous = _exceptionsHolder
886+
val list = ArrayList<Any?>(4)
887+
list.add(previous)
888+
list.add(exception)
889+
_exceptionsHolder = list
890+
}
891+
else -> (_exceptionsHolder as MutableList<Throwable>).add(exception)
892+
}
860893
}
861894

862895
/**
@@ -988,6 +1021,10 @@ private val EmptyNew = Empty(false)
9881021
@Suppress("PrivatePropertyName")
9891022
private val EmptyActive = Empty(true)
9901023

1024+
private enum class NotInitialized {
1025+
NOT_INITIALIZED
1026+
}
1027+
9911028
private class Empty(override val isActive: Boolean) : Incomplete {
9921029
override val list: NodeList? get() = null
9931030
override fun toString(): String = "Empty{${if (isActive) "Active" else "New" }}"

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ private open class TimeoutCoroutine<U, in T: U>(
106106
*
107107
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
108108
* cancellable suspending function inside the block throws [TimeoutCancellationException].
109-
* Even if the code in the block suppresses [TimeoutCancellationException], this
110-
* invocation of `withTimeoutOrNull` still returns `null`.
109+
* **NB**: this method is exception-unsafe. Even if the code in the block suppresses [TimeoutCancellationException], this
110+
* invocation of `withTimeoutOrNull` still returns `null` and thrown exception will be ignored.
111111
*
112112
* The sibling function that throws exception on timeout is [withTimeout].
113113
* Note, that timeout action can be specified for [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
@@ -126,8 +126,8 @@ public suspend fun <T> withTimeoutOrNull(time: Int, block: suspend CoroutineScop
126126
*
127127
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
128128
* cancellable suspending function inside the block throws [TimeoutCancellationException].
129-
* Even if the code in the block suppresses [TimeoutCancellationException], this
130-
* invocation of `withTimeoutOrNull` still returns `null`.
129+
* **NB**: this method is exception-unsafe. Even if the code in the block suppresses [TimeoutCancellationException], this
130+
* invocation of `withTimeoutOrNull` still returns `null` and thrown exception will be ignored.
131131
*
132132
* The sibling function that throws exception on timeout is [withTimeout].
133133
* Note, that timeout action can be specified for [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ internal fun runBlock(
5555
internal fun runBlockForMultipleExceptions(
5656
context: CoroutineContext = EmptyCoroutineContext,
5757
block: suspend CoroutineScope.() -> Unit
58-
): List<Throwable>
58+
): List<Throwable> {
59+
val handler = CapturingHandler()
60+
runBlocking(context + handler, block = block)
61+
return handler.unhandled
62+
}

core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import kotlinx.coroutines.experimental.*
88
import org.junit.*
99
import org.junit.Assert.*
1010

11-
@Ignore
1211
class TestCoroutineContextTest {
1312
private val injectedContext = TestCoroutineContext()
1413

0 commit comments

Comments
 (0)