https://kotlinlang.org logo
#coroutines
Title
# coroutines
j

Joe Altidore

08/20/2022, 10:23 PM
I am attempting to perform parallel API calls with kotlin. However, the second api call is cancelled when the first call's await() method is called. How do i force all methods to finish executing before returning from the method. The code snippet
Copy code
suspend fun isFinished(token: String): Boolean {
    return coroutineScope {
        val a = async {
            updateProfile(token = token)
        }
        val b = async{
            updateStatus(token = token)
        }
        (a.await()) && (b.await())
    }
}
Methods updateProfile(token: String) and updateStatus(token: String) are the api methods
j

Joffrey

08/20/2022, 10:30 PM
The code looks correct. If the first
await
cancels the other
async
, it probably means the first call failed with an exception. You should check that.
j

Joe Altidore

08/21/2022, 5:38 AM
There is no exception. If i swap the position of the awaits, then b.await() fails
It also works fine when i use runBlocking
j

Joffrey

08/21/2022, 7:31 AM
How do you tell
b.await()
fails when you swap? Is that not an exception that is thrown?
If it works fine when using
runBlocking
, it could be that both calls are using something that is not thread safe. Did you check that? It would be nice if you could share enough code to reproduce the problem
u

uli

08/21/2022, 11:06 AM
Another point to look at is boolean short circuit. If a returns false, b is never awaited. Currently not sure if your coroutineScope block is enough to compensate that
j

Joe Altidore

08/21/2022, 1:20 PM
@Joffrey I get an JobCancellationException on the endpoint. That's how I know it fails
However, It finally worked when I did used the approach below.
Copy code
suspend fun isFinished(token: String): Boolean {
    val a = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).async{
        updateProfile(token = token)
    }

    val b = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).async{
        updateStatus(token = token)
    }

    val result = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).async {
        a.await() && b.await()
    }

    return result.await()
}
But I feel there could be a better way
u

uli

08/21/2022, 1:27 PM
I guess it's the boolean short circuit. I guess your coroutineScope block handgs, waiting for b to be awaited until it is cancelled by some outer mechanism.
Try extracting a.await and b.await to variables and then apply && to the variables and everything should work.
j

Joffrey

08/21/2022, 8:47 PM
@Joe Altidore please don't do this. You're creating scopes that you never cancel and just spawn coroutines without structured concurrency, which is usually a bad sign. Also you're creating 3 scopes - why? Did you figure out what was wrong in your initial code? Don't try random stuff without understanding what's wrong in the correct code, otherwise you might just be hiding a deeper problem
l

louiscad

08/23/2022, 6:21 PM
Also, creating scopes on the fly can lead to coroutines being garbage collected mid-way, which is extremely hard to diagnose. If one of the coroutines gets cancelled, it's because they get cancelled, either from within, or from outside, though it seems it's from within in you case @Joe Altidore. Try replacing your
updateXxx
functions with
delay(…)
calls instead, you'll see it works perfectly with your original snippet.
j

Joe Altidore

08/23/2022, 7:17 PM
@Joffrey I was very skeptical of the approach too. The
updateProfile()
and
updateStatus()
methods are recursive methods and I am suspecting that to be the reason.
@louiscad I will love to try that but have limited knowledge on the approach you've suggested. More help will be appreciated
l

louiscad

08/23/2022, 7:23 PM
@Joe Altidore You use this instead:
Copy code
suspend fun isFinished(token: String): Boolean {
    return coroutineScope {
        val a = async {
            delay(3.seconds)
            true
        }
        val b = async{
            delay(2.seconds)
            true
        }
        a.await() && b.await()
    }
}
You'll see that it finishes successfully.
BTW, at the line where you have the
await()
calls, the parenthses around the calls are useless. I edited my snippet to reflect that.
Recursive functions are rareley a good idea, unless you're doing what is sometimes called "sports programming", which is different from writing production code that needs to be reliable and efficient.
j

Joe Altidore

08/23/2022, 7:28 PM
I guess I should state the use case after all. I have an REST API service that returns a list of items. I need to retrieve the items and store them in a local db. However, the service implements pagination hence the use of recursion.
l

louiscad

08/23/2022, 7:30 PM
Why would recursion be needed for pagination?
j

Joe Altidore

08/23/2022, 7:31 PM
I need to ensure that all items are queried successfully before returning a result
j

Joffrey

08/23/2022, 7:32 PM
Still, pagination really is inherently iterative, it feels strange to use recursion for that
j

Joe Altidore

08/23/2022, 7:36 PM
let me share one of the methods
Copy code
private suspend fun getExpense(offset: Int, token: String): Response{
    val expense = api.syncExpense(offset, token)
    return if(expense is Resource.Success){
        val res = expense.data as ExpenseDto.Data
        if(res.rows.isNotEmpty()){
            expenseDao.addExpenses(res.rows.map { it.toExpenseEntity() })
            if(res.rows.size == LIMIT){
                return getExpense(offset + LIMIT, token)
            }
        }
        res
    }else{
        (expense as Resource.Failure).data!!
    }
}
u

uli

08/23/2022, 7:41 PM
@louiscad what happens if your mocked workloads return false instead?
l

louiscad

08/23/2022, 8:01 PM
@uli It waits until both are done anyway, just like in the original version.
@Joe Altidore Your code can lead to a very, very awful UX. Today, I was in the train, just like yesterday where I spent around 9 hours there. In such a situation, network keeps dropping packets from time to time because of the speed, which means a long chain like yours is almost guaranteed to break because of a single failure. Instead, just treat your data as paged, saving one thing after another, or doing multiple calls concurrently for multiple pages, so if the network is stable for a few seconds, it's all done.
An alternative is to change the backend so you don't have to do many network calls, and can do a single big one (but not too big of course)
j

Joe Altidore

08/23/2022, 8:20 PM
I wrote the backend service with ktor and it has the potential of becoming large since its a data synchronisation service
l

louiscad

08/23/2022, 8:49 PM
What do you want to achieve on the client side? I don't quite get the use case with such a vague function signature (
suspend fun isFinished(token: String): Boolean
)
j

Joe Altidore

08/23/2022, 8:53 PM
Well, upon login, I try to sync the user data which is obtained via different service (in this case, get Expense and get Funds services which both implements pagination
296 Views