Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is there a difference between coroutine builders for CompletableFuture and ListenableFuture?

While examining source of Kotlin coroutines, I noticed a difference (marked with **) between JDK 8 CompletableFuture

public fun <T> future(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): CompletableFuture<T> {
    require(!start.isLazy) { "$start start is not supported" }
    val newContext = newCoroutineContext(context)
    val job = Job(newContext[Job])
    val future = CompletableFutureCoroutine<T>(newContext + job)
    job.cancelFutureOnCompletion(future)
    ** future.whenComplete { _, exception -> job.cancel(exception) } **
    start(block, receiver=future, completion=future) // use the specified start strategy
    return future
}


private class CompletableFutureCoroutine<T>(
    override val context: CoroutineContext
) : CompletableFuture<T>(), Continuation<T>, CoroutineScope {
    override val coroutineContext: CoroutineContext get() = context
    override val isActive: Boolean get() = context[Job]!!.isActive
    override fun resume(value: T) { complete(value) }
    override fun resumeWithException(exception: Throwable) { completeExceptionally(exception) }
    ** doesn't override cancel which corresponds to interrupt task **
}

and Guava ListenableFuture

public fun <T> future(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): ListenableFuture<T> {
    require(!start.isLazy) { "$start start is not supported" }
    val newContext = newCoroutineContext(context)
    val job = Job(newContext[Job])
    val future = ListenableFutureCoroutine<T>(newContext + job)
    job.cancelFutureOnCompletion(future)
    start(block, receiver=future, completion=future) // use the specified start strategy
    return future
}

private class ListenableFutureCoroutine<T>(
    override val context: CoroutineContext
) : AbstractFuture<T>(), Continuation<T>, CoroutineScope {
    override val coroutineContext: CoroutineContext get() = context
    override val isActive: Boolean get() = context[Job]!!.isActive
    override fun resume(value: T) { set(value) }
    override fun resumeWithException(exception: Throwable) { setException(exception) }
    ** override fun interruptTask() { context[Job]!!.cancel() } **
}

integrations, though I think of the types as nearly equivalent (except of course ListenableFuture can't be completed directly, but I don't see why this matters here). Is there a specific reason behind this difference?

like image 626
Alexey Romanov Avatar asked Jan 30 '23 04:01

Alexey Romanov


1 Answers

The CompletableFuture.cancel is an open (overridable) method, but it is not designed for override. Its documentation does not provide any guarantee for its invocation on cancellation, so the only future-proof (pun intended) way to learn that CompletableFuture was cancelled is by installing whenComplete listener on it.

For example, it is perfectly legal for a future version of JDK to add yet another method to cancel a future that does not internally invoke cancel. Such a change would not violate any piece of CompletableFuture contract.

Compare this with documentation on AbstractFuture.interruptTask. This method is explicitly designed for override and its documentation guarantees conditions under which it is invoked. Thus, we can provide a slightly more efficient implementation for ListenableFuture builder that avoids creation of lambda to install a cancellation listener on it.

like image 160
Roman Elizarov Avatar answered Feb 02 '23 20:02

Roman Elizarov