how to cancel the coroutines jobs after timeout

762 Views Asked by At

Having a suspend function fetchData(). What it does is to launch a few jobs in the withContext, so that it will only return after the jobs are complete (which are: suspend fun getData(): Boolean).

And also want if it times out then return false from the function.

The problem is when it times out, with withTimeoutOrNull(500) { jobs.joinAll() }, it stuck in the function not exit.

The log shows it times out, also clearly points to last line of the code before exit the function:

     E/+++: +++ in fetchData() after null = withTimeoutOrNull(500), jobs.sizs: 3
     E/+++: +++ --- exit fetchData(), allFresh: false

But the caller of the fetchData() gets stuck and not return from the fetchData().

This is the caller:

suspend fun caller() {
    var allGood = fetchData()

    // never return to here

    Log.e("+++", "+++ caller(), after allGood: $allGood = fetchData()")
    ...
}

Below is the code, how to cancel the jobs if timeout?

suspend fun fetchData(): Boolean = withContext(Dispatchers.IO) {

    var allFresh = requestHandlertMap.size > 0
    if (!allFresh) {
        allFresh
    } else {
        val handlers = requestHandlertMap.values.toList()
        val jobs: List<Deferred<Boolean>> = handlers.map {handler->
            async(start = CoroutineStart.LAZY) {
                if (isActive) handler.getData() else true
                        .also {
                            Log.e("+++", "+++ in fetchData():async{} after handler.getData()")
                        }
            }
        }
        val result = withTimeoutOrNull(500) { jobs.joinAll() }

        Log.e("+++", "+++ in fetchData() after $result = withTimeoutOrNull(500), jobs.size: ${jobs.size} ")

        if (result != null) {
            allFresh = jobs.all { deferred ->
                deferred.await()
            }
            Log.e("+++", "+++ +++ +++ in fetchData() call  onDataReady(), allFresh: $allFresh = deferred.await() ")
            onDataReady()
        } else {

            // how to cancel the jobs ???

            //jobs.all { deferred ->
                //deferred.cancelChildren()
            //}

            allFresh = false
        }
        allFresh
                .also {
                    Log.e("+++", "+++ --- exit fetchData(), allFresh: $allFresh  ")
                }
    }
}
1

There are 1 best solutions below

0
On

After some reading/trying, it seems having a few issues with the implementation.

  1. Somehow the CoroutineStart.LAZY causes a strange behavior, that the async(start = CoroutineStart.LAZY) start sequentially (expecting they should start to be concurrently), so that when it times out it stuck in the function (guess because it is wrapped in the withContext(Dispatchers.IO) and not all child coroutines are completed -- if there is someone not start yet).

Remove the start = CoroutineStart.LAZY makes it returning from the fun fetchData()

val jobs: List<Deferred<Boolean>> = handlers.map {handler->
            async(start = CoroutineStart.LAZY) {
                if (isActive) handler.getData() else true
                        .also {
                            Log.e("+++", "+++ in fetchData():async{} after handler.getData()")
                        }
            }
        }
  1. the suspend fun getData(): Boolean was not implemented cooperate to be cancellable, which may causes it still stay in the function until all children are completed although timeout has already happened.

  2. seems it still need to call the deferred.cancelChildren(), otherwise they are not cancelled by the withTimeoutNotNull(), not sure why, isnt it supposed to cancel the jobs automatically?

the change made

private suspend fun fetchData(): Boolean {
        var allFresh: Boolean? = requestHandlertMap.size > 0
        if (allFresh == true) {

            val handlers = requestHandlertMap.values.toList()
            val jobs: List<Deferred<Boolean>> = handlers.map {
                serviceScope.async(start = CoroutineStart.DEFAULT) { handler -> if (isActive) handler.getData() else false }
            }
            allFresh = withTimeoutOrNull(3000) {
                try {
                    jobs.awaitAll().all { it }
                } catch (ex: Throwable) {
                    false
                }
            }

            if (allFresh != null) {
                onDataReady()
            } else {
                jobs.map { deferred -> deferred.cancelChildren() }
            }
        }
        return allFresh == true // allFresh = {null, true, false}
    }

ref: here and here