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

Explain what happens with async code #2360

Open
dev-ardi opened this issue Dec 18, 2024 · 4 comments
Open

Explain what happens with async code #2360

dev-ardi opened this issue Dec 18, 2024 · 4 comments

Comments

@dev-ardi
Copy link

The current documentation is very lacking regarding what happens with async code.
Everything is very "magical".
I have the following questions:

  1. Who is polling my futures?
    It's certainly not Rust, or is it? I don't know, it's not documented.
  2. How does it work?
  3. Is it the client's runtime code? How does it wake the futures efficiently?
    How can I use my own executor? we currently have a static instance of a tokio rt where we spawn every single one of our async calls, which is not good from an ergonomics POV.
  4. How does error handling happen? Why is it that way?
  5. How does async cancellation happen? Can it happen, as in, is it possible that my future will not be polled to completion?
  6. What threading guarantees do we have? Will the futures be polled from the same thread, will they not, is there work stealing?
@mhammond
Copy link
Member

Better docs literally just landed - https://mozilla.github.io/uniffi-rs/next/internals/async.html. That's quite a laundry list though, we're doing our best.

@extraymond
Copy link

Would you mind dive deep into cancellation by exception? What's the order for propagating foreign exception into rust future?
I've read the bits https://github.com/mozilla/uniffi-rs/blob/main/docs/manual/src/futures.md, but seems like I need to do more to get it work.

I'm trying to capture python KeyboardInterrupt during a for loop while pulling from rust future, it seems that the uniffi generated foreign code are taking control of the exception instead of letting me manually calling the cancel function.

This is the naive setup to test it. Basically I created an background task that fill the rust channel, and allow python to fetch the result using async.

This is the exposed rust async function

    #[uniffi::method]
    async fn try_pull(&self) -> ApiResult<Option<Sample>> {
        let mut rx = self.rx.lock().await;
        let res = rx.recv().await.flatten();

        Ok(res)
    }

And this is the where the future is called

    try:
        h = Handle()

        while msg := await h.try_pull():
             logger.info("incoming")

        logger.info("ended")
    except Exception as e:
        logger.debug(e)
        if h is not None:
            await h.exit()
    finally:
        logger.warning("all done")

And the exception I got is something similar to this:

Exception ignored on calling ctypes callback function
in _uniffi_continuation_callback 
@_UNIFFI_RUST_FUTURE_CONTINUATION_CALLBACK

And my loop just hangs there without letting me call and exit function that drops the background task

@bendk
Copy link
Contributor

bendk commented Dec 31, 2024

Would you mind dive deep into cancellation by exception?

This would be a great addition to the docs, I just made #2372 for that.

In general, I think it's best to implement cancellation explicitly in the interface. For example, maybe your interface can have a cancel method that would cancel the future. I think this means storing an internal cancellation token in the Rust struct.

The alternative is to try to use the language's builtin cancellation mechanisms, but that's not very reliable. I believe it's supported on Kotlin, but not Swift and I don't think it's implemented for Python either. There was a ton of discussion in #1768 on why we didn't want to implement it for Swift.

Separate from all of this is using KeyboardInterrupt to cancel things. In general, I don't know what's the correct behavior is when the Python code sees that exception when it's in the middle of doing some FFI work. For example, we could try to cancel the future, but that requires making another FFI call which is kind of at odds with KeyboardInterrupt. And what happens if we see a second KeyboardInterrupt in the middle of that call? I think there's probably a reasonable choice here, like maybe on the first interrupt we try to cancel then on future interrupts we just bail. I'm really not sure though.

@extraymond
Copy link

@bendk

Happy new year!

Thanks for the detailed diagram about async-ffi.
Should I continue the discussion cancellation topic here or over to the newly created issue?

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

No branches or pull requests

4 participants