Home > Mobile >  How to cancel kotlin coroutine with potentially "un-cancellable" method call inside it?
How to cancel kotlin coroutine with potentially "un-cancellable" method call inside it?

Time:02-03

I have this piece of code:

// this method is used to evaluate the input string, and it returns evaluation result in string format
fun process(input: String): String {
    val timeoutMillis = 5000L
    val page = browser.newPage()
    try {
        val result = runBlocking {
            withTimeout(timeoutMillis) {
                val result = page.evaluate(input).toString()
                return@withTimeout result
            }
        }
        return result
    } catch (playwrightException: PlaywrightException) {
        return "Could not parse template! '${playwrightException.localizedMessage}'"
    } catch (timeoutException: TimeoutCancellationException) {
        return "Could not parse template! (timeout)"
    } finally {
        page.close()
    }
}

It should throw exception after 5 seconds if the method is taking too long to execute (example: input potentially contains infinite loop) but it doesent (becomes deadlock I assume) coz coroutines should be cooperative. But the method I am calling is from another library and I have no control over its computation (for sticking yield() or smth like it).

So the question is: is it even possible to timeout such coroutine? if yes, then how? Should I use java thread insted and just kill it after some time?

CodePudding user response:

But the method I am calling is from another library and I have no control over its computation (for sticking yield() or smth like it).

If that is the case, I see mainly 2 situations here:

  1. the library is aware that this is a long-running operation and supports thread interrupts to cancel it. This is the case for Thread.sleep and some I/O operations.
  2. the library function really does block the calling thread for the whole time of the operation, and wasn't designed to handle thread interrupts

Situation 1: the library function is interruptible

If you are lucky enough to be in situation 1, then simply wrap the library's call into a runInterruptible block, and the coroutines library will translate cancellation into thread interruptions:

fun main() {
    runBlocking {
        val elapsed = measureTimeMillis {
            withTimeoutOrNull(100.milliseconds) {
                runInterruptible {
                    interruptibleBlockingCall()
                }
            }
        }
        println("Done in ${elapsed}ms")
    }
}

private fun interruptibleBlockingCall() {
    Thread.sleep(3000)
}

Situation 2: the library function is NOT interruptible

In the more likely situation 2, you're kind of out of luck.

Should I use java thread insted and just kill it after some time?

There is no such thing as "killing a thread" in Java. See Why is Thread.stop deprecated?, or How do you kill a Thread in Java?. In short, in that case you do not have a choice but to block some thread.

I do not know a solution to this problem that doesn't leak resources. Using an ExecutorService would not help if the task doesn't support thread interrupts - the threads will not die even with shutdownNow() (which uses interrupts).

Of course, the blocked thread doesn't have to be your thread. You can technically launch a separate coroutine on another thread (using another dispatcher if yours is single-threaded), to wrap the libary function call, and then join() the job inside a withTimeout to avoid waiting for it forever. That is however probably bad, because you're basically deferring the problem to whichever scope you use to launch the uncancellable task (this is actually why we can't use a simple withContext here).

If you use GlobalScope or another long-running scope, you effectively leak the hanging coroutine (without knowing for how long).

If you use a more local parent scope, you defer the problem to that scope. This is for instance the case if you use the scope of an enclosing runBlocking (like in your example), which makes this solution pointless:

fun main() {
    val elapsed = measureTimeMillis {
        doStuff()
    }
    println("Completely done in ${elapsed}ms")
}

private fun doStuff() {
    runBlocking {
        val nonCancellableJob = launch(Dispatchers.IO) {
            uncancellableBlockingCall()
        }
        val elapsed = measureTimeMillis {
            withTimeoutOrNull(100.milliseconds) {
                nonCancellableJob.join()
            }
        }
        println("Done waiting in ${elapsed}ms")
        
    } // /!\ runBlocking will still wait here for the uncancellable child coroutine
}

// Thread.sleep is in fact interruptible but let's assume it's not for the sake of the example
private fun uncancellableBlockingCall() {
    Thread.sleep(3000) 
}

Outputs something like:

Done waiting in 122ms
Completely done in 3055ms

So the bottom line is either live with this long thing potentially hanging, or ask the developers of that library to handle interruption or make the task cancellable.

  • Related