Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suspendCancellableCoroutine returns an internal CompletedWithCancellation object instead of the actual resulting type #1966

Closed
manabreak opened this issue Apr 28, 2020 · 10 comments
Assignees
Labels

Comments

@manabreak
Copy link

I ran into a weird issue that manifested itself when I updated the kotlinx-coroutines-core dependency from 1.3.2 to 1.3.3. However, the self-contained example below reproduces the issue with 1.3.2 as well.

I have an extension method for a callback-based operation queue. This extension method uses suspendCancellableCoroutine to wrap the callback usage and to convert it to a suspend function. Now, it all works otherwise, but the resulting object that is returned from the suspending function is not of type T, but CompletedWithCancellation<T>, which is a private class of the coroutine library.

The weird thing is, if I call c.resume("Foobar" as T, {}) inside the suspendCancellableCoroutine, it works just fine. When using the callback routine, the value is a String before passing to to c.resume(), but it gets wrapped in a CompletedWithCancellation object.

Here's the code that reproduces the issue:

@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Timber.plant(Timber.DebugTree())
        setContentView(R.layout.activity_main)

        val vm = ViewModelProviders.of(this)
                .get(MainViewModel::class.java)

        vm.liveData.observe(this, Observer {
            findViewById<TextView>(R.id.mainText).text = "Got result: $it"
        })

        vm.getFoo()
    }
}

@ExperimentalCoroutinesApi
class MainViewModel : ViewModel() {

    private val manager = OperationManager()
    val liveData = MutableLiveData<String>()

    fun getFoo() {
        viewModelScope.launch {
            val op = Operation(manager, "Foobar")
            val rawResult = op.get<Any>()
            Timber.d("Raw result: $rawResult")

            val op2 = Operation(manager, "Foobar")
            val result = op2.get<String>()
            Timber.d("Casted result: $result")
            liveData.postValue(result)
        }
    }
}

class OperationManager {
    private val operationQueue = ConcurrentLinkedQueue<Operation>()
    private val handler = Handler(Looper.getMainLooper())
    private val operationRunnable = Runnable { startOperations() }

    private fun startOperations() {
        val iter = operationQueue.iterator()
        while (iter.hasNext()) {
            val operation = iter.next()
            operationQueue.remove(operation)
            Timber.d("Executing operation $operation")
            operation.onSuccess(operation.response)
        }
    }

    fun run(operation: Operation) {
        addToQueue(operation)
        startDelayed()
    }

    private fun addToQueue(operation: Operation) {
        operationQueue.add(operation)
    }

    private fun startDelayed() {
        handler.removeCallbacks(operationRunnable)
        handler.post(operationRunnable)
    }
}

open class Operation(private val manager: OperationManager, val response: Any) {

    private val listeners = mutableListOf<OperationListener>()

    fun addListener(listener: OperationListener) {
        listeners.add(listener)
    }

    fun execute() = manager.run(this)
    fun onSuccess(data: Any) = listeners.forEach { it.onResult(data) }
}

@ExperimentalCoroutinesApi
suspend fun <T> Operation.get(): T = suspendCancellableCoroutine { c ->

    @Suppress("UNCHECKED_CAST")
    val callback = object : OperationListener {
        override fun onResult(result: Any) {
            Timber.d("get().onResult() -> $result")
            c.resume(result as T, {})
        }
    }

    addListener(callback)
    execute()
}

interface OperationListener {
    fun onResult(result: Any)
}

Do note that just before calling c.resume(), the type of result is String, as it should be. However, it's not String in getFoo() once the suspend function completes. What causes this?

@manabreak
Copy link
Author

Upon investigating this a bit more, I noticed that if the listener is invoked inside a Handler, the result gets wrapped with CompletedWithCancellation.

handler.post { listener.onResult("Foobar") } // CompletedWithCancellation<String>
// VS
listener.onResult("Foobar") // String

Why does using a Handler wrap the object?

@elizarov
Copy link
Contributor

I fail to reproduce it in a standalone non-Android project. Does it reproduce with kotlinx.coroutines version 1.3.5 for you? Does it reproduce if you turn off Proguard/R8 optimizations?

@manabreak
Copy link
Author

manabreak commented Apr 28, 2020

Yes, it happens with 1.3.5 and Proguard/R8 turned off (debug build).

I pinpointed the issue to this line:

c.resume(result as T, {})

If I change this to use the extension method from Continuation class, the issue goes away:

c.resume(result as T)

Why doesn't the former work in this case?

@elizarov
Copy link
Contributor

There's a bug somewhere, but I cannot reproduce it.

@manabreak
Copy link
Author

Here's some more info about my setup, hopefully it helps ya. I run it on Pixel 3a API 29 emulator, with Android targetSdkVersion set to 29. AGP 3.6.3 in use, Kotlin version is 1.3.72, and here are the related Kotlin dependencies:

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'

@elizarov
Copy link
Contributor

Can you publish a mini-project that reproduces it?

@manabreak
Copy link
Author

@qwwdfsad
Copy link
Member

Stacktrace:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.coroutinebugtest, PID: 6125
    java.lang.ClassCastException: kotlinx.coroutines.CompletedWithCancellation cannot be cast to java.lang.String
        at com.example.coroutinebugtest.MainViewModel$getFoo$1.invokeSuspend(MainActivity.kt:41)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:175)
        at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:137)
        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:108)
        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:307)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:317)
        at kotlinx.coroutines.CancellableContinuationImpl.resume(CancellableContinuationImpl.kt:253)
        at com.example.coroutinebugtest.MainActivityKt$get$2$1.invoke(MainActivity.kt:58)
        at com.example.coroutinebugtest.MainActivityKt$get$2$1.invoke(Unknown Source:2)
        at com.example.coroutinebugtest.Operation$execute$1.run(MainActivity.kt:51)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

@qwwdfsad qwwdfsad self-assigned this Apr 28, 2020
@qwwdfsad qwwdfsad added the bug label Apr 28, 2020
@elizarov
Copy link
Contributor

Note: It seems to be a side effect of the immediate UI dispatcher. It might already be fixed in #1937

@brettins
Copy link

I am still seeing this issue in Kotlin 1.3.71, and had to include the implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' to make it work. Is this fix getting merged with a new version of Kotlin itself?

recheej pushed a commit to recheej/kotlinx.coroutines that referenced this issue Dec 28, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants