Running the sample code below with commented / uncommented line 4 in function runSuspension
(that calls suspendDummy
) reveals strange behavior of ContinuationInterceptor
. With mentioned line uncommented the Interceptor of original coroutine (coroutine [0]
which is called in line 3 of main
function) is first called on the coroutine start (which is expected). And than is called again inside function runSuspension
before the second coroutine (coroutine [1]
which is called in line 3 of runSuspension
function). This second call seems does nothing essential and looks like a bug. (What is it trying to intercept there?). From the other hand if the mentioned line is commented Interceptor behaves as expected without extra interception call. The same expected behavior, even with uncommented call to suspendDummy
is observed when the whole body of the function runSuspension
is inlined into the place of the call in main
function.
The prints with commented and uncommented call to suspendDummy
are below.
I’d like to note that this behavior is essential for the library i develop since i use Interceptor’s interceptContinuation
method to trigger a currently executing coroutine, and i fail to do that if it’s called in unexpected occasions.
Coroutine API Authors, please, correct that behavior if it’s faulty or document the cases when it can occur.
fun main(args: Array<String>) {
println("Before Coroutine")
runCoroutine(0) {
println("[0] Before suspension")
runSuspension(1)
println("[0] After suspension")
}
println("After Coroutine")
}
fun runCoroutine(id:Int, code: suspend () -> Unit) = code.startCoroutine(Completion(id))
suspend fun runSuspension(id: Int) = suspendCoroutine<Unit> {
println("[$id] Before Coroutine")
runCoroutine(id) {
suspendDummy() // !!! UN/COMMENT this line to switch behavior
println("[$id] Inside Coroutine")
it.resume(Unit)
}
}
suspend fun suspendDummy() = 1
class Completion<T>(val id: Int): Continuation<T> {
override val context = Interceptor(id)
override fun resume(value: T) = println("[$id] coroutine Complete")
override fun resumeWithException(exception: Throwable) = println("[$id] coroutine Complete with $exception")
}
class Interceptor(val id: Int): AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
var again = false
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
println("[$id] INTERCEPT CONTINUATION ${if (again) "Again !!!" else ""}")
val prev = again
again = true
return Interception("$id${if (prev) "+" else ""}", continuation)
}
}
class Interception<T>(val id: String, val continuation: Continuation<T>): Continuation<T> {
override val context = continuation.context
override fun resume(value: T) {
executor.execute {
println("[$id] resume interception")
continuation.resume(value)
}
}
override fun resumeWithException(exception: Throwable) {
println("[$id] resume interception with $exception")
continuation.resumeWithException(exception)
}
}
val executor = Executors.newCachedThreadPool().apply {
execute {
awaitTermination(1000, TimeUnit.MILLISECONDS)
shutdown()
}
}
Prints when suspendDummy()
line is commented: That is the expected behavior.
Before Coroutine
[0] INTERCEPT CONTINUATION
After Coroutine
[0] resume interception
[0] Before suspension
[1] Before Coroutine
[1] INTERCEPT CONTINUATION
[1] resume interception
[1] Inside Coroutine
[0] resume interception
[1] coroutine Complete
[0] After suspension
[0] coroutine Complete
Prints when suspendDummy()
line is uncommented: Note line 6 and 10. Those are different from the print above. It’s where that strange behavior reveals itself.
Before Coroutine
[0] INTERCEPT CONTINUATION
After Coroutine
[0] resume interception
[0] Before suspension
[0] INTERCEPT CONTINUATION ------- Again !!!
[1] Before Coroutine
[1] INTERCEPT CONTINUATION
[1] resume interception
[1] Inside Coroutine
[0+] resume interception ------- Note that call is from second instance ofInterception
class
[1] coroutine Complete
[0] After suspension
[0] coroutine Complete