Skip to content

Commit 1e36a04

Browse files
authored
Fix stack overflow when calling asDeferred on a Future many times (#4173)
Fixes #4156
1 parent 5e03ca2 commit 1e36a04

File tree

11 files changed

+91
-54
lines changed

11 files changed

+91
-54
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/common/src/Await.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
9999
var disposer: DisposeHandlersOnCancel?
100100
get() = _disposer.value
101101
set(value) { _disposer.value = value }
102+
103+
override val onCancelling get() = false
102104

103105
override fun invoke(cause: Throwable?) {
104106
if (cause != null) {
@@ -114,7 +116,5 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
114116
// Note that all deferreds are complete here, so we don't need to dispose their nodes
115117
}
116118
}
117-
118-
override val onCancelling get() = false
119119
}
120120
}

kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,9 @@ private data class CompletedContinuation<R>(
692692
private class ChildContinuation(
693693
@JvmField val child: CancellableContinuationImpl<*>
694694
) : JobNode() {
695+
override val onCancelling get() = true
696+
695697
override fun invoke(cause: Throwable?) {
696698
child.parentCancelled(child.getContinuationCancellationCause(job))
697699
}
698-
699-
override val onCancelling get() = true
700700
}

kotlinx-coroutines-core/common/src/Job.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ public object NonDisposableHandle : DisposableHandle, ChildHandle {
670670
private class DisposeOnCompletion(
671671
private val handle: DisposableHandle
672672
) : JobNode() {
673-
override fun invoke(cause: Throwable?) = handle.dispose()
674-
675673
override val onCancelling get() = false
674+
675+
override fun invoke(cause: Throwable?) = handle.dispose()
676676
}

kotlinx-coroutines-core/common/src/JobSupport.kt

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -607,10 +607,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
607607
private inner class SelectOnJoinCompletionHandler(
608608
private val select: SelectInstance<*>
609609
) : JobNode() {
610+
override val onCancelling: Boolean get() = false
610611
override fun invoke(cause: Throwable?) {
611612
select.trySelect(this@JobSupport, Unit)
612613
}
613-
override val onCancelling: Boolean get() = false
614614
}
615615

616616
/**
@@ -1263,10 +1263,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
12631263
private val child: ChildHandleNode,
12641264
private val proposedUpdate: Any?
12651265
) : JobNode() {
1266+
override val onCancelling get() = false
12661267
override fun invoke(cause: Throwable?) {
12671268
parent.continueCompleting(state, child, proposedUpdate)
12681269
}
1269-
override val onCancelling: Boolean get() = false
12701270
}
12711271

12721272
private class AwaitContinuation<T>(
@@ -1378,12 +1378,12 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
13781378
private inner class SelectOnAwaitCompletionHandler(
13791379
private val select: SelectInstance<*>
13801380
) : JobNode() {
1381+
override val onCancelling get() = false
13811382
override fun invoke(cause: Throwable?) {
13821383
val state = this@JobSupport.state
13831384
val result = if (state is CompletedExceptionally) state else state.unboxState()
13841385
select.trySelect(this@JobSupport, result)
13851386
}
1386-
override val onCancelling: Boolean get() = false
13871387
}
13881388
}
13891389

@@ -1462,8 +1462,16 @@ internal abstract class JobNode : LockFreeLinkedListNode(), DisposableHandle, In
14621462
* Initialized by [JobSupport.invokeOnCompletionInternal].
14631463
*/
14641464
lateinit var job: JobSupport
1465+
1466+
/**
1467+
* If `false`, [invoke] will be called once the job is cancelled or is complete.
1468+
* If `true`, [invoke] is invoked as soon as the job becomes _cancelling_ instead, and if that doesn't happen,
1469+
* it will be called once the job is cancelled or is complete.
1470+
*/
1471+
abstract val onCancelling: Boolean
14651472
override val isActive: Boolean get() = true
14661473
override val list: NodeList? get() = null
1474+
14671475
override fun dispose() = job.removeNode(this)
14681476
override fun toString() = "$classSimpleName@$hexAddress[job@${job.hexAddress}]"
14691477
/**
@@ -1488,13 +1496,6 @@ internal abstract class JobNode : LockFreeLinkedListNode(), DisposableHandle, In
14881496
* (see [InvokeOnCompletion] and [InvokeOnCancelling]).
14891497
*/
14901498
abstract fun invoke(cause: Throwable?)
1491-
1492-
/**
1493-
* If `false`, [invoke] will be called once the job is cancelled or is complete.
1494-
* If `true`, [invoke] is invoked as soon as the job becomes _cancelling_ instead, and if that doesn't happen,
1495-
* it will be called once the job is cancelled or is complete.
1496-
*/
1497-
abstract val onCancelling: Boolean
14981499
}
14991500

15001501
internal class NodeList : LockFreeLinkedListHead(), Incomplete {
@@ -1529,20 +1530,21 @@ private class InactiveNodeList(
15291530
private class InvokeOnCompletion(
15301531
private val handler: CompletionHandler
15311532
) : JobNode() {
1532-
override fun invoke(cause: Throwable?) = handler.invoke(cause)
15331533
override val onCancelling get() = false
1534+
override fun invoke(cause: Throwable?) = handler.invoke(cause)
15341535
}
15351536

15361537
private class ResumeOnCompletion(
15371538
private val continuation: Continuation<Unit>
15381539
) : JobNode() {
1539-
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
15401540
override val onCancelling get() = false
1541+
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
15411542
}
15421543

15431544
private class ResumeAwaitOnCompletion<T>(
15441545
private val continuation: CancellableContinuationImpl<T>
15451546
) : JobNode() {
1547+
override val onCancelling get() = false
15461548
override fun invoke(cause: Throwable?) {
15471549
val state = job.state
15481550
assert { state !is Incomplete }
@@ -1555,7 +1557,6 @@ private class ResumeAwaitOnCompletion<T>(
15551557
continuation.resume(state.unboxState() as T)
15561558
}
15571559
}
1558-
override val onCancelling get() = false
15591560
}
15601561

15611562
// -------- invokeOnCancellation nodes
@@ -1565,17 +1566,17 @@ private class InvokeOnCancelling(
15651566
) : JobNode() {
15661567
// delegate handler shall be invoked at most once, so here is an additional flag
15671568
private val _invoked = atomic(false)
1569+
override val onCancelling get() = true
15681570
override fun invoke(cause: Throwable?) {
15691571
if (_invoked.compareAndSet(expect = false, update = true)) handler.invoke(cause)
15701572
}
1571-
override val onCancelling get() = true
15721573
}
15731574

15741575
private class ChildHandleNode(
15751576
@JvmField val childJob: ChildJob
15761577
) : JobNode(), ChildHandle {
15771578
override val parent: Job get() = job
1579+
override val onCancelling: Boolean get() = true
15781580
override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
15791581
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
1580-
override val onCancelling: Boolean get() = true
15811582
}

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 val onCancelling get() = false
198+
199+
override fun invoke(cause: Throwable?) {
200+
// Don't interrupt when cancelling future on completion, because no one is going to reset this
201+
// interruption flag and it will cause spurious failures elsewhere.
202+
// We do not cancel the future if it's already completed in some way,
203+
// because `cancel` on a completed future won't change the state but is not guaranteed to behave well
204+
// on reentrancy. See https://github.com/Kotlin/kotlinx.coroutines/issues/4156
205+
if (cause != null && !future.isDone) future.cancel(false)
206+
}
207+
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package kotlinx.coroutines
22

3-
import kotlinx.coroutines.flow.*
43
import kotlinx.coroutines.internal.*
54
import java.io.Closeable
65
import java.util.concurrent.*
@@ -145,7 +144,7 @@ internal class ExecutorCoroutineDispatcherImpl(override val executor: Executor)
145144
)
146145
// If everything went fine and the scheduling attempt was not rejected -- use it
147146
if (future != null) {
148-
continuation.cancelFutureOnCancellation(future)
147+
continuation.invokeOnCancellation(CancelFutureOnCancel(future))
149148
return
150149
}
151150
// Otherwise fallback to default executor
@@ -201,3 +200,12 @@ private class DisposableFutureHandle(private val future: Future<*>) : Disposable
201200
}
202201
override fun toString(): String = "DisposableFutureHandle[$future]"
203202
}
203+
204+
private class CancelFutureOnCancel(private val future: Future<*>) : CancelHandler {
205+
override fun invoke(cause: Throwable?) {
206+
// Don't interrupt when cancelling future on completion, because no one is going to reset this
207+
// interruption flag and it will cause spurious failures elsewhere
208+
future.cancel(false)
209+
}
210+
override fun toString() = "CancelFutureOnCancel[$future]"
211+
}

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

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,23 @@ package kotlinx.coroutines
55

