Coroutines must be cancellation cooperative - kotlin

Looking at the official docs, it seems like a coroutine can only be cancelled if it cooperates with cancellation meaning it checks if it's active before executing its code. LINK
If that's the case, how does scope.cancel() cancels all coroutines cancel/stop running inside that scope?
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // computation loop, just wastes CPU
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

It doesn't actually cancel the job. In the job.cancelAndJoin() the code waits until while (i < 5) is done and then the job exists.
If read the linked article to the end, you'll find the example of a cooperative job, which uses while (isActive). isActive returns false when the job gets cancelled. When this happens, the while-loop will just quit normally.

Related

Properly cancelling kotlin coroutine job

I'm scratching my head around properly cancelling coroutine job.
Test case is simple I have a class with two methods:
class CancellationTest {
private var job: Job? = null
private var scope = MainScope()
fun run() {
job?.cancel()
job = scope.launch { doWork() }
}
fun doWork() {
// gets data from some source and send it to BE
}
}
Method doWork has an api call that is suspending and respects cancellation.
In the above example after counting objects that were successfully sent to backend I can see many duplicates, meaning that cancel did not really cancel previous invocation.
However if I use snippet found on the internet
internal class WorkingCancellation<T> {
private val activeTask = AtomicReference<Deferred<T>?>(null)
suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
activeTask.get()?.cancelAndJoin()
return coroutineScope {
val newTask = async(start = CoroutineStart.LAZY) {
block()
}
newTask.invokeOnCompletion {
activeTask.compareAndSet(newTask, null)
}
val result: T
while (true) {
if (!activeTask.compareAndSet(null, newTask)) {
activeTask.get()?.cancelAndJoin()
yield()
} else {
result = newTask.await()
break
}
}
result
}
}
}
It works properly, objects are not duplicated and sent properly to BE.
One last thing is that I'm calling run method in a for loop - but anyways I'm not quire sure I understand why job?.cancel does not do its job properly and WorkingCancellation is actually working
Short answer: cancellation only works out-of-the box if you call suspending library functions. Non-suspending code needs manual checks to make it cancellable.
Cancellation in Kotlin coroutines is cooperative, and requires the job being cancelled to check for cancellation and terminate whatever work it's doing. If the job doesn't check for cancellation, it can quite happily carry on running forever and never find out it has been cancelled.
Coroutines automatically check for cancellation when you call built-in suspending functions. If you look at the docs for commonly-used suspending functions like await() and yield(), you'll see that they always say "This suspending function is cancellable".
Your doWork isn't a suspend function, so it can't call any other suspending functions and consequently will never hit one of those automatic checks for cancellation. If you do want to cancel it, you will need to have it periodically check whether the job is still active, or change its implementation to use suspending functions. You can manually check for cancellation by calling ensureActive on the Job.
In addition to Sam's answer, consider this example that mocks a continuous transaction, lets say location updates to a server.
var pingInterval = System.currentTimeMillis()
job = launch {
while (true) {
if (System.currentTimeMillis() > pingInterval) {
Log.e("LocationJob", "Executing location updates... ")
pingInterval += 1000L
}
}
}
Continuously it will "ping" the server with location udpates, or like any other common use-cases, say this will continuously fetch something from it.
Then I have a function here that's being called by a button that cancels this job operation.
fun cancel() {
job.cancel()
Log.e("LocationJob", "Location updates done.")
}
When this function is called, the job is cancelled, however the operation keeps on going because nothing ensures the coroutine scope to stop working, all actions above will print
Ping server my location...
Ping server my location...
Ping server my location...
Ping server my location...
Location updates done.
Ping server my location...
Ping server my location...
Now if we insert ensureActive() inside the infinite loop
while (true) {
ensureActive()
if (System.currentTimeMillis() > pingInterval) {
Log.e("LocationJob", "Ping server my location... ")
pingInterval += 1000L
}
}
Cancelling the job will guarantee that the operation will stop. I tested using delay though and it guaranteed total cancellation when the job it is being called in is cancelled. Emplacing ensureActive(), and cancelling after 2 seconds, prints
Ping server my location...
Ping server my location...
Location updates done.

Kotlin coroutines - async withTimeout which stops blocking the thread once it runs out of time

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!
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.
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)
You were using withTimeout on the job.await, which is not really the same as running it on the coroutine itself.
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
}
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.

How delay function is working in Kotlin without blocking the current thread?

Past few days I am learning coroutines, most of thee concepts are clear but I don't understand the implementation of the delay function.
How delay function is resuming the coroutine after the delayed time? For a simple program, there is only one main thread, and to resume the coroutine after the delayed time I assume there should be another timer thread that handles all the delayed invocations and invokes them later. Is it true? Can someone explain the implementation detail of the delay function?
TL; DR;
When using runBlocking, delay is internally wrapped and runs on same thread and when using any other dispatcher it suspends and is resumed by resuming the continuation by event-loop thread. Check the long answer below to understand the internals.
Long answer:
#Francesc answer is pointing correctly but is somewhat abstract, and still does not explains how actually delay works internally.
So, as he pointed to the delay function:
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc# { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
What it does is "Obtains the current continuation instance inside suspend functions and suspends the currently running coroutine after running the block inside the lambda"
So this line cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) is going to be executed and then the current coroutine gets suspended i.e. frees the current thread it was stick on.
cont.context.delay points to
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
that says if ContinuationInterceptor is implementation of Delay then return that otherwise use DefaultDelay which is internal actual val DefaultDelay: Delay = DefaultExecutor a DefaultExecutor which is internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {...} an implementation of EventLoop and has a thread of its own to run on.
Note: ContinuationInterceptor is an implementation of Delay when coroutine is in the runBlocking block in order to make sure the delay run on same thread otherwise it is not. Check this snippet to see the results.
Now I couldn't find implemenation of Delay created by runBlocking since internal expect fun createEventLoop(): EventLoop is an expect function which is implemented from outside, not by the source. But the DefaultDelay is implemented as follows
public override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val timeNanos = delayToNanos(timeMillis)
if (timeNanos < MAX_DELAY_NS) {
val now = nanoTime()
DelayedResumeTask(now + timeNanos, continuation).also { task ->
continuation.disposeOnCancellation(task)
schedule(now, task)
}
}
}
This is how scheduleResumeAfterDelay is implemented it creates a DelayedResumeTask with the continuation passed by delay, and then calls schedule(now, task) which calls scheduleImpl(now, delayedTask) which finally calls delayedTask.scheduleTask(now, delayedQueue, this) passing the delayedQueue in the object
#Synchronized
fun scheduleTask(now: Long, delayed: DelayedTaskQueue, eventLoop: EventLoopImplBase): Int {
if (_heap === kotlinx.coroutines.DISPOSED_TASK) return SCHEDULE_DISPOSED // don't add -- was already disposed
delayed.addLastIf(this) { firstTask ->
if (eventLoop.isCompleted) return SCHEDULE_COMPLETED // non-local return from scheduleTask
/**
* We are about to add new task and we have to make sure that [DelayedTaskQueue]
* invariant is maintained. The code in this lambda is additionally executed under
* the lock of [DelayedTaskQueue] and working with [DelayedTaskQueue.timeNow] here is thread-safe.
*/
if (firstTask == null) {
/**
* When adding the first delayed task we simply update queue's [DelayedTaskQueue.timeNow] to
* the current now time even if that means "going backwards in time". This makes the structure
* self-correcting in spite of wild jumps in `nanoTime()` measurements once all delayed tasks
* are removed from the delayed queue for execution.
*/
delayed.timeNow = now
} else {
/**
* Carefully update [DelayedTaskQueue.timeNow] so that it does not sweep past first's tasks time
* and only goes forward in time. We cannot let it go backwards in time or invariant can be
* violated for tasks that were already scheduled.
*/
val firstTime = firstTask.nanoTime
// compute min(now, firstTime) using a wrap-safe check
val minTime = if (firstTime - now >= 0) now else firstTime
// update timeNow only when going forward in time
if (minTime - delayed.timeNow > 0) delayed.timeNow = minTime
}
/**
* Here [DelayedTaskQueue.timeNow] was already modified and we have to double-check that newly added
* task does not violate [DelayedTaskQueue] invariant because of that. Note also that this scheduleTask
* function can be called to reschedule from one queue to another and this might be another reason
* where new task's time might now violate invariant.
* We correct invariant violation (if any) by simply changing this task's time to now.
*/
if (nanoTime - delayed.timeNow < 0) nanoTime = delayed.timeNow
true
}
return SCHEDULE_OK
}
It finally sets the task into the DelayedTaskQueue with the current time.
// Inside DefaultExecutor
override fun run() {
ThreadLocalEventLoop.setEventLoop(this)
registerTimeLoopThread()
try {
var shutdownNanos = Long.MAX_VALUE
if (!DefaultExecutor.notifyStartup()) return
while (true) {
Thread.interrupted() // just reset interruption flag
var parkNanos = DefaultExecutor.processNextEvent() /* Notice here, it calls the processNextEvent */
if (parkNanos == Long.MAX_VALUE) {
// nothing to do, initialize shutdown timeout
if (shutdownNanos == Long.MAX_VALUE) {
val now = nanoTime()
if (shutdownNanos == Long.MAX_VALUE) shutdownNanos = now + DefaultExecutor.KEEP_ALIVE_NANOS
val tillShutdown = shutdownNanos - now
if (tillShutdown <= 0) return // shut thread down
parkNanos = parkNanos.coerceAtMost(tillShutdown)
} else
parkNanos = parkNanos.coerceAtMost(DefaultExecutor.KEEP_ALIVE_NANOS) // limit wait time anyway
}
if (parkNanos > 0) {
// check if shutdown was requested and bail out in this case
if (DefaultExecutor.isShutdownRequested) return
parkNanos(this, parkNanos)
}
}
} finally {
DefaultExecutor._thread = null // this thread is dead
DefaultExecutor.acknowledgeShutdownIfNeeded()
unregisterTimeLoopThread()
// recheck if queues are empty after _thread reference was set to null (!!!)
if (!DefaultExecutor.isEmpty) DefaultExecutor.thread // recreate thread if it is needed
}
}
// Called by run inside the run of DefaultExecutor
override fun processNextEvent(): Long {
// unconfined events take priority
if (processUnconfinedEvent()) return nextTime
// queue all delayed tasks that are due to be executed
val delayed = _delayed.value
if (delayed != null && !delayed.isEmpty) {
val now = nanoTime()
while (true) {
// make sure that moving from delayed to queue removes from delayed only after it is added to queue
// to make sure that 'isEmpty' and `nextTime` that check both of them
// do not transiently report that both delayed and queue are empty during move
delayed.removeFirstIf {
if (it.timeToExecute(now)) {
enqueueImpl(it)
} else
false
} ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
}
}
// then process one event from queue
dequeue()?.run()
return nextTime
}
And then the event loop (run function) of internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {...} finally handles the tasks by dequeuing the tasks and resuming the actual Continuation which was suspended the function by calling delay if the delay time has reached.
All suspending functions work the same way, when compiled it gets converted into a state machine with callbacks.
When you call delay what happens is that a message is posted on a queue with a certain delay, similar to Handler().postDelayed(delay) and, when the delay has lapsed, it calls back to the suspension point and resumes execution.
You can check the source code for the delay function to see how it works:
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc# { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
So if the delay is positive, it schedules the callback in the delay time.

