Ciao! I have to use a legacy API that runs a backg...
# coroutines
f
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
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
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
😄 whoops, well spotted. That should be fixable, though. I think the general approach is still worth trying.
u
Would
suspendCoroutine
behave as you need it?
f
suspendCoroutine doesn't let me install a cancellation handler that I need to cancel the DownloadTask... Otherwise yes
u
I understand there is no way to cancel the DownloadTask. Otherwise, why would it still be running after you cancel it?
s
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
@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