-
Notifications
You must be signed in to change notification settings - Fork 234
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
Document future cancellation #2372
Comments
Continue on my test case here. After some tinkering, in contrasts to my original attempt to cancel future via KeyboardInterrupt in python, the more reliable way to cancel future seems to be using signal handler or some sort of global control to call the cancel function exposed to python, one candidate would be handle = RustHandle()
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
for s in signals:
loop.add_signal_handler(
s, lambda s=s: handle.shutdown(s, loop)
)
try:
loop.run_until_complete(handle.entry())
except asyncio.exceptions.CancelledError:
logger.debug("shutdown via exit signal") I think on the uniffi side, the current design is mostly fine, because mapping rust's async runtime behaviour, which is configurable, to any of the exposed languages adds complexity, so cancellation should really be provided by the author. Therefore if the event_loop is pulling stuff from the other side it can decide how to delegate to rust for the exposed cancel function. Nonetheless, the current version of uniffi python helper code generated some error msg that is not easy to debug or understand, I think it's better to:
while True:
try:
await RustFututure()
except exception_during_uniffi:
handle.cancel() |
I'm personally more keen on adding a dependency on tokio and starting every async ffi call as a tokio task to avoid every issue with cancellation and make it more reliable. What we do is we have a static instance of a runtime and spawn all tasks there. |
Another alternative is implementing cancellation to the interface exposed from the Rust side. You can add a
Neither of these is an option since this happens inside a C callback that Rust is calling -- there's no realistic way to propagate the Python exception up the stack there. However, I think it might be possible catch any exceptions and cancel/free the future in that case.
There's actually some support right now for this with the |
@bendk Thx for the reply
This occurs to be more reliable with the assumption that one might need to create and handle the async-runtime from the rust side. Functionally it's more than capable of conveying my idea through rust this way with the current implementation of uniffi. One observation I found is that uniffi relied on the host language to run the event_loop where bringing another async-runtime is optional. In that mentality I think one would like to continue relying on the host language to provide cancellation in a way that it's native to them. In the case of python, exception and signal handler seems to be more common.
After some testing, I think it's not only related to async but any call passed through ctypes as well, it's not possible to be effected by runtime exception. #[uniffi::export]
pub fn halting() {
loop {
}
} And it'll not be captured anyhow try:
halting()
except Exception as e:
print(e) where the py-equivalent will play nicely with exception and exit def py_halt():
while True:
pass Would this be an good summary of what will happen with regards of an exception?
If my summary above is correct, I think it's best to advice authors to implement cancellation of any long running task from rust so that it'll be less surprise if users aren't that familiar with this mechanism. From the host language author's perspective, I think a small chapter like this might be enough to keep them going. This is ranged from how much involvement they want to intervene from mostly rust to mostly runtime // actively capture interruption
#[uniffi::export]
async fn rust_fn() {
tokio::singla::ctrl_c().await;
}
// cooperative interruption
#[uniffi::export]
async fn rust_fn(signal_for_cancel: u8) {
// signal mask or some sort that host can passed to cancel the future from rust's side
}
// runtime exception
#[uniffi::export]
async fn runst_fn() {
let token = CancellationToken::new();
let handle = tokio::spawn(async move {
select(token, other_future);
});
};
// store the cancel token in another function so that user can call during runtime exception
});
}
This seems like an good default, I can see it working well as long as it can optionally toggle off. |
This is a good point, but the issue is that not all languages provider a nice way to cancel an async call -- at least not in a way that UniFFI can use. See the Swift discussion. That said, I'm not sure that Python falls into this category. Maybe we can support task cancellation (in fact, maybe we do already see the test below). However, I don't feel like
I would split this up a different way:
I didn't quite follow this one, but I don't believe we make any guarantees like this.
Totally agree, hopefully I'll get around to that sometime relatively soon. |
Update https://mozilla.github.io/uniffi-rs/next/internals/async.html and add some discussion on cancelling futures/async functions.
The text was updated successfully, but these errors were encountered: