Skip to content

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

Closed
@manabreak

Description

@manabreak

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?

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions