@@ -155,12 +155,107 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
155
155
* If this method succeeds, state of this job will never be changed again
156
156
*/
157
157
private fun tryFinalizeState (expect : Incomplete , proposedUpdate : Any? , mode : Int ): Boolean {
158
+ /*
159
+ * If job is in 'cancelling' state and we're finalizing job state, we start intricate dance:
160
+ * 1) Synchronize on state to avoid races with concurrent
161
+ * mutations (e.g. when new child is added)
162
+ * 2) After synchronization check we're still in the expected state
163
+ * 3) Aggregate final exception under the same lock which protects exceptions
164
+ * collection
165
+ * 4) Pass it upstream
166
+ */
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() }
182
+ }
183
+
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
191
+ }
192
+
193
+ completeStateFinalization(expect, update, mode)
194
+ return true
195
+ }
196
+
158
197
val update = coerceProposedUpdate(expect, proposedUpdate)
159
198
if (! tryFinalizeState(expect, update)) return false
160
199
completeStateFinalization(expect, update, mode)
161
200
return true
162
201
}
163
202
203
+ private fun buildException (state : Finishing ): Throwable ? {
204
+ val cancelled = state.cancelled!!
205
+ val suppressed = state.exceptions
206
+
207
+ /*
208
+ * This is a place where we step on our API limitation:
209
+ * We can't distinguish internal JobCancellationException from our parent
210
+ * from external cancellation, thus we ought to collect all exceptions.
211
+ *
212
+ * But it has negative consequences: same exception can be added as suppressed more than once.
213
+ * Consider concurrent parent-child relationship:
214
+ * 1) Child throws E1 and parent throws E2
215
+ * 2) Parent goes to "Cancelling(E1)" and cancels child with E1
216
+ * 3) Child goes to "Cancelling(E1)", but throws an exception E2
217
+ * 4) When child throws, it notifies parent that he is cancelling, adding its exception to parent list of exceptions
218
+ * (again, parent don't know whether it's child exception or external exception)
219
+ * 5) Child builds final exception: E1 with suppressed E2, reports it to parent
220
+ * 6) Parent aggregates three exceptions: original E1, reported E2 and "final" E1.
221
+ * It filters the third exception, but adds the second one to the first one, thus adding suppressed duplicate.
222
+ *
223
+ * Note that it's only happening when both parent and child throw exception simultaneously
224
+ */
225
+ var rootCause = cancelled.cause
226
+ if (rootCause is JobCancellationException ) {
227
+ val cause = unwrap(rootCause)
228
+ rootCause = if (cause != = null ) {
229
+ cause
230
+ } else {
231
+ suppressed.firstOrNull { unwrap(it) != null } ? : return rootCause
232
+ }
233
+ }
234
+
235
+ val seenExceptions = HashSet <Throwable >() // TODO it should be identity set
236
+ suppressed.forEach {
237
+ val unwrapped = unwrap(it)
238
+ if (unwrapped != = null && unwrapped != = rootCause) {
239
+ if (seenExceptions.add(unwrapped)) {
240
+ rootCause.addSuppressedThrowable(unwrapped)
241
+ }
242
+ }
243
+ }
244
+
245
+ return rootCause
246
+ }
247
+
248
+ private tailrec fun unwrap (exception : Throwable ): Throwable ? {
249
+ if (exception is JobCancellationException ) {
250
+ val cause = exception.cause
251
+ if (cause != = null ) return unwrap(cause)
252
+ return null
253
+
254
+ } else {
255
+ return exception
256
+ }
257
+ }
258
+
164
259
/* *
165
260
* Tries to update [_state] of this job to the final state and, if
166
261
* succeeds, disposes parent handle (de-attaching child from parent)
@@ -200,7 +295,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
200
295
try {
201
296
expect.invoke(cause)
202
297
} catch (ex: Throwable ) {
203
- handleException (CompletionHandlerException (" Exception in completion handler $expect for $this " , ex))
298
+ handleOnCompletionException (CompletionHandlerException (" Exception in completion handler $expect for $this " , ex))
204
299
}
205
300
} else {
206
301
expect.list?.notifyCompletion(cause)
@@ -225,8 +320,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
225
320
if (proposedUpdate !is CompletedExceptionally ) return cancelled // not exception -- just use original cancelled
226
321
val exception = proposedUpdate.cause
227
322
if (cancelled.cause == exception) return cancelled // that is the cancelled we need already!
228
- // todo: We need to rework this logic to keep original cancellation cause in the state and suppress other exceptions
229
- // that could have occurred while coroutine is being cancelled.
323
+ // That could have occurred while coroutine is being cancelled.
230
324
// Do not spam with JCE in suppressed exceptions
231
325
if (cancelled.cause !is JobCancellationException ) {
232
326
exception.addSuppressedThrowable(cancelled.cause)
@@ -251,7 +345,7 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
251
345
}
252
346
}
253
347
}
254
- exception?.let { handleException (it) }
348
+ exception?.let { handleOnCompletionException (it) }
255
349
}
256
350
257
351
public final override fun start (): Boolean {
@@ -509,7 +603,15 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
509
603
}
510
604
}
511
605
is Finishing -> { // Completing/Cancelling the job, may cancel
512
- if (state.cancelled != null ) return false // already cancelling
606
+ if (state.cancelled != null ) {
607
+ if (cause == null ) {
608
+ return true
609
+ }
610
+
611
+ // We either successfully added an exception or caller should handle it itself
612
+ return cause.let { state.addException(it) }
613
+ }
614
+
513
615
if (tryMakeCancelling(state, state.list, cause)) return true
514
616
}
515
617
else -> { // is inactive
@@ -666,20 +768,24 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
666
768
* installed via [invokeOnCompletion].
667
769
* @suppress **This is unstable API and it is subject to change.**
668
770
*/
669
- internal open fun handleException (exception : Throwable ) {
771
+ internal open fun handleOnCompletionException (exception : Throwable ) {
670
772
throw exception
671
773
}
672
774
673
775
/* *
674
- * This function is invoked once when job is cancelled or is completed, similarly to [invokeOnCompletion] with
675
- * `onCancelling` set to `true`.
776
+ * This function is invoked once when job is cancelled or is completed.
777
+ * It's an optimization for [invokeOnCompletion] with `onCancelling` set to `true`.
778
+ *
676
779
* @param exceptionally not null when the the job was cancelled or completed exceptionally,
677
780
* null when it has completed normally.
678
- * @suppress **This is unstable API and it is subject to change.**
781
+ * @suppress **This is unstable API and it is subject to change.*
679
782
*/
680
- internal open fun onCancellationInternal (exceptionally : CompletedExceptionally ? ) {}
783
+ internal open fun onCancellationInternal (exceptionally : CompletedExceptionally ? ) {
784
+ // TODO rename to "onCancelling"
785
+ }
681
786
682
787
/* *
788
+ * Whether job has [onFinishingInternal] handler for given [update]
683
789
* @suppress **This is unstable API and it is subject to change.**
684
790
*/
685
791
internal open fun hasOnFinishingHandler (update : Any? ) = false
@@ -689,6 +795,12 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
689
795
*/
690
796
internal open fun onFinishingInternal (update : Any? ) {}
691
797
798
+ /* *
799
+ * Method which is invoked once Job becomes `Cancelled`. It's guaranteed that at the moment
800
+ * of invocation the job and all its children are complete
801
+ */
802
+ internal open fun handleJobException (exception : Throwable ) {}
803
+
692
804
/* *
693
805
* Override for post-completion actions that need to do something with the state.
694
806
* @param mode completion mode.
@@ -726,6 +838,36 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
726
838
@JvmField val completing : Boolean /* true when completing */
727
839
) : Incomplete {
728
840
override val isActive: Boolean get() = cancelled == null
841
+ val exceptions: List <Throwable > get() = _exceptionsHolder as List <Throwable >
842
+
843
+ // TODO optimize
844
+ private var _exceptionsHolder : Any? = if (cancelled == null ) null else ArrayList <Throwable >(2 )
845
+
846
+ fun addException (exception : Throwable ): Boolean {
847
+ synchronized(this ) {
848
+ return if (_exceptionsHolder == null ) {
849
+ false
850
+ } else {
851
+ @Suppress(" UNCHECKED_CAST" )
852
+ (_exceptionsHolder as MutableList <Throwable >).add(exception)
853
+ true
854
+ }
855
+ }
856
+ }
857
+
858
+ fun addLocked (exception : Throwable ) {
859
+ (_exceptionsHolder as MutableList <Throwable >).add(exception)
860
+ }
861
+
862
+ /* *
863
+ * Seals current state. After [seal] call all consecutive calls to [addException]
864
+ * return `false` forcing callers to handle pending exception by themselves.
865
+ * This call should be guarded by `synchronized(finishingState)`
866
+ */
867
+ fun seal () {
868
+ _exceptionsHolder = null
869
+ }
870
+
729
871
}
730
872
731
873
private val Incomplete .isCancelling: Boolean
@@ -854,6 +996,12 @@ private class Empty(override val isActive: Boolean) : Incomplete {
854
996
internal class JobImpl (parent : Job ? = null ) : JobSupport(true ) {
855
997
init { initParentJobInternal(parent) }
856
998
override val onCancelMode: Int get() = ON_CANCEL_MAKE_COMPLETING
999
+
1000
+ override fun cancel (cause : Throwable ? ): Boolean {
1001
+ // JobImpl can't handle an exception, thus always returns false
1002
+ super .cancel(cause)
1003
+ return false
1004
+ }
857
1005
}
858
1006
859
1007
// -------- invokeOnCompletion nodes
0 commit comments