Skip to content
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

Feature Request: invokeOnCompletion handler for suspendCancellableCoroutine #3065

Open
jschools opened this issue Dec 6, 2021 · 6 comments

Comments

@jschools
Copy link

jschools commented Dec 6, 2021

It is inconvenient to use suspendCancellableCoroutine to wrap an API that requires a callback to be unregistered after receiving the call. Examples include "event bus" or "broadcast" APIs which do not unregister a callback automatically and will leak the callback.

We have invokeOnCancellation to handle situations where the coroutine is cancelled, although there is no handler for when the continuation is resumed successfully. This potentially could be done in the callback itself prior to calling cont.resume(), but it is not possible to get a reference to the callback if it is a lambda. It may also duplicate the code in the invokeOnCancellation block.

For example:

suspendCancellableCoroutine<Int> { cont ->
  val callback: (Int) -> Unit = {
    someApi.unregisterCallback(???) // no way to get reference to a lambda - cannot use `this` or `callback`
    cont.resume(it)
  }
  
  someApi.registerCallback(callback)
  
  cont.invokeOnCancellation {
    // not called after successful resume!
    someApi.unregisterCallback(callback)
  }
}

I propose adding invokeOnCompletion to CancelableContinuation which could be used to handle both cancellation and successful resume, similar to Job.invokeOnCompletion.

If there is a simpler way around this that I am missing, please do let me know.

@joffrey-bion
Copy link
Contributor

joffrey-bion commented Dec 6, 2021

Note that you could define your callback as an object instead of a lambda to get access to this:

    val callback = object : (Int) -> Unit {
        override fun invoke(value: Int) {
            someApi.unregisterCallback(this)
            cont.resume(value)
        }
    }

But the argument about repeating code still holds though.

@joffrey-bion
Copy link
Contributor

Examples include "event bus" or "broadcast" APIs which do not unregister a callback automatically and will leak the callback.

@jschools Thinking about this, why would you wrap those APIs into suspend functions, though? Looks like a callbackFlow use case to me instead.

If you just want the first event from those APIs, then simply call first() on the returned flow.

@jschools
Copy link
Author

jschools commented Dec 7, 2021

@joffrey-bion I was thinking it would be convenient for suspendCancelableCoroutine to support a kind of "try/finally" option, in case an API needed to be destroyed or torn down after a successful resume. There are plenty of clumsy SDKs out there...

I agree callbackFlow solves all of these cases. Wouldn't that be an argument that it could replace suspendCancelableCoroutine altogether? I'm happy to close this issue if you think it's not a large enough concern 😀

@joffrey-bion
Copy link
Contributor

I agree callbackFlow solves all of these cases. Wouldn't that be an argument that it could replace suspendCancelableCoroutine altogether?

They solve different problems, though. callbackFlow is meant to wrap APIs that call a callback repeatedly and expose results as a Flow, while suspendCancellableCoroutine is for single-shot callbacks and exposes this call as a suspend function.

There are plenty of clumsy SDKs out there...

Yeah, if some SDKs take a single-shot callback that needs to be explicitly unregistered, it's pretty crappy indeed. But when this happens there are options, like the above.

I'm happy to close this issue if you think it's not a large enough concern

I'm not opposed to this addition, though, don't get me wrong. Also I am by no means of any authority in this repository 😄 I'll let the maintainers decide for themselves

@LouisCAD
Copy link
Contributor

@joffrey-bion I was thinking it would be convenient for suspendCancelableCoroutine to support a kind of "try/finally" option, in case an API needed to be destroyed or torn down after a successful resume. There are plenty of clumsy SDKs out there...

Actually, you can use try/finally with suspendCancellableCoroutine, you can surround the call to suspendCancellableCoroutine itself with the try block. Just make sure you don't risk crashing because resume has been called twice while the coroutine was completing, but didn't reach the finally block yet to unregister the callback because of the dispatch being queued.

@joffrey-bion
Copy link
Contributor

@LouisCAD I think it would not be very convenient to do with a try/finally around the suspendCancellableCoroutine, because the finally likely needs a reference to the callback to unregister it, and the callback needs a reference to the continuation provided by suspendCancellableCoroutine. So it becomes a chicken-and-egg problem here 😄 and this would require some kind of lazy mechanism or lateinit var, which I guess is even uglier than just repeating the cleanup in both places

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