Reputation: 23
I am using the Abortable crate to suspend the execution of a Future. Say I have an abortable future in which the async function itself awaits other async functions. My question is, if I abort the root Future, would the child Futures be aborted instantly at the same time, or would they be dangling?
I read the source code for Abortable, in particular the code for try_poll
:
fn try_poll<I>(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
poll: impl Fn(Pin<&mut T>, &mut Context<'_>) -> Poll<I>,
) -> Poll<Result<I, Aborted>> {
// Check if the task has been aborted
if self.is_aborted() {
return Poll::Ready(Err(Aborted));
}
// attempt to complete the task
if let Poll::Ready(x) = poll(self.as_mut().project().task, cx) {
return Poll::Ready(Ok(x));
}
// Register to receive a wakeup if the task is aborted in the future
self.inner.waker.register(cx.waker());
// Check to see if the task was aborted between the first check and
// registration.
// Checking with `is_aborted` which uses `Relaxed` is sufficient because
// `register` introduces an `AcqRel` barrier.
if self.is_aborted() {
return Poll::Ready(Err(Aborted));
}
Poll::Pending
}
My understanding is that once abort
is called, it will propagate to the downstream Futures in the sense that when the root Future is aborted, it will stop polling its child Future (because Poll::Ready(Err(Aborted))
will be returned), which will in turn stop polling its child. If this reasoning is true, then the effect of calling abort is immediate.
Another argument is that if Future is pull-based, the root node should be invoked first and then propagate to the sub tasks until the leaf one is invoked and aborted (and then go back to root). This means that there is a latency between when the abort
method is called and when the leaf Future actually stops polling. Might be relevant, but this blogpost mentions dangling tasks, and I am concerned this is the case.
For example, here is a toy example that I wrote:
use futures::future::{AbortHandle, Abortable};
use tokio::{time::sleep};
use std::{time::{Duration, SystemTime}};
/*
* main
* \
* child
* | \
* | \
* leaf1 leaf2
*/
async fn leaf2() {
println!("This will not be printed")
}
async fn leaf1(s: String) {
println!("[{:?}] ====== in a ======", SystemTime::now());
for i in 0..100000 {
println!("[{:?}] before sleep i is {}", SystemTime::now(), i);
sleep(Duration::from_millis(1)).await;
println!("[{:?}] {}! i is {}", SystemTime::now(), s.clone(), i);
}
}
async fn child(s: String) {
println!("[{:?}] ====== in child ======", SystemTime::now());
leaf1(s.clone()).await;
leaf2().await
}
#[tokio::main]
async fn main() {
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let result_fut = Abortable::new(child(String::from("Hello")), abort_registration);
tokio::spawn(async move {
println!("{:?} ^^^^^ before sleep ^^^^^", SystemTime::now());
sleep(Duration::from_millis(100)).await;
println!("{:?} ^^^^^ after sleep, about to abort ^^^^^", SystemTime::now());
abort_handle.abort();
println!("{:?} ***** operation aborted *****", SystemTime::now());
});
println!("{:?} ====== before main sleeps ======", SystemTime::now());
sleep(Duration::from_millis(5)).await;
println!("{:?} ====== after main wakes up from sleep and now getting results \
======", SystemTime::now());
result_fut.await.unwrap();
}
Rust playground
I am personally leaning more towards the first argument that there is no latency between abortion of the root and abortion of leaf because the leaf doesn't need to know it needs to abort (the leaf only pulls when root tells it to). The example above prints the time the child is executed and the time when the root is aborted. The execution of child is always before the root is aborted, but I am not sure if this can prove that my first argument is true, so I would like to know what y'all think!
Upvotes: 2
Views: 773
Reputation: 1089
Dropping a Future
type equals to drop all the states within that Future
, including its child Future
s. Remember Future
s in Rust are all state machines. If you succeeded in safely dropping a Future
's states, then it means the execution must have already been stopped before the drop, otherwise it is a data race.
Specifically, dropping a tokio JoinHandle
conceptually only drops the handle itself, but does nothing about the task that the handle represents. Or in other words, if you use tokio::spawn()
, then whatever Future
you throw into that task became unrelated to your current Future
(except you can receive back a result from the JoinHandle
and explicitly call abort). So they call tokio's tasks as detached by default.
Let's talk about timing issues. For Abortable
, it does not directly "drop" your Future
. Instead, it "drops" in an indirect way. If you call AbortHandle::abort()
, then immediately an atomic boolean flag is flipped and then a Waker::wake()
is called, but the async executor would be unaware of this at this moment. The executor would still think your Abortable
is either progressing or suspended at this moment. We should discuss them separately:
The executor was still polling your Future
at the time of the atomic boolean flip. Then according to the source code
Abortable
would return a completion.Pending
in the end, then Abortable
has a high chance of return an aborted error, small chance of going back into suspense, depending on when the atomic flag flip became observable to the thread of this polling.If either a completion or an aborted error is returned, the Abortable
as a Future
would be considered as completed and eventually(*) get dropped. When it gets dropped, it would mean all its child Future
s would have also been dropped.
If you happen to be coding in a very poor style and this polling happens to have touched a long computation / a blocking call, then unfortunately, everything else would have to wait however long for this poll to return, and the executor may be starved during the process.
Your Future
was suspended at the time of the atomic boolean flip. The Waker::wake()
call tells the executor to poll this Abortable
at least once sometime in the future. Some time later, the executor will finally decide to give your Abortable
a poll. Then almost immediately the poll returns an aborted error. Then the same thing happens, your Abortable
as a Future
would be considered as completed and eventually get dropped.
In summary, no, the abort does not happen at the same time as the drop.
*: I'm not sure when the Future
gets dropped after its completion. It would really depend on how Rust compiler transforms the .await
point and I haven't looked into it yet.
Upvotes: 2
Reputation: 70820
Yes, because a future needs to polled to executed but it will not be polled if it is aborted, the child futures will not be polled either and therefore the execution will stop immediately.
Of course, execution will stop only after reaching the next yield point, and spawned tasks using tokio::spawn()
will not stop.
Upvotes: 2