Skip to content

Commit

Permalink
Fix stack overflow when calling asDeferred on a Future many times
Browse files Browse the repository at this point in the history
Fixes #4156
  • Loading branch information
dkhalanskyjb committed Jul 10, 2024
1 parent 2d9f944 commit d598ca0
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 29 deletions.
1 change: 0 additions & 1 deletion kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,6 @@ public final class kotlinx/coroutines/JobKt {
public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;ILjava/lang/Object;)V
public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V
public static final fun cancelFutureOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Ljava/util/concurrent/Future;)V
public static final fun cancelFutureOnCompletion (Lkotlinx/coroutines/Job;Ljava/util/concurrent/Future;)Lkotlinx/coroutines/DisposableHandle;
public static final fun ensureActive (Lkotlin/coroutines/CoroutineContext;)V
public static final fun ensureActive (Lkotlinx/coroutines/Job;)V
public static final fun getJob (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/Job;
Expand Down
17 changes: 16 additions & 1 deletion kotlinx-coroutines-core/jdk8/src/future/Future.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public fun <T> CompletionStage<T>.asDeferred(): Deferred<T> {
handleCoroutineException(EmptyCoroutineContext, e)
}
}
result.cancelFutureOnCompletion(future)
result.invokeOnCompletion(handler = CancelFutureOnCompletion(future))
return result
}

Expand Down Expand Up @@ -190,3 +190,18 @@ private class ContinuationHandler<T>(
}
}
}

private class CancelFutureOnCompletion(
private val future: Future<*>
) : JobNode() {
override fun invoke(cause: Throwable?) {
// Don't interrupt when cancelling future on completion, because no one is going to reset this
// interruption flag and it will cause spurious failures elsewhere.
// We do not cancel the future if it's already completed in some way,
// because `cancel` on a completed future won't change the state but is not guaranteed to behave well
// on reentrancy. See https://github.com/Kotlin/kotlinx.coroutines/issues/4156
if (cause != null && !future.isDone) future.cancel(false)
}

override val onCancelling get() = false
}
29 changes: 2 additions & 27 deletions kotlinx-coroutines-core/jvm/src/Future.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,17 @@ import java.util.concurrent.*
* Cancels a specified [future] when this job is cancelled.
* This is a shortcut for the following code with slightly more efficient implementation (one fewer object created).
* ```
* invokeOnCompletion { if (it != null) future.cancel(false) }
* ```
*
* @suppress **This an internal API and should not be used from general code.**
*/
@InternalCoroutinesApi
public fun Job.cancelFutureOnCompletion(future: Future<*>): DisposableHandle =
invokeOnCompletion(handler = CancelFutureOnCompletion(future))

/**
* Cancels a specified [future] when this job is cancelled.
* This is a shortcut for the following code with slightly more efficient implementation (one fewer object created).
* ```
* invokeOnCancellation { if (it != null) future.cancel(false) }
* invokeOnCancellation { if (it != null && !future.isDone) future.cancel(false) }
* ```
*/
public fun CancellableContinuation<*>.cancelFutureOnCancellation(future: Future<*>): Unit =
invokeOnCancellation(handler = CancelFutureOnCancel(future))

private class CancelFutureOnCompletion(
private val future: Future<*>
) : JobNode() {
override fun invoke(cause: Throwable?) {
// Don't interrupt when cancelling future on completion, because no one is going to reset this
// interruption flag and it will cause spurious failures elsewhere
if (cause != null) future.cancel(false)
}

override val onCancelling get() = false
}

private class CancelFutureOnCancel(private val future: Future<*>) : CancelHandler {
override fun invoke(cause: Throwable?) {
// Don't interrupt when cancelling future on completion, because no one is going to reset this
// interruption flag and it will cause spurious failures elsewhere
if (cause != null) future.cancel(false)
if (cause != null && !future.isDone) future.cancel(false)
}
override fun toString() = "CancelFutureOnCancel[$future]"
}
16 changes: 16 additions & 0 deletions kotlinx-coroutines-core/jvm/test/jdk8/future/FutureTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -595,4 +595,20 @@ class FutureTest : TestBase() {
GlobalScope.future<Unit>(start = CoroutineStart.LAZY) { }
}
}

@Test
fun testStackOverflowOnExceptionalCompletion() = runTest {
val future = CompletableFuture<Unit>()
val didRun = AtomicBoolean(false)
future.whenComplete { _, _ -> didRun.set(true) }
val deferreds = List(100000) { future.asDeferred() }
future.completeExceptionally(TestException())
deferreds.forEach {
assertTrue(it.isCompleted)
val exception = it.getCompletionExceptionOrNull()
assertIs<TestException>(exception)
assertTrue(exception.suppressedExceptions.isEmpty())
}
assertTrue(didRun.get())
}
}

0 comments on commit d598ca0

Please sign in to comment.