https://kotlinlang.org logo
#coroutines
Title
# coroutines
f

franztesca

02/21/2023, 2:08 PM
Ciao! I have to use a legacy API that runs a background task and uses a callback to notify its completion. e.g. fun
downloadSomething(onCompleted: () -> Unit): DownloadTask
I would like to make it suspending and cancellable, therefore I'd use
suspendCancellableCoroutine
to do that, resuming the continuation when onCompleted is invoked. However, there is a problem with that: if the coroutine is cancelled, the
DownloadTask
is cancelled (as intended), but
suspendCancellableCoroutine
resumes immediately with a
CancellationException
(before the
DownloadTask
is actually completed). This is a problem because it is unstructured an the coroutine completes when the background task (the download) is still running. Here is some code that better explains the problem: https://pl.kotl.in/tm08uP_SB To solve this I wrote a
suspendCancellableCoroutineAwaiting
solution that uses some trick to make sure that the background task is actually awaited before the coroutine is resumed, even when cancelled. However, I'm not particularly satisfied with this solution (conceptually). Is there any better way to achieve this? Thank you!
Copy code
@OptIn(ExperimentalCoroutinesApi::class)
private suspend inline fun <T> suspendCancellableCoroutineAwaiting(
    crossinline block: (CancellableContinuation<T>) -> Unit,
): T {
    val job = Job()
    return try {
        suspendCancellableCoroutine { cont ->
            val wrapperContinuation = object : CancellableContinuation<T> by cont {
                override fun resumeWith(result: Result<T>) {
                    job.complete()
                    cont.resumeWith(result)
                }
                override fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?) {
                    job.complete()
                    cont.resume(value, onCancellation)
                }
            }
            block(wrapperContinuation)
        }
    } finally {
        withContext(NonCancellable) {
            job.join()
        }
    }
}
s

Sam

02/21/2023, 2:39 PM
How about something like this?
Copy code
suspend fun download() = coroutineScope {
    val downloadJob = Job()
    val task = downloadSomething { downloadJob.complete() }
    launch(start = CoroutineStart.ATOMIC) {
        try {
            awaitCancellation()
        } finally {
            task.cancel()
        }
    }
    withContext(NonCancellable) {
        downloadJob.join()
    }
}
That’s assuming that the
DownloadTask
has some sort of
cancel
hook. If it doesn’t, it’s effectively not cancellable at all, which makes all of this a bit moot
f

franztesca

02/22/2023, 3:35 PM
The problem with this is that if the export completes normally and nobody cancels the coroutine, the coroutine doesn't finish because of
awaitCancellation
... right?
s

Sam

02/22/2023, 3:38 PM
😄 whoops, well spotted. That should be fixable, though. I think the general approach is still worth trying.
u

uli

02/23/2023, 1:26 PM
Would
suspendCoroutine
behave as you need it?
f

franztesca

02/23/2023, 11:24 PM
suspendCoroutine doesn't let me install a cancellation handler that I need to cancel the DownloadTask... Otherwise yes
u

uli

02/24/2023, 8:56 AM
I understand there is no way to cancel the DownloadTask. Otherwise, why would it still be running after you cancel it?
s

Sam

02/24/2023, 8:59 AM
Cancellation may take some time, so there could be a delay after asking the task to cancel before it actually stops and releases resources. Coroutines model that via cooperative cancellation. If I understand the question here, it's about how to replicate that kind of behaviour with callbacks.
u

uli

02/24/2023, 9:06 AM
@franztesca what exactly is the api of the download job? How do you cancel and how do you know cancelation is final?
your code sample returns void from
downloadSomething
6 Views