Skip to content

Provide API for blocking call invocation on job completion and/or cancellation #728

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
qwwdfsad opened this issue Oct 18, 2018 · 3 comments

Comments

@qwwdfsad
Copy link
Member

qwwdfsad commented Oct 18, 2018

In Ktor, we have a use-case where a call to blocking API should be made when Job is being canceled with an additional requirement that Job should not complete until this blocking call is done.
It can be generalized as "invoke blocking API on Job cancellation as part of job hierarchy".

Currently, it can be emulated with the following pattern:

val job = ...
launch(job) {
    try {
        suspendForever() // e.g. delay(Long.MAX_VALUE)
    } catch (e: CancellationException) {
        withContext(NonCancellable) { blockingCall() }
    }
}

Another solution is to use internal API: job.invokeOnCancellaion(onCancelling = true) { launch(job) { blockingCall() } }

If there is a demand (or compelling use-case) we can provide a much convenient way to plug blocking shutdown sequence into job hierarchy. In that API we can guarantee non-cancellability, well-behaving dispatcher (e.g. not Unconfined) and other goodies (e.g. suspending context, less resource consumption etc.).

Note: described primitive is not equivalent to

job.join()
blockingCall()

because job will completed before blockingCall is eve started.

@cy6erGn0m
Copy link
Contributor

Let me describe the ktor use-case in more details.

We have an interface ApplicationEngine with start and stop functions. Usually these functions simply delegate to an underlying implementation (such as Netty, Jetty, Tomcat). A stop implementation is usually blocking (it is waiting for internal server's services termination) that may take a while.

On the other hand we would like to listen to parent job cancellation to stop server if it get crashed or just cancelled for some reason. Since stop() may take indefinite time so it doesn't look like a good idea to invoke it inside of invokeOnCompletion however at the same time we need to hold the parent job in cancelling state until the underlying server shutdown completion. Also almost all underlying servers don't provide any async shutdown facilities so the only wait to track shutdown is to wait for stop() function return.

@fvasco
Copy link
Contributor

fvasco commented Oct 19, 2018

I see a little issue on the prolongation of cancelling state, the initial task is done, a joiner should not be interested on attached finalizers.
Instead an alternative is to create another job composing the initial task with a finalizer, as example:

fun Job.compose(coroutineContext: CoroutineContext, handler: suspend (isCancelled: Boolean) -> Unit): Job {
    val innerJob = this
    return GlobalScope.launch {
        select<Unit> {
            innerJob.onJoin {} // await completion
            onJoin {} // await cancellation
        }
        withContext(coroutineContext + NonCancellable) {
            if (isCancelled) innerJob.cancel()
            innerJob.join()
            handler(innerJob.isCancelled)
        }
    }
}

fun main(args: Array<String>) = runBlocking {
    val job = Job()
    launch(job) {
        suspendForever()
    }.compose(Dispatchers.IO) { isCancelled ->
        if (isCancelled) blockingCall()
    }
}

This implementation can contains some issues

@dovchinnikov
Copy link
Contributor

dovchinnikov commented Feb 28, 2023

I have just discovered this ticket. We also need this in IJ for another reason but the proposed emulating implementation looks similar #3505

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants