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!
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.