0

I'm trying to make a function which triggers a possibly slow operation which can't be cancelled. I want this operation to run in a coroutine with a timeout. Since the operation cannot be cancelled as mentioned before, I need the function to return after the time out, but the operation to stay in the background.

The code I have been trying to get working runs a lengthy operation of 10 seconds asynchronously which has a time out of 5 seconds, so therefore the function should return after the timeout and let main continue its job, printing "foo execution finished", and finally 5 more seconds later the slow job would print "job ends (10 seconds passed)".

Here's the code:

fun main() {
    println("program execution begins")
    foo()
    println("foo execution finished")
    while(true);
}

fun foo() = runBlocking {
    val job = async {
        val endTimeMillis = System.currentTimeMillis() + (10 * 1000)

        while (System.currentTimeMillis() <= endTimeMillis); //blocks for 10 seconds
        println("job ends (10 seconds passed)")
    }

    try {
        withTimeout(5000) {
            println("start awaiting with 5 secs timeout")
            job.await()
        }
    } catch (ex: TimeoutCancellationException) {
        println("out of time")
    }
}

Which then produces the following result:

program execution begins
start awaiting with 5 secs timeout
job ends (10 seconds passed)
out of time
foo execution finished

But this is not exactly the behavior I need in this case as mentioned before. I need to make it so that output looks something like:

program execution begins
start awaiting with 5 secs timeout
out of time
foo execution finished
job ends (10 seconds passed)

In addition to this, I can't use any sort of "kotlin-coroutines" function in the async to archive this behavior (well, cooperate with the cancellation), since the code called in there will be user code unrelated to the coroutine, possibly written in Java. hence the while loop for blocking the async block instead of a delay() in the sample.

Thanks in advance for the help!

serivesmejia
  • 23
  • 2
  • 5
  • @Alex.T it doesn't "return something" directly, it sends data to another part of my program via a separate mechanism I made. But I still need to await for that data to be sent in order to continue, or time out if it takes too long and move on so that my entire program doesn't freeze. – serivesmejia Feb 14 '21 at 01:48
  • sorry, deleted the comment by mistake. For anybody wondering, I was asking if there is an actual return value expected from the `async` block. – AlexT Feb 14 '21 at 02:28

3 Answers3

3

If you can't interrupt the blocking code, then you'll need to run it in a different thread. Otherwise your thread will have no opportunity whatsoever to process the timeout.

Also you need to make sure that the Job that contains the blocking code is not a child of your waiting Job. Otherwise the timeout will cancel the blocking Job, but it will still spin for 10 seconds, and runBlocking will wait for it to finish.

The easiest way to do both of these things is to use GlobalScope, like this:

fun foo()  = runBlocking {
    try {
        withTimeout(5000) {
            println("start awaiting with 5 secs timeout")
            GlobalScope.async {
                val endTimeMillis = System.currentTimeMillis() + (10 * 1000)

                while (System.currentTimeMillis() <= endTimeMillis); //blocks for 10 seconds
                println("job ends (10 seconds passed)")
            }.await()
        }
    } catch (ex: TimeoutCancellationException) {
        println("out of time")
    }
}

Of course, that thread is going to be spinning for 10 seconds even after you stop waiting for it... which is awful, so I hope you really have a good reason to want this.

Matt Timmermans
  • 53,709
  • 3
  • 46
  • 87
  • Thank you! Yeah, I really don't have any other alternative for doing this unfortunately... It's all up to some user code. The spinning while loop is just for the sake of the sample to formulate the question though, we will never have certain what happens there... – serivesmejia Feb 14 '21 at 04:16
  • Users can write infinite loops. If it's user code then you should really run it in its own process. You can reliably and safely kill a process. – Matt Timmermans Feb 14 '21 at 04:18
  • Multi processing isn’t an option in my case either... But yeah, that should be used if possible with user code. – serivesmejia Feb 14 '21 at 05:46
  • @serivesmejia in practice you wanna start a blocking operation in `Dispatchers.IO` as `GlobalScope.launch(Dispatchers.IO) {}` because that can employ a handful of threads from CommonPool whereas `Dispatchers.Default` the one used in `GlobalScope` has limited amount of threads it can borrow from CommonPool. – Animesh Sahu Feb 14 '21 at 13:43
