Decorated suspend inline function continuation resumes in wrong spot


#1

Using

  • Kotlin 1.2.51
  • Kotlinx 0.23.4

The below code should start a single coroutine, wait ~100ms, resume the coroutine, and then exit.

However, it gets stuck in an infinite loop, and as far as I can tell its because the coroutine is resuming in the wrong spot, which appears to be “one up” the call stack from where it should be.

This results in the coroutine being started again, then waiting until resumed, then getting resumed but in the wrong spot, which starts the coroutine again, ad infinitum…

Code:

import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.runBlocking
import org.junit.Test
import kotlin.coroutines.experimental.Continuation
import kotlin.coroutines.experimental.suspendCoroutine

class InlineSuspendFail {

    @Test
    fun showFail() {

        val decoratedWork = decorate {
            work {
                Thread {
                    Thread.sleep(100)
                    it.resume(42)
                }.start()
            }
        }

        val deferred = async { decoratedWork() }
        runBlocking { deferred.await() }
    }

    fun decorate(actualWork: suspend () -> Int): suspend () -> Int {
        return {
            actualWork
                    .let { workDecorator(it) }
                    .invoke()
        }
    }

    suspend inline fun work(crossinline triggerResume: (Continuation<Int>) -> Unit): Int {
        return suspendCoroutine { continuation ->
            triggerResume(continuation)
        }
    }

    suspend inline fun workDecorator(crossinline work: suspend () -> Int): suspend () -> Int = suspend {
        // coroutine appears to resume here, rather than inside the `suspendCoroutine` block
        // this causes an infinite loop, since calling work() below starts another coroutine
        work()
    }
}

Even stranger is that having the second thread trigger the continuation is key, since the problem goes away if we immediately resume the coroutine. i.e. decorate { work { it.resume(42) } } doesn’t have this problem.

Removing the inline also gets rid of the problem, even when keeping the second thread.


I tried to create an example with logging output to show the control flow, however doing that broke the compiler as described in this topic


For what its worth, this is a contrived example derived from a real use case I had where I was updating some existing Kotlin code to use coroutines. All my tests passed just fine (which are full black box system tests with just the external dependencies mocked out), but then the system hung at runtime when trying to publish a message to a message queue. The queue client provided a callback mechanism so I created an adapter similar to the triggerResume above. I was trying to load test sending 1,000 messages and ended up sending 1.15 million messages before I killed the service to investigate why the process hadn’t completed yet.

Luckily I was able to work around this problem by removing the inlines, but getting rid of the function decorators would have been a deal breaker.


#2

I tried your code with out the inlining and it works on my machine :slight_smile: i.e. it finished execution almost immediately.
AFAIK, removing the inline and crossinline should not change the execution result.

If it does work for you too, this could be useful to narrowing the problem down and simplifying the bug report.

Also maybe as a short-term solution you can continue coding without inlining and hope that the performance impact is not very high :slight_smile:


#3

I think the last part of my initial post addresses all of your questions.

Is this bug being tracked anywhere?


Kotlin error compiling decorated suspend inline functions
#4

Created https://youtrack.jetbrains.com/issue/KT-26925.