why delay makes co routine cancelable

fun main() = runBlocking {
var i = 1
var job = launch (Dispatchers.Default){
println("Thread name = ${Thread.currentThread().name}")
while (i < 10) { // replace i < 10 to isActive to make coroutine cancellable
delay(500L)
// runBlocking { delay(500L) }
println("$isActive ${i++}")
}
}
println("Thread name = ${Thread.currentThread().name}")
delay(2000L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
output
Thread name = main
Thread name = DefaultDispatcher-worker-1
true 1
true 2
true 3
main: I'm tired of waiting!
main: Now I can quit.
if i use runBlocking { delay(500L) } then the above co-routine is not cancelable. So, it will print all values upto 9.
but when i use delay(500L) automatically the co-routine can be cancelled. Why ?
delay doesn't actually do anything on its own, it just schedules the coroutine to be resumed at a later point in time. Continuations can, of course, be cancelled at any time.
runBlocking, on the other hand, actually blocks a thread (which is why the compiler will complain about a blocking operation within a coroutine, and why you should never use runBlocking outside of e.g. unit tests).
Note: Since main can now be a suspending function, there's generally no need to use it whatsoever in your core application code.
This function should not be used from a coroutine
runBlocking
Coroutines, like threads, are not truly interruptible; they have to rely on cooperative cancellation (see also why stopping threads is bad). This means that cancellation doesn't actually do anything either; when you cancel a context, it simply notifies all of its child contexts to cancel themselves, but any code that is still running will continue to run until a cancellation check is reached.
It is important to realize that runBlocking is not a suspend function. It cannot be paused, resumed, or cancelled. The parent context is not passed to it by default (it receives an EmptyCoroutineContext), so the coroutine used for the execution of runBlocking won't react to anything that happens upstream.
When you write
while (i < 10) {
runBlocking { delay(500L) }
println("$isActive ${i++}")
}
there are no operations here that are cancellable. Therefore, the code never checks whether its context has been cancelled, so it will continue until it finishes.
delay, however, is cancellable; as soon as its parent context is cancelled, it resumes immediately and throws an exception (i.e., it stops.)
Take a look at the generated code:
#Nullable
public final Object invokeSuspend(#NotNull Object $result) {
switch (this.label) {
case 0:
while (i.element < 10) {
BuildersKt.runBlocking$default( ... );
...
System.out.println(var3);
}
return Unit.INSTANCE;
default:
throw new IllegalStateException( ... );
}
}
Contrast this
while (i.element < 10) {
BuildersKt.runBlocking$default( ... );
...
System.out.println(var3);
}
with
do {
...
System.out.println(var3);
if (i.element >= 10) {
return Unit.INSTANCE;
}
...
} while (DelayKt.delay(500L, this) != var5);
Variable declarations and arguments omitted (...) for brevity.
runBlocking will terminate early if the current thread is interrupted, but again this is the exact same cooperative mechanism, except that it operates at the level of the thread rather than on a coroutine.
The official documentation states:
All the suspending functions in kotlinx.coroutines are cancellable.
and delay is one of them.
You can check that here.
I think the real question should be: Why a nested runBlocking is not cancellable? at least an attempt to create a new coroutine with runBlocking when isActive is false should fail, altough making a coroutine cooperative is your responsability. Besides runBlocking shouldn't be used in the first place.
Turns out if you pass this.coroutineContext as CoroutineContext to runBlocking, it gets cancelled:
fun main() = runBlocking {
var i = 1
var job = launch (Dispatchers.Default){
println("Thread name = ${Thread.currentThread().name}")
while (i < 10) { // replace i < 10 to isActive to make coroutine cancellable
runBlocking(this.coroutineContext) { delay(500L) }
println("$isActive ${i++}")
}
}
println("Thread name = ${Thread.currentThread().name}")
delay(2000L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
}
I modified your code a little
try {
while (i < 10) { // replace i < 10 to isActive to make coroutine cancellable
delay(500L)
println("$isActive ${i++}")
}
} catch (e : Exception){
println("Exception $e")
if (e is CancellationException) throw e
}
the output
Thread name = main
Thread name = DefaultDispatcher-worker-1
true 1
true 2
true 3
main: I'm tired of waiting!
Exception kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}#40bcb892
main: Now I can quit.
can you see the exception StandaloneCoroutine was cancelled its because,
If the Job of the current coroutine is cancelled or completed while this suspending function is waiting i.e delay(500L), this function immediately resumes with CancellationException.
So, the point is if you add a suspending function inside your launch it can be cancellable.
you can try that with user defined suspend fun also

Launch a number of coroutines and join them all with timeout (without cancelling)

I need to launch a number of jobs which will return a result.
In the main code (which is not a coroutine), after launching the jobs I need to wait for them all to complete their task OR for a given timeout to expire, whichever comes first.
If I exit from the wait because all the jobs completed before the timeout, that's great, I will collect their results.
But if some of the jobs are taking longer that the timeout, my main function needs to wake as soon as the timeout expires, inspect which jobs did finish in time (if any) and which ones are still running, and work from there, without cancelling the jobs that are still running.
How would you code this kind of wait?
The solution follows directly from the question. First, we'll design a suspending function for the task. Let's see our requirements:
if some of the jobs are taking longer that the timeout... without cancelling the jobs that are still running.
It means that the jobs we launch have to be standalone (not children), so we'll opt-out of structured concurrency and use GlobalScope to launch them, manually collecting all the jobs. We use async coroutine builder because we plan to collect their results of some type R later:
val jobs: List<Deferred<R>> = List(numberOfJobs) {
GlobalScope.async { /* our code that produces R */ }
}
after launching the jobs I need to wait for them all to complete their task OR for a given timeout to expire, whichever comes first.
Let's wait for all of them and do this waiting with timeout:
withTimeoutOrNull(timeoutMillis) { jobs.joinAll() }
We use joinAll (as opposed to awaitAll) to avoid exception if one of the jobs fail and withTimeoutOrNull to avoid exception on timeout.
my main function needs to wake as soon as the timeout expires, inspect which jobs did finish in time (if any) and which ones are still running
jobs.map { deferred -> /* ... inspect results */ }
In the main code (which is not a coroutine) ...
Since our main code is not a coroutine it has to wait in a blocking way, so we bridge the code we wrote using runBlocking. Putting it all together:
fun awaitResultsWithTimeoutBlocking(
timeoutMillis: Long,
numberOfJobs: Int
) = runBlocking {
val jobs: List<Deferred<R>> = List(numberOfJobs) {
GlobalScope.async { /* our code that produces R */ }
}
withTimeoutOrNull(timeoutMillis) { jobs.joinAll() }
jobs.map { deferred -> /* ... inspect results */ }
}
P.S. I would not recommend deploying this kind of solution in any kind of a serious production environment, since letting your background jobs running (leak) after timeout will invariably badly bite you later on. Do so only if you throughly understand all the deficiencies and risks of such an approach.
You can try to work with whileSelect and the onTimeout clause. But you still have to overcome the problem that your main code is not a coroutine. The next lines are an example of whileSelect statement. The function returns a Deferred with a list of results evaluated in the timeout period and another list of Deferreds of the unfinished results.
fun CoroutineScope.runWithTimeout(timeoutMs: Int): Deferred<Pair<List<Int>, List<Deferred<Int>>>> = async {
val deferredList = (1..100).mapTo(mutableListOf()) {
async {
val random = Random.nextInt(0, 100)
delay(random.toLong())
random
}
}
val finished = mutableListOf<Int>()
val endTime = System.currentTimeMillis() + timeoutMs
whileSelect {
var waitTime = endTime - System.currentTimeMillis()
onTimeout(waitTime) {
false
}
deferredList.toList().forEach { deferred ->
deferred.onAwait { random ->
deferredList.remove(deferred)
finished.add(random)
true
}
}
}
finished.toList() to deferredList.toList()
}
In your main code you can use the discouraged method runBlocking to access the Deferrred.
fun main() = runBlocking<Unit> {
val deferredResult = runWithTimeout(75)
val (finished, pending) = deferredResult.await()
println("Finished: ${finished.size} vs Pending: ${pending.size}")
}
Here is the solution I came up with. Pairing each job with a state (among other info):
private enum class State { WAIT, DONE, ... }
private data class MyJob(
val job: Deferred<...>,
var state: State = State.WAIT,
...
)
and writing an explicit loop:
// wait until either all jobs complete, or a timeout is reached
val waitJob = launch { delay(TIMEOUT_MS) }
while (waitJob.isActive && myJobs.any { it.state == State.WAIT }) {
select<Unit> {
waitJob.onJoin {}
myJobs.filter { it.state == State.WAIT }.forEach {
it.job.onJoin {}
}
}
// mark any finished jobs as DONE to exclude them from the next loop
myJobs.filter { !it.job.isActive }.forEach {
it.state = State.DONE
}
}
The initial state is called WAIT (instead of RUN) because it doesn't necessarily mean that the job is still running, only that my loop has not yet taken it into account.
I'm interested to know if this is idiomatic enough, or if there are better ways to code this kind of behaviour.