gtk-rs-core
gtk-rs-core copied to clipboard
No way to know when a cancelled Future async operation is actually cancelled
https://github.com/rust-lang-nursery/futures-rs/issues/1278#issuecomment-428071710
Problem here is that the callback-based API would give us a Cancelled
error at that time, while simply dropping the future allows for no way of signalling that the cancellation has happened. But before cancellation happened, it is impossible to start a new async operation on most objects.
It can actually be known: once the closure is not used anymore and is destroyed, i.e. at the end of the callback. We could add a method to the gio::GioFuture
that allows to cancel an operation and gives back a handle back that can be await
ed on.
I just got curious about this myself when looking into cancellation for futures running via MainContext.spawn()
/.spawn_local()
.
My use case here is a file/folder-view widget. I want to asynchronously enumerate and load files into the view, but if the user navigates to a new folder while the enumeration is still happening, it should cancel the in-progress enumeration. I experimented with holding onto the SourceId
and then calling .remove()
on it, and that does seem to work? The stuff in the source does indeed stop running, though I can't be 100% sure that the GioFuture
that was running inside it actually got dropped (thus causing the Cancellable
to trigger cancellation on the GIO operation). (Reading through the TaskSource
source, I do think this does work how I think it does, though.)
I guess this is also more about Rust async/await behavior: I hope that when there is a Future that wraps an async function, and that function has another Future that is being .await
'ed on, I expect that the inner future gets dropped as well when the outer future gets dropped; I can't imagine how all this would work otherwise. But I also expected the GIO operation to return with gio::IOErrorEnum::Cancelled
, but I guess I can see why that doesn't happen (since the outer future is no longer running at all?).
At any rate, I'm not even sure how this would work in my situation. Since I'm using spawn()
, I "lose" the GioFuture
, so the TaskSource
that ends up getting built doesn't even know there's a GIO operation running inside it.
One solution I can think of that doesn't involve API breakage might be to add a .cancellable()
function to GioFuture
, which returns the underlying gio::Cancellable
. Then that could have a new function that returns a Future that waits on the underlying GIO cancellation to finish. (Or, hmm, Cancellable
itself could just implement Future
?) So any time I call a function that creates a GioFuture
, I could grab the Cancellable
, connect to its cancelled
signal, stash it somewhere, and .await
if I need to. The downside here is that the call sites get much more verbose; I would have to do something like:
let future = gio_file.enumerate_children_future(...);
let cancellable = future.cancellable();
cancellable.connect_cancelled(|cancellable| {
MainContext::default().spawn_local(async move {
cancellable.await; // or cancellable.completion().await or whatever
println!("operation cancelled");
});
});
match future.await {
// ...
}
... when it used to be just a simpler match gio_file.enumerate_children_future(...).await { ... }
.
(Sorry this is all a bit stream-of-consciousness; in some ways I'm thinking out loud here.)
At any rate, I'd be interested in having a go at implementing whatever seems to make the most sense, given some guidance from y'all.
The problem here is that you can cancel a GioFuture
/ remove a SourceId
while the future itself is running, too. It's not guaranteed that it is actually finished running at that point but it might continue running for a while.
My plan for GioFuture
s here is to add a way to await on the future to be finished and to also expose the Cancellable
inside it for explicit cancellation, but I didn't get to that yet. A GioFuture
is fully done once its callback is called, so that can be used as a waiting point. The callback will be called with gio::IOError::Cancelled
if it was cancelled.
Makes sense. I think in the case where someone manually cancels the GIO operation (using the gio::Cancellable
that is still yet to be exposed), there's nothing extra to do, right? The GioFuture
can still wait, and then will return the Cancelled
error, and that's how the caller knows the GIO operation has finished being cancelled.
But in the case where the GioFuture
is dropped, or doesn't get run at all for whatever reason, I think that's a bit trickier to implement. Err... hmm... is this already implemented, actually, just not in the way I'm thinking? Looking at git master, I see there's a CancellableExtManual
trait not in the latest stable release, and it includes a .future()
method that waits until the cancelled
signal is emitted on the Cancellable
. That's not quite the same thing as the async callback returning a Cancelled
error, but is it maybe good enough?
Makes sense. I think in the case where someone manually cancels the GIO operation (using the
gio::Cancellable
that is still yet to be exposed), there's nothing extra to do, right? TheGioFuture
can still wait, and then will return theCancelled
error, and that's how the caller knows the GIO operation has finished being cancelled.
Yes, that's correct. For convenience we would also provide some kind of JoinHandle
though that can be awaited until the operation is cancelled or otherwise finished. That could be just a oneshot channel that is filled at the end of the GIO callback.
But in the case where the
GioFuture
is dropped, or doesn't get run at all for whatever reason, I think that's a bit trickier to implement.
That's also not really a problem and can be handled the same way as above. The JoinHandle
would also continue functioning.
The main problem here is that the async / completion-based nature of GIO operations makes it impossible to borrow references to them. You can't synchronously (!) guarantee that the reference is not used anymore when the future is dropped.
That's the underlying reason why e.g. InputStream::read_future()
requires passing an owned buffer (and returns it back to the caller) instead of just allowing to borrow e.g. a &mut [u8]
.