@@ -14,7 +14,7 @@ import kotlin.coroutines.*
14
14
/* *
15
15
* Starts [block] in a new coroutine and returns a [ListenableFuture] pointing to its result.
16
16
*
17
- * The coroutine is immediately started. Passing [CoroutineStart.LAZY] to [start] throws
17
+ * The coroutine is started immediately . Passing [CoroutineStart.LAZY] to [start] throws
18
18
* [IllegalArgumentException], because Futures don't have a way to start lazily.
19
19
*
20
20
* When the created coroutine [isCompleted][Job.isCompleted], it will try to
@@ -35,10 +35,12 @@ import kotlin.coroutines.*
35
35
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging
36
36
* facilities.
37
37
*
38
- * Note that the error and cancellation semantics of [future] are _subtly different_ than [asListenableFuture]'s.
39
- * In particular, any exception that happens in the coroutine after returned future is
40
- * successfully cancelled will be passed to the [CoroutineExceptionHandler] from the [context].
41
- * See [ListenableFutureCoroutine] for details.
38
+ * Note that the error and cancellation semantics of [future] are _different_ than [async]'s.
39
+ * In contrast to [Deferred], [Future] doesn't have an intermediate `Cancelling` state. If
40
+ * the returned `Future` is successfully cancelled, and `block` throws afterward, the thrown
41
+ * error is dropped, and getting the `Future`'s value will throw a `CancellationException` with
42
+ * no cause. This is to match the specification and behavior of
43
+ * `java.util.concurrent.FutureTask`.
42
44
*
43
45
* @param context added overlaying [CoroutineScope.coroutineContext] to form the new context.
44
46
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
@@ -241,8 +243,8 @@ public suspend fun <T> ListenableFuture<T>.await(): T {
241
243
242
244
return suspendCancellableCoroutine { cont: CancellableContinuation <T > ->
243
245
addListener(
244
- ToContinuation (this , cont),
245
- MoreExecutors .directExecutor())
246
+ ToContinuation (this , cont),
247
+ MoreExecutors .directExecutor())
246
248
cont.invokeOnCancellation {
247
249
cancel(false )
248
250
}
@@ -284,16 +286,13 @@ private class ToContinuation<T>(
284
286
* By documented contract, a [Future] has been cancelled if
285
287
* and only if its `isCancelled()` method returns true.
286
288
*
287
- * Any error that occurs after successfully cancelling a [ListenableFuture] will be passed
288
- * to the [CoroutineExceptionHandler] from the context. The contract of [Future] does not permit
289
- * it to return an error after it is successfully cancelled.
290
- *
291
- * By calling [asListenableFuture] on a [Deferred], any error that occurs after successfully
292
- * cancelling the [ListenableFuture] representation of the [Deferred] will _not_ be passed to
293
- * the [CoroutineExceptionHandler]. Cancelling a [Deferred] places that [Deferred] in the
294
- * cancelling/cancelled states defined by [Job], which _can_ show the error. It's assumed that
295
- * the [Deferred] pointing to the task will be used to observe any error outcome occurring after
296
- * cancellation.
289
+ * Any error that occurs after successfully cancelling a [ListenableFuture] is lost.
290
+ * The contract of [Future] does not permit it to return an error after it is successfully cancelled.
291
+ * On the other hand, we can't report an unhandled exception to [CoroutineExceptionHandler],
292
+ * otherwise [Future.cancel] can lead to an app crash which arguably is a contract violation.
293
+ * In contrast to [Future] which can't change its outcome after a successful cancellation,
294
+ * cancelling a [Deferred] places that [Deferred] in the cancelling/cancelled states defined by [Job],
295
+ * which _can_ show the error.
297
296
*
298
297
* This may be counterintuitive, but it maintains the error and cancellation contracts of both
299
298
* the [Deferred] and [ListenableFuture] types, while permitting both kinds of promise to point
@@ -312,10 +311,14 @@ private class ListenableFutureCoroutine<T>(
312
311
}
313
312
314
313
override fun onCancelled (cause : Throwable , handled : Boolean ) {
315
- if (! future.completeExceptionallyOrCancel(cause) && ! handled) {
316
- // prevents loss of exception that was not handled by parent & could not be set to JobListenableFuture
317
- handleCoroutineException(context, cause)
318
- }
314
+ // Note: if future was cancelled in a race with a cancellation of this
315
+ // coroutine, and the future was successfully cancelled first, the cause of coroutine
316
+ // cancellation is dropped in this promise. A Future can only be completed once.
317
+ //
318
+ // This is consistent with FutureTask behaviour. A race between a Future.cancel() and
319
+ // a FutureTask.setException() for the same Future will similarly drop the
320
+ // cause of a failure-after-cancellation.
321
+ future.completeExceptionallyOrCancel(cause)
319
322
}
320
323
}
321
324
0 commit comments