66
import java.util.concurrent.*
77

8-
/**
9-
* Cancels a specified [future] when this job is cancelled.
10-
* This is a shortcut for the following code with slightly more efficient implementation (one fewer object created).
11-
* ```
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-
218
/**
229
* Cancels a specified [future] when this job is cancelled.
2310
* This is a shortcut for the following code with slightly more efficient implementation (one fewer object created).
2411
* ```
2512
* invokeOnCancellation { if (it != null) future.cancel(false) }
2613
* ```
2714
*/
15+
// Warning since 1.9.0, error in 1., hidden in 1.7
16+
@Deprecated(
17+
"This function does not do what its name implies: it will not cancel the future if just cancel() was called.",
18+
level = DeprecationLevel.WARNING,
19+
replaceWith = ReplaceWith("this.invokeOnCancellation { future.cancel(false) }")
20+
)
2821
public fun CancellableContinuation<*>.cancelFutureOnCancellation(future: Future<*>): Unit =
29-
invokeOnCancellation(handler = CancelFutureOnCancel(future))
30-
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-
}
22+
invokeOnCancellation(handler = PublicCancelFutureOnCancel(future))
4223

43-
private class CancelFutureOnCancel(private val future: Future<*>) : CancelHandler {
24+
private class PublicCancelFutureOnCancel(private val future: Future<*>) : CancelHandler {
4425
override fun invoke(cause: Throwable?) {
4526
// Don't interrupt when cancelling future on completion, because no one is going to reset this
4627
// interruption flag and it will cause spurious failures elsewhere

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ private class ThreadState : JobNode() {
9595
// Registered cancellation handler
9696
private var cancelHandle: DisposableHandle? = null
9797

98+
override val onCancelling get() = true
99+
98100
fun setup(job: Job) {
99101
cancelHandle = job.invokeOnCompletion(handler = this)
100102
// Either we successfully stored it or it was immediately cancelled
@@ -154,7 +156,5 @@ private class ThreadState : JobNode() {
154156
}
155157
}
156158

157-
override val onCancelling get() = true
158-
159159
private fun invalidState(state: Int): Nothing = error("Illegal state $state")
160160
}

kotlinx-coroutines-core/jvm/test/ExecutorAsCoroutineDispatcherDelayTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,21 @@ class ExecutorAsCoroutineDispatcherDelayTest : TestBase() {
3838
executor.shutdown()
3939
assertEquals(1, callsToSchedule)
4040
}
41+
42+
@Test
43+
fun testCancelling() = runTest {
44+
val executor = STPE()
45+
launch(start = CoroutineStart.UNDISPATCHED) {
46+
suspendCancellableCoroutine<Unit> { cont ->
47+
expect(1)
48+
(executor.asCoroutineDispatcher() as Delay).scheduleResumeAfterDelay(1_000_000, cont)
49+
cont.cancel()
50+
expect(2)
51+
}
52+
}
53+
expect(3)
54+
assertTrue(executor.getQueue().isEmpty())
55+
executor.shutdown()
56+
finish(4)
57+
}
4158
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,4 +595,20 @@ 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 = List(100000) { future.asDeferred() }
605+
future.completeExceptionally(TestException())
606+
deferreds.forEach {
607+
assertTrue(it.isCompleted)
608+
val exception = it.getCompletionExceptionOrNull()
609+
assertIs<TestException>(exception)
610+
assertTrue(exception.suppressedExceptions.isEmpty())
611+
}
612+
assertTrue(didRun.get())
613+
}
598614
}

0 commit comments

Comments
 (0)