Skip to content

Commit 15fb3dc

Browse files
committed
Fix stack overflow when calling asDeferred on a Future many times
Fixes #4156
1 parent 2d9f944 commit 15fb3dc

File tree

4 files changed

+28
-29
lines changed

4 files changed

+28
-29
lines changed

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,6 @@ public final class kotlinx/coroutines/JobKt {
454454
public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/lang/Throwable;ILjava/lang/Object;)V
455455
public static synthetic fun cancelChildren$default (Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V
456456
public static final fun cancelFutureOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Ljava/util/concurrent/Future;)V
457-
public static final fun cancelFutureOnCompletion (Lkotlinx/coroutines/Job;Ljava/util/concurrent/Future;)Lkotlinx/coroutines/DisposableHandle;
458457
public static final fun ensureActive (Lkotlin/coroutines/CoroutineContext;)V
459458
public static final fun ensureActive (Lkotlinx/coroutines/Job;)V
460459
public static final fun getJob (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/Job;

kotlinx-coroutines-core/jdk8/src/future/Future.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public fun <T> CompletionStage<T>.asDeferred(): Deferred<T> {
138138
handleCoroutineException(EmptyCoroutineContext, e)
139139
}
140140
}
141-
result.cancelFutureOnCompletion(future)
141+
result.invokeOnCompletion(handler = CancelFutureOnCompletion(future))
142142
return result
143143
}
144144

@@ -190,3 +190,18 @@ private class ContinuationHandler<T>(
190190
}
191191
}
192192
}
193+
194+
private class CancelFutureOnCompletion(
195+
private val future: Future<*>
196+
) : JobNode() {
197+
override fun invoke(cause: Throwable?) {
198+
// Don't interrupt when cancelling future on completion, because no one is going to reset this
199+
// interruption flag and it will cause spurious failures elsewhere.
200+
// We do not cancel the future if it's already completed in some way,
201+
// because `cancel` on a completed future won't change the state but is not guaranteed to behave well
202+
// on reentrancy. See https://github.com/Kotlin/kotlinx.coroutines/issues/4156
203+
if (cause != null && !future.isDone) future.cancel(false)
204+
}
205+
206+
override val onCancelling get() = false
207+
}

kotlinx-coroutines-core/jvm/src/Future.kt

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,17 @@ import java.util.concurrent.*
99
* Cancels a specified [future] when this job is cancelled.
1010
* This is a shortcut for the following code with slightly more efficient implementation (one fewer object created).
1111
* ```
12-
* invokeOnCompletion { if (it != null) future.cancel(false) }
13-
* ```
14-
*
15-
* @suppress **This an internal API and should not be used from general code.**
16-
*/
17-
@InternalCoroutinesApi
18-
public fun Job.cancelFutureOnCompletion(future: Future<*>): DisposableHandle =
19-
invokeOnCompletion(handler = CancelFutureOnCompletion(future))
20-
21-
/**
22-
* Cancels a specified [future] when this job is cancelled.
23-
* This is a shortcut for the following code with slightly more efficient implementation (one fewer object created).
24-
* ```
25-
* invokeOnCancellation { if (it != null) future.cancel(false) }
12+
* invokeOnCancellation { if (it != null && !future.isDone) future.cancel(false) }
2613
* ```
2714
*/
2815
public fun CancellableContinuation<*>.cancelFutureOnCancellation(future: Future<*>): Unit =
2916
invokeOnCancellation(handler = CancelFutureOnCancel(future))
3017

31-
private class CancelFutureOnCompletion(
32-
private val future: Future<*>
33-
) : JobNode() {
34-
override fun invoke(cause: Throwable?) {
35-
// Don't interrupt when cancelling future on completion, because no one is going to reset this
36-
// interruption flag and it will cause spurious failures elsewhere
37-
if (cause != null) future.cancel(false)
38-
}
39-
40-
override val onCancelling get() = false
41-
}
42-
4318
private class CancelFutureOnCancel(private val future: Future<*>) : CancelHandler {
4419
override fun invoke(cause: Throwable?) {
4520
// Don't interrupt when cancelling future on completion, because no one is going to reset this
4621
// interruption flag and it will cause spurious failures elsewhere
47-
if (cause != null) future.cancel(false)
22+
if (cause != null && !future.isDone) future.cancel(false)
4823
}
4924
override fun toString() = "CancelFutureOnCancel[$future]"
5025
}

kotlinx-coroutines-core/jvm/test/jdk8/future/FutureTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,4 +595,14 @@ class FutureTest : TestBase() {
595595
GlobalScope.future<Unit>(start = CoroutineStart.LAZY) { }
596596
}
597597
}
598+
599+
@Test
600+
fun testStackOverflowOnExceptionalCompletion() = runTest {
601+
val future = CompletableFuture<Unit>()
602+
val didRun = AtomicBoolean(false)
603+
future.whenComplete { _, _ -> didRun.set(true) }
604+
val deferreds = (0 until 100000L).map { future.asDeferred() }
605+
future.completeExceptionally(RuntimeException())
606+
assertTrue(deferreds.all { it.isCompleted })
607+
}
598608
}

0 commit comments

Comments
 (0)