We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 21 of 25
Cancelling a future by dropping it
An unfinished async function keeps its local state in a future. Dropping that future runs the destructors for the values it still owns. In this exercise, those destructors write to a log so you can see exactly what gets cleaned up.
Fix cancel_log(polls_before_cancel: u32) -> Vec<String> so it returns the
complete log, including cleanup entries. The starter takes its snapshot too
early. Keep the supplied Guard, body and Yield2 behavior.
What the driver must do
Create and pin the body future, then poll it at most polls_before_cancel
times. Stop immediately if a poll returns Ready; do not poll it again.
If it has not completed, append "cancelling". Finally, drop the owned future
and return the log in its original order.
body performs these steps:
- Write
"started"and create the guard namedinner. - Await a new
Yield2, write"resumed", and create the guard namedlate. - Await another new
Yield2, then write"finished".
Each Yield2 returns Pending twice and Ready on its third poll. Completing
the first await continues straight into the second await in the same outer
poll. The whole body therefore needs five polls, not six.
| Poll budget | Returned log |
|---|---|
| 0 | ['cancelling'] |
| 1 or 2 | ['started', 'cancelling', 'drop:inner'] |
| 3 or 4 | ['started', 'resumed', 'cancelling', 'drop:late', 'drop:inner'] |
| 5 or more | ['started', 'resumed', 'finished', 'drop:late', 'drop:inner'] |
The table uses single quotes for readability; each entry is a Rust String.
All u32 budgets are valid. Even a very large budget stops after five polls.
Cleanup depends on how far the body got
With zero polls, no statement inside body has run, so neither guard exists.
Calling an async function still evaluates its arguments and stores them in
its future. Dropping an unpolled future also drops those captured arguments.
After one or two polls, only inner exists. After three or four, both guards
exist. These named locals are dropped in reverse creation order: late, then
inner. At normal completion, both are dropped as the body exits, before
the fifth poll returns Ready.
Cleanup does not undo earlier work. The "started" and "resumed" entries
remain after cancellation. Likewise, dropping a future does not automatically
reverse a file write or a request already sent to another service. Ordinary
Drop cleanup is synchronous; it cannot await an asynchronous cleanup step.
Drop the owner before reading the log
The driver, body and guards share an Rc<RefCell<Vec<String>>>. Cloning the
vector before dropping a suspended future misses the later destructor entries.
That is why the starter fails for budgets 1 through 4, but happens to work for
zero polls and for a completed body.
Here is a smaller, complete example of observing cleanup:
use std::cell::Cell;
use std::future::{Future, pending};
use std::rc::Rc;
use std::task::{Context, Waker};
struct Guard(Rc<Cell<bool>>);
impl Drop for Guard {
fn drop(&mut self) {
self.0.set(true);
}
}
fn main() {
let cleaned = Rc::new(Cell::new(false));
let flag = Rc::clone(&cleaned);
let mut future = Box::pin(async move {
let _guard = Guard(flag);
pending::<()>().await;
});
let mut context = Context::from_waker(Waker::noop());
assert!(future.as_mut().poll(&mut context).is_pending());
assert!(!cleaned.get());
drop(future);
assert!(cleaned.get());
}
Dropping the Pin<Box<_>> destroys the owned future. Dropping only a
Pin<&mut _> obtained from as_mut() would discard a borrowed handle and
leave the future alive.
Once the exercise's future has been dropped, Rc::into_inner(log) can take
the vector's RefCell without cloning it. This returns Some only when
log is the last strong owner; weak handles do not prevent it. That checks
ownership of the log, not whether a future object still exists. A completed
body has already released its copy of the log.
Where this cancellation model stops
The owner can drop an unpolled future or drop it after a poll returns. It
cannot use dropping to interrupt that same future while its poll call is
still executing. A long CPU loop or blocking operation inside poll delays
the owner's next chance to act, even if other threads are running.
An .await yields control only when the awaited future returns Pending.
A ready await continues immediately, and a hand-written future can return
Pending without using .await at all. Cancellation tokens and runtime task
APIs provide other ways to request cancellation. Dropping a handle to a
separately running task does not necessarily stop that task.
This driver uses a no-op waker because Yield2 makes progress on every poll
and requests another poll before returning Pending. It is a small teaching
driver, not an executor for arbitrary I/O futures. Real executors use wakeups
to decide when to poll again.
Finally, std::mem::forget(future) skips destruction of the future's remaining
state. It does not undo destructors that already ran. Forgetting a suspended
future can leak its resources, so it is not a substitute for cleanup.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.