Skip to content

Commit 1d04e79

Browse files
elizarovqwwdfsad
authored andcommitted
Stricter checking for CompletableContinuation state machine
* Always throw ISE on the second resume attempt * Always throw ISE on the second invokeOnCancellation attempt when cancelled * Moved error-throwing code into separate funs (slight optimization) Fixes #901
1 parent 7e8b52e commit 1d04e79

File tree

3 files changed

+46
-36
lines changed

3 files changed

+46
-36
lines changed

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

+29-18
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ internal open class CancellableContinuationImpl<in T>(
106106
_state.loop { state ->
107107
if (state !is NotCompleted) return false // false if already complete or cancelling
108108
// Active -- update to final state
109-
if (!_state.compareAndSet(state, CancelledContinuation(this, cause))) return@loop // retry on cas failure
109+
val update = CancelledContinuation(this, cause, handled = state is CancelHandler)
110+
if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
110111
// Invoke cancel handler if it was present
111112
if (state is CancelHandler) invokeHandlerSafely { state.invoke(cause) }
112113
// Complete state update
@@ -177,28 +178,37 @@ internal open class CancellableContinuationImpl<in T>(
177178
val node = handleCache ?: makeHandler(handler).also { handleCache = it }
178179
if (_state.compareAndSet(state, node)) return // quit on cas success
179180
}
180-
is CancelHandler -> {
181-
error("It's prohibited to register multiple handlers, tried to register $handler, already has $state")
182-
}
181+
is CancelHandler -> multipleHandlersError(handler, state)
183182
is CancelledContinuation -> {
184183
/*
185184
* Continuation was already cancelled, invoke directly.
186-
* NOTE: multiple invokeOnCancellation calls with different handlers are allowed on cancelled continuation.
187-
* It's inconsistent with running continuation, but currently, we have no mechanism to check
188-
* whether any handler was registered during continuation lifecycle without additional overhead.
189-
* This may be changed in the future.
190-
*
185+
* NOTE: multiple invokeOnCancellation calls with different handlers are not allowed,
186+
* so we check to make sure that handler was installed just once.
187+
*/
188+
if (!state.makeHandled()) multipleHandlersError(handler, state)
189+
/*
191190
* :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
192191
* because we play type tricks on Kotlin/JS and handler is not necessarily a function there
193192
*/
194193
invokeHandlerSafely { handler.invokeIt((state as? CompletedExceptionally)?.cause) }
195194
return
196195
}
197-
else -> return
196+
else -> {
197+
/*
198+
* Continuation was already completed, do nothing.
199+
* NOTE: multiple invokeOnCancellation calls with different handlers are not allowed,
200+
* but we have no way to check that it was installed just once in this case.
201+
*/
202+
return
203+
}
198204
}
199205
}
200206
}
201207

208+
private fun multipleHandlersError(handler: CompletionHandler, state: Any?) {
209+
error("It's prohibited to register multiple handlers, tried to register $handler, already has $state")
210+
}
211+
202212
private fun makeHandler(handler: CompletionHandler): CancelHandler =
203213
if (handler is CancelHandler) handler else InvokeOnCancel(handler)
204214

@@ -219,20 +229,21 @@ internal open class CancellableContinuationImpl<in T>(
219229
}
220230
is CancelledContinuation -> {
221231
/*
222-
* If continuation was cancelled, then all further resumes must be
223-
* ignored, because cancellation is asynchronous and may race with resume.
224-
* Racy exceptions will be lost, too. There does not see to be a safe way to
225-
* handle them without producing spurious crashes.
226-
*
227-
* :todo: we should somehow remember the attempt to invoke resume and fail on the second attempt.
232+
* If continuation was cancelled, then resume attempt must be ignored,
233+
* because cancellation is asynchronous and may race with resume.
234+
* Racy exceptions will be lost, too.
228235
*/
229-
return
236+
if (state.makeResumed()) return // ok -- resumed just once
230237
}
231-
else -> error("Already resumed, but proposed with update $proposedUpdate")
232238
}
239+
alreadyResumedError(proposedUpdate) // otherwise -- an error (second resume attempt)
233240
}
234241
}
235242

243+
private fun alreadyResumedError(proposedUpdate: Any?) {
244+
error("Already resumed, but proposed with update $proposedUpdate")
245+
}
246+
236247
// Unregister from parent job
237248
private fun disposeParentHandle() {
238249
parentHandle?.let { // volatile read parentHandle (once)

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.coroutines
66

7+
import kotlinx.atomicfu.*
78
import kotlin.coroutines.*
89
import kotlin.jvm.*
910

@@ -31,5 +32,12 @@ internal open class CompletedExceptionally(
3132
*/
3233
internal class CancelledContinuation(
3334
continuation: Continuation<*>,
34-
cause: Throwable?
35-
) : CompletedExceptionally(cause ?: CancellationException("Continuation $continuation was cancelled normally"))
35+
cause: Throwable?,
36+
handled: Boolean
37+
) : CompletedExceptionally(cause ?: CancellationException("Continuation $continuation was cancelled normally")) {
38+
private val resumed = atomic(false)
39+
private val handled = atomic(handled)
40+
41+
fun makeResumed(): Boolean = resumed.compareAndSet(false, true)
42+
fun makeHandled(): Boolean = handled.compareAndSet(false, true)
43+
}

common/kotlinx-coroutines-core-common/test/CancellableContinuationHandlersTest.kt

+7-16
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class CancellableContinuationHandlersTest : TestBase() {
2323
c.resume(Unit)
2424
// Nothing happened
2525
c.invokeOnCancellation { expectUnreached() }
26+
// Cannot validate after completion
2627
c.invokeOnCancellation { expectUnreached() }
2728
}
2829
}
@@ -36,13 +37,10 @@ class CancellableContinuationHandlersTest : TestBase() {
3637
assertTrue(it is CancellationException)
3738
expect(1)
3839
}
39-
c.invokeOnCancellation {
40-
assertTrue(it is CancellationException)
41-
expect(2)
42-
}
40+
assertFailsWith<IllegalStateException> { c.invokeOnCancellation { expectUnreached() } }
4341
}
4442
} catch (e: CancellationException) {
45-
finish(3)
43+
finish(2)
4644
}
4745
}
4846

@@ -55,13 +53,10 @@ class CancellableContinuationHandlersTest : TestBase() {
5553
require(it is AssertionError)
5654
expect(1)
5755
}
58-
c.invokeOnCancellation {
59-
require(it is AssertionError)
60-
expect(2)
61-
}
56+
assertFailsWith<IllegalStateException> { c.invokeOnCancellation { expectUnreached() } }
6257
}
6358
} catch (e: AssertionError) {
64-
finish(3)
59+
finish(2)
6560
}
6661
}
6762

@@ -73,15 +68,11 @@ class CancellableContinuationHandlersTest : TestBase() {
7368
require(it is IndexOutOfBoundsException)
7469
expect(1)
7570
}
76-
7771
c.cancel(IndexOutOfBoundsException())
78-
c.invokeOnCancellation {
79-
require(it is IndexOutOfBoundsException)
80-
expect(2)
81-
}
72+
assertFailsWith<IllegalStateException> { c.invokeOnCancellation { expectUnreached() } }
8273
}
8374
} catch (e: IndexOutOfBoundsException) {
84-
finish(3)
75+
finish(2)
8576
}
8677
}
8778

0 commit comments

Comments
 (0)