codingtim
codingtim

Reputation: 375

kotlin coroutine withTimeout does not cancel when using withContext to get non-blocking code

I am using withContext to transform a function into a suspending function which does not block the calling thread. For this I used https://medium.com/@elizarov/blocking-threads-suspending-coroutines-d33e11bf4761 as reference.

Now I would like to invoke this function with a timeout. For this I use withTimeout to call the function as such:

@Test
internal fun timeout() {
    runBlocking {
        logger.info("launching")
        try {
            withTimeout(1000) {
                execute()
            }
        } catch (e: TimeoutCancellationException) {
            logger.info("timed out", e)
        }
    }
}

private suspend fun execute() {
    withContext(Dispatchers.IO) {
        logger.info("sleeping")
        Thread.sleep(2000)
    }
}

So what I would expect is that after 1000 millis the async launched coroutine is cancelled and the TimeoutCancellationException is thrown.
But what happens is that the full 2000 millis pass and when the coroutine is completed the exception is thrown:

14:46:29.231 [main @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - launching
14:46:29.250 [DefaultDispatcher-worker-1 @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - sleeping
14:46:31.261 [main@coroutine#1] INFO b.t.c.c.CoroutineControllerTest - timed out kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:128) at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:94) at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.kt:307) at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116) at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:68) at java.lang.Thread.run(Thread.java:748)

Am I using something wrong?

Or perhaps this is the intended behavior? In the documentation the counter also gets to 2 which means 1500 millis have passed before the coroutine is cancelled: https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/cancellation-and-timeouts.md#timeout

Upvotes: 8

Views: 15431

Answers (2)

Marko Topolnik
Marko Topolnik

Reputation: 200158

You can achieve progress after timeout if you launch a child coroutine and await on its completion:

fun timeout() {
    runBlocking {
        logger.info("launching")
        try {
            withTimeout(100) {
                execute()
            }
        } catch (e: TimeoutCancellationException) {
            logger.info("timed out", e)
        }
    }
}

private suspend fun execute() =
    GlobalScope.launch(Dispatchers.IO) {
        logger.info("sleeping")
        Thread.sleep(2000)
    }.join()

With this you have decoupled the blocked child coroutine from the dispatcher in which you join() it, so the suspend fun join() can immediately react to cancellation.

Note that this is more of a workaround than a full solution because one of the threads in the IO dispatcher will still remain blocked until the sleep() expires.

Upvotes: 5

codingtim
codingtim

Reputation: 375

After rereading the documentation on cancellation it seems coroutines have to cooperate to be cancellable:

Coroutine cancellation is cooperative. A coroutine code has to cooperate to be cancellable.

https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#cancellation-is-cooperative

I also found that threads by design do not get interrupted:

Cancelling a coroutine does not interrupt a thread. This is done so by design, because, unfortunately, many Java libraries incorrectly operate in interrupted threads.

https://discuss.kotlinlang.org/t/calling-blocking-code-in-coroutines/2368/6

This explains why the code waits for the sleep to finish.
This also means that it is not possible to use withTimeout on a coroutine that blocks the thread to add a timeout.
When using a non-blocking library which returns futures the withTimeout can be used as outlined here:

In order to properly integrate with cancellation, CompletableFuture.await() uses the same convention as all future combinators do — it cancels the underlying future if the await call itself is cancelled.

https://medium.com/@elizarov/futures-cancellation-and-coroutines-b5ce9c3ede3a

Side note on the example from the documentation: By adding log statements to the delay/timeout example I found that only 1300 millis pass so delay works perfectly with withTimeout.

08:02:24.736 [main @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - I'm sleeping 0 ...
08:02:25.242 [main @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - I'm sleeping 1 ...
08:02:25.742 [main @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - I'm sleeping 2 ...
08:02:26.041 [main @coroutine#1] INFO b.t.c.c.CoroutineControllerTest - cancelled

Upvotes: 7

Related Questions