1

Something like this should work:

fun main() {
    println("start")
    foo()
    println("foo finished")
    while (true);
}

fun foo() {
    val start = System.currentTimeMillis()
    GlobalScope.launch {
        val endTimeMillis = System.currentTimeMillis() + (10 * 1000)

        while (System.currentTimeMillis() <= endTimeMillis); //blocks for 10 seconds
        println("${start.secondsSince()} job ends (10 seconds passed)")
    }
    println("${start.secondsSince()}  waiting")
    runBlocking {
        delay(5000)
    }
    println("${start.secondsSince()}  finished waiting")
}

fun Long.secondsSince() = (System.currentTimeMillis() - this) / 1000.00

This will output:

start
0.04  waiting
5.049  finished waiting
foo finished
10.043 job ends (10 seconds passed)

Ignore the horrible way of counting seconds please, it is 2am, but it does prove the point.

Now to why this works. Firstly I'd recommend reading this SO question on the difference between async and launch. TLDR launch is used for fire and forget, since you are not interested in any result (no return value needed), there is no need to use async. Secondly the docs on delay.

The reason that yours did not work is (from what I can tell)

  1. You were using withTimeout on the job.await, which is not really the same as running it on the coroutine itself.
  2. The foo fun was blocking, that means that it will always wait for all the coroutines that are launched from inside it, before it continues. This means that you would have never gotten the foo execution finished before job ends (10 seconds passed).

Btw, the actual final code would just be:

fun foo() {
    GlobalScope.launch {
       //code that takes a long time
    }
    
    runBlocking { delay(5000) }
    
    //other code that will continue after 5 seconds
}

AlexT
  • 2,524
  • 1
  • 11
  • 23
  • Yo, thanks for the explanation! That's a good solution for this specific case, although this wont work precisely well in what I need.. Your solution always waits "timeout" (5) secs, but I need the function to return when the "code that takes a long time" finishes its job, or it timeouts (and the "code that takes a long time" continues running in the background in that case of a timeout). Therefore always blocking for 5 seconds wouldn't be optimal for my use case. might have been my bad for not being specific about that part. I really appreciate the answer tho! – serivesmejia Feb 14 '21 at 02:55
0

While I have already marked an accepted answer, it is worth noting that my solution ended up being a bit different. [for future generations reading this :), hello future!]

From what @Alex.T pointed out:

The foo fun was blocking, that means that it will always wait for all the coroutines that are launched from inside it, before it continues. This means that you would have never gotten the foo execution finished before job ends (10 seconds passed).

With this in mind, I came out with the following solution:

fun main() {
    println("program execution begins")
    foo()
    println("foo execution finished")
    while(true);
}

fun foo() {
    val job = GlobalScope.launch {
        val endTimeMillis = System.currentTimeMillis() + 10000

        while (System.currentTimeMillis() <= endTimeMillis);
        println("job ends (10 seconds passed)")
    }

    runBlocking {
        try {
            withTimeout(5000) {
                println("start awaiting with 5 secs timeout")
                job.join()
            }
        } catch (ex: TimeoutCancellationException) {
            println("out of time")
            job.cancel()
        }
    }
}

Which doesn't wrap both coroutines with runBlocking, only the withTimeout. So our actual blocking operation runs separately in global scope, and therefore, it produces the following output

program execution begins
start awaiting with 5 secs timeout
out of time
foo execution finished
job ends (10 seconds passed)

(The actual output I wanted in the question, wow!)

It's also worth pointing out here what @Matt Timmermans said:

Of course, that thread is going to be spinning for 10 seconds even after you stop waiting for it... which is awful, so I hope you really have a good reason to want this.

So use this with caution.

serivesmejia
  • 23
  • 2
  • 5