Kotlin Coroutines lockup/freezing

I experience strange lockups (freezes) when running coroutines. Sometimes it works perfectly fine, sometimes it will just wait and continue running after a long time (or not at all). I don’t know If my setup is incorrect, there is some race condition or maybe I misunderstand how coroutines are supposed to be used. The app itself doesn’t freeze or crash, it’s just the coroutines stop/pause execution.

For example, in this bit the scope.launch would sometimes just not continue executing into billingClientDeferred.await()

    override suspend fun getPurchases(): Result<List<Purchase>> = suspendCoroutine { continuation ->
    scope.launch {
        billingClientDeferred.await().success { client ->
            client.queryPurchasesAsync(BillingClient.SkuType.SUBS) Subs@{ billingResultSubs, purchasesSubs ->
                if (billingResultSubs.responseCode != BillingClient.BillingResponseCode.OK) {
                    continuation.resume(Result.Failure.Unknown())
                    return@Subs
                }
                continuation.resume(Result.Success(purchasesSubs))
            }
        }
    }
}

scope is declared as:

private val scope = CoroutineScope(Job() + Dispatchers.Default)

Then it is called from WalletManager

override suspend fun verifyProducts(): Result<Unit> = billingProvider.getPurchases()
        .successSuspend { purchases ->
            purchases.forEach {
                activatePurchase(it)
            }
        }.map { }

Which in the end is called from a ViewModel like this

override fun verify() {
    viewModelScope.launch {
        userSupervisor.walletManager.mapResult {
            it.verifyProducts()
        }
    }
}

I’m guessing there is some issue with the combination of what dispatchers and scopes I’m using. I was trying different things with Dispatchers.Default, Dispatchers.Main, conforming class to CoroutineScope, using global scopes, but I always run into some threading/lockup issues I don’t fully understand.

Answer

First issue is that you don’t want to launch a new coroutine from within the suspendCoroutine, you need to restructure your code so that you don’t have to do this. this could be done as

private fun someMethod() = viewModelScope.launch {
    val client = billingClientDeferred.await()
    val finalResult = getPurchases(client)
}

after this simply update getPurchases to except the client as a parameter and remove all the coroutine code from it, just call the client and return the result using continuation.

Fundamental issue with your code is that you are not following principle of structured concurrency.

its essentially a concurrency management pattern that implicitly takes care of the scope and lifetime of a coroutine. the idea is that when you launch coroutines using differnt scopes, those scopes must have a parent child relationship, this allows the coroutine management system to take care of all the cases such as

  1. What happens when an inner coroutine throws exception
  2. What happens when an outer coroutine is canceled or failed

I am afraid your code doesn’t really follow this pattern and has following issues

scope doesn’t have any relation with the CoroutineScope that launches getPurchases

This is a big problem, what happens if outer scope gets canceled, will it cancel the inner coroutine or the inner coroutine will keep consuming resources?

You are launching a coroutine from a suspending function

As Roman Elizarov explains here

Suspending functions, on the other hand, are designed to be non-blocking and should not have side-effects of launching any concurrent work. Suspending functions can and should wait for all their work to complete before returning to the caller.